13
0
livetrax/libs/backends/portaudio/winmmemidi_output_device.cc
Tim Mayberry 4ffe8ffc0f Put Windows timer functions into PBD namespace in preparation for moving them to libpbd
Add functions for get/set the Multimedia timer resolution, although we are
really only interested in the minimum, this will facilitate testing

Put timer utility functions inside nested namespaces as they are platform
specific
2015-09-16 11:22:16 +10:00

510 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 stop 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;
signal (m_queue_semaphore);
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 = PBD::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 = PBD::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