Skip to content

Commit

Permalink
Made it possible for MIKMIDISynthesizer subclasses to commandeer the …
Browse files Browse the repository at this point in the history
…MIDI event scheduling process by setting sendMIDICommand, and manually calling MIKMIDISynthesizerScheduleUpcomingMIDICommands as needed.
  • Loading branch information
kris2point0 committed Apr 25, 2016
1 parent 1962a1a commit 87b38ea
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 67 deletions.
143 changes: 77 additions & 66 deletions Source/MIKMIDISynthesizer.m
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ @interface MIKMIDISynthesizer ()

dispatch_queue_t _scheduledCommandQueue;
}

@end


Expand All @@ -37,6 +38,10 @@ - (instancetype)initWithAudioUnitDescription:(AudioComponentDescription)componen
if (self) {
_componentDescription = componentDescription;
if (![self setupAUGraph]) return nil;

self.sendMIDICommand = ^(MIKMIDISynthesizer *synth, MusicDeviceComponent inUnit, UInt32 inStatus, UInt32 inData1, UInt32 inData2, UInt32 inOffsetSampleFrame) {
return MusicDeviceMIDIEvent(inUnit, inStatus, inData1, inData2, inOffsetSampleFrame);
};
}
return self;
}
Expand Down Expand Up @@ -208,15 +213,15 @@ - (BOOL)sendBankSelectAndProgramChangeForInstrumentID:(MusicDeviceInstrumentID)i
UInt32 bankSelectStatus = 0xB0 | channel;

UInt8 bankSelectMSB = (instrumentID >> 16) & 0x7F;
err = MusicDeviceMIDIEvent(self.instrumentUnit, bankSelectStatus, 0x00, bankSelectMSB, 0);
err = _sendMIDICommand(self, self.instrumentUnit, bankSelectStatus, 0x00, bankSelectMSB, 0);
if (err) {
NSLog(@"MusicDeviceMIDIEvent() (MSB Bank Select) failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil];
return NO;
}

UInt8 bankSelectLSB = (instrumentID >> 8) & 0x7F;
err = MusicDeviceMIDIEvent(self.instrumentUnit, bankSelectStatus, 0x20, bankSelectLSB, 0);
err = _sendMIDICommand(self, self.instrumentUnit, bankSelectStatus, 0x20, bankSelectLSB, 0);
if (err) {
NSLog(@"MusicDeviceMIDIEvent() (LSB Bank Select) failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil];
Expand All @@ -226,7 +231,7 @@ - (BOOL)sendBankSelectAndProgramChangeForInstrumentID:(MusicDeviceInstrumentID)i

UInt32 programChangeStatus = 0xC0 | channel;
UInt8 programChange = instrumentID & 0x7F;
err = MusicDeviceMIDIEvent(self.instrumentUnit, programChangeStatus, programChange, 0, 0);
err = _sendMIDICommand(self, self.instrumentUnit, programChangeStatus, programChange, 0, 0);
if (err) {
NSLog(@"MusicDeviceMIDIEvent() (Program Change) failed with error %@ in %s.", @(err), __PRETTY_FUNCTION__);
*error = [NSError errorWithDomain:NSPOSIXErrorDomain code:err userInfo:nil];
Expand Down Expand Up @@ -401,6 +406,73 @@ - (void)scheduleMIDICommands:(NSArray *)commands

#pragma mark - Callbacks

OSStatus MIKMIDISynthesizerScheduleUpcomingMIDICommands(MIKMIDISynthesizer *synth, AudioUnit instrumentUnit, UInt32 inNumberFrames, Float64 sampleRate, const AudioTimeStamp *inTimeStamp)
{
dispatch_queue_t queue = synth->_scheduledCommandQueue;
if (!queue) return noErr; // no commands have been scheduled with this synth

static NSTimeInterval lastTimeUntilNextCallback = 0;
static MIDITimeStamp lastMIDITimeStampsUntilNextCallback = 0;
NSTimeInterval timeUntilNextCallback = inNumberFrames / sampleRate;
MIDITimeStamp midiTimeStampsUntilNextCallback = lastMIDITimeStampsUntilNextCallback;

if (lastTimeUntilNextCallback != timeUntilNextCallback) {
midiTimeStampsUntilNextCallback = MIKMIDIClockMIDITimeStampsPerTimeInterval(timeUntilNextCallback);
lastTimeUntilNextCallback = timeUntilNextCallback;
lastMIDITimeStampsUntilNextCallback = midiTimeStampsUntilNextCallback;
}

MIDITimeStamp toTimeStamp = inTimeStamp->mHostTime + midiTimeStampsUntilNextCallback;
CFMutableArrayRef commandsToSend = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);;

dispatch_sync(queue, ^{
CFMutableDictionaryRef commandsByTimeStamp = synth->_scheduledCommandsByTimeStamp;
if (!commandsByTimeStamp || !CFDictionaryGetCount(commandsByTimeStamp)) return;


CFMutableSetRef commandTimeStampsSet = synth->_scheduledCommandTimeStampsSet;
CFMutableArrayRef commandTimeStampsArray = synth->_scheduledCommandTimeStampsArray;
if (!commandTimeStampsSet || !commandTimeStampsArray) return;

CFArrayRef commandTimeStampsArrayCopy = CFArrayCreateCopy(NULL, commandTimeStampsArray);
CFIndex count = CFArrayGetCount(commandTimeStampsArrayCopy);
for (CFIndex i = 0; i < count; i++) {
NSNumber *timeStampNumber = (__bridge NSNumber *)CFArrayGetValueAtIndex(commandTimeStampsArrayCopy, i);
MIDITimeStamp timeStamp = timeStampNumber.unsignedLongLongValue;
if (timeStamp >= toTimeStamp) break;

CFMutableArrayRef commandsAtTimeStamp = (CFMutableArrayRef)CFDictionaryGetValue(commandsByTimeStamp, (__bridge void*)timeStampNumber);
CFArrayAppendArray(commandsToSend, commandsAtTimeStamp, CFRangeMake(0, CFArrayGetCount(commandsAtTimeStamp)));

CFDictionaryRemoveValue(commandsByTimeStamp, (__bridge void *)timeStampNumber);
CFSetRemoveValue(commandTimeStampsSet, (__bridge void*)timeStampNumber);
CFArrayRemoveValueAtIndex(commandTimeStampsArray, 0);
}
CFRelease(commandTimeStampsArrayCopy);
});

NSTimeInterval secondsPerMIDITimeStamp = MIKMIDIClockSecondsPerMIDITimeStamp();

CFIndex commandCount = CFArrayGetCount(commandsToSend);
for (CFIndex i = 0; i < commandCount; i++) {
MIKMIDICommand *command = (__bridge MIKMIDICommand *)CFArrayGetValueAtIndex(commandsToSend, i);

MIDITimeStamp sendTimeStamp = command.midiTimestamp;
if (sendTimeStamp < inTimeStamp->mHostTime) sendTimeStamp = inTimeStamp->mHostTime;
MIDITimeStamp timeStampOffset = sendTimeStamp - inTimeStamp->mHostTime;
Float64 sampleOffset = secondsPerMIDITimeStamp * timeStampOffset * sampleRate;

OSStatus err = synth->_sendMIDICommand(synth, instrumentUnit, command.statusByte, command.dataByte1, command.dataByte2, sampleOffset);
if (err) {
NSLog(@"Unable to schedule MIDI command %@ for instrument unit %p: %@", command, instrumentUnit, @(err));
return err;
}
}

CFRelease(commandsToSend);
return noErr;
}

static OSStatus MIKMIDISynthesizerInstrumentUnitRenderCallback(void * inRefCon,
AudioUnitRenderActionFlags * ioActionFlags,
const AudioTimeStamp * inTimeStamp,
Expand All @@ -413,9 +485,6 @@ static OSStatus MIKMIDISynthesizerInstrumentUnitRenderCallback(void * inRef
if (!(inTimeStamp->mFlags & kAudioTimeStampSampleTimeValid)) return noErr;

MIKMIDISynthesizer *synth = (__bridge MIKMIDISynthesizer *)inRefCon;
dispatch_queue_t queue = synth->_scheduledCommandQueue;
if (!queue) return noErr; // no commands have been scheduled with this synth

AudioUnit instrumentUnit = synth.instrumentUnit;
AudioStreamBasicDescription LPCMASBD;
UInt32 sizeOfLPCMASBD = sizeof(LPCMASBD);
Expand All @@ -425,65 +494,7 @@ static OSStatus MIKMIDISynthesizerInstrumentUnitRenderCallback(void * inRef
return err;
}

static NSTimeInterval lastTimeUntilNextCallback = 0;
static MIDITimeStamp lastMIDITimeStampsUntilNextCallback = 0;
NSTimeInterval timeUntilNextCallback = inNumberFrames / LPCMASBD.mSampleRate;
MIDITimeStamp midiTimeStampsUntilNextCallback = lastMIDITimeStampsUntilNextCallback;

if (lastTimeUntilNextCallback != timeUntilNextCallback) {
midiTimeStampsUntilNextCallback = MIKMIDIClockMIDITimeStampsPerTimeInterval(timeUntilNextCallback);
lastTimeUntilNextCallback = timeUntilNextCallback;
lastMIDITimeStampsUntilNextCallback = midiTimeStampsUntilNextCallback;
}

MIDITimeStamp toTimeStamp = inTimeStamp->mHostTime + midiTimeStampsUntilNextCallback;
CFMutableArrayRef commandsToSend = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);;

dispatch_sync(queue, ^{
CFMutableDictionaryRef commandsByTimeStamp = synth->_scheduledCommandsByTimeStamp;
if (!commandsByTimeStamp || !CFDictionaryGetCount(commandsByTimeStamp)) return;


CFMutableSetRef commandTimeStampsSet = synth->_scheduledCommandTimeStampsSet;
CFMutableArrayRef commandTimeStampsArray = synth->_scheduledCommandTimeStampsArray;
if (!commandTimeStampsSet || !commandTimeStampsArray) return;

CFArrayRef commandTimeStampsArrayCopy = CFArrayCreateCopy(NULL, commandTimeStampsArray);
CFIndex count = CFArrayGetCount(commandTimeStampsArrayCopy);
for (CFIndex i = 0; i < count; i++) {
NSNumber *timeStampNumber = (__bridge NSNumber *)CFArrayGetValueAtIndex(commandTimeStampsArrayCopy, i);
MIDITimeStamp timeStamp = timeStampNumber.unsignedLongLongValue;
if (timeStamp >= toTimeStamp) break;

CFMutableArrayRef commandsAtTimeStamp = (CFMutableArrayRef)CFDictionaryGetValue(commandsByTimeStamp, (__bridge void*)timeStampNumber);
CFArrayAppendArray(commandsToSend, commandsAtTimeStamp, CFRangeMake(0, CFArrayGetCount(commandsAtTimeStamp)));

CFDictionaryRemoveValue(commandsByTimeStamp, (__bridge void *)timeStampNumber);
CFSetRemoveValue(commandTimeStampsSet, (__bridge void*)timeStampNumber);
CFArrayRemoveValueAtIndex(commandTimeStampsArray, 0);
}
CFRelease(commandTimeStampsArrayCopy);
});

NSTimeInterval secondsPerMIDITimeStamp = MIKMIDIClockSecondsPerMIDITimeStamp();

CFIndex commandCount = CFArrayGetCount(commandsToSend);
for (CFIndex i = 0; i < commandCount; i++) {
MIKMIDICommand *command = (__bridge MIKMIDICommand *)CFArrayGetValueAtIndex(commandsToSend, i);

MIDITimeStamp sendTimeStamp = command.midiTimestamp;
if (sendTimeStamp < inTimeStamp->mHostTime) sendTimeStamp = inTimeStamp->mHostTime;
MIDITimeStamp timeStampOffset = sendTimeStamp - inTimeStamp->mHostTime;
Float64 sampleOffset = secondsPerMIDITimeStamp * timeStampOffset * LPCMASBD.mSampleRate;

OSStatus err = MusicDeviceMIDIEvent(instrumentUnit, command.statusByte, command.dataByte1, command.dataByte2, sampleOffset);
if (err) {
NSLog(@"Unable to schedule MIDI command %@ for instrument unit %p: %@", command, instrumentUnit, @(err));
return err;
}
}

CFRelease(commandsToSend);
return MIKMIDISynthesizerScheduleUpcomingMIDICommands(synth, instrumentUnit, inNumberFrames, LPCMASBD.mSampleRate, inTimeStamp);
}
return noErr;
}
Expand All @@ -501,7 +512,7 @@ - (void)setGraph:(AUGraph)graph
- (void)handleMIDIMessages:(NSArray *)commands
{
for (MIKMIDICommand *command in commands) {
OSStatus err = MusicDeviceMIDIEvent(self.instrumentUnit, command.statusByte, command.dataByte1, command.dataByte2, 0);
OSStatus err = _sendMIDICommand(self, self.instrumentUnit, command.statusByte, command.dataByte1, command.dataByte2, 0);
if (err) NSLog(@"Unable to send MIDI command to synthesizer %@: %@", command, @(err));
}
}
Expand Down
9 changes: 8 additions & 1 deletion Source/MIKMIDISynthesizer_SubclassMethods.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,14 @@ NS_ASSUME_NONNULL_BEGIN
- (BOOL)sendBankSelectAndProgramChangeForInstrumentID:(MusicDeviceInstrumentID)instrumentID error:(NSError **)error;

@property (nonatomic, readwrite, nullable) AudioUnit instrumentUnit;
@property (nonatomic) OSStatus (^sendMIDICommand)(MIKMIDISynthesizer *synth, MusicDeviceComponent inUnit, UInt32 inStatus, UInt32 inData1, UInt32 inData2, UInt32 inOffsetSampleFrame);

@end

NS_ASSUME_NONNULL_END
FOUNDATION_EXPORT OSStatus MIKMIDISynthesizerScheduleUpcomingMIDICommands(MIKMIDISynthesizer *synth,
AudioUnit _Nullable instrumentUnit,
UInt32 inNumberFrames,
Float64 sampleRate,
const AudioTimeStamp *inTimeStamp);

NS_ASSUME_NONNULL_END

0 comments on commit 87b38ea

Please sign in to comment.