very, very simple WP with midi CC out

This commit is contained in:
Sebastian
2024-10-09 02:01:23 +02:00
commit 3d6fc151f1
126 changed files with 19539 additions and 0 deletions
@@ -0,0 +1,31 @@
#pragma once
#include <libremidi/config.hpp>
#include <chrono>
#include <functional>
namespace libremidi
{
struct winmm_input_configuration
{
int sysex_buffer_size = 1024;
int sysex_buffer_count = 4;
};
struct winmm_output_configuration
{
};
struct poll_parameters
{
std::function<void()> callback;
};
struct winmm_observer_configuration
{
std::chrono::milliseconds poll_period{100};
std::function<void(const poll_parameters&)> manual_poll;
};
}
@@ -0,0 +1,81 @@
#pragma once
#define NOMINMAX 1
#define WIN32_LEAN_AND_MEAN 1
#include <libremidi/detail/midi_api.hpp>
#include <algorithm>
// clang-format off
#include <windows.h>
#include <mmsystem.h>
// clang-format on
namespace libremidi
{
// Convert a nullptr-terminated wide string or ANSI-encoded string to UTF-8.
inline std::string ConvertToUTF8(const TCHAR* str)
{
std::string u8str;
const WCHAR* wstr = L"";
#if defined(UNICODE) || defined(_UNICODE)
wstr = str;
#else
// Convert from ANSI encoding to wide string
int wlength = MultiByteToWideChar(CP_ACP, 0, str, -1, nullptr, 0);
std::wstring wstrtemp;
if (wlength)
{
wstrtemp.assign(wlength - 1, 0);
MultiByteToWideChar(CP_ACP, 0, str, -1, &wstrtemp[0], wlength);
wstr = &wstrtemp[0];
}
#endif
// Convert from wide string to UTF-8
int length = WideCharToMultiByte(CP_UTF8, 0, wstr, -1, nullptr, 0, nullptr, nullptr);
if (length)
{
u8str.assign(static_cast<std::string::size_type>(length - 1), 0);
/*length =*/WideCharToMultiByte(CP_UTF8, 0, wstr, -1, &u8str[0], length, nullptr, nullptr);
}
return u8str;
}
// Next functions add the portNumber to the name so that
// the device's names are sure to be listed with individual names
// even when they have the same brand name
inline void MakeUniqueInPortName(std::string& deviceName, unsigned int portNumber)
{
int x = 1;
for (unsigned int i = 0; i < portNumber; i++)
{
MIDIINCAPS deviceCaps;
midiInGetDevCaps(i, &deviceCaps, sizeof(MIDIINCAPS));
auto stringName = ConvertToUTF8(deviceCaps.szPname);
if (deviceName == stringName)
{
x++;
}
}
deviceName += " ";
deviceName += std::to_string(x);
}
inline void MakeUniqueOutPortName(std::string& deviceName, unsigned int portNumber)
{
int x = 1;
for (unsigned int i = 0; i < portNumber; i++)
{
MIDIOUTCAPS deviceCaps;
midiOutGetDevCaps(i, &deviceCaps, sizeof(MIDIOUTCAPS));
auto stringName = ConvertToUTF8(deviceCaps.szPname);
if (deviceName == stringName)
{
x++;
}
}
deviceName += " ";
deviceName += std::to_string(x);
}
}
@@ -0,0 +1,309 @@
#pragma once
#include <libremidi/backends/winmm/config.hpp>
#include <libremidi/backends/winmm/helpers.hpp>
#include <libremidi/backends/winmm/observer.hpp>
#include <libremidi/detail/midi_in.hpp>
#include <libremidi/detail/midi_stream_decoder.hpp>
namespace libremidi
{
class midi_in_winmm final
: public midi1::in_api
, public error_handler
{
public:
struct
: input_configuration
, winmm_input_configuration
{
} configuration;
explicit midi_in_winmm(input_configuration&& conf, winmm_input_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
// We'll issue a warning here if no devices are available but not
// throw an error since the user can plugin something later.
if (midiInGetNumDevs() == 0)
{
warning(
configuration, "midi_in_winmm::initialize: no MIDI input devices currently available.");
}
if (!InitializeCriticalSectionAndSpinCount(&(this->_mutex), 0x00000400))
{
warning(
configuration,
"midi_in_winmm::initialize: InitializeCriticalSectionAndSpinCount failed.");
}
}
~midi_in_winmm() override
{
// Close a connection if it exists.
midi_in_winmm::close_port();
DeleteCriticalSection(&(this->_mutex));
}
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_in_winmm: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_in_winmm: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_in_winmm: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_MM; }
bool do_open(unsigned int portNumber)
{
MMRESULT result = midiInOpen(
&this->inHandle, portNumber, std::bit_cast<DWORD_PTR>(&midiInputCallback),
std::bit_cast<DWORD_PTR>(this), CALLBACK_FUNCTION);
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_in_winmm::open_port: error creating Windows MM MIDI input port.");
return false;
}
// Allocate and init the sysex buffers.
const auto bufferCount = static_cast<std::size_t>(configuration.sysex_buffer_count);
this->sysexBuffer.resize(bufferCount);
for (std::size_t i = 0; i < bufferCount; ++i)
{
this->sysexBuffer[i] = new MIDIHDR;
this->sysexBuffer[i]->lpData = new char[configuration.sysex_buffer_size];
this->sysexBuffer[i]->dwBufferLength = static_cast<DWORD>(configuration.sysex_buffer_size);
this->sysexBuffer[i]->dwUser = i; // We use the dwUser parameter as buffer indicator
this->sysexBuffer[i]->dwFlags = 0;
result = midiInPrepareHeader(this->inHandle, this->sysexBuffer[i], sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
midiInClose(this->inHandle);
this->inHandle = nullptr;
error<driver_error>(
configuration,
"midi_in_winmm::open_port: error starting Windows MM MIDI input port "
"(PrepareHeader).");
return false;
}
// Register the buffer.
result = midiInAddBuffer(this->inHandle, this->sysexBuffer[i], sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
midiInClose(this->inHandle);
this->inHandle = nullptr;
error<driver_error>(
configuration,
"midi_in_winmm::open_port: error starting Windows MM MIDI input port "
"(AddBuffer).");
return false;
}
}
result = midiInStart(this->inHandle);
midi_start_timestamp = std::chrono::steady_clock::now();
if (result != MMSYSERR_NOERROR)
{
midiInClose(this->inHandle);
this->inHandle = nullptr;
error<driver_error>(
configuration, "midi_in_winmm::open_port: error starting Windows MM MIDI input port.");
return false;
}
return true;
}
bool open_port(const input_port& p, std::string_view) override
{
observer_winmm obs{{}, winmm_observer_configuration{}};
auto ports = obs.get_input_ports();
// First check with the display name, e.g. MIDI KEYBOARD 2 will match MIDI KEYBOARD 2
for (auto& port : ports)
{
if (p.display_name == port.display_name)
return do_open(port.port);
}
// If nothing is found, try to check with the raw name
for (auto& port : ports)
{
if (p.port_name == port.port_name)
return do_open(port.port);
}
error<invalid_parameter_error>(
configuration, "midi_in_winmm::open_port: port not found: " + p.port_name);
return false;
}
void close_port() override
{
if (connected_)
{
EnterCriticalSection(&(this->_mutex));
midiInReset(this->inHandle);
midiInStop(this->inHandle);
for (std::size_t i = 0; i < static_cast<std::size_t>(configuration.sysex_buffer_count); ++i)
{
MMRESULT res{};
int wait_count = 5;
while (
((res = midiInUnprepareHeader(this->inHandle, this->sysexBuffer[i], sizeof(MIDIHDR)))
== MIDIERR_STILLPLAYING)
&& wait_count-- >= 0)
{
Sleep(1);
}
if (res != MMSYSERR_NOERROR)
{
warning(
configuration,
"midi_in_winmm::open_port: error closing Windows MM MIDI input "
"port (midiInUnprepareHeader).");
continue;
}
else
{
delete[] this->sysexBuffer[i]->lpData;
delete[] this->sysexBuffer[i];
}
}
midiInClose(this->inHandle);
this->inHandle = nullptr;
LeaveCriticalSection(&(this->_mutex));
}
}
private:
timestamp absolute_timestamp() const noexcept override
{
return std::chrono::duration_cast<std::chrono::nanoseconds>(
std::chrono::steady_clock::now() - midi_start_timestamp)
.count();
}
static constexpr int bytes_for_message(uint8_t status)
{
if (status < 0xC0)
return 3;
else if (status < 0xE0)
return 2;
else if (status < 0xF0)
return 3;
else if (status == 0xF1)
return 2;
else if (status == 0xF2)
return 3;
else if (status == 0xF3)
return 2;
else if (status == 0xF8)
return 1;
else if (status == 0xFE)
return 1;
else
return 0;
}
static void CALLBACK midiInputCallback(
HMIDIIN /*hmin*/, UINT inputStatus, DWORD_PTR instancePtr, DWORD_PTR midiMessage,
DWORD_PTR timestamp)
{
if (inputStatus != MIM_DATA && inputStatus != MIM_LONGDATA && inputStatus != MIM_LONGERROR)
return;
auto& self = *reinterpret_cast<midi_in_winmm*>(instancePtr);
static constexpr timestamp_backend_info timestamp_info{
.has_absolute_timestamps = true,
.absolute_is_monotonic = false,
.has_samples = false,
};
const auto to_ns = [timestamp] { return timestamp * 1'000'000; };
if (inputStatus == MIM_DATA)
{
// Channel or system message
uint8_t message[sizeof(DWORD_PTR)];
memcpy(message, &midiMessage, sizeof(DWORD_PTR));
// Make sure the first byte is a status byte.
if (message[0] & 0x80)
{
self.m_processing.on_bytes(
{message, message + bytes_for_message(message[0])},
self.m_processing.timestamp<timestamp_info>(to_ns, 0));
}
}
else
{
// Sysex message ( MIM_LONGDATA or MIM_LONGERROR )
const auto* sysex = reinterpret_cast<MIDIHDR*>(midiMessage);
if(inputStatus == MIM_LONGERROR)
{
self.m_processing.message.bytes.clear();
self.m_processing.state = self.m_processing.main;
}
else if (!self.configuration.ignore_sysex)
{
if(sysex->dwBytesRecorded > 0)
{
const auto sysex_bytes = reinterpret_cast<uint8_t*>(sysex->lpData);
self.m_processing.on_bytes(
{sysex_bytes, sysex_bytes + sysex->dwBytesRecorded},
self.m_processing.timestamp<timestamp_info>(to_ns, 0));
}
}
// The WinMM API requires that the sysex buffer be requeued after
// input of each sysex message. Even if we are ignoring sysex
// messages, we still need to requeue the buffer in case the user
// decides to not ignore sysex messages in the future. However,
// it seems that WinMM calls this function with an empty sysex
// buffer when an application closes and in this case, we should
// avoid requeueing it, else the computer suddenly reboots after
// one or two minutes.
if (self.sysexBuffer[sysex->dwUser]->dwBytesRecorded > 0)
{
EnterCriticalSection(&(self._mutex));
MMRESULT result
= midiInAddBuffer(self.inHandle, self.sysexBuffer[sysex->dwUser], sizeof(MIDIHDR));
LeaveCriticalSection(&(self._mutex));
if (result != MMSYSERR_NOERROR)
{
#if defined(__LIBREMIDI_DEBUG__)
std::cerr << "\nmidi_in::midiInputCallback: error sending sysex to "
"Midi device!!\n\n";
#endif
}
}
}
}
HMIDIIN inHandle; // Handle to Midi Input Device
std::vector<LPMIDIHDR> sysexBuffer;
// [Patrice] see
// https://groups.google.com/forum/#!topic/mididev/6OUjHutMpEo
CRITICAL_SECTION _mutex;
std::chrono::steady_clock::time_point midi_start_timestamp;
midi1::input_state_machine m_processing{this->configuration};
};
}
@@ -0,0 +1,181 @@
#pragma once
#include <libremidi/backends/winmm/config.hpp>
#include <libremidi/backends/winmm/helpers.hpp>
#include <libremidi/detail/midi_out.hpp>
namespace libremidi
{
class midi_out_winmm final
: public midi1::out_api
, public error_handler
{
public:
struct
: output_configuration
, winmm_output_configuration
{
} configuration;
midi_out_winmm(output_configuration&& conf, winmm_output_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
// We'll issue a warning here if no devices are available but not
// throw an error since the user can plug something in later.
if (midiOutGetNumDevs() == 0)
{
warning(
configuration,
"midi_out_winmm::initialize: no MIDI output devices currently "
"available.");
}
}
~midi_out_winmm() override
{
// Close a connection if it exists.
midi_out_winmm::close_port();
}
bool open_virtual_port(std::string_view) override
{
warning(configuration, "midi_out_winmm: open_virtual_port unsupported");
return false;
}
void set_client_name(std::string_view) override
{
warning(configuration, "midi_out_winmm: set_client_name unsupported");
}
void set_port_name(std::string_view) override
{
warning(configuration, "midi_out_winmm: set_port_name unsupported");
}
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_MM; }
[[nodiscard]] bool do_open(unsigned int portNumber)
{
MMRESULT result = midiOutOpen(&this->outHandle, portNumber, 0, 0, CALLBACK_NULL);
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration,
"midi_out_winmm::open_port: error creating Windows MM MIDI output "
"port.");
return false;
}
return true;
}
bool open_port(const output_port& p, std::string_view) override
{
observer_winmm obs{{}, winmm_observer_configuration{}};
auto ports = obs.get_output_ports();
// First check with the display name, e.g. MIDI KEYBOARD 2 will match MIDI KEYBOARD 2
for (auto& port : ports)
{
if (p.display_name == port.display_name)
return do_open(port.port);
}
// If nothing is found, try to check with the raw name
for (auto& port : ports)
{
if (p.port_name == port.port_name)
return do_open(port.port);
}
error<invalid_parameter_error>(
configuration, "midi_out_winmm::open_port: port not found: " + p.port_name);
return false;
}
void close_port() override
{
if (this->outHandle)
midiOutClose(this->outHandle);
this->outHandle = nullptr;
connected_ = false;
}
void send_message(const unsigned char* message, size_t size) override
{
if (!connected_)
return;
if (size == 0)
{
warning(configuration, "midi_out_winmm::send_message: message argument is empty!");
return;
}
if (message[0] == 0xF0)
{ // Sysex message
buffer.assign(message, message + size);
// FIXME this can be made asynchronous... see Chrome source.
// But need to know whe buffers are freed.
// Create and prepare MIDIHDR structure.
MIDIHDR sysex{};
sysex.lpData = (LPSTR)buffer.data();
sysex.dwBufferLength = size;
sysex.dwFlags = 0;
auto result = midiOutPrepareHeader(this->outHandle, &sysex, sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_out_winmm::send_message: error preparing sysex header.");
return;
}
// Send the message.
result = midiOutLongMsg(this->outHandle, &sysex, sizeof(MIDIHDR));
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_out_winmm::send_message: error sending sysex message.");
return;
}
// Unprepare the buffer and MIDIHDR.
// FIXME yuck
while (MIDIERR_STILLPLAYING
== midiOutUnprepareHeader(this->outHandle, &sysex, sizeof(MIDIHDR)))
Sleep(1);
}
else
{ // Channel or system message.
// Make sure the message size isn't too big.
if (size > 3)
{
warning(
configuration,
"midi_out_winmm::send_message: message size is greater than 3 bytes "
"(and not sysex)!");
return;
}
// Pack MIDI bytes into double word.
DWORD packet;
std::copy_n(message, size, (unsigned char*)&packet);
// Send the message immediately.
auto result = midiOutShortMsg(this->outHandle, packet);
if (result != MMSYSERR_NOERROR)
{
error<driver_error>(
configuration, "midi_out_winmm::send_message: error sending MIDI message.");
}
}
}
private:
HMIDIOUT outHandle; // Handle to Midi Output Device
std::vector<char> buffer;
};
}
@@ -0,0 +1,271 @@
#pragma once
#include <libremidi/backends/winmm/config.hpp>
#include <libremidi/backends/winmm/helpers.hpp>
#include <libremidi/detail/observer.hpp>
#include <condition_variable>
#include <mutex>
#include <ranges>
#include <thread>
namespace libremidi
{
class observer_winmm : public observer_api
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_winmm(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: configuration{std::move(conf), std::move(apiconf)}
{
if (!configuration.has_callbacks())
return;
if (configuration.notify_in_constructor)
check_new_ports<true>();
else
check_new_ports<false>();
}
~observer_winmm() { }
libremidi::API get_current_api() const noexcept override { return libremidi::API::WINDOWS_MM; }
std::vector<input_port> get_input_ports() const noexcept override
{
return get_port_list<true>();
}
std::vector<output_port> get_output_ports() const noexcept override
{
return get_port_list<false>();
}
protected:
template <bool Notify>
void check_new_ports()
{
auto currInputPortList = get_port_list<true>();
if constexpr (Notify)
{
compare_port_lists_and_notify_clients(
inputPortList, currInputPortList, configuration.input_added,
configuration.input_removed);
}
inputPortList = std::move(currInputPortList);
auto currOutputPortList = get_port_list<false>();
if constexpr (Notify)
{
compare_port_lists_and_notify_clients(
outputPortList, currOutputPortList, configuration.output_added,
configuration.output_removed);
}
outputPortList = std::move(currOutputPortList);
}
void compare_port_lists_and_notify_clients(
const auto& prevList, const auto& currList, const auto& portAddedFunc,
const auto& portRemovedFunc)
{
if (portAddedFunc)
{
for (const auto& port : currList)
{
auto iter
= std::ranges::find(prevList, port.display_name, &port_information::display_name);
if (iter == prevList.end())
portAddedFunc(port);
}
}
if (portRemovedFunc)
{
for (const auto& port : prevList)
{
auto iter
= std::ranges::find(currList, port.display_name, &port_information::display_name);
if (iter == currList.end())
portRemovedFunc(port);
}
}
}
input_port to_in_port_info(std::size_t index) const noexcept
{
MIDIINCAPS deviceCaps;
midiInGetDevCaps(index, &deviceCaps, sizeof(MIDIINCAPS));
auto rawName = ConvertToUTF8(deviceCaps.szPname);
auto portName = rawName;
MakeUniqueInPortName(portName, index);
return {
{.client = 0,
.port = index,
.manufacturer = "",
.device_name = "",
.port_name = rawName,
.display_name = portName}};
}
output_port to_out_port_info(std::size_t index) const noexcept
{
MIDIOUTCAPS deviceCaps;
midiOutGetDevCaps(index, &deviceCaps, sizeof(MIDIOUTCAPS));
auto rawName = ConvertToUTF8(deviceCaps.szPname);
auto portName = rawName;
MakeUniqueOutPortName(portName, index);
return {
{.client = 0,
.port = index,
.manufacturer = "",
.device_name = "",
.port_name = rawName,
.display_name = portName}};
}
template <bool Input>
auto get_port_list() const noexcept
-> std::vector<std::conditional_t<Input, input_port, output_port>>
{
std::vector<std::conditional_t<Input, input_port, output_port>> portList;
if constexpr (Input)
{
std::size_t nDevices = midiInGetNumDevs();
for (std::size_t i = 0; i < nDevices; ++i)
{
portList.push_back(to_in_port_info(i));
}
}
else
{
std::size_t nDevices = midiOutGetNumDevs();
for (std::size_t i = 0; i < nDevices; ++i)
{
portList.push_back(to_out_port_info(i));
}
}
return portList;
}
static constexpr bool INPUT = true;
static constexpr bool OUTPUT = false;
std::vector<input_port> inputPortList;
std::vector<output_port> outputPortList;
};
}
#if __has_include(<stop_token>) && __cpp_lib_jthread >= 201911L
#include <stop_token>
namespace libremidi::winmm
{
class observer_threaded final : public observer_winmm
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_threaded(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: observer_winmm{std::move(conf), std::move(apiconf)}
{
thread = std::jthread([this](std::stop_token tk) {
while (!tk.stop_requested())
{
check_new_ports<true>();
std::this_thread::sleep_for(this->configuration.poll_period);
}
});
}
private:
std::jthread thread;
};
}
#else
#include <atomic>
#include <semaphore>
namespace libremidi::winmm
{
class observer_threaded final : public observer_winmm
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_threaded(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: observer_winmm{std::move(conf), std::move(apiconf)}
, sema{0}
{
thread = std::thread([this] {
while (!stop_flag.test(std::memory_order_acquire))
{
check_new_ports<true>();
std::this_thread::sleep_for(this->configuration.poll_period);
}
sema.release();
});
}
~observer_threaded()
{
stop_flag.test_and_set();
sema.acquire();
thread.join();
}
private:
std::thread thread;
std::atomic_flag stop_flag = ATOMIC_FLAG_INIT;
std::binary_semaphore sema;
};
}
#endif
namespace libremidi::winmm
{
class observer_manual final : public observer_winmm
{
public:
struct
: observer_configuration
, winmm_observer_configuration
{
} configuration;
explicit observer_manual(observer_configuration&& conf, winmm_observer_configuration&& apiconf)
: observer_winmm{std::move(conf), std::move(apiconf)}
{
this->configuration.manual_poll({.callback = [this] { this->check_new_ports<true>(); }});
}
~observer_manual() { }
};
}
namespace libremidi
{
template <>
inline std::unique_ptr<observer_api> make<observer_winmm>(
libremidi::observer_configuration&& conf, libremidi::winmm_observer_configuration&& api)
{
if (api.manual_poll)
return std::make_unique<winmm::observer_manual>(std::move(conf), std::move(api));
else
return std::make_unique<winmm::observer_threaded>(std::move(conf), std::move(api));
}
}