From 5360cd73f434cbcb7505cd792459ae1f59433c77 Mon Sep 17 00:00:00 2001 From: tildearrow Date: Mon, 28 Mar 2022 03:46:50 -0500 Subject: [PATCH] earliest MIDI input! (no note input tho) --- CMakeLists.txt | 2 + TODO.md | 15 ++ amongus/RtMidiConfigUninstall.cmake.in | 21 +++ src/audio/abstract.cpp | 58 +++++++ src/audio/jack.cpp | 1 + src/audio/midi.cpp | 61 ++++++++ src/audio/rtmidi.cpp | 200 +++++++++++++++++++++++++ src/audio/rtmidi.h | 28 +++- src/audio/sdl.cpp | 1 + src/audio/taAudio.h | 50 ++++--- src/engine/engine.cpp | 38 +++++ src/engine/engine.h | 8 + src/engine/playback.cpp | 25 ++++ src/gui/gui.h | 6 +- src/gui/settings.cpp | 38 ++++- 15 files changed, 525 insertions(+), 27 deletions(-) create mode 100644 TODO.md create mode 100644 amongus/RtMidiConfigUninstall.cmake.in create mode 100644 src/audio/midi.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 80120b85c..3b9b5729a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -177,6 +177,7 @@ endif() set(AUDIO_SOURCES src/audio/abstract.cpp +src/audio/midi.cpp src/audio/sdl.cpp ) @@ -199,6 +200,7 @@ endif() if (USE_RTMIDI) list(APPEND AUDIO_SOURCES src/audio/rtmidi.cpp) message(STATUS "Building with RtMidi") + list(APPEND DEPENDENCIES_DEFINES HAVE_RTMIDI) else() message(STATUS "Building without RtMidi") endif() diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..256b81d1e --- /dev/null +++ b/TODO.md @@ -0,0 +1,15 @@ +# to-do for 0.6 + +- **auto-backup. please.** +- non-monospaced fonts in pattern editor +- panning macro +- pitch macro +- sample editor + - keybinds + - don't allow editing samples that aren't 8/16-bit + - maybe an hex editor +- piano/input pad +- MIDI input +- all the systems +- Also confused as to why after playing a row with Ctrl-Enter, you are locked out of moving the cursor outside of +/- 1 position and THEN typing something places it under the actual cursor, and not the ghost cursor. it seems like just a way to get "please can we play rows" people to be quiet because it's otherwise effectively useless +- edit latch! and maybe separate edit masks diff --git a/amongus/RtMidiConfigUninstall.cmake.in b/amongus/RtMidiConfigUninstall.cmake.in new file mode 100644 index 000000000..db894b3fc --- /dev/null +++ b/amongus/RtMidiConfigUninstall.cmake.in @@ -0,0 +1,21 @@ +if(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + message(FATAL_ERROR "Cannot find install manifest: \"@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt\"") +endif(NOT EXISTS "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt") + +file(READ "@CMAKE_CURRENT_BINARY_DIR@/install_manifest.txt" files) +string(REGEX REPLACE "\n" ";" files "${files}") +foreach(file ${files}) + message(STATUS "Uninstalling \"$ENV{DESTDIR}${file}\"") + if(EXISTS "$ENV{DESTDIR}${file}") + exec_program( + "@CMAKE_COMMAND@" ARGS "-E remove \"$ENV{DESTDIR}${file}\"" + OUTPUT_VARIABLE rm_out + RETURN_VALUE rm_retval + ) + if(NOT "${rm_retval}" STREQUAL 0) + message(FATAL_ERROR "Problem when removing \"$ENV{DESTDIR}${file}\"") + endif(NOT "${rm_retval}" STREQUAL 0) + else(EXISTS "$ENV{DESTDIR}${file}") + message(STATUS "File \"$ENV{DESTDIR}${file}\" does not exist.") + endif(EXISTS "$ENV{DESTDIR}${file}") +endforeach(file) diff --git a/src/audio/abstract.cpp b/src/audio/abstract.cpp index 7f9404828..d4973180a 100644 --- a/src/audio/abstract.cpp +++ b/src/audio/abstract.cpp @@ -55,4 +55,62 @@ bool TAAudio::init(TAAudioDesc& request, TAAudioDesc& response) { } TAAudio::~TAAudio() { +} + +bool TAMidiIn::gather() { + return false; +} + +bool TAMidiIn::isDeviceOpen() { + return false; +} + +bool TAMidiOut::isDeviceOpen() { + return false; +} + +bool TAMidiIn::openDevice(String name) { + return false; +} + +bool TAMidiOut::openDevice(String name) { + return false; +} + +bool TAMidiIn::closeDevice() { + return false; +} + +bool TAMidiOut::closeDevice() { + return false; +} + +std::vector TAMidiIn::listDevices() { + return std::vector(); +} + +std::vector TAMidiOut::listDevices() { + return std::vector(); +} + +bool TAMidiIn::init() { + return false; +} + +bool TAMidiOut::init() { + return false; +} + +bool TAMidiIn::quit() { + return true; +} + +bool TAMidiOut::quit() { + return true; +} + +TAMidiIn::~TAMidiIn() { +} + +TAMidiOut::~TAMidiOut() { } \ No newline at end of file diff --git a/src/audio/jack.cpp b/src/audio/jack.cpp index 080dad2c5..33a464d51 100644 --- a/src/audio/jack.cpp +++ b/src/audio/jack.cpp @@ -52,6 +52,7 @@ void TAAudioJACK::onBufferSize(jack_nframes_t bufsize) { void TAAudioJACK::onProcess(jack_nframes_t nframes) { if (audioProcCallback!=NULL) { + if (midiIn!=NULL) midiIn->gather(); audioProcCallback(audioProcCallbackUser,inBufs,outBufs,desc.inChans,desc.outChans,desc.bufsize); } for (int i=0; iinit()) { + delete midiIn; + midiIn=NULL; + return false; + } + + if (!midiOut->init()) { + midiIn->quit(); + delete midiOut; + delete midiIn; + midiOut=NULL; + midiIn=NULL; + return false; + } + return true; +#endif +} + +void TAAudio::quitMidi() { + if (midiIn!=NULL) { + midiIn->quit(); + delete midiIn; + midiIn=NULL; + } + if (midiOut!=NULL) { + midiOut->quit(); + delete midiOut; + midiOut=NULL; + } +} \ No newline at end of file diff --git a/src/audio/rtmidi.cpp b/src/audio/rtmidi.cpp index e8f0c60d6..a9b2d4b11 100644 --- a/src/audio/rtmidi.cpp +++ b/src/audio/rtmidi.cpp @@ -1 +1,201 @@ +/** + * Furnace Tracker - multi-system chiptune tracker + * Copyright (C) 2021-2022 tildearrow and contributors + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along + * with this program; if not, write to the Free Software Foundation, Inc., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ + #include "rtmidi.h" +#include "../ta-log.h" + +// --- IN --- + +bool TAMidiInRtMidi::gather() { + std::vector msg; + if (port==NULL) return false; + while (true) { + TAMidiMessage m; + double t=port->getMessage(&msg); + if (msg.empty()) break; + + // parse message + m.time=t; + m.type=msg[0]; + if (m.type!=TA_MIDI_SYSEX && msg.size()>1) { + memcpy(m.data,msg.data()+1,MIN(msg.size()-1,7)); + } + queue.push(m); + } + return true; +} + +std::vector TAMidiInRtMidi::listDevices() { + std::vector ret; + logD("listing devices.\n"); + if (port==NULL) return ret; + + try { + unsigned int count=port->getPortCount(); + logD("got port count.\n"); + for (unsigned int i=0; igetPortName(i); + if (name!="") ret.push_back(name); + } + } catch (RtMidiError& e) { + logW("could not get MIDI inputs! %s\n",e.what()); + } + return ret; +} + +bool TAMidiInRtMidi::isDeviceOpen() { + return isOpen; +} + +bool TAMidiInRtMidi::openDevice(String name) { + if (port==NULL) return false; + if (isOpen) return false; + try { + bool portOpen=false; + unsigned int count=port->getPortCount(); + for (unsigned int i=0; igetPortName(i)==name) { + port->openPort(i); + portOpen=true; + break; + } + } + isOpen=portOpen; + return portOpen; + } catch (RtMidiError& e) { + logW("could not open MIDI in device! %s\n",e.what()); + return false; + } + return true; +} + +bool TAMidiInRtMidi::closeDevice() { + if (port==NULL) return false; + if (!isOpen) return false; + try { + port->closePort(); + } catch (RtMidiError& e) { + logW("could not close MIDI in device! %s\n",e.what()); + isOpen=false; // still + return false; + } + isOpen=false; + return true; +} + +bool TAMidiInRtMidi::init() { + if (port!=NULL) return true; + try { + port=new RtMidiIn; + } catch (RtMidiError& e) { + logW("could not initialize RtMidi in! %s\n",e.what()); + return false; + } + return true; +} + +bool TAMidiInRtMidi::quit() { + if (port!=NULL) { + delete port; + port=NULL; + } + return true; +} + +// --- OUT --- + +bool TAMidiOutRtMidi::send(TAMidiMessage& what) { + // TODO + return false; +} + +bool TAMidiOutRtMidi::isDeviceOpen() { + return isOpen; +} + +bool TAMidiOutRtMidi::openDevice(String name) { + if (port==NULL) return false; + if (isOpen) return false; + try { + bool portOpen=false; + unsigned int count=port->getPortCount(); + for (unsigned int i=0; igetPortName(i)==name) { + port->openPort(i); + portOpen=true; + break; + } + } + isOpen=portOpen; + return portOpen; + } catch (RtMidiError& e) { + logW("could not open MIDI out device! %s\n",e.what()); + return false; + } + return true; +} + +bool TAMidiOutRtMidi::closeDevice() { + if (port==NULL) return false; + if (!isOpen) return false; + try { + port->closePort(); + } catch (RtMidiError& e) { + logW("could not close MIDI out device! %s\n",e.what()); + isOpen=false; // still + return false; + } + isOpen=false; + return true; +} + +std::vector TAMidiOutRtMidi::listDevices() { + std::vector ret; + if (port==NULL) return ret; + + try { + unsigned int count=port->getPortCount(); + for (unsigned int i=0; igetPortName(i); + if (name!="") ret.push_back(name); + } + } catch (RtMidiError& e) { + logW("could not get MIDI outputs! %s\n",e.what()); + } + return ret; +} + +bool TAMidiOutRtMidi::init() { + if (port!=NULL) return true; + try { + port=new RtMidiOut; + } catch (RtMidiError& e) { + logW("could not initialize RtMidi out! %s\n",e.what()); + return false; + } + return true; +} + +bool TAMidiOutRtMidi::quit() { + if (port!=NULL) { + delete port; + port=NULL; + } + return true; +} \ No newline at end of file diff --git a/src/audio/rtmidi.h b/src/audio/rtmidi.h index 0803c3fcd..31d7119b1 100644 --- a/src/audio/rtmidi.h +++ b/src/audio/rtmidi.h @@ -21,9 +21,33 @@ #include "taAudio.h" class TAMidiInRtMidi: public TAMidiIn { - + RtMidiIn* port; + bool isOpen; + public: + bool gather(); + bool isDeviceOpen(); + bool openDevice(String name); + bool closeDevice(); + std::vector listDevices(); + bool quit(); + bool init(); + TAMidiInRtMidi(): + port(NULL), + isOpen(false) {} }; class TAMidiOutRtMidi: public TAMidiOut { - + RtMidiOut* port; + bool isOpen; + public: + bool send(TAMidiMessage& what); + bool isDeviceOpen(); + bool openDevice(String name); + bool closeDevice(); + std::vector listDevices(); + bool quit(); + bool init(); + TAMidiOutRtMidi(): + port(NULL), + isOpen(false) {} }; \ No newline at end of file diff --git a/src/audio/sdl.cpp b/src/audio/sdl.cpp index 9f330fd59..7fbbceb55 100644 --- a/src/audio/sdl.cpp +++ b/src/audio/sdl.cpp @@ -30,6 +30,7 @@ void taSDLProcess(void* inst, unsigned char* buf, int nframes) { void TAAudioSDL::onProcess(unsigned char* buf, int nframes) { if (audioProcCallback!=NULL) { + if (midiIn!=NULL) midiIn->gather(); audioProcCallback(audioProcCallbackUser,inBufs,outBufs,desc.inChans,desc.outChans,desc.bufsize); } float* fbuf=(float*)buf; diff --git a/src/audio/taAudio.h b/src/audio/taAudio.h index 1f8c32305..f532e2a7e 100644 --- a/src/audio/taAudio.h +++ b/src/audio/taAudio.h @@ -90,28 +90,9 @@ enum TAMidiMessageTypes { }; struct TAMidiMessage { + double time; unsigned char type; - union { - struct { - unsigned char note, vol; - } note; - struct { - unsigned char which, val; - } control; - unsigned char patch; - unsigned char pressure; - struct { - unsigned char low, high; - } pitch; - struct { - unsigned int vendor; - } sysEx; - unsigned char timeCode; - struct { - unsigned char low, high; - } position; - unsigned char song; - } data; + unsigned char data[7]; unsigned char* sysExData; size_t sysExLen; @@ -119,6 +100,7 @@ struct TAMidiMessage { void done(); TAMidiMessage(): + time(0.0), type(0), sysExData(NULL), sysExLen(0) { @@ -127,16 +109,34 @@ struct TAMidiMessage { }; class TAMidiIn { - std::queue queue; public: + std::queue queue; virtual bool gather(); bool next(TAMidiMessage& where); + virtual bool isDeviceOpen(); + virtual bool openDevice(String name); + virtual bool closeDevice(); + virtual std::vector listDevices(); + virtual bool init(); + virtual bool quit(); + TAMidiIn() { + } + virtual ~TAMidiIn(); }; class TAMidiOut { std::queue queue; public: bool send(TAMidiMessage& what); + virtual bool isDeviceOpen(); + virtual bool openDevice(String name); + virtual bool closeDevice(); + virtual std::vector listDevices(); + virtual bool init(); + virtual bool quit(); + TAMidiOut() { + } + virtual ~TAMidiOut(); }; class TAAudio { @@ -162,6 +162,8 @@ class TAAudio { virtual bool quit(); virtual bool setRun(bool run); virtual std::vector listAudioDevices(); + bool initMidi(bool jack); + void quitMidi(); virtual bool init(TAAudioDesc& request, TAAudioDesc& response); TAAudio(): @@ -172,7 +174,9 @@ class TAAudio { outBufs(NULL), audioProcCallback(NULL), sampleRateChanged(NULL), - bufferSizeChanged(NULL) {} + bufferSizeChanged(NULL), + midiIn(NULL), + midiOut(NULL) {} virtual ~TAAudio(); }; diff --git a/src/engine/engine.cpp b/src/engine/engine.cpp index 4973b44f6..431146f1a 100644 --- a/src/engine/engine.cpp +++ b/src/engine/engine.cpp @@ -2653,10 +2653,24 @@ std::vector& DivEngine::getAudioDevices() { return audioDevs; } +std::vector& DivEngine::getMidiIns() { + return midiIns; +} + +std::vector& DivEngine::getMidiOuts() { + return midiOuts; +} + void DivEngine::rescanAudioDevices() { audioDevs.clear(); if (output!=NULL) { audioDevs=output->listAudioDevices(); + if (output->midiIn!=NULL) { + midiIns=output->midiIn->listDevices(); + } + if (output->midiOut!=NULL) { + midiOuts=output->midiOut->listDevices(); + } } } @@ -2786,11 +2800,35 @@ bool DivEngine::initAudioBackend() { return false; } + if (output->initMidi(false)) { + midiIns=output->midiIn->listDevices(); + midiOuts=output->midiOut->listDevices(); + } else { + logW("error while initializing MIDI!\n"); + } + if (output->midiIn) { + String inName=getConfString("midiInDevice",""); + if (!inName.empty()) { + // try opening device + logI("opening MIDI input.\n"); + if (!output->midiIn->openDevice(inName)) { + logW("could not open MIDI input device!\n"); + } + } + } + return true; } bool DivEngine::deinitAudioBackend() { if (output!=NULL) { + if (output->midiIn) { + if (output->midiIn->isDeviceOpen()) { + logI("closing MIDI input.\n"); + output->midiIn->closeDevice(); + } + } + output->quitMidi(); output->quit(); delete output; output=NULL; diff --git a/src/engine/engine.h b/src/engine/engine.h index 0751941ee..8e20dd418 100644 --- a/src/engine/engine.h +++ b/src/engine/engine.h @@ -214,6 +214,8 @@ class DivEngine { String lastError; String warnings; std::vector audioDevs; + std::vector midiIns; + std::vector midiOuts; std::vector cmdStream; struct SamplePreview { @@ -570,6 +572,12 @@ class DivEngine { // get available audio devices std::vector& getAudioDevices(); + // get available MIDI inputs + std::vector& getMidiIns(); + + // get available MIDI inputs + std::vector& getMidiOuts(); + // rescan audio devices void rescanAudioDevices(); diff --git a/src/engine/playback.cpp b/src/engine/playback.cpp index 3d990bb2d..515ae8e6d 100644 --- a/src/engine/playback.cpp +++ b/src/engine/playback.cpp @@ -1537,7 +1537,32 @@ void DivEngine::nextBuf(float** in, float** out, int inChans, int outChans, unsi isBusy.lock(); } got.bufsize=size; + + // process MIDI events (TODO: everything) + if (output->midiIn) while (!output->midiIn->queue.empty()) { + TAMidiMessage& msg=output->midiIn->queue.front(); + int chan=msg.type&15; + switch (msg.type&0xf0) { + case TA_MIDI_NOTE_OFF: { + if (chan<0 || chan>=chans) break; + pendingNotes.push(DivNoteEvent(msg.type&15,-1,-1,-1,false)); + break; + } + case TA_MIDI_NOTE_ON: { + if (chan<0 || chan>=chans) break; + pendingNotes.push(DivNoteEvent(msg.type&15,-1,(int)msg.data[0]-12,msg.data[1],true)); + break; + } + case TA_MIDI_PROGRAM: { + // TODO: change instrument event thingy + break; + } + } + logD("%.2x\n",msg.type); + output->midiIn->queue.pop(); + } + // process audio if (out!=NULL && ((sPreview.sample>=0 && sPreview.sample<(int)song.sample.size()) || (sPreview.wave>=0 && sPreview.wave<(int)song.wave.size()))) { unsigned int samp_bbOff=0; unsigned int prevAvail=blip_samples_avail(samp_bb); diff --git a/src/gui/gui.h b/src/gui/gui.h index 3b6056f4f..1b06d1526 100644 --- a/src/gui/gui.h +++ b/src/gui/gui.h @@ -628,6 +628,8 @@ class FurnaceGUI { String mainFontPath; String patFontPath; String audioDevice; + String midiInDevice; + String midiOutDevice; Settings(): mainFontSize(18), @@ -679,7 +681,9 @@ class FurnaceGUI { maxUndoSteps(100), mainFontPath(""), patFontPath(""), - audioDevice("") {} + audioDevice(""), + midiInDevice(""), + midiOutDevice("") {} } settings; char finalLayoutPath[4096]; diff --git a/src/gui/settings.cpp b/src/gui/settings.cpp index 9484762b8..c2281d633 100644 --- a/src/gui/settings.cpp +++ b/src/gui/settings.cpp @@ -222,7 +222,7 @@ void FurnaceGUI::drawSettings() { ImGui::EndTabItem(); } - if (ImGui::BeginTabItem("Audio")) { + if (ImGui::BeginTabItem("Audio/MIDI")) { ImGui::Text("Backend"); ImGui::SameLine(); ImGui::Combo("##Backend",&settings.audioEngine,audioBackends,2); @@ -286,6 +286,38 @@ void FurnaceGUI::drawSettings() { ImGui::Text("want: %d samples @ %.0fHz\n",audioWant.bufsize,audioWant.rate); ImGui::Text("got: %d samples @ %.0fHz\n",audioGot.bufsize,audioGot.rate); + ImGui::Separator(); + + ImGui::Text("MIDI input"); + ImGui::SameLine(); + String midiInName=settings.midiInDevice.empty()?"":settings.midiInDevice; + if (ImGui::BeginCombo("##MidiInDevice",midiInName.c_str())) { + if (ImGui::Selectable("",settings.midiInDevice.empty())) { + settings.midiInDevice=""; + } + for (String& i: e->getMidiIns()) { + if (ImGui::Selectable(i.c_str(),i==settings.midiInDevice)) { + settings.midiInDevice=i; + } + } + ImGui::EndCombo(); + } + + ImGui::Text("MIDI output"); + ImGui::SameLine(); + String midiOutName=settings.midiOutDevice.empty()?"":settings.midiOutDevice; + if (ImGui::BeginCombo("##MidiOutDevice",midiOutName.c_str())) { + if (ImGui::Selectable("",settings.midiOutDevice.empty())) { + settings.midiOutDevice=""; + } + for (String& i: e->getMidiIns()) { + if (ImGui::Selectable(i.c_str(),i==settings.midiOutDevice)) { + settings.midiOutDevice=i; + } + } + ImGui::EndCombo(); + } + ImGui::EndTabItem(); } if (ImGui::BeginTabItem("Emulation")) { @@ -970,6 +1002,8 @@ void FurnaceGUI::syncSettings() { settings.iconSize=e->getConfInt("iconSize",16); settings.audioEngine=(e->getConfString("audioEngine","SDL")=="SDL")?1:0; settings.audioDevice=e->getConfString("audioDevice",""); + settings.midiInDevice=e->getConfString("midiInDevice",""); + settings.midiOutDevice=e->getConfString("midiOutDevice",""); settings.audioQuality=e->getConfInt("audioQuality",0); settings.audioBufSize=e->getConfInt("audioBufSize",1024); settings.audioRate=e->getConfInt("audioRate",44100); @@ -1252,6 +1286,8 @@ void FurnaceGUI::commitSettings() { e->setConf("iconSize",settings.iconSize); e->setConf("audioEngine",String(audioBackends[settings.audioEngine])); e->setConf("audioDevice",settings.audioDevice); + e->setConf("midiInDevice",settings.midiInDevice); + e->setConf("midiOutDevice",settings.midiOutDevice); e->setConf("audioQuality",settings.audioQuality); e->setConf("audioBufSize",settings.audioBufSize); e->setConf("audioRate",settings.audioRate);