qt_files = qt5.preprocess(
moc_headers: ['nageru/analyzer.h', 'nageru/clickable_label.h', 'nageru/compression_reduction_meter.h', 'nageru/correlation_meter.h',
'nageru/ellipsis_label.h', 'nageru/glwidget.h', 'nageru/input_mapping_dialog.h', 'nageru/lrameter.h', 'nageru/mainwindow.h', 'nageru/midi_mapping_dialog.h',
- 'nageru/nonlinear_fader.h', 'nageru/vumeter.h'],
+ 'nageru/nonlinear_fader.h', 'nageru/vumeter.h', 'nageru/controller_spin_box.h'],
ui_files: ['nageru/analyzer.ui', 'nageru/audio_expanded_view.ui', 'nageru/audio_miniview.ui', 'nageru/display.ui',
'nageru/input_mapping.ui', 'nageru/mainwindow.ui', 'nageru/midi_mapping.ui'],
dependencies: qt5deps)
--- /dev/null
+#ifndef _CONTROLLER_SPIN_BOX_H
+#define _CONTROLLER_SPIN_BOX_H 1
+
+// ControllerSpinBox is like QSpinBox, except it has a second special value
+// "PB" (in addition to the standard minimum value of -1, representing blank),
+// representing the virtual pitch bend controller.
+
+#include <QSpinBox>
+#include <QString>
+
+#include "shared/midi_device.h"
+
+class ControllerSpinBox : public QSpinBox {
+ Q_OBJECT
+
+public:
+ ControllerSpinBox(QWidget *parent) : QSpinBox(parent) {}
+
+ int valueFromText(const QString &text) const override
+ {
+ if (text == "PB") {
+ return MIDIReceiver::PITCH_BEND_CONTROLLER;
+ } else {
+ return QSpinBox::valueFromText(text);
+ }
+ }
+
+ QString textFromValue(int value) const override
+ {
+ if (value == MIDIReceiver::PITCH_BEND_CONTROLLER) {
+ return "PB";
+ } else {
+ return QSpinBox::textFromValue(value);
+ }
+ }
+
+ QValidator::State validate(QString &input, int &pos) const override
+ {
+ if (input == "PB") {
+ return QValidator::Acceptable;
+ } else {
+ return QSpinBox::validate(input, pos);
+ }
+ }
+};
+
+#endif // !defined(_CONTROLLER_SPIN_BOX_H)
#include "audio_mixer.h"
#include "nageru_midi_mapping.pb.h"
+#include "shared/midi_device.h"
#include "shared/midi_mapper_util.h"
#include "shared/text_proto.h"
namespace {
-double map_controller_to_float(int val)
+double map_controller_to_float(int controller, int val)
{
+ if (controller == MIDIReceiver::PITCH_BEND_CONTROLLER) {
+ // We supposedly go from -8192 to 8191 (inclusive), but there are
+ // controllers that only have 10-bit precision and do the upconversion
+ // to 14-bit wrong (just padding with zeros), making 8176 the highest
+ // attainable value. We solve this by making the effective range
+ // -8176..8176 (inclusive).
+ if (val <= -8176) {
+ return 0.0;
+ } else if (val >= 8176) {
+ return 1.0;
+ } else {
+ return 0.5 * (double(val) / 8176.0) + 0.5;
+ }
+ }
+
// Slightly hackish mapping so that we can represent exactly 0.0, 0.5 and 1.0.
if (val <= 0) {
return 0.0;
void MIDIMapper::controller_received(int controller, int value_int)
{
- const float value = map_controller_to_float(value_int);
+ const float value = map_controller_to_float(controller, value_int);
receiver->controller_changed(controller);
#include <limits>
#include <string>
+#include "controller_spin_box.h"
#include "midi_mapper.h"
#include "nageru_midi_mapping.pb.h"
#include "shared/post_to_main_thread.h"
item->setText(0, QString::fromStdString(control.label + " "));
for (unsigned bus_idx = 0; bus_idx < num_buses; ++bus_idx) {
- QSpinBox *spinner = new QSpinBox(this);
- spinner->setRange(-1, 127);
+ QSpinBox *spinner;
+ if (control_type == ControlType::CONTROLLER) {
+ spinner = new ControllerSpinBox(this);
+ spinner->setRange(-1, 128); // 128 for pitch bend.
+ } else {
+ spinner = new QSpinBox(this);
+ spinner->setRange(-1, 127);
+ }
spinner->setAutoFillBackground(true);
spinner->setSpecialValueText("\u200d"); // Zero-width joiner (ie., empty).
spinner->installEventFilter(this); // So we know when the focus changes.
receiver->controller_received(event->data.control.param, event->data.control.value);
break;
}
+ case SND_SEQ_EVENT_PITCHBEND: {
+ // Note, -8192 to 8191 instead of 0 to 127.
+ receiver->controller_received(MIDIReceiver::PITCH_BEND_CONTROLLER, event->data.control.value);
+ break;
+ }
case SND_SEQ_EVENT_NOTEON: {
receiver->note_on_received(event->data.note.note);
break;
}
unsigned num_events = 0;
- for (unsigned note_num = 1; note_num <= 127; ++note_num) {
+ for (unsigned note_num = 1; note_num <= 127; ++note_num) { // Note: Pitch bend is ignored.
bool active = active_lights.count(note_num);
if (current_light_status.count(note_num) &&
current_light_status[note_num] == active) {
class MIDIReceiver {
public:
+ // Pitch bend events are received as a virtual controller with
+ // range -8192..8191 instead of 0..127 (but see the comment
+ // in map_controller_to_float() in midi_mapper.cpp).
+ static constexpr int PITCH_BEND_CONTROLLER = 128;
+
virtual ~MIDIReceiver() {}
virtual void controller_received(int controller, int value) = 0;
virtual void note_on_received(int note) = 0;