123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547 |
- /*
- * MidiInOut.cpp
- * -------------
- * Purpose: A plugin for sending and receiving MIDI data.
- * Notes : (currently none)
- * Authors: Johannes Schultz (OpenMPT Devs)
- * The OpenMPT source code is released under the BSD license. Read LICENSE for more details.
- */
- #include "stdafx.h"
- #include "MidiInOut.h"
- #include "MidiInOutEditor.h"
- #include "../../common/FileReader.h"
- #include "../../soundlib/Sndfile.h"
- #include "../Reporting.h"
- #include <algorithm>
- #include <sstream>
- #ifdef MODPLUG_TRACKER
- #include "../Mptrack.h"
- #endif
- #include "mpt/io/io.hpp"
- #include "mpt/io/io_stdstream.hpp"
- OPENMPT_NAMESPACE_BEGIN
- IMixPlugin* MidiInOut::Create(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct)
- {
- try
- {
- return new (std::nothrow) MidiInOut(factory, sndFile, mixStruct);
- } catch(RtMidiError &)
- {
- return nullptr;
- }
- }
- MidiInOut::MidiInOut(VSTPluginLib &factory, CSoundFile &sndFile, SNDMIXPLUGIN *mixStruct)
- : IMidiPlugin(factory, sndFile, mixStruct)
- , m_inputDevice(m_midiIn)
- , m_outputDevice(m_midiOut)
- #ifdef MODPLUG_TRACKER
- , m_programName(_T("Default"))
- #endif // MODPLUG_TRACKER
- {
- m_mixBuffer.Initialize(2, 2);
- InsertIntoFactoryList();
- }
- MidiInOut::~MidiInOut()
- {
- MidiInOut::Suspend();
- }
- uint32 MidiInOut::GetLatency() const
- {
- // There is only a latency if the user-provided latency value is greater than the negative output latency.
- return mpt::saturate_round<uint32>(std::min(0.0, m_latency + GetOutputLatency()) * m_SndFile.GetSampleRate());
- }
- void MidiInOut::SaveAllParameters()
- {
- auto chunk = GetChunk(false);
- if(chunk.empty())
- return;
- m_pMixStruct->defaultProgram = -1;
- m_pMixStruct->pluginData.assign(chunk.begin(), chunk.end());
- }
- void MidiInOut::RestoreAllParameters(int32 program)
- {
- IMixPlugin::RestoreAllParameters(program); // First plugin version didn't use chunks.
- SetChunk(mpt::as_span(m_pMixStruct->pluginData), false);
- }
- enum ChunkFlags
- {
- kLatencyCompensation = 0x01, // Implicit in current plugin version
- kLatencyPresent = 0x02, // Latency value is present as double-precision float
- kIgnoreTiming = 0x04, // Do not send timing and sequencing information
- kFriendlyInputName = 0x08, // Preset also stores friendly name of input device
- kFriendlyOutputName = 0x10, // Preset also stores friendly name of output device
- };
- IMixPlugin::ChunkData MidiInOut::GetChunk(bool /*isBank*/)
- {
- const std::string programName8 = mpt::ToCharset(mpt::Charset::UTF8, m_programName);
- uint32 flags = kLatencyCompensation | kLatencyPresent | (m_sendTimingInfo ? 0 : kIgnoreTiming);
- #ifdef MODPLUG_TRACKER
- const std::string inFriendlyName = (m_inputDevice.index == MidiDevice::NO_MIDI_DEVICE) ? m_inputDevice.name : mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, m_inputDevice.name), true, false));
- const std::string outFriendlyName = (m_outputDevice.index == MidiDevice::NO_MIDI_DEVICE) ? m_outputDevice.name : mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, m_outputDevice.name), false, false));
- if(inFriendlyName != m_inputDevice.name)
- {
- flags |= kFriendlyInputName;
- }
- if(outFriendlyName != m_outputDevice.name)
- {
- flags |= kFriendlyOutputName;
- }
- #endif
- std::ostringstream s;
- mpt::IO::WriteRaw(s, "fEvN", 4); // VST program chunk magic
- mpt::IO::WriteIntLE< int32>(s, GetVersion());
- mpt::IO::WriteIntLE<uint32>(s, 1); // Number of programs
- mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(programName8.size()));
- mpt::IO::WriteIntLE<uint32>(s, m_inputDevice.index);
- mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(m_inputDevice.name.size()));
- mpt::IO::WriteIntLE<uint32>(s, m_outputDevice.index);
- mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(m_outputDevice.name.size()));
- mpt::IO::WriteIntLE<uint32>(s, flags);
- mpt::IO::WriteRaw(s, programName8.c_str(), programName8.size());
- mpt::IO::WriteRaw(s, m_inputDevice.name.c_str(), m_inputDevice.name.size());
- mpt::IO::WriteRaw(s, m_outputDevice.name.c_str(), m_outputDevice.name.size());
- mpt::IO::WriteIntLE<uint64>(s, IEEE754binary64LE(m_latency).GetInt64());
- #ifdef MODPLUG_TRACKER
- if(flags & kFriendlyInputName)
- {
- mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(inFriendlyName.size()));
- mpt::IO::WriteRaw(s, inFriendlyName.c_str(), inFriendlyName.size());
- }
- if(flags & kFriendlyOutputName)
- {
- mpt::IO::WriteIntLE<uint32>(s, static_cast<uint32>(outFriendlyName.size()));
- mpt::IO::WriteRaw(s, outFriendlyName.c_str(), outFriendlyName.size());
- }
- #endif
- m_chunkData = s.str();
- return mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(m_chunkData));
- }
- // Try to match a port name against stored name or friendly name (preferred)
- static void FindPort(MidiDevice::ID &id, unsigned int numPorts, const std::string &name, const std::string &friendlyName, MidiDevice &midiDevice, bool isInput)
- {
- bool foundFriendly = false;
- for(unsigned int i = 0; i < numPorts; i++)
- {
- try
- {
- auto portName = midiDevice.GetPortName(i);
- bool deviceNameMatches = (portName == name);
- #ifdef MODPLUG_TRACKER
- if(!friendlyName.empty() && friendlyName == mpt::ToCharset(mpt::Charset::UTF8, theApp.GetFriendlyMIDIPortName(mpt::ToUnicode(mpt::Charset::UTF8, portName), isInput, false)))
- {
- // Preferred match
- id = i;
- foundFriendly = true;
- if(deviceNameMatches)
- {
- return;
- }
- }
- #else
- MPT_UNREFERENCED_PARAMETER(friendlyName)
- #endif
- if(deviceNameMatches && !foundFriendly)
- {
- id = i;
- }
- } catch(const RtMidiError &)
- {
- }
- }
- }
- void MidiInOut::SetChunk(const ChunkData &chunk, bool /*isBank*/)
- {
- FileReader file(chunk);
- if(!file.CanRead(9 * sizeof(uint32))
- || !file.ReadMagic("fEvN") // VST program chunk magic
- || file.ReadInt32LE() > GetVersion() // Plugin version
- || file.ReadUint32LE() < 1) // Number of programs
- return;
- uint32 nameStrSize = file.ReadUint32LE();
- MidiDevice::ID inID = file.ReadUint32LE();
- uint32 inStrSize = file.ReadUint32LE();
- MidiDevice::ID outID = file.ReadUint32LE();
- uint32 outStrSize = file.ReadUint32LE();
- uint32 flags = file.ReadUint32LE();
- std::string progName, inName, outName, inFriendlyName, outFriendlyName;
- file.ReadString<mpt::String::maybeNullTerminated>(progName, nameStrSize);
- m_programName = mpt::ToCString(mpt::Charset::UTF8, progName);
- file.ReadString<mpt::String::maybeNullTerminated>(inName, inStrSize);
- file.ReadString<mpt::String::maybeNullTerminated>(outName, outStrSize);
- if(flags & kLatencyPresent)
- m_latency = file.ReadDoubleLE();
- else
- m_latency = 0.0f;
- m_sendTimingInfo = !(flags & kIgnoreTiming);
- if(flags & kFriendlyInputName)
- file.ReadString<mpt::String::maybeNullTerminated>(inFriendlyName, file.ReadUint32LE());
- if(flags & kFriendlyOutputName)
- file.ReadString<mpt::String::maybeNullTerminated>(outFriendlyName, file.ReadUint32LE());
- // Try to match an input port name against stored name or friendly name (preferred)
- FindPort(inID, m_midiIn.getPortCount(), inName, inFriendlyName, m_inputDevice, true);
- FindPort(outID, m_midiOut.getPortCount(), outName, outFriendlyName, m_outputDevice, false);
- SetParameter(MidiInOut::kInputParameter, DeviceIDToParameter(inID));
- SetParameter(MidiInOut::kOutputParameter, DeviceIDToParameter(outID));
- }
- void MidiInOut::SetParameter(PlugParamIndex index, PlugParamValue value)
- {
- value = mpt::safe_clamp(value, 0.0f, 1.0f);
- MidiDevice::ID newDevice = ParameterToDeviceID(value);
- OpenDevice(newDevice, (index == kInputParameter));
- // Update selection in editor
- MidiInOutEditor *editor = dynamic_cast<MidiInOutEditor *>(GetEditor());
- if(editor != nullptr)
- editor->SetCurrentDevice((index == kInputParameter), newDevice);
- }
- float MidiInOut::GetParameter(PlugParamIndex index)
- {
- const MidiDevice &device = (index == kInputParameter) ? m_inputDevice : m_outputDevice;
- return DeviceIDToParameter(device.index);
- }
- #ifdef MODPLUG_TRACKER
- CString MidiInOut::GetParamName(PlugParamIndex param)
- {
- if(param == kInputParameter)
- return _T("MIDI In");
- else
- return _T("MIDI Out");
- }
- // Parameter value as text
- CString MidiInOut::GetParamDisplay(PlugParamIndex param)
- {
- const MidiDevice &device = (param == kInputParameter) ? m_inputDevice : m_outputDevice;
- return mpt::ToCString(mpt::Charset::UTF8, device.name);
- }
- CAbstractVstEditor *MidiInOut::OpenEditor()
- {
- try
- {
- return new MidiInOutEditor(*this);
- } catch(mpt::out_of_memory e)
- {
- mpt::delete_out_of_memory(e);
- return nullptr;
- }
- }
- #endif // MODPLUG_TRACKER
- // Processing (we don't process any audio, only MIDI messages)
- void MidiInOut::Process(float *, float *, uint32 numFrames)
- {
- if(m_midiOut.isPortOpen())
- {
- mpt::lock_guard<mpt::mutex> lock(m_mutex);
- // Send MIDI clock
- if(m_nextClock < 1)
- {
- if(m_sendTimingInfo)
- {
- m_outQueue.push_back(Message(GetOutputTimestamp(), 0xF8));
- }
- double bpm = m_SndFile.GetCurrentBPM();
- if(bpm > 0.0)
- {
- m_nextClock += 2.5 * m_SndFile.GetSampleRate() / bpm;
- }
- }
- m_nextClock -= numFrames;
- double now = m_clock.Now() * (1.0 / 1000.0);
- auto message = m_outQueue.begin();
- while(message != m_outQueue.end() && message->m_time <= now)
- {
- try
- {
- m_midiOut.sendMessage(message->m_message, message->m_size);
- } catch(const RtMidiError &)
- {
- }
- message++;
- }
- m_outQueue.erase(m_outQueue.begin(), message);
- }
- }
- void MidiInOut::InputCallback(double /*deltatime*/, std::vector<unsigned char> &message)
- {
- // We will check the bypass status before passing on the message, and not before entering the function,
- // because otherwise we might read garbage if we toggle bypass status in the middle of a SysEx message.
- bool isBypassed = IsBypassed();
- if(message.empty())
- {
- return;
- } else if(!m_bufferedInput.empty())
- {
- // SysEx message (continued)
- m_bufferedInput.insert(m_bufferedInput.end(), message.begin(), message.end());
- if(message.back() == 0xF7)
- {
- // End of message found!
- if(!isBypassed)
- ReceiveSysex(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(m_bufferedInput)));
- m_bufferedInput.clear();
- }
- } else if(message.front() == 0xF0)
- {
- // Start of SysEx message...
- if(message.back() != 0xF7)
- m_bufferedInput.insert(m_bufferedInput.end(), message.begin(), message.end()); // ...but not the end!
- else if(!isBypassed)
- ReceiveSysex(mpt::byte_cast<mpt::const_byte_span>(mpt::as_span(message)));
- } else if(!isBypassed)
- {
- // Regular message
- uint32 msg = 0;
- memcpy(&msg, message.data(), std::min(message.size(), sizeof(msg)));
- ReceiveMidi(msg);
- }
- }
- // Resume playback
- void MidiInOut::Resume()
- {
- // Resume MIDI I/O
- m_isResumed = true;
- m_nextClock = 0;
- m_outQueue.clear();
- m_clock.SetResolution(1);
- OpenDevice(m_inputDevice.index, true);
- OpenDevice(m_outputDevice.index, false);
- if(m_midiOut.isPortOpen() && m_sendTimingInfo)
- {
- MidiSend(0xFA); // Start
- }
- }
- // Stop playback
- void MidiInOut::Suspend()
- {
- // Suspend MIDI I/O
- if(m_midiOut.isPortOpen() && m_sendTimingInfo)
- {
- try
- {
- unsigned char message[1] = { 0xFC }; // Stop
- m_midiOut.sendMessage(message, 1);
- } catch(const RtMidiError &)
- {
- }
- }
- //CloseDevice(inputDevice);
- CloseDevice(m_outputDevice);
- m_clock.SetResolution(0);
- m_isResumed = false;
- }
- // Playback discontinuity
- void MidiInOut::PositionChanged()
- {
- if(m_sendTimingInfo)
- {
- MidiSend(0xFC); // Stop
- MidiSend(0xFA); // Start
- }
- }
- void MidiInOut::Bypass(bool bypass)
- {
- if(bypass)
- {
- mpt::lock_guard<mpt::mutex> lock(m_mutex);
- m_outQueue.clear();
- }
- IMidiPlugin::Bypass(bypass);
- }
- bool MidiInOut::MidiSend(uint32 midiCode)
- {
- if(!m_midiOut.isPortOpen() || IsBypassed())
- {
- // We need an output device to send MIDI messages to.
- return true;
- }
- mpt::lock_guard<mpt::mutex> lock(m_mutex);
- m_outQueue.push_back(Message(GetOutputTimestamp(), &midiCode, 3));
- return true;
- }
- bool MidiInOut::MidiSysexSend(mpt::const_byte_span sysex)
- {
- if(!m_midiOut.isPortOpen() || IsBypassed())
- {
- // We need an output device to send MIDI messages to.
- return true;
- }
- mpt::lock_guard<mpt::mutex> lock(m_mutex);
- m_outQueue.push_back(Message(GetOutputTimestamp(), sysex.data(), sysex.size()));
- return true;
- }
- void MidiInOut::HardAllNotesOff()
- {
- const bool wasSuspended = !IsResumed();
- if(wasSuspended)
- {
- Resume();
- }
- for(uint8 mc = 0; mc < std::size(m_MidiCh); mc++) //all midi chans
- {
- PlugInstrChannel &channel = m_MidiCh[mc];
- channel.ResetProgram();
- SendMidiPitchBend(mc, EncodePitchBendParam(MIDIEvents::pitchBendCentre)); // centre pitch bend
- MidiSend(MIDIEvents::CC(MIDIEvents::MIDICC_AllSoundOff, mc, 0)); // all sounds off
- for(size_t i = 0; i < std::size(channel.noteOnMap); i++)
- {
- for(auto &c : channel.noteOnMap[i])
- {
- while(c != 0)
- {
- MidiSend(MIDIEvents::NoteOff(mc, static_cast<uint8>(i), 0));
- c--;
- }
- }
- }
- }
- if(wasSuspended)
- {
- Suspend();
- }
- }
- // Open a device for input or output.
- void MidiInOut::OpenDevice(MidiDevice::ID newDevice, bool asInputDevice)
- {
- MidiDevice &device = asInputDevice ? m_inputDevice : m_outputDevice;
- if(device.index == newDevice && device.stream.isPortOpen())
- {
- // No need to re-open this device.
- return;
- }
- CloseDevice(device);
- device.index = newDevice;
- device.stream.closePort();
- if(device.index == kNoDevice)
- {
- // Dummy device
- device.name = "<none>";
- return;
- }
- device.name = device.GetPortName(newDevice);
- //if(m_isResumed)
- {
- mpt::lock_guard<mpt::mutex> lock(m_mutex);
-
- try
- {
- device.stream.openPort(newDevice);
- if(asInputDevice)
- {
- m_midiIn.setCallback(InputCallback, this);
- m_midiIn.ignoreTypes(false, true, true);
- }
- } catch(RtMidiError &error)
- {
- device.name = "Unavailable";
- MidiInOutEditor *editor = dynamic_cast<MidiInOutEditor *>(GetEditor());
- if(editor != nullptr)
- {
- Reporting::Error("MIDI device cannot be opened. Is it open in another application?\n\n" + error.getMessage(), "MIDI Input / Output", editor);
- }
- }
- }
- }
- // Close an active device.
- void MidiInOut::CloseDevice(MidiDevice &device)
- {
- if(device.stream.isPortOpen())
- {
- mpt::lock_guard<mpt::mutex> lock(m_mutex);
- device.stream.closePort();
- }
- }
- // Calculate the current output timestamp
- double MidiInOut::GetOutputTimestamp() const
- {
- return m_clock.Now() * (1.0 / 1000.0) + GetOutputLatency() + m_latency;
- }
- // Get a device name
- std::string MidiDevice::GetPortName(MidiDevice::ID port)
- {
- return stream.getPortName(port);
- }
- OPENMPT_NAMESPACE_END
|