Skip to content

Commit

Permalink
Fix MIDI output for OSX.
Browse files Browse the repository at this point in the history
This incorporates the changes that were previously in our fork of RtMidi (https://github.com/powertab/rtmidi/commit/a98913b9416659bdecfaaf97f09893896d707729)

Now, we set up a MIDI endpoint that can be used with RtMidi / CoreMIDI, which then forwards the MIDI events to the software synth. This enables MIDI playback when the user doesn't have any other MIDI devices available.

Fixes: #285
  • Loading branch information
cameronwhite committed May 22, 2020
1 parent 0e75414 commit 56efa86
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 19 deletions.
6 changes: 1 addition & 5 deletions cmake/third_party/rtmidi.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,13 @@ elseif ( PLATFORM_OSX )
find_library( coreaudio_lib CoreAudio )
find_library( coremidi_lib CoreMIDI )
find_library( corefoundation_lib CoreFoundation )
find_library( audiotoolbox_lib AudioToolbox )
find_library( audiounit_lib AudioUnit )

set( _midi_libs
${coreaudio_lib}
${coremidi_lib}
${corefoundation_lib}
${audiotoolbox_lib}
${audiounit_lib}
)
set( _midi_defs __MACOSX_AU__ __MACOSX_CORE__ )
set( _midi_defs __MACOSX_CORE__ )
elseif ( PLATFORM_LINUX )
find_package( ALSA REQUIRED )

Expand Down
26 changes: 23 additions & 3 deletions source/audio/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,33 @@ set( moc_headers
midiplayer.h
)

set( platform_deps )
if ( PLATFORM_OSX )
find_library( audiotoolbox_lib AudioToolbox REQUIRED )
find_library( audiounit_lib AudioUnit REQUIRED )
set( platform_deps
${audiotoolbox_lib}
${audiounit_lib}
)

list( APPEND srcs
midisoftwaresynth.cpp
)
list( APPEND headers
midisoftwaresynth.h
)
endif ()

pte_library(
NAME pteaudio
SOURCES ${srcs}
HEADERS ${headers}
MOC_HEADERS ${moc_headers}
DEPENDS
ptescore
Qt5::Core
rtmidi::rtmidi
PUBLIC
ptescore
Qt5::Core
PRIVATE
rtmidi::rtmidi
${platform_deps}
)
39 changes: 28 additions & 11 deletions source/audio/midioutputdevice.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,30 @@
#include "midioutputdevice.h"

#include <RtMidi.h>
#include <exception>
#include <score/dynamic.h>
#include <score/generalmidi.h>
#include <cassert>

#ifdef __APPLE__
#include "midisoftwaresynth.h"
#endif

MidiOutputDevice::MidiOutputDevice() : myMidiOut(nullptr)
{
// Initialize the OSX software synth.
#ifdef __APPLE__
try
{
static MidiSoftwareSynth synth;
synth.initialize();
}
catch (std::exception &e)
{
std::cerr << e.what() << std::endl;
};
#endif

myMaxVolumes.fill(Midi::MAX_MIDI_CHANNEL_VOLUME);
myActiveVolumes.fill(Dynamic::fff);

Expand All @@ -37,12 +55,10 @@ MidiOutputDevice::MidiOutputDevice() : myMidiOut(nullptr)
{
myMidiOuts.emplace_back(new RtMidiOut(api));
}
catch (...)
catch (RtMidiError &e)
{
// continue anyway, another api might work
// found on mac that the Core API kept failing after repeated
// creations and the exceptions weren't caught
// TODO investigate why.
// Continue anyway, another API might work.
e.printMessage();
}
}
}
Expand All @@ -54,8 +70,7 @@ MidiOutputDevice::~MidiOutputDevice()
void
MidiOutputDevice::sendMessage(const std::vector<uint8_t> &data)
{
// FIXME - fix const correctness in RtMidi api.
myMidiOut->sendMessage(const_cast<std::vector<uint8_t> *>(&data));
myMidiOut->sendMessage(&data);
}

bool MidiOutputDevice::sendMidiMessage(unsigned char a, unsigned char b,
Expand All @@ -75,9 +90,10 @@ bool MidiOutputDevice::sendMidiMessage(unsigned char a, unsigned char b,
{
myMidiOut->sendMessage(&message);
}
catch (...)
catch (RtMidiError &e)
{
return false;
e.printMessage();
return false;
}

return true;
Expand All @@ -102,9 +118,10 @@ bool MidiOutputDevice::initialize(size_t preferredApi,
{
myMidiOut->openPort(preferredPort);
}
catch (...)
catch (RtMidiError &e)
{
return false;
e.printMessage();
return false;
}

return true;
Expand Down
172 changes: 172 additions & 0 deletions source/audio/midisoftwaresynth.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
/*
* Copyright (C) 2020 Cameron White
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#include "midisoftwaresynth.h"

#include <MacTypes.h>

#include <memory>
#include <type_traits>

/// RAII wrapper for CFStringRef.
struct CFStringDeleter
{
void operator()(CFStringRef p)
{
if (p)
CFRelease(p);
}
};

/// RAII wrapper for CFStringRef.
using CFStringHandle =
std::unique_ptr<std::remove_pointer<CFStringRef>::type, CFStringDeleter>;

MidiSoftwareSynth::~MidiSoftwareSynth()
{
if (myGraph)
DisposeAUGraph(*myGraph);

if (myEndpoint)
MIDIEndpointDispose(*myEndpoint);
if (myClient)
MIDIClientDispose(*myClient);
}

void MidiSoftwareSynth::initialize()
{
if (myClient)
return;

// Create the MIDI client.
{
MIDIClientRef client;
CFStringHandle name(CFStringCreateWithCString(
nullptr, "Power Tab Editor", kCFStringEncodingASCII));
OSStatus result =
MIDIClientCreate(name.get(), nullptr, nullptr, &client);
if (result != noErr)
throw std::runtime_error("Failed to create MIDI client");

myClient = client;
}

// Create the MIDI endpoint.
{
MIDIEndpointRef endpoint;
CFStringHandle name(CFStringCreateWithCString(
nullptr, "Power Tab Software Synth", kCFStringEncodingASCII));
OSStatus result = MIDIDestinationCreate(*myClient, name.get(),
&readProc, this, &endpoint);
if (result != noErr)
throw std::runtime_error("Failed to create MIDI client");

myEndpoint = endpoint;
}

// Set up AudioUnit synth.
{
AUGraph graph;
if (NewAUGraph(&graph) != noErr)
throw std::runtime_error("Failed to create audio graph.");

myGraph = graph;
}

{
AudioComponentDescription cd;
cd.componentManufacturer = kAudioUnitManufacturer_Apple;
cd.componentFlags = 0;
cd.componentFlagsMask = 0;

// Create the AU synthesizer (make audio from midi). Owned by the
// graph.
AUNode synthNode;
cd.componentType = kAudioUnitType_MusicDevice;
cd.componentSubType = kAudioUnitSubType_DLSSynth;

if (AUGraphAddNode(*myGraph, &cd, &synthNode) != noErr)
throw std::runtime_error("Failed to create synth node.");

// Create the Peak Limiter (prevents erm peaks!)
AUNode limiterNode;
cd.componentType = kAudioUnitType_Effect;
cd.componentSubType = kAudioUnitSubType_PeakLimiter;

if (AUGraphAddNode(*myGraph, &cd, &limiterNode) != noErr)
throw std::runtime_error("Failed to create limiter node.");

// Audio output node (e.g. speakers).
AUNode outNode;
cd.componentType = kAudioUnitType_Output;
cd.componentSubType = kAudioUnitSubType_DefaultOutput;

if (AUGraphAddNode(*myGraph, &cd, &outNode) != noErr)
throw std::runtime_error("Failed to create out node.");

// Initialize and connect the audio graph.
if (AUGraphOpen(*myGraph) != noErr)
{
throw std::runtime_error("Failed to open graph.");
}
else if (AUGraphConnectNodeInput(*myGraph, synthNode, 0, limiterNode,
0) != noErr)
{
throw std::runtime_error("Failed to connect synth to limiter.");
}
else if (AUGraphConnectNodeInput(*myGraph, limiterNode, 0, outNode,
0) != noErr)
{
throw std::runtime_error("Failed to connect limiter to output.");
}
else if (AUGraphInitialize(*myGraph) != noErr)
{
throw std::runtime_error("Failed to initialize graph.");
}

if (AUGraphNodeInfo(*myGraph, synthNode, 0, &mySynthesizer) != noErr)
throw std::runtime_error("Failed to cache synthesizer.");

if (AUGraphStart(*myGraph) != noErr)
throw std::runtime_error("Failed to start synthesizer.");
}
}

void MidiSoftwareSynth::readProc(const MIDIPacketList *packets,
void *readProcRefCon, void *)
{
auto me = static_cast<const MidiSoftwareSynth *>(readProcRefCon);

const MIDIPacket *packet = &packets->packet[0];
for (UInt32 i = 0, n = packets->numPackets; i < n; ++i)
{
// Forward the data to AudioUnit. We don't expect to have any long
// sysex messages etc.
UInt32 statusByte = packet->data[0];
UInt32 dataByte1 = packet->length > 1 ? packet->data[1] : 0;
UInt32 dataByte2 = packet->length > 2 ? packet->data[2] : 0;
UInt32 offsetSampleFrame = 0;

if (MusicDeviceMIDIEvent(me->mySynthesizer, statusByte, dataByte1,
dataByte2, offsetSampleFrame) != noErr)
{
throw std::runtime_error("Failed to send message to synthesizer.");
}

packet = MIDIPacketNext(packet);
}
}
49 changes: 49 additions & 0 deletions source/audio/midisoftwaresynth.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright (C) 2020 Cameron White
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

#ifndef AUDIO_MIDISOFTWARESYNTH_H
#define AUDIO_MIDISOFTWARESYNTH_H

#include <AudioToolbox/AUGraph.h>
#include <AudioToolbox/AudioUnit.h>
#include <CoreMIDI/MIDIServices.h>

#include <optional>

/// AudioUnit-based software synth. This is registered as a MIDI destination so
/// that we can use it via RtMidi if the user doesn't have any other MIDI
/// outputs.
class MidiSoftwareSynth
{
public:
~MidiSoftwareSynth();

void initialize();

private:
/// Callback invoked when MIDI packets arrive.
static void readProc(const MIDIPacketList *packets, void *readProcRefCon,
void *);

std::optional<MIDIClientRef> myClient;
std::optional<MIDIEndpointRef> myEndpoint;

std::optional<AUGraph> myGraph;
AudioUnit mySynthesizer = nullptr;
};

#endif

0 comments on commit 56efa86

Please sign in to comment.