~keithsalisbury/mixxx/mixxx

« back to all changes in this revision

Viewing changes to mixxx/src/controllers/midi/midicontroller.cpp

  • Committer: Keith Salisbury
  • Date: 2012-05-06 13:44:20 UTC
  • mfrom: (2994.1.100 mixxx-trunk)
  • Revision ID: keithsalisbury@gmail.com-20120506134420-8k1dqq10aqmx0ecq
merge with trunk

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/**
 
2
 * @file midicontroller.cpp
 
3
 * @author Sean Pappalardo spappalardo@mixxx.org
 
4
 * @date Tue 7 Feb 2012
 
5
 * @brief MIDI Controller base class
 
6
 *
 
7
 */
 
8
 
 
9
#include "controllers/midi/midicontroller.h"
 
10
 
 
11
#include "controllers/defs_controllers.h"
 
12
#include "controlobject.h"
 
13
#include "errordialoghandler.h"
 
14
 
 
15
MidiController::MidiController() : Controller() {
 
16
}
 
17
 
 
18
MidiController::~MidiController() {
 
19
    destroyOutputHandlers();
 
20
    // Don't close the device here. Sub-classes should close the device in their
 
21
    // destructors.
 
22
}
 
23
 
 
24
QString MidiController::defaultPreset() {
 
25
    QString name = getName();
 
26
    return USER_PRESETS_PATH.append(name.right(name.size()
 
27
            -name.indexOf(" ")-1).replace(" ", "_")
 
28
            + presetExtension());
 
29
}
 
30
 
 
31
QString MidiController::presetExtension() {
 
32
    return MIDI_MAPPING_EXTENSION;
 
33
}
 
34
 
 
35
void MidiController::visit(const MidiControllerPreset* preset) {
 
36
    m_preset = *preset;
 
37
    emit(presetLoaded(getPreset()));
 
38
}
 
39
 
 
40
void MidiController::clearInputMappings() {
 
41
    m_preset.mappings.clear();
 
42
}
 
43
 
 
44
void MidiController::clearOutputMappings() {
 
45
    m_preset.outputMappings.clear();
 
46
}
 
47
 
 
48
int MidiController::close() {
 
49
    destroyOutputHandlers();
 
50
    return 0;
 
51
}
 
52
 
 
53
void MidiController::visit(const HidControllerPreset* preset) {
 
54
    Q_UNUSED(preset);
 
55
    qWarning() << "ERROR: Attempting to load an HidControllerPreset to a MidiController!";
 
56
    // TODO(XXX): throw a hissy fit.
 
57
}
 
58
 
 
59
bool MidiController::savePreset(const QString fileName) const {
 
60
    MidiControllerPresetFileHandler handler;
 
61
    return handler.save(m_preset, getName(), fileName);
 
62
}
 
63
 
 
64
void MidiController::applyPreset(QString configPath) {
 
65
    // Handles the engine
 
66
    Controller::applyPreset(configPath);
 
67
 
 
68
    // Only execute this code if this is an output device
 
69
    if (isOutputDevice()) {
 
70
        if (m_outputs.count() > 0) {
 
71
            destroyOutputHandlers();
 
72
        }
 
73
        createOutputHandlers();
 
74
        updateAllOutputs();
 
75
    }
 
76
}
 
77
 
 
78
void MidiController::createOutputHandlers() {
 
79
    if (m_preset.outputMappings.isEmpty()) {
 
80
        return;
 
81
    }
 
82
 
 
83
    QHashIterator<MixxxControl, MidiOutput> outIt(m_preset.outputMappings);
 
84
    while (outIt.hasNext()) {
 
85
        outIt.next();
 
86
 
 
87
        MidiOutput outputPack = outIt.value();
 
88
        QString group = outIt.key().group();
 
89
        QString key = outIt.key().item();
 
90
 
 
91
        unsigned char status = outputPack.status;
 
92
        unsigned char control = outputPack.control;
 
93
        unsigned char on = outputPack.on;
 
94
        unsigned char off = outputPack.off;
 
95
        float min = outputPack.min;
 
96
        float max = outputPack.max;
 
97
 
 
98
        if (debugging()) {
 
99
            qDebug() << QString(
 
100
                "Creating output handler for %1,%2 between %3 and %4 to MIDI out: 0x%5 0x%6, on: 0x%7 off: 0x%8")
 
101
                    .arg(group, key,
 
102
                            QString::number(min), QString::number(max),
 
103
                            QString::number(status, 16).toUpper(),
 
104
                            QString::number(control, 16).toUpper().rightJustified(2,'0'),
 
105
                            QString::number(on, 16).toUpper().rightJustified(2,'0'),
 
106
                            QString::number(off, 16).toUpper().rightJustified(2,'0'));
 
107
        }
 
108
 
 
109
        MidiOutputHandler* moh = new MidiOutputHandler(group, key, this,
 
110
                                                       min, max, status, control,
 
111
                                                       on, off);
 
112
        if (!moh->validate()) {
 
113
            QString errorLog = QString("Invalid MixxxControl: %1, %2")
 
114
                                        .arg(group, key).toUtf8();
 
115
            if (debugging()) {
 
116
                qCritical() << errorLog;
 
117
            } else {
 
118
                qWarning() << errorLog;
 
119
                ErrorDialogProperties* props = ErrorDialogHandler::instance()->newDialogProperties();
 
120
                props->setType(DLG_WARNING);
 
121
                props->setTitle(tr("MixxxControl not found"));
 
122
                props->setText(QString(tr("The MixxxControl '%1, %2' specified in the "
 
123
                                "loaded mapping is invalid."))
 
124
                                .arg(group, key));
 
125
                props->setInfoText(QString(tr("The MIDI output message 0x%1 0x%2 will not be bound."
 
126
                                "\n(Click Show Details for hints.)"))
 
127
                                   .arg(QString::number(status, 16).toUpper(),
 
128
                                        QString::number(control, 16).toUpper().rightJustified(2,'0')));
 
129
                QString detailsText = QString(tr("* Check to see that the "
 
130
                    "MixxxControl name is spelled correctly in the mapping "
 
131
                    "file (.xml)\n"));
 
132
                detailsText += QString(tr("* Make sure the MixxxControl you're trying to use actually exists."
 
133
                    " Visit this wiki page for a complete list:"));
 
134
                detailsText += QString("\nhttp://mixxx.org/wiki/doku.php/midi_controller_mapping_file_format#ui_midi_controls_and_names");
 
135
                props->setDetails(detailsText);
 
136
                ErrorDialogHandler::instance()->requestErrorDialog(props);
 
137
            }
 
138
            delete moh;
 
139
            continue;
 
140
        }
 
141
        m_outputs.append(moh);
 
142
    }
 
143
}
 
144
 
 
145
void MidiController::updateAllOutputs() {
 
146
    foreach (MidiOutputHandler* pOutput, m_outputs) {
 
147
        pOutput->update();
 
148
    }
 
149
}
 
150
 
 
151
void MidiController::destroyOutputHandlers() {
 
152
    while (m_outputs.size() > 0) {
 
153
        delete m_outputs.takeLast();
 
154
    }
 
155
}
 
156
 
 
157
void MidiController::receive(unsigned char status, unsigned char control,
 
158
                             unsigned char value) {
 
159
    unsigned char channel = status & 0x0F;
 
160
    unsigned char opCode = status & 0xF0;
 
161
    if (opCode >= 0xF0) {
 
162
        opCode = status;
 
163
    }
 
164
 
 
165
    QString message;
 
166
    bool twoBytes = true;
 
167
 
 
168
    switch (opCode) {
 
169
        case MIDI_PITCH_BEND:
 
170
            twoBytes = false;
 
171
            message = QString("MIDI status 0x%1: pitch bend ch %2, value 0x%3")
 
172
                 .arg(QString::number(status, 16).toUpper(),
 
173
                      QString::number(channel+1, 10),
 
174
                      QString::number((value << 7) | control, 16).toUpper().rightJustified(4,'0'));
 
175
            break;
 
176
        case MIDI_SONG_POS:
 
177
            twoBytes = false;
 
178
            message = QString("MIDI status 0x%1: song position 0x%2")
 
179
                 .arg(QString::number(status, 16).toUpper(),
 
180
                      QString::number((value << 7) | control, 16).toUpper().rightJustified(4,'0'));
 
181
            break;
 
182
        case MIDI_PROGRAM_CH:
 
183
        case MIDI_CH_AFTERTOUCH:
 
184
            twoBytes = false;
 
185
            message = QString("MIDI status 0x%1 (ch %2, opcode 0x%3), value 0x%4")
 
186
                 .arg(QString::number(status, 16).toUpper(),
 
187
                      QString::number(channel+1, 10),
 
188
                      QString::number((status & 255)>>4, 16).toUpper(),
 
189
                      QString::number(control, 16).toUpper().rightJustified(2,'0'));
 
190
            break;
 
191
        case MIDI_SONG:
 
192
            message = QString("MIDI status 0x%1: select song #%2")
 
193
                 .arg(QString::number(status, 16).toUpper(),
 
194
                      QString::number(control+1, 10));
 
195
            break;
 
196
        case MIDI_NOTE_OFF:
 
197
        case MIDI_NOTE_ON:
 
198
        case MIDI_AFTERTOUCH:
 
199
        case MIDI_CC:
 
200
            message = QString("MIDI status 0x%1 (ch %2, opcode 0x%3), ctrl 0x%4, val 0x%5")
 
201
                 .arg(QString::number(status, 16).toUpper(),
 
202
                      QString::number(channel+1, 10),
 
203
                      QString::number((status & 255)>>4, 16).toUpper(),
 
204
                      QString::number(control, 16).toUpper().rightJustified(2,'0'),
 
205
                      QString::number(value, 16).toUpper().rightJustified(2,'0'));
 
206
            break;
 
207
        default:
 
208
            twoBytes = false;
 
209
            message = QString("MIDI status 0x%1")
 
210
                 .arg(QString::number(status, 16).toUpper());
 
211
            break;
 
212
    }
 
213
 
 
214
    if (debugging()) {
 
215
        qDebug() << message;
 
216
    }
 
217
 
 
218
    //if (m_bReceiveInhibit) return;
 
219
 
 
220
    MidiKey mappingKey;
 
221
    mappingKey.status = status;
 
222
 
 
223
    // When it's part of the message, include it
 
224
    if (twoBytes) {
 
225
        mappingKey.control = control;
 
226
    } else {
 
227
        // Signifies that the second byte is part of the payload, default
 
228
        mappingKey.control = 0xFF;
 
229
    }
 
230
 
 
231
    // Learning
 
232
    if (isLearning()) {
 
233
        MixxxControl control = controlToLearn();
 
234
        if (!control.isNull()) {
 
235
            MidiOptions options;
 
236
            options.all = 0;
 
237
 
 
238
            QPair<MixxxControl, MidiOptions> target;
 
239
            target.first = control;
 
240
            target.second = options;
 
241
 
 
242
            // TODO: store these in a temporary hash to be applied on learning
 
243
            //  success, or thrown away on cancel.
 
244
            m_preset.mappings.insert(mappingKey.key,target);
 
245
 
 
246
            //If we caught a NOTE_ON message, add a binding for NOTE_OFF as well.
 
247
            if (status == MIDI_NOTE_ON) {
 
248
                MidiKey mappingKeyTemp;
 
249
                mappingKeyTemp.status = MIDI_NOTE_OFF;
 
250
                mappingKeyTemp.control = mappingKey.control;
 
251
                m_preset.mappings.insert(mappingKeyTemp.key,target);
 
252
            }
 
253
 
 
254
            //Reset the saved control.
 
255
            setControlToLearn(MixxxControl());
 
256
 
 
257
            QString message = "error";
 
258
            if (twoBytes) {
 
259
                message = QString("0x%1 0x%2")
 
260
                            .arg(QString::number(mappingKey.status, 16).toUpper(),
 
261
                                QString::number(mappingKey.control, 16).toUpper()
 
262
                                    .rightJustified(2,'0'));
 
263
            }
 
264
            else {
 
265
                message = QString("0x%1")
 
266
                            .arg(QString::number(mappingKey.status, 16).toUpper());
 
267
            }
 
268
            emit(learnedMessage(message));
 
269
        }
 
270
    }
 
271
 
 
272
    // If no control is bound to this MIDI message, return
 
273
    if (!m_preset.mappings.contains(mappingKey.key)) {
 
274
        return;
 
275
    }
 
276
 
 
277
    QPair<MixxxControl, MidiOptions> controlOptions = m_preset.mappings.value(mappingKey.key);
 
278
    MixxxControl mc = controlOptions.first;
 
279
    MidiOptions options = controlOptions.second;
 
280
 
 
281
    if (options.script) {
 
282
        ControllerEngine* pEngine = getEngine();
 
283
        if (pEngine == NULL) {
 
284
            return;
 
285
        }
 
286
 
 
287
        QScriptValueList args;
 
288
        args << QScriptValue(status & 0x0F);
 
289
        args << QScriptValue(control);
 
290
        args << QScriptValue(value);
 
291
        args << QScriptValue(status);
 
292
        args << QScriptValue(mc.group());
 
293
 
 
294
        pEngine->execute(mc.item(), args);
 
295
        return;
 
296
    }
 
297
 
 
298
    // Only pass values on to valid ControlObjects.
 
299
    ControlObject* p = mc.getControlObject();
 
300
    if (p == NULL) {
 
301
        return;
 
302
    }
 
303
 
 
304
    double currMixxxControlValue = p->GetMidiValue();
 
305
 
 
306
    double newValue = value;
 
307
 
 
308
    //qDebug() << "MIDI Options" << QString::number(options.all, 2).rightJustified(16,'0');
 
309
 
 
310
    // compute 14-bit number for pitch bend messages
 
311
    if (opCode == MIDI_PITCH_BEND) {
 
312
        unsigned int ivalue;
 
313
        ivalue = (value << 7) | control;
 
314
 
 
315
        currMixxxControlValue = p->get();
 
316
 
 
317
        // Range is 0x0000..0x3FFF center @ 0x2000, i.e. 0..16383 center @ 8192
 
318
        newValue = (ivalue-8192)/8191;
 
319
        // computeValue not done on pitch messages because it all assumes 7-bit numbers
 
320
    } else {
 
321
        newValue = computeValue(options, currMixxxControlValue, value);
 
322
    }
 
323
 
 
324
    if (options.soft_takeover) {
 
325
        // This is the only place to enable it if it isn't already.
 
326
        m_st.enable(p);
 
327
        if (m_st.ignore(p, newValue, true)) {
 
328
            return;
 
329
        }
 
330
    }
 
331
 
 
332
    ControlObject::sync();
 
333
    p->queueFromMidi(static_cast<MidiOpCode>(opCode), newValue);
 
334
}
 
335
 
 
336
double MidiController::computeValue(MidiOptions options, double _prevmidivalue, double _newmidivalue) {
 
337
    double tempval = 0.;
 
338
    double diff = 0.;
 
339
 
 
340
    if (options.all == 0) {
 
341
        return _newmidivalue;
 
342
    }
 
343
 
 
344
    if (options.invert) {
 
345
        return 127. - _newmidivalue;
 
346
    }
 
347
 
 
348
    if (options.rot64 || options.rot64_inv) {
 
349
        tempval = _prevmidivalue;
 
350
        diff = _newmidivalue - 64.;
 
351
        if (diff == -1 || diff == 1)
 
352
            diff /= 16;
 
353
        else
 
354
            diff += (diff > 0 ? -1 : +1);
 
355
        if (options.rot64)
 
356
            tempval += diff;
 
357
        else
 
358
            tempval -= diff;
 
359
        return (tempval < 0. ? 0. : (tempval > 127. ? 127.0 : tempval));
 
360
    }
 
361
 
 
362
    if (options.rot64_fast) {
 
363
        tempval = _prevmidivalue;
 
364
        diff = _newmidivalue - 64.;
 
365
        diff *= 1.5;
 
366
        tempval += diff;
 
367
        return (tempval < 0. ? 0. : (tempval > 127. ? 127.0 : tempval));
 
368
    }
 
369
 
 
370
    if (options.diff) {
 
371
        //Interpret 7-bit signed value using two's compliment.
 
372
        if (_newmidivalue >= 64.)
 
373
            _newmidivalue = _newmidivalue - 128.;
 
374
        //Apply sensitivity to signed value. FIXME
 
375
       // if(sensitivity > 0)
 
376
        //    _newmidivalue = _newmidivalue * ((double)sensitivity / 50.);
 
377
        //Apply new value to current value.
 
378
        _newmidivalue = _prevmidivalue + _newmidivalue;
 
379
    }
 
380
 
 
381
    if (options.selectknob) {
 
382
        //Interpret 7-bit signed value using two's compliment.
 
383
        if (_newmidivalue >= 64.)
 
384
            _newmidivalue = _newmidivalue - 128.;
 
385
        //Apply sensitivity to signed value. FIXME
 
386
        //if(sensitivity > 0)
 
387
        //    _newmidivalue = _newmidivalue * ((double)sensitivity / 50.);
 
388
        //Since this is a selection knob, we do not want to inherit previous values.
 
389
    }
 
390
 
 
391
    if (options.button) {
 
392
        _newmidivalue = _newmidivalue != 0;
 
393
    }
 
394
 
 
395
    if (options.sw) {
 
396
        _newmidivalue = 1;
 
397
    }
 
398
 
 
399
    if (options.spread64) {
 
400
        //qDebug() << "MIDI_OPT_SPREAD64";
 
401
        // BJW: Spread64: Distance away from centre point (aka "relative CC")
 
402
        // Uses a similar non-linear scaling formula as ControlTTRotary::getValueFromWidget()
 
403
        // but with added sensitivity adjustment. This formula is still experimental.
 
404
 
 
405
        _newmidivalue = _newmidivalue - 64.;
 
406
        //FIXME
 
407
        //double distance = _newmidivalue - 64.;
 
408
        // _newmidivalue = distance * distance * sensitivity / 50000.;
 
409
        //if (distance < 0.)
 
410
        //    _newmidivalue = -_newmidivalue;
 
411
 
 
412
        //qDebug() << "Spread64: in " << distance << "  out " << _newmidivalue;
 
413
    }
 
414
 
 
415
    if (options.herc_jog) {
 
416
        if (_newmidivalue > 64.) {
 
417
            _newmidivalue -= 128.;
 
418
        }
 
419
        _newmidivalue += _prevmidivalue;
 
420
        //if (_prevmidivalue != 0.0) { qDebug() << "AAAAAAAAAAAA" << _prevmidivalue; }
 
421
    }
 
422
 
 
423
    return _newmidivalue;
 
424
}
 
425
 
 
426
void MidiController::receive(QByteArray data) {
 
427
    int length = data.size();
 
428
    QString message = getName() + ": [";
 
429
    for (int i = 0; i < length; ++i) {
 
430
        message += QString("%1%2").arg(
 
431
            QString("%1").arg((unsigned char)(data.at(i)), 2, 16, QChar('0')).toUpper(),
 
432
            QString("%1").arg((i < (length-1)) ? ' ' : ']'));
 
433
    }
 
434
 
 
435
    if (debugging())
 
436
        qDebug() << message;
 
437
 
 
438
    //if (m_bReceiveInhibit) return;
 
439
 
 
440
    MidiKey mappingKey;
 
441
    mappingKey.status = data.at(0);
 
442
    // Signifies that the second byte is part of the payload, default
 
443
    mappingKey.control = 0xFF;
 
444
 
 
445
    // Learning
 
446
    if (isLearning()) {
 
447
        MixxxControl control = controlToLearn();
 
448
        if (!control.isNull()) {
 
449
            MidiOptions options;
 
450
            options.all = 0;
 
451
 
 
452
            QPair<MixxxControl, MidiOptions> target;
 
453
            target.first = control;
 
454
            target.second = options;
 
455
 
 
456
            // TODO: store these in a temporary hash to be applied on learning
 
457
            //  success, or thrown away on cancel.
 
458
            m_preset.mappings.insert(mappingKey.key,target);
 
459
 
 
460
            //Reset the saved control.
 
461
            setControlToLearn(MixxxControl());
 
462
 
 
463
            QString message = QString("0x%1")
 
464
                        .arg(QString::number(mappingKey.status, 16).toUpper());
 
465
            emit(learnedMessage(message));
 
466
        }
 
467
        // Don't process MIDI messages when learning
 
468
        return;
 
469
    }
 
470
 
 
471
    // If no control is bound to this MIDI status, return
 
472
    if (!m_preset.mappings.contains(mappingKey.key)) {
 
473
        return;
 
474
    }
 
475
 
 
476
    QPair<MixxxControl, MidiOptions> control = m_preset.mappings.value(mappingKey.key);
 
477
 
 
478
    MixxxControl mc = control.first;
 
479
    MidiOptions options = control.second;
 
480
 
 
481
    // Custom script handler
 
482
    if (options.script) {
 
483
        ControllerEngine* pEngine = getEngine();
 
484
        if (pEngine == NULL) {
 
485
            return;
 
486
        }
 
487
        if (!pEngine->execute(mc.item(), data)) {
 
488
            qDebug() << "MidiController: Invalid script function" << mc.item();
 
489
        }
 
490
        return;
 
491
    }
 
492
    qWarning() << "MidiController: No script function specified for" << message;
 
493
}
 
494
 
 
495
void MidiController::sendShortMsg(unsigned char status, unsigned char byte1, unsigned char byte2) {
 
496
    unsigned int word = (((unsigned int)byte2) << 16) |
 
497
            (((unsigned int)byte1) << 8) | status;
 
498
    send(word);
 
499
}