Tim Mayberry
acd17a9b53
use AVRT_PRIORITY_NORMAL for audio threads and AVRT_PRIORITY_HIGH for MIDI threads
509 lines
14 KiB
C++
509 lines
14 KiB
C++
/*
|
|
* Copyright (C) 2015 Tim Mayberry <mojofunk@gmail.com>
|
|
*
|
|
* 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., 675 Mass Ave, Cambridge, MA 02139, USA.
|
|
*/
|
|
|
|
#include "winmmemidi_output_device.h"
|
|
|
|
#include <glibmm.h>
|
|
|
|
#include "pbd/debug.h"
|
|
#include "pbd/compose.h"
|
|
|
|
#include "rt_thread.h"
|
|
#include "win_utils.h"
|
|
#include "midi_util.h"
|
|
|
|
#include "mmcss.h"
|
|
#include "debug.h"
|
|
|
|
// remove dup with input_device
|
|
static const uint32_t MIDI_BUFFER_SIZE = 32768;
|
|
static const uint32_t MAX_MIDI_MSG_SIZE = 256; // fix this for sysex
|
|
static const uint32_t MAX_QUEUE_SIZE = 4096;
|
|
|
|
namespace ARDOUR {
|
|
|
|
WinMMEMidiOutputDevice::WinMMEMidiOutputDevice (int index)
|
|
: m_handle(0)
|
|
, m_queue_semaphore(0)
|
|
, m_sysex_semaphore(0)
|
|
, m_timer(0)
|
|
, m_started(false)
|
|
, m_enabled(false)
|
|
, m_thread_running(false)
|
|
, m_thread_quit(false)
|
|
, m_midi_buffer(new RingBuffer<uint8_t>(MIDI_BUFFER_SIZE))
|
|
{
|
|
DEBUG_MIDI (string_compose ("Creating midi output device index: %1\n", index));
|
|
|
|
std::string error_msg;
|
|
|
|
if (!open (index, error_msg)) {
|
|
DEBUG_MIDI (error_msg);
|
|
throw std::runtime_error (error_msg);
|
|
}
|
|
|
|
set_device_name (index);
|
|
}
|
|
|
|
WinMMEMidiOutputDevice::~WinMMEMidiOutputDevice ()
|
|
{
|
|
std::string error_msg;
|
|
if (!close (error_msg)) {
|
|
DEBUG_MIDI (error_msg);
|
|
}
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::enqueue_midi_event (uint64_t timestamp,
|
|
const uint8_t* data,
|
|
size_t size)
|
|
{
|
|
const uint32_t total_bytes = sizeof(MidiEventHeader) + size;
|
|
if (m_midi_buffer->write_space () < total_bytes) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: ring buffer overflow\n");
|
|
return false;
|
|
}
|
|
|
|
MidiEventHeader h (timestamp, size);
|
|
m_midi_buffer->write ((uint8_t*)&h, sizeof(MidiEventHeader));
|
|
m_midi_buffer->write (data, size);
|
|
|
|
signal (m_queue_semaphore);
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::open (UINT index, std::string& error_msg)
|
|
{
|
|
MMRESULT result = midiOutOpen (&m_handle,
|
|
index,
|
|
(DWORD_PTR)winmm_output_callback,
|
|
(DWORD_PTR) this,
|
|
CALLBACK_FUNCTION);
|
|
if (result != MMSYSERR_NOERROR) {
|
|
error_msg = get_error_string (result);
|
|
return false;
|
|
}
|
|
|
|
m_queue_semaphore = CreateSemaphore (NULL, 0, MAX_QUEUE_SIZE, NULL);
|
|
if (m_queue_semaphore == NULL) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: Unable to create queue semaphore\n");
|
|
return false;
|
|
}
|
|
m_sysex_semaphore = CreateSemaphore (NULL, 0, 1, NULL);
|
|
if (m_sysex_semaphore == NULL) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: Unable to create sysex semaphore\n");
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::close (std::string& error_msg)
|
|
{
|
|
// return error message for first error encountered?
|
|
bool success = true;
|
|
MMRESULT result = midiOutReset (m_handle);
|
|
if (result != MMSYSERR_NOERROR) {
|
|
error_msg = get_error_string (result);
|
|
DEBUG_MIDI (error_msg);
|
|
success = false;
|
|
}
|
|
result = midiOutClose (m_handle);
|
|
if (result != MMSYSERR_NOERROR) {
|
|
error_msg = get_error_string (result);
|
|
DEBUG_MIDI (error_msg);
|
|
success = false;
|
|
}
|
|
|
|
if (m_sysex_semaphore) {
|
|
if (!CloseHandle (m_sysex_semaphore)) {
|
|
DEBUG_MIDI ("WinMMEMidiOut Unable to close sysex semaphore\n");
|
|
success = false;
|
|
} else {
|
|
m_sysex_semaphore = 0;
|
|
}
|
|
}
|
|
if (m_queue_semaphore) {
|
|
if (!CloseHandle (m_queue_semaphore)) {
|
|
DEBUG_MIDI ("WinMMEMidiOut Unable to close queue semaphore\n");
|
|
success = false;
|
|
} else {
|
|
m_queue_semaphore = 0;
|
|
}
|
|
}
|
|
|
|
m_handle = 0;
|
|
return success;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::set_device_name (UINT index)
|
|
{
|
|
MIDIOUTCAPS capabilities;
|
|
MMRESULT result =
|
|
midiOutGetDevCaps (index, &capabilities, sizeof(capabilities));
|
|
|
|
if (result != MMSYSERR_NOERROR) {
|
|
DEBUG_MIDI (get_error_string (result));
|
|
m_name = "Unknown Midi Output Device";
|
|
return false;
|
|
} else {
|
|
m_name = capabilities.szPname;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
std::string
|
|
WinMMEMidiOutputDevice::get_error_string (MMRESULT error_code)
|
|
{
|
|
char error_msg[MAXERRORLENGTH];
|
|
MMRESULT result = midiOutGetErrorText (error_code, error_msg, MAXERRORLENGTH);
|
|
if (result != MMSYSERR_NOERROR) {
|
|
return error_msg;
|
|
}
|
|
return "WinMMEMidiOutput: Unknown Error code";
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::start ()
|
|
{
|
|
if (m_thread_running) {
|
|
DEBUG_MIDI (
|
|
string_compose ("WinMMEMidiOutput: device %1 already started\n", m_name));
|
|
return true;
|
|
}
|
|
|
|
m_timer = CreateWaitableTimer (NULL, FALSE, NULL);
|
|
|
|
if (!m_timer) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: unable to create waitable timer\n");
|
|
return false;
|
|
}
|
|
|
|
if (!start_midi_output_thread ()) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: Failed to start MIDI output thread\n");
|
|
|
|
if (!CloseHandle (m_timer)) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: unable to close waitable timer\n");
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::stop ()
|
|
{
|
|
if (!m_thread_running) {
|
|
DEBUG_MIDI ("WinMMEMidiOutputDevice: device already stopped\n");
|
|
return true;
|
|
}
|
|
|
|
if (!stop_midi_output_thread ()) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: Failed to start MIDI output thread\n");
|
|
return false;
|
|
}
|
|
|
|
if (!CloseHandle (m_timer)) {
|
|
DEBUG_MIDI ("WinMMEMidiOutput: unable to close waitable timer\n");
|
|
return false;
|
|
}
|
|
m_timer = 0;
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::start_midi_output_thread ()
|
|
{
|
|
m_thread_quit = false;
|
|
|
|
//pthread_attr_t attr;
|
|
size_t stacksize = 100000;
|
|
|
|
// TODO Use native threads
|
|
if (_realtime_pthread_create (SCHED_FIFO, -21, stacksize,
|
|
&m_output_thread_handle, midi_output_thread, this)) {
|
|
return false;
|
|
}
|
|
|
|
int timeout = 5000;
|
|
while (!m_thread_running && --timeout > 0) { Glib::usleep (1000); }
|
|
if (timeout == 0 || !m_thread_running) {
|
|
DEBUG_MIDI (string_compose ("Unable to start midi output device thread: %1\n",
|
|
m_name));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::stop_midi_output_thread ()
|
|
{
|
|
int timeout = 5000;
|
|
m_thread_quit = true;
|
|
|
|
while (m_thread_running && --timeout > 0) { Glib::usleep (1000); }
|
|
if (timeout == 0 || m_thread_running) {
|
|
DEBUG_MIDI (string_compose ("Unable to stop midi output device thread: %1\n",
|
|
m_name));
|
|
return false;
|
|
}
|
|
|
|
void *status;
|
|
if (pthread_join (m_output_thread_handle, &status)) {
|
|
DEBUG_MIDI (string_compose ("Unable to join midi output device thread: %1\n",
|
|
m_name));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::signal (HANDLE semaphore)
|
|
{
|
|
bool result = (bool)ReleaseSemaphore (semaphore, 1, NULL);
|
|
if (!result) {
|
|
DEBUG_MIDI ("WinMMEMidiOutDevice: Cannot release semaphore\n");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::wait (HANDLE semaphore)
|
|
{
|
|
DWORD result = WaitForSingleObject (semaphore, INFINITE);
|
|
switch (result) {
|
|
case WAIT_FAILED:
|
|
DEBUG_MIDI ("WinMMEMidiOutDevice: WaitForSingleObject Failed\n");
|
|
break;
|
|
case WAIT_OBJECT_0:
|
|
return true;
|
|
default:
|
|
DEBUG_MIDI ("WinMMEMidiOutDevice: Unexpected result from WaitForSingleObject\n");
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void CALLBACK
|
|
WinMMEMidiOutputDevice::winmm_output_callback (HMIDIOUT handle,
|
|
UINT msg,
|
|
DWORD_PTR instance,
|
|
DWORD_PTR midi_data,
|
|
DWORD_PTR timestamp)
|
|
{
|
|
((WinMMEMidiOutputDevice*)instance)
|
|
->midi_output_callback (msg, midi_data, timestamp);
|
|
}
|
|
|
|
void
|
|
WinMMEMidiOutputDevice::midi_output_callback (UINT message,
|
|
DWORD_PTR midi_data,
|
|
DWORD_PTR timestamp)
|
|
{
|
|
switch (message) {
|
|
case MOM_CLOSE:
|
|
DEBUG_MIDI ("WinMMEMidiOutput - MIDI device closed\n");
|
|
break;
|
|
case MOM_DONE:
|
|
signal (m_sysex_semaphore);
|
|
break;
|
|
case MOM_OPEN:
|
|
DEBUG_MIDI ("WinMMEMidiOutput - MIDI device opened\n");
|
|
break;
|
|
case MOM_POSITIONCB:
|
|
LPMIDIHDR header = (LPMIDIHDR)midi_data;
|
|
DEBUG_MIDI (string_compose ("WinMMEMidiOut - %1 bytes out of %2 bytes of "
|
|
"the current sysex message have been sent.\n",
|
|
header->dwOffset,
|
|
header->dwBytesRecorded));
|
|
}
|
|
}
|
|
|
|
void*
|
|
WinMMEMidiOutputDevice::midi_output_thread (void *arg)
|
|
{
|
|
WinMMEMidiOutputDevice* output_device = reinterpret_cast<WinMMEMidiOutputDevice*> (arg);
|
|
output_device->midi_output_thread ();
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
WinMMEMidiOutputDevice::midi_output_thread ()
|
|
{
|
|
m_thread_running = true;
|
|
|
|
DEBUG_MIDI ("WinMMEMidiOut: MIDI output thread started\n");
|
|
|
|
#ifdef USE_MMCSS_THREAD_PRIORITIES
|
|
HANDLE task_handle;
|
|
|
|
mmcss::set_thread_characteristics ("Pro Audio", &task_handle);
|
|
mmcss::set_thread_priority (task_handle, mmcss::AVRT_PRIORITY_HIGH);
|
|
#endif
|
|
|
|
while (!m_thread_quit) {
|
|
if (!wait (m_queue_semaphore)) {
|
|
break;
|
|
}
|
|
|
|
MidiEventHeader h (0, 0);
|
|
uint8_t data[MAX_MIDI_MSG_SIZE];
|
|
|
|
const uint32_t read_space = m_midi_buffer->read_space ();
|
|
|
|
if (read_space > sizeof(MidiEventHeader)) {
|
|
if (m_midi_buffer->read ((uint8_t*)&h, sizeof(MidiEventHeader)) !=
|
|
sizeof(MidiEventHeader)) {
|
|
DEBUG_MIDI ("WinMMEMidiOut: Garbled MIDI EVENT HEADER!!\n");
|
|
break;
|
|
}
|
|
assert (read_space >= h.size);
|
|
|
|
if (h.size > MAX_MIDI_MSG_SIZE) {
|
|
m_midi_buffer->increment_read_idx (h.size);
|
|
DEBUG_MIDI ("WinMMEMidiOut: MIDI event too large!\n");
|
|
continue;
|
|
}
|
|
if (m_midi_buffer->read (&data[0], h.size) != h.size) {
|
|
DEBUG_MIDI ("WinMMEMidiOut: Garbled MIDI EVENT DATA!!\n");
|
|
break;
|
|
}
|
|
} else {
|
|
// error/assert?
|
|
DEBUG_MIDI ("WinMMEMidiOut: MIDI buffer underrun, shouldn't occur\n");
|
|
continue;
|
|
}
|
|
uint64_t current_time = utils::get_microseconds ();
|
|
|
|
DEBUG_TIMING (string_compose (
|
|
"WinMMEMidiOut: h.time = %1, current_time = %2\n", h.time, current_time));
|
|
|
|
if (h.time > current_time) {
|
|
|
|
DEBUG_TIMING (string_compose ("WinMMEMidiOut: waiting at %1 for %2 "
|
|
"milliseconds before sending message\n",
|
|
((double)current_time) / 1000.0,
|
|
((double)(h.time - current_time)) / 1000.0));
|
|
|
|
if (!wait_for_microseconds (h.time - current_time))
|
|
{
|
|
DEBUG_MIDI ("WinMMEMidiOut: Error waiting for timer\n");
|
|
break;
|
|
}
|
|
|
|
uint64_t wakeup_time = utils::get_microseconds ();
|
|
DEBUG_TIMING (string_compose ("WinMMEMidiOut: woke up at %1(ms)\n",
|
|
((double)wakeup_time) / 1000.0));
|
|
if (wakeup_time > h.time) {
|
|
DEBUG_TIMING (string_compose ("WinMMEMidiOut: overslept by %1(ms)\n",
|
|
((double)(wakeup_time - h.time)) / 1000.0));
|
|
} else if (wakeup_time < h.time) {
|
|
DEBUG_TIMING (string_compose ("WinMMEMidiOut: woke up %1(ms) too early\n",
|
|
((double)(h.time - wakeup_time)) / 1000.0));
|
|
}
|
|
|
|
} else if (h.time < current_time) {
|
|
DEBUG_TIMING (string_compose (
|
|
"WinMMEMidiOut: MIDI event at sent to driver %1(ms) late\n",
|
|
((double)(current_time - h.time)) / 1000.0));
|
|
}
|
|
|
|
DWORD message = 0;
|
|
MMRESULT result;
|
|
switch (h.size) {
|
|
case 3:
|
|
message |= (((DWORD)data[2]) << 16);
|
|
// Fallthrough on purpose.
|
|
case 2:
|
|
message |= (((DWORD)data[1]) << 8);
|
|
// Fallthrough on purpose.
|
|
case 1:
|
|
message |= (DWORD)data[0];
|
|
result = midiOutShortMsg (m_handle, message);
|
|
if (result != MMSYSERR_NOERROR) {
|
|
DEBUG_MIDI (
|
|
string_compose ("WinMMEMidiOutput: %1\n", get_error_string (result)));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
#if ENABLE_SYSEX
|
|
MIDIHDR header;
|
|
header.dwBufferLength = h.size;
|
|
header.dwFlags = 0;
|
|
header.lpData = (LPSTR)data;
|
|
|
|
result = midiOutPrepareHeader (m_handle, &header, sizeof(MIDIHDR));
|
|
if (result != MMSYSERR_NOERROR) {
|
|
DEBUG_MIDI (string_compose ("WinMMEMidiOutput: midiOutPrepareHeader %1\n",
|
|
get_error_string (result)));
|
|
continue;
|
|
}
|
|
|
|
result = midiOutLongMsg (m_handle, &header, sizeof(MIDIHDR));
|
|
if (result != MMSYSERR_NOERROR) {
|
|
DEBUG_MIDI (string_compose ("WinMMEMidiOutput: midiOutLongMsg %1\n",
|
|
get_error_string (result)));
|
|
continue;
|
|
}
|
|
|
|
// Sysex messages may be sent synchronously or asynchronously. The
|
|
// choice is up to the WinMME driver. So, we wait until the message is
|
|
// sent, regardless of the driver's choice.
|
|
if (!wait (m_sysex_semaphore)) {
|
|
break;
|
|
}
|
|
|
|
result = midiOutUnprepareHeader (m_handle, &header, sizeof(MIDIHDR));
|
|
if (result != MMSYSERR_NOERROR) {
|
|
DEBUG_MIDI (string_compose ("WinMMEMidiOutput: midiOutUnprepareHeader %1\n",
|
|
get_error_string (result)));
|
|
break;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
#ifdef USE_MMCSS_THREAD_PRIORITIES
|
|
mmcss::revert_thread_characteristics (task_handle);
|
|
#endif
|
|
|
|
m_thread_running = false;
|
|
}
|
|
|
|
bool
|
|
WinMMEMidiOutputDevice::wait_for_microseconds (int64_t wait_us)
|
|
{
|
|
LARGE_INTEGER due_time;
|
|
|
|
// 100 ns resolution
|
|
due_time.QuadPart = -((LONGLONG)(wait_us * 10));
|
|
if (!SetWaitableTimer (m_timer, &due_time, 0, NULL, NULL, 0)) {
|
|
DEBUG_MIDI ("WinMMEMidiOut: Error waiting for timer\n");
|
|
return false;
|
|
}
|
|
|
|
if (!wait (m_timer)) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
} // namespace ARDOUR
|