Last active
May 19, 2023 08:23
-
-
Save LingDong-/61e0a68fd77a35c5b77014c4f89de393 to your computer and use it in GitHub Desktop.
a barebones midi synthesizer calling mac's low level API (CoreMIDI and CoreAudio) directly
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// core_midi_synth.mm | |
// | |
// a barebones midi synthesizer demo that calls | |
// mac's low level API (CoreMIDI and CoreAudio) directly | |
// (because I can't be bothered to compile other people's libraries) | |
// | |
// with code fragments adapted from | |
// - https://stackoverflow.com/a/7964300 | |
// - https://github.com/thestk/rtmidi | |
// | |
// see also | |
// - https://developer.apple.com/documentation/coreaudiotypes/ | |
// - https://developer.apple.com/documentation/coremidi | |
// | |
// this is a single-file software with no dependencies, | |
// other than mac system frameworks, | |
// to compile with gcc/clang: | |
// | |
// g++ core_midi_synth.mm -framework CoreMIDI -framework AudioUnit -framework Foundation -O3 -o core_midi_synth | |
// | |
// to use: | |
// - plug in your midi keyboard | |
// - run this software by typing: ./core_midi_synth | |
// - play your midi keyboard | |
// | |
// lingdong 2022, MIT License | |
// | |
#include <CoreMIDI/CoreMIDI.h> | |
#include <AudioUnit/AudioUnit.h> | |
#include <Foundation/Foundation.h> | |
#include <iostream> | |
#include <vector> | |
#define SAMPLE_RATE 44100 | |
const int n_wave = 2048; | |
double waveform[n_wave]; | |
double phase = 0; | |
double amp_targs[128] = {0}; | |
double amps[128] = {0}; | |
double frqs[128]; | |
double phases[128] = {0}; | |
static OSStatus playbackCallback(void *inRefCon, | |
AudioUnitRenderActionFlags *ioActionFlags, | |
const AudioTimeStamp *inTimeStamp, | |
UInt32 inBusNumber, | |
UInt32 inNumberFrames, | |
AudioBufferList *ioData) { | |
// cout << "?" << endl; | |
// for (int k = 0; k < 128; k++){ | |
// printf("%c",((int)(amps[k]*26)+'a')); | |
// } | |
// printf("\n"); | |
for(UInt32 i = 0; i < ioData->mNumberBuffers; ++i){ | |
int numSamples = ioData->mBuffers[i].mDataByteSize / 4; | |
for (int j = 0; j < numSamples; j++){ | |
((int*)ioData->mBuffers[i].mData)[j] = 0; | |
} | |
} | |
int nKeyOn = 0; | |
for (int k = 0; k < 128; k++){ | |
if (amps[k] < 0.001){ | |
phases[k] = 0; | |
}else{ | |
nKeyOn ++; | |
} | |
} | |
// float eachAmp = 32000.0/(float)nKeyOn; | |
float eachAmp = fmin(10000.0, 32000.0/((float)nKeyOn+1)); | |
// float eachAmp = 10000.0; | |
for (int k = 0; k < 128; k++){ | |
amps[k] = amp_targs[k] * 0.08 + amps[k] * 0.92; | |
double frq = frqs[k] + ((double)rand()/(double)RAND_MAX)*4-2; | |
double amp = amps[k]; | |
double phaseStep = frq / (double)SAMPLE_RATE; | |
for(UInt32 i = 0; i < ioData->mNumberBuffers; ++i){ | |
int numSamples = ioData->mBuffers[i].mDataByteSize / 4; | |
for (int j = 0; j < numSamples; j++){ | |
phases[k] += phaseStep; | |
int waveformIndex = (int)(phases[k] * n_wave) % n_wave; | |
// std::cout << amp << std::endl; | |
((int*)ioData->mBuffers[i].mData)[j] += (waveform[waveformIndex]*amp)*eachAmp; | |
} | |
} | |
} | |
return noErr; | |
} | |
void coreaudioinit(){ | |
// adapted from https://stackoverflow.com/questions/1361148/how-do-i-synthesize-sounds-with-coreaudio-on-iphone-mac | |
std::cout << "initializing CoreAudio..." << std::endl; | |
const int kOutputBus = 0; | |
const int kInputBus = 1; | |
OSStatus status; | |
AudioComponentInstance audioUnit; | |
AudioComponentDescription desc; | |
desc.componentType = kAudioUnitType_Output; | |
// desc.componentSubType = 'rioc'; | |
desc.componentFlags = 0; | |
desc.componentFlagsMask = 0; | |
desc.componentManufacturer = kAudioUnitManufacturer_Apple; | |
AudioComponent inputComponent = AudioComponentFindNext(NULL, &desc); | |
status = AudioComponentInstanceNew(inputComponent, &audioUnit); | |
std::cout << "instanced? " << (status==noErr) << std::endl; | |
UInt32 flag = 1; | |
status = AudioUnitSetProperty(audioUnit, | |
kAudioOutputUnitProperty_EnableIO, | |
kAudioUnitScope_Output, | |
kOutputBus, | |
&flag, | |
sizeof(flag)); | |
std::cout << "flag set? " << (status==noErr) << std::endl; | |
AudioStreamBasicDescription audioFormat; | |
audioFormat.mSampleRate = SAMPLE_RATE; | |
audioFormat.mFormatID = kAudioFormatLinearPCM; | |
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagIsPacked; | |
// audioFormat.mFormatFlags = kAudioFormatFlagIsFloat | kAudioFormatFlagIsPacked; | |
audioFormat.mFramesPerPacket = 1; | |
audioFormat.mChannelsPerFrame = 2; | |
audioFormat.mBitsPerChannel = 16; | |
audioFormat.mBytesPerPacket = 4; | |
audioFormat.mBytesPerFrame = 4; | |
status = AudioUnitSetProperty(audioUnit, | |
kAudioUnitProperty_StreamFormat, | |
kAudioUnitScope_Input, | |
kOutputBus, | |
&audioFormat, | |
sizeof(audioFormat)); | |
std::cout << "format set? " << (status==noErr) << std::endl; | |
AURenderCallbackStruct callbackStruct; | |
callbackStruct.inputProc = playbackCallback; | |
callbackStruct.inputProcRefCon = NULL; | |
status = AudioUnitSetProperty(audioUnit, | |
kAudioUnitProperty_SetRenderCallback, | |
kAudioUnitScope_Global, | |
kOutputBus, | |
&callbackStruct, | |
sizeof(callbackStruct)); | |
std::cout << "callback set? " << (status==noErr) << std::endl; | |
status = AudioUnitInitialize(audioUnit); | |
std::cout << "initialized? " << (status==noErr) << std::endl; | |
status = AudioOutputUnitStart(audioUnit); | |
std::cout << "started? " << (status==noErr) << std::endl; | |
} | |
void note_on(int pitch, int velocity){ | |
std::cout << "note on: pitch=" << pitch << " velocity=" << velocity << std::endl; | |
amp_targs[pitch] = ((float)velocity/128.0) * 0.8 + 0.2; | |
} | |
void note_off(int pitch){ | |
std::cout << "note off: pitch=" << pitch << std::endl; | |
amp_targs[pitch] = 0.0; | |
} | |
void midicallback( std::vector< unsigned char > *bytes ){ | |
unsigned int nBytes = bytes->size(); | |
if (nBytes < 1){ | |
return; | |
} | |
int isNoteOn = (bytes->at(0)>>4)==0b1001; | |
int isNoteOff = (bytes->at(0)>>4)==0b1000; | |
if (!isNoteOn && !isNoteOff){ | |
return; | |
} | |
int pitch = bytes->at(1); | |
int velocity = bytes->at(2); | |
if (velocity == 0 || isNoteOff){ | |
note_off(pitch); | |
}else if (isNoteOn){ | |
note_on(pitch,velocity); | |
} | |
} | |
static void midiInputCallback( const MIDIPacketList *list, void *procRef, void */*srcRef*/ ){ | |
// adapted from https://github.com/thestk/rtmidi | |
unsigned char status; | |
unsigned short nBytes, iByte, size; | |
unsigned long long time; | |
int ignoreFlags = 7; | |
bool continueSysex = false; | |
std::vector<unsigned char> bytes; | |
const MIDIPacket *packet = &list->packet[0]; | |
for ( unsigned int i=0; i<list->numPackets; ++i ) { | |
nBytes = packet->length; | |
if ( nBytes == 0 ) { | |
packet = MIDIPacketNext( packet ); | |
continue; | |
} | |
bool foundNonFiltered = false; | |
iByte = 0; | |
if ( continueSysex ) { | |
if ( !( ignoreFlags & 0x01 ) ) { | |
for ( unsigned int j=0; j<nBytes; ++j ) | |
bytes.push_back( packet->data[j] ); | |
} | |
continueSysex = packet->data[nBytes-1] != 0xF7; | |
if ( !( ignoreFlags & 0x01 ) && !continueSysex ) { | |
midicallback( &bytes ); | |
bytes.clear(); | |
} | |
} | |
else { | |
while ( iByte < nBytes ) { | |
size = 0; | |
status = packet->data[iByte]; | |
if ( !(status & 0x80) ) break; | |
if ( status < 0xC0 ) size = 3; | |
else if ( status < 0xE0 ) size = 2; | |
else if ( status < 0xF0 ) size = 3; | |
else if ( status == 0xF0 ) { | |
if ( ignoreFlags & 0x01 ) { | |
size = 0; | |
iByte = nBytes; | |
} | |
else size = nBytes - iByte; | |
continueSysex = packet->data[nBytes-1] != 0xF7; | |
} | |
else if ( status == 0xF1 ) { | |
if ( ignoreFlags & 0x02 ) { | |
size = 0; | |
iByte += 2; | |
} | |
else size = 2; | |
} | |
else if ( status == 0xF2 ) size = 3; | |
else if ( status == 0xF3 ) size = 2; | |
else if ( status == 0xF8 && ( ignoreFlags & 0x02 ) ) { | |
size = 0; | |
iByte += 1; | |
} | |
else if ( status == 0xFE && ( ignoreFlags & 0x04 ) ) { | |
size = 0; | |
iByte += 1; | |
} | |
else size = 1; | |
if ( size ) { | |
foundNonFiltered = true; | |
bytes.assign( &packet->data[iByte], &packet->data[iByte+size] ); | |
if ( !continueSysex ) { | |
midicallback( &bytes ); | |
bytes.clear(); | |
} | |
iByte += size; | |
} | |
} | |
} | |
packet = MIDIPacketNext(packet); | |
} | |
} | |
MIDIPortRef port; | |
MIDIEndpointRef endpoint; | |
void coremidiquit(){ | |
MIDIEndpointDispose( endpoint ); | |
MIDIPortDispose( port ); | |
} | |
void coremidiinit(){ | |
std::cout << "initializing CoreMIDI..." << std::endl; | |
OSStatus result; | |
MIDIClientRef client; | |
CFStringRef clientName = CFStringCreateWithCString( NULL, "client", kCFStringEncodingASCII ); | |
result = MIDIClientCreate(clientName, NULL, NULL, &client ); | |
std::cout << "client created? " << (result==noErr) << std::endl; | |
CFRelease( clientName ); | |
int numPorts = MIDIGetNumberOfSources(); | |
std::cout << "num ports? " << numPorts << std::endl; | |
if (numPorts <= 0){ | |
std::cout << "exiting because no port." << std::endl; | |
exit(0); | |
} | |
char portName[128]; | |
int portNumber; | |
for (int i = 0; i < numPorts; i++){ | |
MIDIEndpointRef portRef = MIDIGetDestination(i); | |
portNumber = i; | |
CFStringRef nameRef = CFSTR(""); | |
MIDIObjectGetStringProperty(portRef, kMIDIPropertyDisplayName, &nameRef); | |
CFStringGetCString( nameRef, portName, sizeof(portName), kCFStringEncodingUTF8 ); | |
CFRelease( nameRef ); | |
std::cout << "port #" << i << " : " << portName << std::endl; | |
} | |
std::cout << "using last port..." << std::endl; | |
CFStringRef portNameRef = CFStringCreateWithCString( NULL, portName, kCFStringEncodingASCII ); | |
result = MIDIInputPortCreate( client, portNameRef, midiInputCallback, NULL, &port ); | |
CFRelease( portNameRef ); | |
endpoint = MIDIGetSource( portNumber ); | |
std::cout << "endpoint = " << endpoint << std::endl; | |
result = MIDIPortConnectSource( port, endpoint, NULL ); | |
std::cout << "connected? " << (result==noErr) << std::endl; | |
// atexit(coremidiquit); | |
} | |
int main(){ | |
for (int i = 0; i < 128; i++){ | |
frqs[i] = 440.0 * pow(2, (float)(i-69)/12.0); | |
} | |
for(int i = 0; i < n_wave; i++) { | |
// SIN | |
// waveform[i] = sin(i * (M_PI * 2.) / (double) n_wave); | |
// SAWTOOTH | |
// waveform[i] = i/(double)n_wave; | |
// FRANKENSTEIN | |
waveform[i] = (i/(double)n_wave)*0.5 + sin(i * (M_PI * 2.) / (double) n_wave)*0.5; | |
} | |
coremidiinit(); | |
coreaudioinit(); | |
std::cout << "listening now: (press ctrl-C to quit)" << std::endl; | |
char input; | |
std::cin.get(input); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment