diff options
Diffstat (limited to 'libs/backends/portaudio/winmmemidi_output_device.cc')
-rw-r--r-- | libs/backends/portaudio/winmmemidi_output_device.cc | 494 |
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 |