summaryrefslogtreecommitdiff
path: root/libs/backends/portaudio/winmmemidi_output_device.cc
diff options
context:
space:
mode:
Diffstat (limited to 'libs/backends/portaudio/winmmemidi_output_device.cc')
-rw-r--r--libs/backends/portaudio/winmmemidi_output_device.cc494
1 files changed, 494 insertions, 0 deletions
diff --git a/libs/backends/portaudio/winmmemidi_output_device.cc b/libs/backends/portaudio/winmmemidi_output_device.cc
new file mode 100644
index 0000000000..6b31488b00
--- /dev/null
+++ b/libs/backends/portaudio/winmmemidi_output_device.cc
@@ -0,0 +1,494 @@
+/*
+ * 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 "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;
+
+ 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
+ }
+
+ 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