Messy MIDI File Work on OS X
I know, who in their right mind still deals with MIDI files?
I’d been contributing to a project (periodically) which does still use them, however. Previously on OS X, you’d just shove it into QTKit, and let it handle the whole thing. It worked well, though slightly poorly documented.
Well, in more recent versions of OS X things have changed a bit. (XCode 8, OS X 10.11, 10.12, at the very least. Not sure on 10.9.) QTKit is now gone from the latest OS X SDKs. Even a couple years ago it was sort of known that QTKit was going to be on its way out, but it worked, and wasn’t quite deprecated. They replaced it with AudioToolkit, and a slew of related items (AudioUnit, in particular).
If you’re just trying to play compressed music, things are great. Well documented on playing mp3, aac, etc. MIDI though is apparently its own beast. It sounded really great in the new system - fire up a MusicSequence, and tell it to play. It’s problematic though, as part of a larger program that wants to independently adjust the volume of the MIDI playback. I’d searched quite a bit before finally finding putting a bunch of pieces together.
It turns out that you need an AudioUnit Graph (AUGraph), and a few AudioUnit Nodes for it. A lot of tutorials exist on building the nodes from scratching, and connecting them up, but I found something that worked better for me. When you set up a MusicSequence, it automatically sets up an AUGraph. The AUGraph it creates is 3 nodes: MusicDevice -> Effect -> Output. I wasn’t interested in the Effect node, but I did want a Mixer node, since the Mixer node is able to control the volume. So, instead of creating a new graph and assigning it to the sequence, I figured I’d just remove the effect node, and insert a mixer node. Then I don’t have to recreate anything linking the sequence into the MusicDevice node, and I get everything I’m after.
So, here’s the code I used to do that. I do have a feeling that I could’ve by-passed the idea of loading the data from a file, but it was already there, so I left it alone.
#import <Foundation/Foundation.h>
#include <AudioToolbox/AudioToolbox.h>
#include <AudioUnit/AudioUnit.h>
#include "i_musicinterns.h"
#include "templates.h"
EXTERN_CVAR (Float, snd_mastervolume)
EXTERN_CVAR (Float, snd_midivolume)
#define DEBUGMIDI 0
#define FailIf(inCondition, inHandler, inMessage) \
if(inCondition) { \
DPrintf("OSX MIDI: %s\n", inMessage); \
goto inHandler; \
}
MacSong::MacSong(FILE *file, int length)
: tempMidiFilename("ztemp", ".mid"), theGraph(0)
{
bool success;
FILE *tempFile;
AudioComponentDescription desc = {};
AudioUnit unit;
AUNode node;
OSStatus status;
UInt32 nodecount = -1;
Boolean running = false;
UInt32 busCount = 2;
tempFile = fopen((const char *)tempMidiFilename, "wb");
if(NULL == tempFile) {
Printf("Could not open temp music file %s\n", (const char *)tempMidiFilename);
return;
}
BYTE *buff = new BYTE[length];
fread(buff, 1, length, file);
// Earlier handling has determined if this is a MUS or MIDI.
// check for it being a MUS, and thus convert.
if('U' == buff[1]) {
success = ProduceMIDI(buff, tempFile);
} else {
success = (fwrite(buff, 1, length, tempFile) == (size_t)length);
}
fclose(tempFile);
if(NULL != file) {
delete[] buff;
}
if(!success) {
Printf("Could not create temp music file %s\n", (const char *)tempMidiFilename);
}
pool = [[NSAutoreleasePool alloc] init];
NSString *filename = [NSString stringWithUTF8String:(const char *)tempMidiFilename];
NSURL *fileURL = [NSURL fileURLWithPath:filename];
DPrintf("Debug MIDI filename: %s\n", (const char *)tempMidiFilename);
// Initialize MIDI sequence.
NewMusicSequence(&sequence);
if(MusicSequenceFileLoad(sequence, (CFURLRef)fileURL, 0, 0 != noErr)) {
[NSException raise:@"play" format:@"Can't load MusicSequence"];
}
FailIf((status = MusicSequenceGetAUGraph(sequence, &theGraph)), fail, "MusicSequenceGetAUGraph");
#ifdef DEBUGMIDI
DPrintf("DEBUG: kAudioUnitType_Output %ud\n", (unsigned int) kAudioUnitType_Output);
DPrintf("DEBUG: kAudioUnitType_Mixer %ud\n", (unsigned int) kAudioUnitType_Mixer);
DPrintf("DEBUG: kAudioUnitType_Effect %ud\n", (unsigned int) kAudioUnitType_Effect);
DPrintf("DEBUG: kAudioUnitType_Panner %ud\n", (unsigned int) kAudioUnitType_Panner);
DPrintf("DEBUG: kAudioUnitType_Generator %ud\n", (unsigned int) kAudioUnitType_Generator);
DPrintf("DEBUG: kAudioUnitType_MusicDevice %ud\n", (unsigned int) kAudioUnitType_MusicDevice);
DPrintf("DEBUG: kAudioUnitType_MusicEffect %ud\n", (unsigned int) kAudioUnitType_MusicEffect);
DPrintf("DEBUG: kAudioUnitType_MIDIProcessor %ud\n", (unsigned int) kAudioUnitType_MIDIProcessor);
DPrintf("DEBUG: kAudioUnitType_OfflineEffect %ud\n", (unsigned int) kAudioUnitType_OfflineEffect);
DPrintf("DEBUG: kAudioUnitType_FormatConverter %ud\n", (unsigned int) kAudioUnitType_FormatConverter);
#endif
// Startup the MIDI player (just pre-roll, don't play yet).
NewMusicPlayer(&musicPlayer);
MusicPlayerSetSequence(musicPlayer, sequence);
MusicPlayerPreroll(musicPlayer);
// Grab the AUGraph from the sequence.
// Need to modify this for volume control.
FailIf((status = AUGraphOpen(theGraph)), fail, "AUGraphOpen");
// remove effects node from augraph, it's unnecessary for us.
FailIf((status = AUGraphGetNodeCount(theGraph, &nodecount)), fail, "AUGraphGetNodeCount (remove)");
for(int ix = 0; ix < (int)nodecount; ++ix) {
FailIf((status = AUGraphGetIndNode(theGraph, ix, &node)), fail, "AUGraphGetIndNode (remove)");
FailIf((status = AUGraphNodeInfo(theGraph, node, &desc, &unit)), fail, "AUGraphNodeInfo (remove)");
//DPrintf("DEBUG: Node %d: %u\n", ix, (unsigned int)desc.componentType);
if(desc.componentType == kAudioUnitType_Effect) {
// connect mixer to output.
FailIf((status = AUGraphRemoveNode(theGraph, node)), fail, "AUGraphRemoveNode: (effect)");
}
}
// create mixer node. (Used for volume control)
desc.componentManufacturer = kAudioUnitManufacturer_Apple;
desc.componentType = kAudioUnitType_Mixer;
desc.componentSubType = kAudioUnitSubType_MultiChannelMixer;
FailIf((status = AUGraphAddNode(theGraph, &desc, &mixerNode)), fail, "AUGraphAddNode: mixer node");
FailIf((status = AUGraphNodeInfo (theGraph, mixerNode, 0, &_mixerUnit)), fail, "AUGraphNodeInfo (mixernode)");
FailIf((status = AudioUnitSetProperty(_mixerUnit, kAudioUnitProperty_ElementCount,
kAudioUnitScope_Input, 0, &busCount, sizeof(busCount))), fail, "AudioUnitSetProperty (mixernode)");
// Connect mixer in between sequencer (MusicDevice) & output.
FailIf((status = AUGraphGetNodeCount(theGraph, &nodecount)), fail, "AUGraphGetNodeCount (connect1)");
DPrintf("OS X Music: %ld nodes in AUGraph\n", nodecount);
for(int ix = 0; ix < (int)nodecount; ++ix) {
FailIf((status = AUGraphGetIndNode(theGraph, ix, &node)), fail, "AUGraphGetIndNode");
FailIf((status = AUGraphNodeInfo(theGraph, node, &desc, &unit)), fail, "AUGraphNodeInfo");
//DPrintf("DEBUG: Node %d: %u\n", ix, (unsigned int)desc.componentType);
if(desc.componentType == kAudioUnitType_MusicDevice) {
// connect mixer to output.
FailIf((status = AUGraphConnectNodeInput(theGraph, node, 0, mixerNode, 0)), fail, "AUGraphConnectNodeInput: musicdevice->mixerNode");
}
if(desc.componentType == kAudioUnitType_Output) {
// connect mixer to output.
FailIf((status = AUGraphConnectNodeInput(theGraph, mixerNode, 0, node, 0)), fail, "AUGraphConnectNodeInput: mixerNode->output");
}
}
#ifdef DEBUGMIDI
FailIf((status = AUGraphIsRunning(theGraph, &running)), fail, "AUGraphIsRunning");
DPrintf("OS X Music: AUGraph is %s\n", (running ? "running" : "not running"));
#endif
FailIf((status = AUGraphStart(theGraph)), fail, "AUGraphStart");
FailIf((status = AUGraphIsRunning(theGraph, &running)), fail, "AUGraphIsRunning");
DPrintf("OS X Music: AUGraph is %s\n", (running ? "running" : "not running"));
m_Status = STATE_Stopped;
set_initial_volume();
return;
fail:
Printf("OS X Music: Unable to initialize MIDI playback: %ld\n", status);
}
MacSong::~MacSong( )
{
MusicPlayerStop(musicPlayer);
DisposeMusicSequence(sequence);
DisposeMusicPlayer(musicPlayer);
AUGraphStop(theGraph);
AUGraphUninitialize(theGraph);
AUGraphClose(theGraph);
unlink((const char *)tempMidiFilename);
}
void MacSong::set_initial_volume()
{
// since we have no other way to set the initial volume,
// let's just load in the cvars and do it ourselves.
float vol = snd_midivolume * snd_mastervolume;
float realvolume = clamp<float>(vol, 0.f, 1.f);
SetVolume(realvolume);
}
void MacSong::SetVolume(float volume)
{
AudioUnitSetParameter(_mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Input, 0, volume, 0);
AudioUnitSetParameter(_mixerUnit, kMultiChannelMixerParam_Volume, kAudioUnitScope_Output, 0, volume, 0);
}
void MacSong::Play(bool looping)
{
m_Looping = looping;
if(m_Looping) {
MusicTrackLoopInfo loopInfo;
MusicTimeStamp trackLen;
MusicTrack track;
UInt32 numTracks, trackLenLen;
MusicSequenceGetTrackCount(sequence, &numTracks);
for(int ix = 0; ix < (int)numTracks; ++ix) {
MusicSequenceGetIndTrack(sequence, ix, &track);
MusicTrackGetProperty(track, kSequenceTrackProperty_TrackLength, &trackLen, &trackLenLen);
loopInfo.loopDuration = trackLen;
loopInfo.numberOfLoops = 0;
MusicTrackSetProperty(track, kSequenceTrackProperty_LoopInfo, &loopInfo, sizeof(kSequenceTrackProperty_LoopInfo));
}
}
MusicPlayerStart(musicPlayer);
m_Status = STATE_Playing;
}
void MacSong::Pause()
{
if (m_Status == STATE_Playing)
{
m_Status = STATE_Paused;
MusicPlayerStop(musicPlayer);
}
}
void MacSong::Resume()
{
if (m_Status == STATE_Paused)
{
m_Status = STATE_Playing;
MusicPlayerStart(musicPlayer);
}
}
void MacSong::Stop()
{
if (m_Status != STATE_Stopped)
{
m_Status = STATE_Stopped;
MusicPlayerStop(musicPlayer);
}
}
I know there’s a few functions, variables, etc, defined elsewhere in the project, but they’re not terribly important to this topic. One’s I’ve noticed:
- Printf
- DPrintf
- ProduceMIDI (this converts the buffer to a MIDI if necessary)
- snd_midivolume & snd_mastervolume
- The type of tempFileName is unusual. Tt’s basically a class that will generate a temporary file to the correct system tempfile location.
- STATE_* are just an enum.
I did grab & tweak the FailIf macro from a sample project from Apple. It was really convenient way to test & fail (and not try further operations) if something failed along the way. Otherwise, there’d be a dozen if statements in there just checking for a failure result, outputting where they were at when it failed, and returning.