Skip to content

Commit

Permalink
fix #5163 Add PortAudio Midi Output
Browse files Browse the repository at this point in the history
  • Loading branch information
Eric Fontaine committed May 26, 2017
1 parent f646c18 commit 4e46930
Show file tree
Hide file tree
Showing 17 changed files with 454 additions and 124 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Expand Up @@ -32,6 +32,7 @@ matrix:
- curl
- libasound2-dev
- portaudio19-dev
- libportmidi-dev
- libsndfile1-dev
- zlib1g-dev
- libfreetype6-dev
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Expand Up @@ -82,7 +82,7 @@ endif (APPLE)
# Disable components not supported on Linux/BSD
if (NOT APPLE AND NOT MINGW)
set(NIX_NOT_AVAIL "Not available on Linux/BSD")
option(BUILD_PORTMIDI "PortMidi disabled on Linux. (It uses ALSA but it's better to use ALSA directly)" OFF)
#option(BUILD_PORTMIDI "PortMidi disabled on Linux. (It uses ALSA but it's better to use ALSA directly)" OFF)
endif (NOT APPLE AND NOT MINGW)

option(AEOLUS "Enable pipe organ synthesizer" OFF)
Expand Down
1 change: 1 addition & 0 deletions build/Linux+BSD/portable/RecipeDebian
Expand Up @@ -52,6 +52,7 @@ fetch-build-dependencies() {
libsndfile1-dev:$ARCH \
libasound2-dev:$ARCH \
portaudio19-dev:$ARCH \
libportmidi-dev:$ARCH \
zlib1g-dev:$ARCH \
libfreetype6-dev:$ARCH \
libmp3lame-dev:$ARCH \
Expand Down
1 change: 1 addition & 0 deletions build/Linux+BSD/portable/copy-libs
Expand Up @@ -50,6 +50,7 @@ getCrossPlatformDependencies() {
#copyLib libvorbis.so # Assume provided by system
#copyLib libvorbisfile.so # Assume provided by system
copyLib libportaudio.so.2
copyLib libportmidi.so
#copyLib libeay32.so # Assume provided by system
#copyLib ssleay32.so # Assume provided by system

Expand Down
1 change: 1 addition & 0 deletions build/Linux+BSD/portable/x86_32/Recipe
Expand Up @@ -48,6 +48,7 @@ yum -y install \
libxslt-devel \
mesa-libGL-devel \
portaudio-devel \
portmidi-devel \
pulseaudio-libs-devel

# get Qt
Expand Down
1 change: 1 addition & 0 deletions build/Linux+BSD/portable/x86_64/Recipe
Expand Up @@ -42,6 +42,7 @@ apt-get -y install \
alsa \
libasound2-dev \
portaudio19-dev \
libportmidi-dev \
libsndfile1-dev \
zlib1g-dev \
libfreetype6-dev \
Expand Down
11 changes: 7 additions & 4 deletions fluid/fluid.cpp
Expand Up @@ -191,9 +191,11 @@ void Fluid::play(const PlayEvent& event)
int midiPitch = event.dataB() * 128 + event.dataA(); // msb * 128 + lsb
cp->pitchBend(midiPitch);
}
if (err)
qWarning("FluidSynth error: event 0x%2x channel %d: %s",
type, ch, qPrintable(error()));
if (err) {
// TODO: distinguish between types of error code.
// Lack of a soundfont should not produce qDebug messages, because user could deliberately be using MIDI out only.
//qWarning("FluidSynth error: event 0x%2x channel %d: %s", type, ch, qPrintable(error()));
}
}

//---------------------------------------------------------
Expand Down Expand Up @@ -335,7 +337,8 @@ void Fluid::program_change(int chan, int prognum)

Preset* preset = find_preset(banknum, prognum);
if (!preset) {
qDebug("Fluid::program_change: preset %d %d not found", banknum, prognum);
//Suppressing qDebug because might not have soundfont if using MIDI out only.
//qDebug("Fluid::program_change: preset %d %d not found", banknum, prognum);
preset = find_preset(0, 0);
}

Expand Down
1 change: 1 addition & 0 deletions mscore/mididriver.h
Expand Up @@ -59,6 +59,7 @@ class Port {
bool operator<(const Port& p) const;
friend class MidiDriver;
friend class AlsaMidiDriver;
friend class PortMidiDriver;
};

//---------------------------------------------------------
Expand Down
82 changes: 81 additions & 1 deletion mscore/pa.cpp
Expand Up @@ -47,6 +47,7 @@ static PaStream* stream;
int paCallback(const void*, void* out, long unsigned frames,
const PaStreamCallbackTimeInfo*, PaStreamCallbackFlags, void *)
{
seq->setInitialMillisecondTimestampWithLatency();
seq->process((unsigned)frames, (float*)out);
return 0;
}
Expand Down Expand Up @@ -96,8 +97,10 @@ bool Portaudio::init(bool)
qDebug("using PortAudio Version: %s", Pa_GetVersionText());

PaDeviceIndex idx = preferences.portaudioDevice;
if (idx < 0)
if (idx < 0) {
idx = Pa_GetDefaultOutputDevice();
qDebug("No device selected. PortAudio detected %d devices. Will use the default device (index %d).", Pa_GetDeviceCount(), idx);
}

const PaDeviceInfo* di = Pa_GetDeviceInfo(idx);

Expand Down Expand Up @@ -273,6 +276,83 @@ void Portaudio::midiRead()
midiDriver->read();
}

//---------------------------------------------------------
// putEvent
//---------------------------------------------------------

// Prevent killing sequencer with wrong data
#define less128(__less) ((__less >=0 && __less <= 127) ? __less : 0)

// TODO: this was copied from Jack version...I'd like to eventually unify these two, so that they handle midi event types in the same manner
void Portaudio::putEvent(const NPlayEvent& e, unsigned framePos)
{
PortMidiDriver* portMidiDriver = static_cast<PortMidiDriver*>(midiDriver);
if (!portMidiDriver || !portMidiDriver->getOutputStream() || !portMidiDriver->canOutput())
return;

int portIdx = seq->score()->midiPort(e.channel());
int chan = seq->score()->midiChannel(e.channel());

if (portIdx < 0 ) {
qDebug("Portaudio::putEvent: invalid port %d", portIdx);
return;
}

if (midiOutputTrace) {
int a = e.dataA();
int b = e.dataB();
qDebug("MidiOut<%d>: Portaudio: %02x %02x %02x, chan: %i", portIdx, e.type(), a, b, chan);
}

switch(e.type()) {
case ME_NOTEON:
case ME_NOTEOFF:
case ME_POLYAFTER:
case ME_CONTROLLER:
// Catch CTRL_PROGRAM and let other ME_CONTROLLER events to go
if (e.dataA() == CTRL_PROGRAM) {
// Convert CTRL_PROGRAM event to ME_PROGRAM
long msg = Pm_Message(ME_PROGRAM | chan, less128(e.dataB()), 0);
PmError error = Pm_WriteShort(portMidiDriver->getOutputStream(), seq->getCurrentMillisecondTimestampWithLatency(framePos), msg);
if (error != pmNoError) {
qDebug("Portaudio: error %d", error);
return;
}
break;
}
//Fallback
case ME_PITCHBEND:
{
long msg = Pm_Message(e.type() | chan, less128(e.dataA()), less128(e.dataB()));
PmError error = Pm_WriteShort(portMidiDriver->getOutputStream(), seq->getCurrentMillisecondTimestampWithLatency(framePos), msg);
if (error != pmNoError) {
qDebug("Portaudio: error %d", error);
return;
}
}
break;

case ME_PROGRAM:
case ME_AFTERTOUCH:
{
long msg = Pm_Message(e.type() | chan, less128(e.dataA()), 0);
PmError error = Pm_WriteShort(portMidiDriver->getOutputStream(), seq->getCurrentMillisecondTimestampWithLatency(framePos), msg);
if (error != pmNoError) {
qDebug("Portaudio: error %d", error);
return;
}
}
break;
case ME_SONGPOS:
case ME_CLOCK:
case ME_START:
case ME_CONTINUE:
case ME_STOP:
qDebug("Portaudio: event type %x not supported", e.type());
break;
}
}

//---------------------------------------------------------
// currentApi
//---------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion mscore/pa.h
Expand Up @@ -28,6 +28,7 @@ namespace Ms {
class Synth;
class Seq;
class MidiDriver;
class NPlayEvent;
enum class Transport : char;

//---------------------------------------------------------
Expand Down Expand Up @@ -56,6 +57,7 @@ class Portaudio : public Driver {
virtual Transport getState() override;
virtual int sampleRate() const { return _sampleRate; }
virtual void midiRead();
virtual void putEvent(const NPlayEvent&, unsigned framePos);

int framePos() const;
float* getLBuffer(long n);
Expand All @@ -67,7 +69,7 @@ class Portaudio : public Driver {
int deviceIndex(int apiIdx, int apiDevIdx);
int currentApi() const;
int currentDevice() const;
MidiDriver* mididriver() {return midiDriver;}
MidiDriver* mididriver() { return midiDriver; }
};


Expand Down
98 changes: 88 additions & 10 deletions mscore/pm.cpp
Expand Up @@ -43,8 +43,11 @@ namespace Ms {
PortMidiDriver::PortMidiDriver(Seq* s)
: MidiDriver(s)
{
inputStream = 0;
inputId = -1;
outputId = -1;
timer = 0;
inputStream = 0;
outputStream = 0;
}

PortMidiDriver::~PortMidiDriver()
Expand All @@ -63,22 +66,23 @@ PortMidiDriver::~PortMidiDriver()
bool PortMidiDriver::init()
{
inputId = getDeviceIn(preferences.portMidiInput);
if(inputId == -1)
inputId = Pm_GetDefaultInputDeviceID();
outputId = Pm_GetDefaultOutputDeviceID();
if (inputId == -1)
inputId = Pm_GetDefaultInputDeviceID();

if (inputId == pmNoDevice)
return false;

static const int INPUT_BUFFER_SIZE = 100;
outputId = getDeviceOut(preferences.portMidiOutput); // Note: allow init even if outputId == pmNoDevice, since input is more important than output.

static const int DRIVER_INFO = 0;
static const int TIME_INFO = 0;

Pt_Start(20, 0, 0); // timer started, 20 millisecond accuracy

PmError error = Pm_OpenInput(&inputStream,
inputId,
(void*)DRIVER_INFO, INPUT_BUFFER_SIZE,
(void*)DRIVER_INFO,
preferences.portMidiInputBufferCount,
((PmTimeProcPtr) Pt_Time),
(void*)TIME_INFO);
if (error != pmNoError) {
Expand All @@ -94,6 +98,22 @@ bool PortMidiDriver::init()
while (Pm_Poll(inputStream))
Pm_Read(inputStream, buffer, 1);

if (outputId != pmNoDevice) {
error = Pm_OpenOutput(&outputStream,
outputId,
(void*)DRIVER_INFO,
preferences.portMidiOutputBufferCount,
((PmTimeProcPtr) Pt_Time),
(void*)TIME_INFO,
preferences.portMidiOutputLatencyMilliseconds);
if (error != pmNoError) {
const char* p = Pm_GetErrorText(error);
qDebug("PortMidi: open output (id=%d) failed: %s", int(outputId), p);
Pt_Stop();
return false;
}
}

timer = new QTimer();
timer->setInterval(20); // 20 msec
timer->start();
Expand Down Expand Up @@ -190,27 +210,85 @@ QStringList PortMidiDriver::deviceInList() const
for (PmDeviceID id = 0; id < interf; id++) {
const PmDeviceInfo* info = Pm_GetDeviceInfo((PmDeviceID)id);
if(info->input)
il.append(QString(info->interf) +","+ QString(info->name));
il.append(QString(info->interf) + "," + QString(info->name));
}
return il;
}

//---------------------------------------------------------
// deviceOutList
//---------------------------------------------------------

QStringList PortMidiDriver::deviceOutList() const
{
QStringList ol;
int interf = Pm_CountDevices();
for (PmDeviceID id = 0; id < interf; id++) {
const PmDeviceInfo* info = Pm_GetDeviceInfo((PmDeviceID)id);
if(info->output)
ol.append(QString(info->interf) + "," + QString(info->name));
}
return ol;
}

//---------------------------------------------------------
// getDeviceIn
//---------------------------------------------------------

int PortMidiDriver::getDeviceIn(const QString& name)
int PortMidiDriver::getDeviceIn(const QString& interfaceAndName)
{
int interf = Pm_CountDevices();
for (int id = 0; id < interf; id++) {
const PmDeviceInfo* info = Pm_GetDeviceInfo((PmDeviceID)id);
if (info->input) {
QString n = QString(info->interf) + "," + QString(info->name);
if (n == name)
if (QString(info->interf) + "," + QString(info->name) == interfaceAndName)
return id;
}
}
return -1;
}

//---------------------------------------------------------
// getDeviceOut
//---------------------------------------------------------

int PortMidiDriver::getDeviceOut(const QString& interfaceAndName)
{
int interf = Pm_CountDevices();
for (int id = 0; id < interf; id++) {
const PmDeviceInfo* info = Pm_GetDeviceInfo((PmDeviceID)id);
if (info->output) {
if (QString(info->interf) + "," + QString(info->name) == interfaceAndName)
return id;
}
}
return -1;
}

//---------------------------------------------------------
// isSameCoreMidiIacBus
// determines if both the input and output devices are the same shared CoreMIDI "IAC" bus
//---------------------------------------------------------

bool PortMidiDriver::isSameCoreMidiIacBus(const QString& inInterfaceAndName, const QString& outInterfaceAndName)
{
int interf = Pm_CountDevices();
const PmDeviceInfo* inInfo = 0;
const PmDeviceInfo* outInfo = 0;
for (PmDeviceID id = 0; id < interf; id++) {
const PmDeviceInfo* info = Pm_GetDeviceInfo((PmDeviceID)id);
if (info->input && inInterfaceAndName.contains(info->name))
inInfo = info;
if (info->output && outInterfaceAndName.contains(info->name))
outInfo = info;
}

if (inInfo && outInfo &&
QString(inInfo->interf) == "CoreMIDI" && QString(outInfo->interf) == "CoreMIDI" &&
inInterfaceAndName.contains("IAC") && outInterfaceAndName == inInterfaceAndName)
return true;
else
return false;
}
}

11 changes: 9 additions & 2 deletions mscore/pm.h
Expand Up @@ -41,8 +41,9 @@ class Seq;
class PortMidiDriver : public MidiDriver {
int inputId;
int outputId;
PmStream* inputStream;
QTimer* timer;
PmStream* inputStream;
PmStream* outputStream;

public:
PortMidiDriver(Seq*);
Expand All @@ -55,7 +56,13 @@ class PortMidiDriver : public MidiDriver {
virtual void read();
virtual void write(const Event&);
QStringList deviceInList() const;
int getDeviceIn(const QString& name);
QStringList deviceOutList() const;
int getDeviceIn(const QString& interfaceAndName);
int getDeviceOut(const QString& interfaceAndName);
PmStream* getInputStream() { return inputStream; }
PmStream* getOutputStream() { return outputStream; }
bool canOutput() { return outputStream != 0; }
bool isSameCoreMidiIacBus(const QString& inInterfaceAndName, const QString& outInterfaceAndName);
};


Expand Down

0 comments on commit 4e46930

Please sign in to comment.