| @@ -36,6 +36,8 @@ struct AudioIO { | |||||
| std::string getDeviceDetail(int device); | std::string getDeviceDetail(int device); | ||||
| void openStream(); | void openStream(); | ||||
| void closeStream(); | void closeStream(); | ||||
| /** Returns whether the audio stream is open and running */ | |||||
| bool isActive(); | |||||
| std::vector<int> listSampleRates(); | std::vector<int> listSampleRates(); | ||||
| @@ -63,8 +63,9 @@ struct Module { | |||||
| /** Advances the module by 1 audio frame with duration 1.0 / gSampleRate */ | /** Advances the module by 1 audio frame with duration 1.0 / gSampleRate */ | ||||
| virtual void step() {} | virtual void step() {} | ||||
| virtual void onSampleRateChange() {} | |||||
| /** Called when the engine sample rate is changed */ | |||||
| virtual void onSampleRateChange() {} | |||||
| /** Called when module is created by the Add Module popup, cloning, or when loading a patch or autosave */ | /** Called when module is created by the Add Module popup, cloning, or when loading a patch or autosave */ | ||||
| virtual void onCreate() {} | virtual void onCreate() {} | ||||
| /** Called when user explicitly deletes the module, not when Rack is closed or a new patch is loaded */ | /** Called when user explicitly deletes the module, not when Rack is closed or a new patch is loaded */ | ||||
| @@ -22,7 +22,7 @@ struct MidiMessage { | |||||
| struct MidiIO { | struct MidiIO { | ||||
| int port = -1; | |||||
| int device = -1; | |||||
| /* For MIDI output, the channel to output messages. | /* For MIDI output, the channel to output messages. | ||||
| For MIDI input, the channel to filter. | For MIDI input, the channel to filter. | ||||
| Set to -1 to allow all MIDI channels (for input). | Set to -1 to allow all MIDI channels (for input). | ||||
| @@ -32,9 +32,11 @@ struct MidiIO { | |||||
| RtMidi *rtMidi = NULL; | RtMidi *rtMidi = NULL; | ||||
| virtual ~MidiIO() {} | virtual ~MidiIO() {} | ||||
| int getPortCount(); | |||||
| std::string getPortName(int port); | |||||
| void openPort(int port); | |||||
| int getDeviceCount(); | |||||
| std::string getDeviceName(int device); | |||||
| void openDevice(int device); | |||||
| /** Returns whether the audio stream is open and running */ | |||||
| bool isActive(); | |||||
| json_t *toJson(); | json_t *toJson(); | ||||
| void fromJson(json_t *rootJ); | void fromJson(json_t *rootJ); | ||||
| }; | }; | ||||
| @@ -5,11 +5,11 @@ | |||||
| namespace rack { | namespace rack { | ||||
| struct MidiPortItem : MenuItem { | |||||
| struct MidiDeviceItem : MenuItem { | |||||
| MidiIO *midiIO; | MidiIO *midiIO; | ||||
| int port; | |||||
| int device; | |||||
| void onAction(EventAction &e) override { | void onAction(EventAction &e) override { | ||||
| midiIO->openPort(port); | |||||
| midiIO->openDevice(device); | |||||
| } | } | ||||
| }; | }; | ||||
| @@ -31,13 +31,21 @@ void MidiWidget::onMouseDown(EventMouseDown &e) { | |||||
| Menu *menu = gScene->createMenu(); | Menu *menu = gScene->createMenu(); | ||||
| menu->addChild(construct<MenuLabel>(&MenuLabel::text, "MIDI port")); | |||||
| for (int port = 0; port < midiIO->getPortCount(); port++) { | |||||
| MidiPortItem *item = new MidiPortItem(); | |||||
| menu->addChild(construct<MenuLabel>(&MenuLabel::text, "MIDI device")); | |||||
| { | |||||
| MidiDeviceItem *item = new MidiDeviceItem(); | |||||
| item->midiIO = midiIO; | item->midiIO = midiIO; | ||||
| item->port = port; | |||||
| item->text = midiIO->getPortName(port); | |||||
| item->rightText = CHECKMARK(item->port == midiIO->port); | |||||
| item->device = -1; | |||||
| item->text = "No device"; | |||||
| item->rightText = CHECKMARK(item->device == midiIO->device); | |||||
| menu->addChild(item); | |||||
| } | |||||
| for (int device = 0; device < midiIO->getDeviceCount(); device++) { | |||||
| MidiDeviceItem *item = new MidiDeviceItem(); | |||||
| item->midiIO = midiIO; | |||||
| item->device = device; | |||||
| item->text = midiIO->getDeviceName(device); | |||||
| item->rightText = CHECKMARK(item->device == midiIO->device); | |||||
| menu->addChild(item); | menu->addChild(item); | ||||
| } | } | ||||
| menu->addChild(construct<MenuEntry>()); | menu->addChild(construct<MenuEntry>()); | ||||
| @@ -216,6 +216,14 @@ void AudioIO::closeStream() { | |||||
| onCloseStream(); | onCloseStream(); | ||||
| } | } | ||||
| bool AudioIO::isActive() { | |||||
| if (rtAudio) | |||||
| return rtAudio->isStreamRunning(); | |||||
| // TODO Bridge | |||||
| return false; | |||||
| } | |||||
| std::vector<int> AudioIO::listSampleRates() { | std::vector<int> AudioIO::listSampleRates() { | ||||
| if (rtAudio) { | if (rtAudio) { | ||||
| try { | try { | ||||
| @@ -18,7 +18,7 @@ | |||||
| #define MAX_OUTPUTS 8 | #define MAX_OUTPUTS 8 | ||||
| #define MAX_INPUTS 8 | #define MAX_INPUTS 8 | ||||
| static auto audioTimeout = std::chrono::milliseconds(100); | |||||
| static const auto audioTimeout = std::chrono::milliseconds(100); | |||||
| using namespace rack; | using namespace rack; | ||||
| @@ -45,7 +45,7 @@ struct AudioInterfaceIO : AudioIO { | |||||
| void processStream(const float *input, float *output, int length) override { | void processStream(const float *input, float *output, int length) override { | ||||
| if (numInputs > 0) { | if (numInputs > 0) { | ||||
| // TODO Do we need to wait on the input to be consumed here? | |||||
| // TODO Do we need to wait on the input to be consumed here? Experimentally, it works fine if we don't. | |||||
| for (int i = 0; i < length; i++) { | for (int i = 0; i < length; i++) { | ||||
| if (inputBuffer.full()) | if (inputBuffer.full()) | ||||
| break; | break; | ||||
| @@ -97,8 +97,13 @@ struct AudioInterface : Module { | |||||
| ENUMS(AUDIO_OUTPUT, MAX_OUTPUTS), | ENUMS(AUDIO_OUTPUT, MAX_OUTPUTS), | ||||
| NUM_OUTPUTS | NUM_OUTPUTS | ||||
| }; | }; | ||||
| enum LightIds { | |||||
| ACTIVE_LIGHT, | |||||
| NUM_LIGHTS | |||||
| }; | |||||
| AudioInterfaceIO audioIO; | AudioInterfaceIO audioIO; | ||||
| int lastSampleRate = 0; | |||||
| SampleRateConverter<MAX_INPUTS> inputSrc; | SampleRateConverter<MAX_INPUTS> inputSrc; | ||||
| SampleRateConverter<MAX_OUTPUTS> outputSrc; | SampleRateConverter<MAX_OUTPUTS> outputSrc; | ||||
| @@ -107,7 +112,8 @@ struct AudioInterface : Module { | |||||
| DoubleRingBuffer<Frame<MAX_INPUTS>, 16> inputBuffer; | DoubleRingBuffer<Frame<MAX_INPUTS>, 16> inputBuffer; | ||||
| DoubleRingBuffer<Frame<MAX_OUTPUTS>, 16> outputBuffer; | DoubleRingBuffer<Frame<MAX_OUTPUTS>, 16> outputBuffer; | ||||
| AudioInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS) { | |||||
| AudioInterface() : Module(NUM_PARAMS, NUM_INPUTS, NUM_OUTPUTS, NUM_LIGHTS) { | |||||
| onSampleRateChange(); | |||||
| } | } | ||||
| void step() override; | void step() override; | ||||
| @@ -123,6 +129,17 @@ struct AudioInterface : Module { | |||||
| audioIO.fromJson(audioJ); | audioIO.fromJson(audioJ); | ||||
| } | } | ||||
| void onSampleRateChange() override { | |||||
| // for (int i = 0; i < MAX_INPUTS; i++) { | |||||
| // inputSrc[i].setRates(audioIO.sampleRate, engineGetSampleRate()); | |||||
| // } | |||||
| // for (int i = 0; i < MAX_OUTPUTS; i++) { | |||||
| // outputSrc[i].setRates(engineGetSampleRate(), audioIO.sampleRate); | |||||
| // } | |||||
| inputSrc.setRates(audioIO.sampleRate, engineGetSampleRate()); | |||||
| outputSrc.setRates(engineGetSampleRate(), audioIO.sampleRate); | |||||
| } | |||||
| void onReset() override { | void onReset() override { | ||||
| audioIO.closeStream(); | audioIO.closeStream(); | ||||
| } | } | ||||
| @@ -133,9 +150,14 @@ void AudioInterface::step() { | |||||
| Frame<MAX_INPUTS> inputFrame; | Frame<MAX_INPUTS> inputFrame; | ||||
| memset(&inputFrame, 0, sizeof(inputFrame)); | memset(&inputFrame, 0, sizeof(inputFrame)); | ||||
| // Update sample rate if changed by audio driver | |||||
| if (audioIO.sampleRate != lastSampleRate) { | |||||
| onSampleRateChange(); | |||||
| lastSampleRate = audioIO.sampleRate; | |||||
| } | |||||
| if (audioIO.numInputs > 0) { | if (audioIO.numInputs > 0) { | ||||
| if (inputBuffer.empty()) { | if (inputBuffer.empty()) { | ||||
| inputSrc.setRates(audioIO.sampleRate, engineGetSampleRate()); | |||||
| int inLen = audioIO.inputBuffer.size(); | int inLen = audioIO.inputBuffer.size(); | ||||
| int outLen = inputBuffer.capacity(); | int outLen = inputBuffer.capacity(); | ||||
| inputSrc.process(audioIO.inputBuffer.startData(), &inLen, inputBuffer.endData(), &outLen); | inputSrc.process(audioIO.inputBuffer.startData(), &inLen, inputBuffer.endData(), &outLen); | ||||
| @@ -169,7 +191,6 @@ void AudioInterface::step() { | |||||
| }; | }; | ||||
| if (audioIO.engineCv.wait_for(lock, audioTimeout, cond)) { | if (audioIO.engineCv.wait_for(lock, audioTimeout, cond)) { | ||||
| // Push converted output | // Push converted output | ||||
| outputSrc.setRates(engineGetSampleRate(), audioIO.sampleRate); | |||||
| int inLen = outputBuffer.size(); | int inLen = outputBuffer.size(); | ||||
| int outLen = audioIO.outputBuffer.capacity(); | int outLen = audioIO.outputBuffer.capacity(); | ||||
| outputSrc.process(outputBuffer.startData(), &inLen, audioIO.outputBuffer.endData(), &outLen); | outputSrc.process(outputBuffer.startData(), &inLen, audioIO.outputBuffer.endData(), &outLen); | ||||
| @@ -183,6 +204,9 @@ void AudioInterface::step() { | |||||
| } | } | ||||
| audioIO.audioCv.notify_all(); | audioIO.audioCv.notify_all(); | ||||
| // Lights | |||||
| lights[ACTIVE_LIGHT].value = audioIO.isActive() ? 1.0 : 0.0; | |||||
| } | } | ||||
| @@ -196,10 +220,10 @@ AudioInterfaceWidget::AudioInterfaceWidget() { | |||||
| addChild(panel); | addChild(panel); | ||||
| } | } | ||||
| // addChild(createScrew<ScrewSilver>(Vec(15, 0))); | |||||
| // addChild(createScrew<ScrewSilver>(Vec(box.size.x-30, 0))); | |||||
| // addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| // addChild(createScrew<ScrewSilver>(Vec(box.size.x-30, 365))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x-30, 0))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(15, 365))); | |||||
| addChild(createScrew<ScrewSilver>(Vec(box.size.x-30, 365))); | |||||
| Vec margin = Vec(5, 2); | Vec margin = Vec(5, 2); | ||||
| float labelHeight = 15; | float labelHeight = 15; | ||||
| @@ -251,7 +275,8 @@ AudioInterfaceWidget::AudioInterfaceWidget() { | |||||
| yPos += 5; | yPos += 5; | ||||
| xPos = 10; | xPos = 10; | ||||
| for (int i = 0; i < 4; i++) { | for (int i = 0; i < 4; i++) { | ||||
| addOutput(createOutput<PJ3410Port>(Vec(xPos, yPos), module, AudioInterface::AUDIO_OUTPUT + i)); | |||||
| Port *port = createOutput<PJ3410Port>(Vec(xPos, yPos), module, AudioInterface::AUDIO_OUTPUT + i); | |||||
| addOutput(port); | |||||
| Label *label = new Label(); | Label *label = new Label(); | ||||
| label->box.pos = Vec(xPos + 4, yPos + 28); | label->box.pos = Vec(xPos + 4, yPos + 28); | ||||
| label->text = stringf("%d", i + 1); | label->text = stringf("%d", i + 1); | ||||
| @@ -277,4 +302,7 @@ AudioInterfaceWidget::AudioInterfaceWidget() { | |||||
| AudioWidget *audioWidget = construct<USB_B_AudioWidget>(); | AudioWidget *audioWidget = construct<USB_B_AudioWidget>(); | ||||
| audioWidget->audioIO = &module->audioIO; | audioWidget->audioIO = &module->audioIO; | ||||
| addChild(audioWidget); | addChild(audioWidget); | ||||
| // Lights | |||||
| addChild(createLight<SmallLight<GreenLight>>(Vec(40, 20), module, AudioInterface::ACTIVE_LIGHT)); | |||||
| } | } | ||||
| @@ -15,6 +15,7 @@ struct MidiValue { | |||||
| bool changed = false; // Value has been changed by midi message (only if it is in sync!) | bool changed = false; // Value has been changed by midi message (only if it is in sync!) | ||||
| }; | }; | ||||
| struct MIDIToCVInterface : Module { | struct MIDIToCVInterface : Module { | ||||
| enum ParamIds { | enum ParamIds { | ||||
| RESET_PARAM, | RESET_PARAM, | ||||
| @@ -33,6 +34,7 @@ struct MIDIToCVInterface : Module { | |||||
| NUM_OUTPUTS | NUM_OUTPUTS | ||||
| }; | }; | ||||
| enum LightIds { | enum LightIds { | ||||
| ACTIVE_LIGHT, | |||||
| RESET_LIGHT, | RESET_LIGHT, | ||||
| NUM_LIGHTS | NUM_LIGHTS | ||||
| }; | }; | ||||
| @@ -136,6 +138,9 @@ void MIDIToCVInterface::step() { | |||||
| outputs[CHANNEL_AFTERTOUCH_OUTPUT].value = afterTouch.val / 127.0 * 10.0; | outputs[CHANNEL_AFTERTOUCH_OUTPUT].value = afterTouch.val / 127.0 * 10.0; | ||||
| */ | */ | ||||
| // Lights | |||||
| lights[ACTIVE_LIGHT].value = midiInput.isActive() ? 1.0 : 0.0; | |||||
| } | } | ||||
| void MIDIToCVInterface::pressNote(int note) { | void MIDIToCVInterface::pressNote(int note) { | ||||
| @@ -272,4 +277,7 @@ MidiToCVWidget::MidiToCVWidget() { | |||||
| MidiWidget *midiWidget = construct<MIDI_DIN_MidiWidget>(); | MidiWidget *midiWidget = construct<MIDI_DIN_MidiWidget>(); | ||||
| midiWidget->midiIO = &module->midiInput; | midiWidget->midiIO = &module->midiInput; | ||||
| addChild(midiWidget); | addChild(midiWidget); | ||||
| // Lights | |||||
| addChild(createLight<SmallLight<GreenLight>>(Vec(40, 20), module, MIDIToCVInterface::ACTIVE_LIGHT)); | |||||
| } | } | ||||
| @@ -8,41 +8,54 @@ namespace rack { | |||||
| // MidiIO | // MidiIO | ||||
| //////////////////// | //////////////////// | ||||
| int MidiIO::getPortCount() { | |||||
| return rtMidi->getPortCount(); | |||||
| int MidiIO::getDeviceCount() { | |||||
| if (rtMidi) | |||||
| return rtMidi->getPortCount(); | |||||
| return 0; | |||||
| } | } | ||||
| std::string MidiIO::getPortName(int port) { | |||||
| if (port < 0) | |||||
| return ""; | |||||
| return rtMidi->getPortName(port); | |||||
| std::string MidiIO::getDeviceName(int device) { | |||||
| if (rtMidi) { | |||||
| if (device < 0) | |||||
| return ""; | |||||
| return rtMidi->getPortName(device); | |||||
| } | |||||
| return ""; | |||||
| } | } | ||||
| void MidiIO::openPort(int port) { | |||||
| rtMidi->closePort(); | |||||
| void MidiIO::openDevice(int device) { | |||||
| if (rtMidi) { | |||||
| rtMidi->closePort(); | |||||
| if (port >= 0) { | |||||
| rtMidi->openPort(port); | |||||
| if (device >= 0) { | |||||
| rtMidi->openPort(device); | |||||
| } | |||||
| this->device = device; | |||||
| } | } | ||||
| this->port = port; | |||||
| } | |||||
| bool MidiIO::isActive() { | |||||
| if (rtMidi) | |||||
| return rtMidi->isPortOpen(); | |||||
| return false; | |||||
| } | } | ||||
| json_t *MidiIO::toJson() { | json_t *MidiIO::toJson() { | ||||
| json_t *rootJ = json_object(); | json_t *rootJ = json_object(); | ||||
| std::string portName = getPortName(port); | |||||
| json_object_set_new(rootJ, "port", json_string(portName.c_str())); | |||||
| std::string deviceName = getDeviceName(device); | |||||
| json_object_set_new(rootJ, "device", json_string(deviceName.c_str())); | |||||
| json_object_set_new(rootJ, "channel", json_integer(channel)); | json_object_set_new(rootJ, "channel", json_integer(channel)); | ||||
| return rootJ; | return rootJ; | ||||
| } | } | ||||
| void MidiIO::fromJson(json_t *rootJ) { | void MidiIO::fromJson(json_t *rootJ) { | ||||
| json_t *portNameJ = json_object_get(rootJ, "port"); | |||||
| if (portNameJ) { | |||||
| std::string portName = json_string_value(portNameJ); | |||||
| // Search for port with equal name | |||||
| for (int port = 0; port < getPortCount(); port++) { | |||||
| if (getPortName(port) == portName) { | |||||
| openPort(port); | |||||
| json_t *deviceNameJ = json_object_get(rootJ, "device"); | |||||
| if (deviceNameJ) { | |||||
| std::string deviceName = json_string_value(deviceNameJ); | |||||
| // Search for device with equal name | |||||
| for (int device = 0; device < getDeviceCount(); device++) { | |||||
| if (getDeviceName(device) == deviceName) { | |||||
| openDevice(device); | |||||
| break; | break; | ||||
| } | } | ||||
| } | } | ||||