summaryrefslogtreecommitdiffstats
path: root/src/controllers/midi/portmidicontroller.cpp
blob: cb419033ff500042706bfac79843c311dd581da0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
 * @file portmidicontroller.h
 * @author Albert Santoni alberts@mixxx.org
 * @author Sean M. Pappalardo  spappalardo@mixxx.org
 * @date Thu 15 Mar 2012
 * @brief PortMidi-based MIDI backend
 *
 */

#include "controllers/midi/portmidicontroller.h"
#include "controllers/controllerdebug.h"

PortMidiController::PortMidiController(const PmDeviceInfo* inputDeviceInfo,
                                       const PmDeviceInfo* outputDeviceInfo,
                                       int inputDeviceIndex,
                                       int outputDeviceIndex)
        : MidiController(),
          m_cReceiveMsg_index(0),
          m_bInSysex(false) {
    for (unsigned int k = 0; k < MIXXX_PORTMIDI_BUFFER_LEN; ++k) {
        // Can be shortened to `m_midiBuffer[k] = {}` with C++11.
        m_midiBuffer[k].message = 0;
        m_midiBuffer[k].timestamp = 0;
    }

    // Note: We prepend the input stream's index to the device's name to prevent
    // duplicate devices from causing mayhem.
    //setDeviceName(QString("%1. %2").arg(QString::number(m_iInputDeviceIndex), inputDeviceInfo->name));
    if (inputDeviceInfo) {
        setDeviceName(QString("%1").arg(inputDeviceInfo->name));
        setInputDevice(inputDeviceInfo->input);
        m_pInputDevice.reset(new PortMidiDevice(
            inputDeviceInfo, inputDeviceIndex));
    }
    if (outputDeviceInfo) {
        if (inputDeviceInfo == NULL) {
            setDeviceName(QString("%1").arg(outputDeviceInfo->name));
        }
        setOutputDevice(outputDeviceInfo->output);
        m_pOutputDevice.reset(new PortMidiDevice(
            outputDeviceInfo, outputDeviceIndex));
    }
}

PortMidiController::~PortMidiController() {
    if (isOpen()) {
        close();
    }
}

int PortMidiController::open() {
    if (isOpen()) {
        qDebug() << "PortMIDI device" << getName() << "already open";
        return -1;
    }

    if (getName() == MIXXX_PORTMIDI_NO_DEVICE_STRING)
        return -1;

    m_bInSysex = false;
    m_cReceiveMsg_index = 0;

    if (m_pInputDevice && isInputDevice()) {
        controllerDebug("PortMidiController: Opening"
                        << m_pInputDevice->info()->name << "index"
                        << m_pInputDevice->index() << "for input");
        PmError err = m_pInputDevice->openInput(MIXXX_PORTMIDI_BUFFER_LEN);

        if (err != pmNoError) {
            qWarning() << "PortMidi error:" << Pm_GetErrorText(err);
            return -2;
        }
    }
    if (m_pOutputDevice && isOutputDevice()) {
        controllerDebug("PortMidiController: Opening"
                        << m_pOutputDevice->info()->name << "index"
                        << m_pOutputDevice->index() << "for output");

        PmError err = m_pOutputDevice->openOutput();
        if (err != pmNoError) {
            qWarning() << "PortMidi error:" << Pm_GetErrorText(err);
            return -2;
        }
    }

    setOpen(true);
    startEngine();
    return 0;
}

int PortMidiController::close() {
    if (!isOpen()) {
        qDebug() << "PortMIDI device" << getName() << "already closed";
        return -1;
    }

    stopEngine();
    MidiController::close();

    int result = 0;

    if (m_pInputDevice && m_pInputDevice->isOpen()) {
        PmError err = m_pInputDevice->close();
        if (err != pmNoError) {
            qWarning() << "PortMidi error:" << Pm_GetErrorText(err);
            result = -1;
        }
    }

    if (m_pOutputDevice && m_pOutputDevice->isOpen()) {
        PmError err = m_pOutputDevice->close();
        if (err != pmNoError) {
            qWarning() << "PortMidi error:" << Pm_GetErrorText(err);
            result = -1;
        }
    }

    setOpen(false);
    return result;
}

bool PortMidiController::poll() {
    // Poll the controller for new data if it's an input device
    if (m_pInputDevice.isNull() || !m_pInputDevice->isOpen()) {
        return false;
    }

    // Returns true if events are available or an error code.
    PmError gotEvents = m_pInputDevice->poll();
    if (gotEvents == FALSE) {
        return false;
    }
    if (gotEvents < 0) {
        qWarning() << "PortMidi error:" << Pm_GetErrorText(gotEvents);
        return false;
    }

    int numEvents = m_pInputDevice->read(m_midiBuffer, MIXXX_PORTMIDI_BUFFER_LEN);

    //qDebug() << "PortMidiController::poll()" << numEvents;

    if (numEvents < 0) {
        qWarning() << "PortMidi error:" << Pm_GetErrorText((PmError)numEvents);
        return false;
    }

    for (int i = 0; i < numEvents; i++) {
        unsigned char status = Pm_MessageStatus(m_midiBuffer[i].message);
        mixxx::Duration timestamp = mixxx::Duration::fromMillis(m_midiBuffer[i].timestamp);

        if ((status & 0xF8) == 0xF8) {
            // Handle real-time MIDI messages at any time
            receive(status, 0, 0, timestamp);
            continue;
        }

        reprocessMessage:

        if (!m_bInSysex) {
            if (status == 0xF0) {
                m_bInSysex = true;
                status = 0;
            } else {
                //unsigned char channel = status & 0x0F;
                unsigned char note = Pm_MessageData1(m_midiBuffer[i].message);
                unsigned char velocity = Pm_MessageData2(m_midiBuffer[i].message);
                receive(status, note, velocity, timestamp);
            }
        }

        if (m_bInSysex) {
            // Abort (drop) the current System Exclusive message if a
            //  non-realtime status byte was received
            if (status > 0x7F && status < 0xF7) {
                m_bInSysex = false;
                m_cReceiveMsg_index = 0;
                qWarning() << "Buggy MIDI device: SysEx interrupted!";
                goto reprocessMessage;    // Don't lose the new message
            }

            // Collect bytes from PmMessage
            unsigned char data = 0;
            for (int shift = 0; shift < 32 && (data != MIDI_EOX); shift += 8) {
                // TODO(rryan): This prevents buffer overflow if the sysex is
                // larger than 1024 bytes. I don't want to radically change
                // anything before the 2.0 release so this will do for now.
                data = (m_midiBuffer[i].message >> shift) & 0xFF;
                if (m_cReceiveMsg_index < MIXXX_SYSEX_BUFFER_LEN) {
                    m_cReceiveMsg[m_cReceiveMsg_index++] = data;
                }
            }

            // End System Exclusive message if the EOX byte was received
            if (data == MIDI_EOX) {
                m_bInSysex = false;
                const char* buffer = reinterpret_cast<const char*>(m_cReceiveMsg);
                receive(QByteArray::fromRawData(buffer, m_cReceiveMsg_index),
                        timestamp);
                m_cReceiveMsg_index = 0;
            }
        }
    }
    return numEvents > 0;
}

void PortMidiController::sendWord(unsigned int word) {
    if (m_pOutputDevice.isNull() || !m_pOutputDevice->isOpen()) {
        return;
    }

    PmError err = m_pOutputDevice->writeShort(word);
    if (err != pmNoError) {
        qWarning() << "PortMidi sendShortMsg error:" << Pm_GetErrorText(err);
    }
}

void PortMidiController::send(QByteArray data) {
    // PortMidi does not receive a length argument for the buffer we provide to
    // Pm_WriteSysEx. Instead, it scans for a MIDI_EOX byte to know when the
    // message is over. If one is not provided, it will overflow the buffer and
    // cause a segfault.
    if (!data.endsWith(MIDI_EOX)) {
        controllerDebug("SysEx message does not end with 0xF7 -- ignoring.");
        return;
    }

    if (m_pOutputDevice.isNull() || !m_pOutputDevice->isOpen()) {
        return;
    }

    PmError err = m_pOutputDevice->writeSysEx((unsigned char*)data.constData());
    if (err != pmNoError) {
        qWarning() << "PortMidi sendSysexMsg error:"
                   << Pm_GetErrorText(err);
    }
}