// Copyright 2013 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "media/midi/midi_manager_win.h" #include // Prevent unnecessary functions from being included from #define MMNODRV #define MMNOSOUND #define MMNOWAVE #define MMNOAUX #define MMNOMIXER #define MMNOTIMER #define MMNOJOY #define MMNOMCI #define MMNOMMIO #include #include "base/bind.h" #include "base/debug/trace_event.h" #include "base/message_loop/message_loop.h" #include "base/strings/string_number_conversions.h" #include "base/strings/utf_string_conversions.h" #include "base/threading/thread.h" #include "media/midi/midi_message_queue.h" #include "media/midi/midi_message_util.h" #include "media/midi/midi_port_info.h" namespace media { namespace { std::string GetInErrorMessage(MMRESULT result) { wchar_t text[MAXERRORLENGTH]; MMRESULT get_result = midiInGetErrorText(result, text, arraysize(text)); if (get_result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to get error message." << " original error: " << result << " midiInGetErrorText error: " << get_result; return std::string(); } return base::WideToUTF8(text); } std::string GetOutErrorMessage(MMRESULT result) { wchar_t text[MAXERRORLENGTH]; MMRESULT get_result = midiOutGetErrorText(result, text, arraysize(text)); if (get_result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to get error message." << " original error: " << result << " midiOutGetErrorText error: " << get_result; return std::string(); } return base::WideToUTF8(text); } class MIDIHDRDeleter { public: void operator()(MIDIHDR* header) { if (!header) return; delete[] static_cast(header->lpData); header->lpData = NULL; header->dwBufferLength = 0; delete header; } }; typedef scoped_ptr ScopedMIDIHDR; ScopedMIDIHDR CreateMIDIHDR(size_t size) { ScopedMIDIHDR header(new MIDIHDR); ZeroMemory(header.get(), sizeof(*header)); header->lpData = new char[size]; header->dwBufferLength = size; return header.Pass(); } void SendShortMidiMessageInternal(HMIDIOUT midi_out_handle, const std::vector& message) { if (message.size() >= 4) return; DWORD packed_message = 0; for (size_t i = 0; i < message.size(); ++i) packed_message |= (static_cast(message[i]) << (i * 8)); MMRESULT result = midiOutShortMsg(midi_out_handle, packed_message); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to output short message: " << GetOutErrorMessage(result); } void SendLongMidiMessageInternal(HMIDIOUT midi_out_handle, const std::vector& message) { // Implementation note: // Sending long MIDI message can be performed synchronously or asynchronously // depending on the driver. There are 2 options to support both cases: // 1) Call midiOutLongMsg() API and wait for its completion within this // function. In this approach, we can avoid memory copy by directly pointing // |message| as the data buffer to be sent. // 2) Allocate a buffer and copy |message| to it, then call midiOutLongMsg() // API. The buffer will be freed in the MOM_DONE event hander, which tells // us that the task of midiOutLongMsg() API is completed. // Here we choose option 2) in favor of asynchronous design. // Note for built-in USB-MIDI driver: // From an observation on Windows 7/8.1 with a USB-MIDI keyboard, // midiOutLongMsg() will be always blocked. Sending 64 bytes or less data // takes roughly 300 usecs. Sending 2048 bytes or more data takes roughly // |message.size() / (75 * 1024)| secs in practice. Here we put 60 KB size // limit on SysEx message, with hoping that midiOutLongMsg will be blocked at // most 1 sec or so with a typical USB-MIDI device. const size_t kSysExSizeLimit = 60 * 1024; if (message.size() >= kSysExSizeLimit) { DVLOG(1) << "Ingnoreing SysEx message due to the size limit" << ", size = " << message.size(); return; } ScopedMIDIHDR midi_header(CreateMIDIHDR(message.size())); for (size_t i = 0; i < message.size(); ++i) midi_header->lpData[i] = static_cast(message[i]); MMRESULT result = midiOutPrepareHeader( midi_out_handle, midi_header.get(), sizeof(*midi_header)); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to prepare output buffer: " << GetOutErrorMessage(result); return; } result = midiOutLongMsg( midi_out_handle, midi_header.get(), sizeof(*midi_header)); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to output long message: " << GetOutErrorMessage(result); result = midiOutUnprepareHeader( midi_out_handle, midi_header.get(), sizeof(*midi_header)); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to uninitialize output buffer: " << GetOutErrorMessage(result); return; } // The ownership of |midi_header| is moved to MOM_DONE event handler. midi_header.release(); } } // namespace class MidiManagerWin::InDeviceInfo { public: ~InDeviceInfo() { Uninitialize(); } void set_port_index(int index) { port_index_ = index; } int port_index() const { return port_index_; } bool device_to_be_closed() const { return device_to_be_closed_; } HMIDIIN midi_handle() const { return midi_handle_; } static scoped_ptr Create(MidiManagerWin* manager, UINT device_id) { scoped_ptr obj(new InDeviceInfo(manager)); if (!obj->Initialize(device_id)) obj.reset(); return obj.Pass(); } private: static const int kInvalidPortIndex = -1; static const size_t kBufferLength = 32 * 1024; explicit InDeviceInfo(MidiManagerWin* manager) : manager_(manager), port_index_(kInvalidPortIndex), midi_handle_(NULL), started_(false), device_to_be_closed_(false) { } bool Initialize(DWORD device_id) { Uninitialize(); midi_header_ = CreateMIDIHDR(kBufferLength); // Here we use |CALLBACK_FUNCTION| to subscribe MIM_DATA, MIM_LONGDATA, and // MIM_CLOSE events. // - MIM_DATA: This is the only way to get a short MIDI message with // timestamp information. // - MIM_LONGDATA: This is the only way to get a long MIDI message with // timestamp information. // - MIM_CLOSE: This event is sent when 1) midiInClose() is called, or 2) // the MIDI device becomes unavailable for some reasons, e.g., the cable // is disconnected. As for the former case, HMIDIOUT will be invalidated // soon after the callback is finished. As for the later case, however, // HMIDIOUT continues to be valid until midiInClose() is called. MMRESULT result = midiInOpen(&midi_handle_, device_id, reinterpret_cast(&HandleMessage), reinterpret_cast(this), CALLBACK_FUNCTION); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to open output device. " << " id: " << device_id << " message: " << GetInErrorMessage(result); return false; } result = midiInPrepareHeader( midi_handle_, midi_header_.get(), sizeof(*midi_header_)); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to initialize input buffer: " << GetInErrorMessage(result); return false; } result = midiInAddBuffer( midi_handle_, midi_header_.get(), sizeof(*midi_header_)); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to attach input buffer: " << GetInErrorMessage(result); return false; } result = midiInStart(midi_handle_); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to start input port: " << GetInErrorMessage(result); return false; } started_ = true; start_time_ = base::TimeTicks::Now(); return true; } void Uninitialize() { MMRESULT result = MMSYSERR_NOERROR; if (midi_handle_ && started_) { result = midiInStop(midi_handle_); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to stop input port: " << GetInErrorMessage(result); started_ = false; start_time_ = base::TimeTicks(); } if (midi_handle_) { // midiInReset flushes pending messages. We ignore these messages. device_to_be_closed_ = true; result = midiInReset(midi_handle_); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to reset input port: " << GetInErrorMessage(result); result = midiInClose(midi_handle_); device_to_be_closed_ = false; DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to close input port: " << GetInErrorMessage(result); midi_header_.reset(); midi_handle_ = NULL; port_index_ = kInvalidPortIndex; } } static void CALLBACK HandleMessage(HMIDIIN midi_in_handle, UINT message, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2) { // This method can be called back on any thread depending on Windows // multimedia subsystem and underlying MIDI drivers. InDeviceInfo* self = reinterpret_cast(instance); if (!self) return; if (self->midi_handle() != midi_in_handle) return; switch (message) { case MIM_DATA: self->OnShortMessageReceived(static_cast(param1 & 0xff), static_cast((param1 >> 8) & 0xff), static_cast((param1 >> 16) & 0xff), param2); return; case MIM_LONGDATA: self->OnLongMessageReceived(reinterpret_cast(param1), param2); return; case MIM_CLOSE: // TODO(yukawa): Implement crbug.com/279097. return; } } void OnShortMessageReceived(uint8 status_byte, uint8 first_data_byte, uint8 second_data_byte, DWORD elapsed_ms) { if (device_to_be_closed()) return; const size_t len = GetMidiMessageLength(status_byte); if (len == 0 || port_index() == kInvalidPortIndex) return; const uint8 kData[] = { status_byte, first_data_byte, second_data_byte }; DCHECK_LE(len, arraysize(kData)); OnMessageReceived(kData, len, elapsed_ms); } void OnLongMessageReceived(MIDIHDR* header, DWORD elapsed_ms) { if (header != midi_header_.get()) return; MMRESULT result = MMSYSERR_NOERROR; if (device_to_be_closed()) { if (midi_header_ && (midi_header_->dwFlags & MHDR_PREPARED) == MHDR_PREPARED) { result = midiInUnprepareHeader( midi_handle_, midi_header_.get(), sizeof(*midi_header_)); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to uninitialize input buffer: " << GetInErrorMessage(result); } return; } if (header->dwBytesRecorded > 0 && port_index() != kInvalidPortIndex) { OnMessageReceived(reinterpret_cast(header->lpData), header->dwBytesRecorded, elapsed_ms); } result = midiInAddBuffer(midi_handle_, header, sizeof(*header)); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to attach input port: " << GetInErrorMessage(result); } void OnMessageReceived(const uint8* data, size_t length, DWORD elapsed_ms) { // MIM_DATA/MIM_LONGDATA message treats the time when midiInStart() is // called as the origin of |elapsed_ms|. // http://msdn.microsoft.com/en-us/library/windows/desktop/dd757284.aspx // http://msdn.microsoft.com/en-us/library/windows/desktop/dd757286.aspx const base::TimeTicks event_time = start_time_ + base::TimeDelta::FromMilliseconds(elapsed_ms); // MidiManager::ReceiveMidiData() expects |timestamp| as the elapsed seconds // from base::TimeTicks::Now(). // TODO(yukawa): Update MidiManager::ReceiveMidiData() so that it can // receive |event_time| directly if the precision of base::TimeTicks is // sufficient. const double timestamp = (event_time - base::TimeTicks()).InSecondsF(); manager_->ReceiveMidiData(port_index_, data, length, timestamp); } MidiManagerWin* manager_; int port_index_; HMIDIIN midi_handle_; ScopedMIDIHDR midi_header_; base::TimeTicks start_time_; bool started_; bool device_to_be_closed_; DISALLOW_COPY_AND_ASSIGN(InDeviceInfo); }; class MidiManagerWin::OutDeviceInfo { public: ~OutDeviceInfo() { Uninitialize(); } static scoped_ptr Create(UINT device_id) { scoped_ptr obj(new OutDeviceInfo); if (!obj->Initialize(device_id)) obj.reset(); return obj.Pass(); } HMIDIOUT midi_handle() const { return midi_handle_; } void Quit() { quitting_ = true; } void Send(const std::vector& data) { // Check if the attached device is still available or not. if (!midi_handle_) return; // Give up sending MIDI messages here if the device is already closed. // Note that this check is optional. Regardless of that we check |closed_| // or not, nothing harmful happens as long as |midi_handle_| is still valid. if (closed_) return; // MIDI Running status must be filtered out. MidiMessageQueue message_queue(false); message_queue.Add(data); std::vector message; while (!quitting_) { message_queue.Get(&message); if (message.empty()) break; // SendShortMidiMessageInternal can send a MIDI message up to 3 bytes. if (message.size() <= 3) SendShortMidiMessageInternal(midi_handle_, message); else SendLongMidiMessageInternal(midi_handle_, message); } } private: OutDeviceInfo() : midi_handle_(NULL), closed_(false), quitting_(false) {} bool Initialize(DWORD device_id) { Uninitialize(); // Here we use |CALLBACK_FUNCTION| to subscribe MOM_DONE and MOM_CLOSE // events. // - MOM_DONE: SendLongMidiMessageInternal() relies on this event to clean // up the backing store where a long MIDI message is stored. // - MOM_CLOSE: This event is sent when 1) midiOutClose() is called, or 2) // the MIDI device becomes unavailable for some reasons, e.g., the cable // is disconnected. As for the former case, HMIDIOUT will be invalidated // soon after the callback is finished. As for the later case, however, // HMIDIOUT continues to be valid until midiOutClose() is called. MMRESULT result = midiOutOpen(&midi_handle_, device_id, reinterpret_cast(&HandleMessage), reinterpret_cast(this), CALLBACK_FUNCTION); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to open output device. " << " id: " << device_id << " message: "<< GetOutErrorMessage(result); midi_handle_ = NULL; return false; } return true; } void Uninitialize() { if (!midi_handle_) return; MMRESULT result = midiOutReset(midi_handle_); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to reset output port: " << GetOutErrorMessage(result); result = midiOutClose(midi_handle_); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to close output port: " << GetOutErrorMessage(result); midi_handle_ = NULL; closed_ = true; } static void CALLBACK HandleMessage(HMIDIOUT midi_out_handle, UINT message, DWORD_PTR instance, DWORD_PTR param1, DWORD_PTR param2) { // This method can be called back on any thread depending on Windows // multimedia subsystem and underlying MIDI drivers. OutDeviceInfo* self = reinterpret_cast(instance); if (!self) return; if (self->midi_handle() != midi_out_handle) return; switch (message) { case MOM_DONE: { // Take ownership of the MIDIHDR object. ScopedMIDIHDR header(reinterpret_cast(param1)); if (!header) return; MMRESULT result = midiOutUnprepareHeader( self->midi_handle(), header.get(), sizeof(*header)); DLOG_IF(ERROR, result != MMSYSERR_NOERROR) << "Failed to uninitialize output buffer: " << GetOutErrorMessage(result); return; } case MOM_CLOSE: // No lock is required since this flag is just a hint to avoid // unnecessary API calls that will result in failure anyway. self->closed_ = true; // TODO(yukawa): Implement crbug.com/279097. return; } } HMIDIOUT midi_handle_; // True if the device is already closed. volatile bool closed_; // True if the MidiManagerWin is trying to stop the sender thread. volatile bool quitting_; DISALLOW_COPY_AND_ASSIGN(OutDeviceInfo); }; MidiManagerWin::MidiManagerWin() : send_thread_("MidiSendThread") { } bool MidiManagerWin::Initialize() { TRACE_EVENT0("midi", "MidiManagerWin::Initialize"); const UINT num_in_devices = midiInGetNumDevs(); in_devices_.reserve(num_in_devices); for (UINT device_id = 0; device_id < num_in_devices; ++device_id) { MIDIINCAPS caps = {}; MMRESULT result = midiInGetDevCaps(device_id, &caps, sizeof(caps)); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to obtain input device info: " << GetInErrorMessage(result); continue; } scoped_ptr in_device(InDeviceInfo::Create(this, device_id)); if (!in_device) continue; MidiPortInfo info( base::IntToString(static_cast(device_id)), "", base::WideToUTF8(caps.szPname), base::IntToString(static_cast(caps.vDriverVersion))); AddInputPort(info); in_device->set_port_index(input_ports_.size() - 1); in_devices_.push_back(in_device.Pass()); } const UINT num_out_devices = midiOutGetNumDevs(); out_devices_.reserve(num_out_devices); for (UINT device_id = 0; device_id < num_out_devices; ++device_id) { MIDIOUTCAPS caps = {}; MMRESULT result = midiOutGetDevCaps(device_id, &caps, sizeof(caps)); if (result != MMSYSERR_NOERROR) { DLOG(ERROR) << "Failed to obtain output device info: " << GetOutErrorMessage(result); continue; } scoped_ptr out_port(OutDeviceInfo::Create(device_id)); if (!out_port) continue; MidiPortInfo info( base::IntToString(static_cast(device_id)), "", base::WideToUTF8(caps.szPname), base::IntToString(static_cast(caps.vDriverVersion))); AddOutputPort(info); out_devices_.push_back(out_port.Pass()); } return true; } MidiManagerWin::~MidiManagerWin() { // Cleanup order is important. |send_thread_| must be stopped before // |out_devices_| is cleared. for (size_t i = 0; i < output_ports_.size(); ++i) out_devices_[i]->Quit(); send_thread_.Stop(); out_devices_.clear(); output_ports_.clear(); in_devices_.clear(); input_ports_.clear(); } void MidiManagerWin::DispatchSendMidiData(MidiManagerClient* client, uint32 port_index, const std::vector& data, double timestamp) { if (out_devices_.size() <= port_index) return; base::TimeDelta delay; if (timestamp != 0.0) { base::TimeTicks time_to_send = base::TimeTicks() + base::TimeDelta::FromMicroseconds( timestamp * base::Time::kMicrosecondsPerSecond); delay = std::max(time_to_send - base::TimeTicks::Now(), base::TimeDelta()); } if (!send_thread_.IsRunning()) send_thread_.Start(); OutDeviceInfo* out_port = out_devices_[port_index].get(); send_thread_.message_loop()->PostDelayedTask( FROM_HERE, base::Bind(&OutDeviceInfo::Send, base::Unretained(out_port), data), delay); // Call back AccumulateMidiBytesSent() on |send_thread_| to emulate the // behavior of MidiManagerMac::SendMidiData. // TODO(yukawa): Do this task in a platform-independent way if possible. // See crbug.com/325810. send_thread_.message_loop()->PostTask( FROM_HERE, base::Bind(&MidiManagerClient::AccumulateMidiBytesSent, base::Unretained(client), data.size())); } MidiManager* MidiManager::Create() { return new MidiManagerWin(); } } // namespace media