From 56efa86281089c1e30deb2b8b029e1902aaf06e7 Mon Sep 17 00:00:00 2001 From: Cameron White Date: Fri, 22 May 2020 07:42:42 -0400 Subject: [PATCH] Fix MIDI output for OSX. 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 --- cmake/third_party/rtmidi.cmake | 6 +- source/audio/CMakeLists.txt | 26 ++++- source/audio/midioutputdevice.cpp | 39 +++++-- source/audio/midisoftwaresynth.cpp | 172 +++++++++++++++++++++++++++++ source/audio/midisoftwaresynth.h | 49 ++++++++ 5 files changed, 273 insertions(+), 19 deletions(-) create mode 100644 source/audio/midisoftwaresynth.cpp create mode 100644 source/audio/midisoftwaresynth.h diff --git a/cmake/third_party/rtmidi.cmake b/cmake/third_party/rtmidi.cmake index 8d199298a..f8af72022 100644 --- a/cmake/third_party/rtmidi.cmake +++ b/cmake/third_party/rtmidi.cmake @@ -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 ) diff --git a/source/audio/CMakeLists.txt b/source/audio/CMakeLists.txt index e60df4d83..4e5f88486 100644 --- a/source/audio/CMakeLists.txt +++ b/source/audio/CMakeLists.txt @@ -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} ) diff --git a/source/audio/midioutputdevice.cpp b/source/audio/midioutputdevice.cpp index 556c99d8c..360222fce 100644 --- a/source/audio/midioutputdevice.cpp +++ b/source/audio/midioutputdevice.cpp @@ -18,12 +18,30 @@ #include "midioutputdevice.h" #include +#include #include #include #include +#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); @@ -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(); } } } @@ -54,8 +70,7 @@ MidiOutputDevice::~MidiOutputDevice() void MidiOutputDevice::sendMessage(const std::vector &data) { - // FIXME - fix const correctness in RtMidi api. - myMidiOut->sendMessage(const_cast *>(&data)); + myMidiOut->sendMessage(&data); } bool MidiOutputDevice::sendMidiMessage(unsigned char a, unsigned char b, @@ -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; @@ -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; diff --git a/source/audio/midisoftwaresynth.cpp b/source/audio/midisoftwaresynth.cpp new file mode 100644 index 000000000..ad9d916d2 --- /dev/null +++ b/source/audio/midisoftwaresynth.cpp @@ -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 . +*/ + +#include "midisoftwaresynth.h" + +#include + +#include +#include + +/// RAII wrapper for CFStringRef. +struct CFStringDeleter +{ + void operator()(CFStringRef p) + { + if (p) + CFRelease(p); + } +}; + +/// RAII wrapper for CFStringRef. +using CFStringHandle = + std::unique_ptr::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(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); + } +} diff --git a/source/audio/midisoftwaresynth.h b/source/audio/midisoftwaresynth.h new file mode 100644 index 000000000..d518c419b --- /dev/null +++ b/source/audio/midisoftwaresynth.h @@ -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 . +*/ + +#ifndef AUDIO_MIDISOFTWARESYNTH_H +#define AUDIO_MIDISOFTWARESYNTH_H + +#include +#include +#include + +#include + +/// 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 myClient; + std::optional myEndpoint; + + std::optional myGraph; + AudioUnit mySynthesizer = nullptr; +}; + +#endif