| // 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 <windows.h> |
| #include <ks.h> |
| #include <ksmedia.h> |
| #include <mmreg.h> |
| // Prevent unnecessary functions from being included from <mmsystem.h> |
| #define MMNODRV |
| #define MMNOSOUND |
| #define MMNOWAVE |
| #define MMNOAUX |
| #define MMNOMIXER |
| #define MMNOTIMER |
| #define MMNOJOY |
| #define MMNOMCI |
| #define MMNOMMIO |
| #include <mmsystem.h> |
| |
| #include <algorithm> |
| #include <functional> |
| #include <queue> |
| #include <string> |
| |
| #include "base/bind.h" |
| #include "base/containers/hash_tables.h" |
| #include "base/message_loop/message_loop.h" |
| #include "base/strings/string16.h" |
| #include "base/strings/string_number_conversions.h" |
| #include "base/strings/string_piece.h" |
| #include "base/strings/stringprintf.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/system_monitor/system_monitor.h" |
| #include "base/threading/thread_checker.h" |
| #include "base/timer/timer.h" |
| #include "base/win/message_window.h" |
| #include "device/usb/usb_ids.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 midi { |
| namespace { |
| |
| static const size_t kBufferLength = 32 * 1024; |
| |
| // We assume that nullpter represents an invalid MIDI handle. |
| const HMIDIIN kInvalidMidiInHandle = nullptr; |
| const HMIDIOUT kInvalidMidiOutHandle = nullptr; |
| |
| 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); |
| } |
| |
| std::string MmversionToString(MMVERSION version) { |
| return base::StringPrintf("%d.%d", HIBYTE(version), LOBYTE(version)); |
| } |
| |
| class MIDIHDRDeleter { |
| public: |
| void operator()(MIDIHDR* header) { |
| if (!header) |
| return; |
| delete[] static_cast<char*>(header->lpData); |
| header->lpData = NULL; |
| header->dwBufferLength = 0; |
| delete header; |
| } |
| }; |
| |
| typedef scoped_ptr<MIDIHDR, MIDIHDRDeleter> ScopedMIDIHDR; |
| |
| ScopedMIDIHDR CreateMIDIHDR(size_t size) { |
| ScopedMIDIHDR header(new MIDIHDR); |
| ZeroMemory(header.get(), sizeof(*header)); |
| header->lpData = new char[size]; |
| header->dwBufferLength = static_cast<DWORD>(size); |
| return header.Pass(); |
| } |
| |
| void SendShortMidiMessageInternal(HMIDIOUT midi_out_handle, |
| const std::vector<uint8>& message) { |
| DCHECK_LE(message.size(), static_cast<size_t>(3)) |
| << "A short MIDI message should be up to 3 bytes."; |
| |
| DWORD packed_message = 0; |
| for (size_t i = 0; i < message.size(); ++i) |
| packed_message |= (static_cast<uint32>(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<uint8>& message) { |
| // Implementation note: |
| // Sending a 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())); |
| std::copy(message.begin(), message.end(), midi_header->lpData); |
| |
| 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(); |
| } |
| |
| template <size_t array_size> |
| base::string16 AsString16(const wchar_t(&buffer)[array_size]) { |
| size_t len = 0; |
| for (len = 0; len < array_size; ++len) { |
| if (buffer[len] == L'\0') |
| break; |
| } |
| return base::string16(buffer, len); |
| } |
| |
| struct MidiDeviceInfo final { |
| explicit MidiDeviceInfo(const MIDIINCAPS2W& caps) |
| : manufacturer_id(caps.wMid), |
| product_id(caps.wPid), |
| driver_version(caps.vDriverVersion), |
| product_name(AsString16(caps.szPname)), |
| usb_vendor_id(ExtractUsbVendorIdIfExists(caps)), |
| usb_product_id(ExtractUsbProductIdIfExists(caps)), |
| is_usb_device(IsUsbDevice(caps)) {} |
| explicit MidiDeviceInfo(const MIDIOUTCAPS2W& caps) |
| : manufacturer_id(caps.wMid), |
| product_id(caps.wPid), |
| driver_version(caps.vDriverVersion), |
| product_name(AsString16(caps.szPname)), |
| usb_vendor_id(ExtractUsbVendorIdIfExists(caps)), |
| usb_product_id(ExtractUsbProductIdIfExists(caps)), |
| is_usb_device(IsUsbDevice(caps)) {} |
| explicit MidiDeviceInfo(const MidiDeviceInfo& info) |
| : manufacturer_id(info.manufacturer_id), |
| product_id(info.product_id), |
| driver_version(info.driver_version), |
| product_name(info.product_name), |
| usb_vendor_id(info.usb_vendor_id), |
| usb_product_id(info.usb_product_id), |
| is_usb_device(info.is_usb_device) {} |
| // Currently only following entities are considered when testing the equality |
| // of two MIDI devices. |
| // TODO(toyoshim): Consider to calculate MIDIPort.id here and use it as the |
| // key. See crbug.com/467448. Then optimize the data for |MidiPortInfo|. |
| const uint16 manufacturer_id; |
| const uint16 product_id; |
| const uint32 driver_version; |
| const base::string16 product_name; |
| const uint16 usb_vendor_id; |
| const uint16 usb_product_id; |
| const bool is_usb_device; |
| |
| // Required to be used as the key of base::hash_map. |
| bool operator==(const MidiDeviceInfo& that) const { |
| return manufacturer_id == that.manufacturer_id && |
| product_id == that.product_id && |
| driver_version == that.driver_version && |
| product_name == that.product_name && |
| is_usb_device == that.is_usb_device && |
| (is_usb_device && usb_vendor_id == that.usb_vendor_id && |
| usb_product_id == that.usb_product_id); |
| } |
| |
| // Hash function to be used in base::hash_map. |
| struct Hasher { |
| size_t operator()(const MidiDeviceInfo& info) const { |
| size_t hash = info.manufacturer_id; |
| hash *= 131; |
| hash += info.product_id; |
| hash *= 131; |
| hash += info.driver_version; |
| hash *= 131; |
| hash += info.product_name.size(); |
| hash *= 131; |
| if (!info.product_name.empty()) { |
| hash += info.product_name[0]; |
| } |
| hash *= 131; |
| hash += info.usb_vendor_id; |
| hash *= 131; |
| hash += info.usb_product_id; |
| return hash; |
| } |
| }; |
| |
| private: |
| static bool IsUsbDevice(const MIDIINCAPS2W& caps) { |
| return IS_COMPATIBLE_USBAUDIO_MID(&caps.ManufacturerGuid) && |
| IS_COMPATIBLE_USBAUDIO_PID(&caps.ProductGuid); |
| } |
| static bool IsUsbDevice(const MIDIOUTCAPS2W& caps) { |
| return IS_COMPATIBLE_USBAUDIO_MID(&caps.ManufacturerGuid) && |
| IS_COMPATIBLE_USBAUDIO_PID(&caps.ProductGuid); |
| } |
| static uint16 ExtractUsbVendorIdIfExists(const MIDIINCAPS2W& caps) { |
| if (!IS_COMPATIBLE_USBAUDIO_MID(&caps.ManufacturerGuid)) |
| return 0; |
| return EXTRACT_USBAUDIO_MID(&caps.ManufacturerGuid); |
| } |
| static uint16 ExtractUsbVendorIdIfExists(const MIDIOUTCAPS2W& caps) { |
| if (!IS_COMPATIBLE_USBAUDIO_MID(&caps.ManufacturerGuid)) |
| return 0; |
| return EXTRACT_USBAUDIO_MID(&caps.ManufacturerGuid); |
| } |
| static uint16 ExtractUsbProductIdIfExists(const MIDIINCAPS2W& caps) { |
| if (!IS_COMPATIBLE_USBAUDIO_PID(&caps.ProductGuid)) |
| return 0; |
| return EXTRACT_USBAUDIO_PID(&caps.ProductGuid); |
| } |
| static uint16 ExtractUsbProductIdIfExists(const MIDIOUTCAPS2W& caps) { |
| if (!IS_COMPATIBLE_USBAUDIO_PID(&caps.ProductGuid)) |
| return 0; |
| return EXTRACT_USBAUDIO_PID(&caps.ProductGuid); |
| } |
| }; |
| |
| std::string GetManufacturerName(const MidiDeviceInfo& info) { |
| if (info.is_usb_device) { |
| const char* name = device::UsbIds::GetVendorName(info.usb_vendor_id); |
| return std::string(name ? name : ""); |
| } |
| |
| switch (info.manufacturer_id) { |
| case MM_MICROSOFT: |
| return "Microsoft Corporation"; |
| default: |
| // TODO(toyoshim): Support other manufacture IDs. crbug.com/472341. |
| return ""; |
| } |
| } |
| |
| bool IsUnsupportedDevice(const MidiDeviceInfo& info) { |
| return info.manufacturer_id == MM_MICROSOFT && |
| (info.product_id == MM_MSFT_WDMAUDIO_MIDIOUT || |
| info.product_id == MM_MSFT_GENERIC_MIDISYNTH); |
| } |
| |
| using PortNumberCache = base::hash_map< |
| MidiDeviceInfo, |
| std::priority_queue<uint32, std::vector<uint32>, std::greater<uint32>>, |
| MidiDeviceInfo::Hasher>; |
| |
| struct MidiInputDeviceState final : base::RefCounted<MidiInputDeviceState> { |
| explicit MidiInputDeviceState(const MidiDeviceInfo& device_info) |
| : device_info(device_info), |
| midi_handle(kInvalidMidiInHandle), |
| port_index(0), |
| port_age(0), |
| start_time_initialized(false) {} |
| |
| const MidiDeviceInfo device_info; |
| HMIDIIN midi_handle; |
| ScopedMIDIHDR midi_header; |
| // Since Win32 multimedia system uses a relative time offset from when |
| // |midiInStart| API is called, we need to record when it is called. |
| base::TimeTicks start_time; |
| // 0-based port index. We will try to reuse the previous port index when the |
| // MIDI device is closed then reopened. |
| uint32 port_index; |
| // A sequence number which represents how many times |port_index| is reused. |
| // We can remove this field if we decide not to clear unsent events |
| // when the device is disconnected. |
| // See https://github.com/WebAudio/web-midi-api/issues/133 |
| uint64 port_age; |
| // True if |start_time| is initialized. This field is not used so far, but |
| // kept for the debugging purpose. |
| bool start_time_initialized; |
| |
| private: |
| friend class base::RefCounted<MidiInputDeviceState>; |
| ~MidiInputDeviceState() {} |
| }; |
| |
| struct MidiOutputDeviceState final : base::RefCounted<MidiOutputDeviceState> { |
| explicit MidiOutputDeviceState(const MidiDeviceInfo& device_info) |
| : device_info(device_info), |
| midi_handle(kInvalidMidiOutHandle), |
| port_index(0), |
| port_age(0), |
| closed(false) {} |
| |
| const MidiDeviceInfo device_info; |
| HMIDIOUT midi_handle; |
| // 0-based port index. We will try to reuse the previous port index when the |
| // MIDI device is closed then reopened. |
| uint32 port_index; |
| // A sequence number which represents how many times |port_index| is reused. |
| // We can remove this field if we decide not to clear unsent events |
| // when the device is disconnected. |
| // See https://github.com/WebAudio/web-midi-api/issues/133 |
| uint64 port_age; |
| // True if the device is already closed and |midi_handle| is considered to be |
| // invalid. |
| // TODO(toyoshim): Use std::atomic<bool> when it is allowed in Chromium |
| // project. |
| volatile bool closed; |
| |
| private: |
| friend class base::RefCounted<MidiOutputDeviceState>; |
| ~MidiOutputDeviceState() {} |
| }; |
| |
| // The core logic of MIDI device handling for Windows. Basically this class is |
| // shared among following 4 threads: |
| // 1. Chrome IO Thread |
| // 2. OS Multimedia Thread |
| // 3. Task Thread |
| // 4. Sender Thread |
| // |
| // Chrome IO Thread: |
| // MidiManager runs on Chrome IO thread. Device change notification is |
| // delivered to the thread through the SystemMonitor service. |
| // OnDevicesChanged() callback is invoked to update the MIDI device list. |
| // Note that in the current implementation we will try to open all the |
| // existing devices in practice. This is OK because trying to reopen a MIDI |
| // device that is already opened would simply fail, and there is no unwilling |
| // side effect. |
| // |
| // OS Multimedia Thread: |
| // This thread is maintained by the OS as a part of MIDI runtime, and |
| // responsible for receiving all the system initiated events such as device |
| // close, and receiving data. For performance reasons, most of potentially |
| // blocking operations will be dispatched into Task Thread. |
| // |
| // Task Thread: |
| // This thread will be used to call back following methods of MidiManager. |
| // - MidiManager::CompleteInitialization |
| // - MidiManager::AddInputPort |
| // - MidiManager::AddOutputPort |
| // - MidiManager::SetInputPortState |
| // - MidiManager::SetOutputPortState |
| // - MidiManager::ReceiveMidiData |
| // |
| // Sender Thread: |
| // This thread will be used to call Win32 APIs to send MIDI message at the |
| // specified time. We don't want to call MIDI send APIs on Task Thread |
| // because those APIs could be performed synchronously, hence they could block |
| // the caller thread for a while. See the comment in |
| // SendLongMidiMessageInternal for details. Currently we expect that the |
| // blocking time would be less than 1 second. |
| class MidiServiceWinImpl : public MidiServiceWin, |
| public base::SystemMonitor::DevicesChangedObserver { |
| public: |
| MidiServiceWinImpl() |
| : delegate_(nullptr), |
| sender_thread_("Windows MIDI sender thread"), |
| task_thread_("Windows MIDI task thread"), |
| destructor_started(false) {} |
| |
| ~MidiServiceWinImpl() final { |
| // Start() and Stop() of the threads, and AddDevicesChangeObserver() and |
| // RemoveDevicesChangeObserver() should be called on the same thread. |
| CHECK(thread_checker_.CalledOnValidThread()); |
| |
| destructor_started = true; |
| base::SystemMonitor::Get()->RemoveDevicesChangedObserver(this); |
| { |
| std::vector<HMIDIIN> input_devices; |
| { |
| base::AutoLock auto_lock(input_ports_lock_); |
| for (auto it : input_device_map_) |
| input_devices.push_back(it.first); |
| } |
| { |
| for (const auto handle : input_devices) { |
| MMRESULT result = midiInClose(handle); |
| if (result == MIDIERR_STILLPLAYING) { |
| result = midiInReset(handle); |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR) |
| << "midiInReset failed: " << GetInErrorMessage(result); |
| result = midiInClose(handle); |
| } |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR) |
| << "midiInClose failed: " << GetInErrorMessage(result); |
| } |
| } |
| } |
| { |
| std::vector<HMIDIOUT> output_devices; |
| { |
| base::AutoLock auto_lock(output_ports_lock_); |
| for (auto it : output_device_map_) |
| output_devices.push_back(it.first); |
| } |
| { |
| for (const auto handle : output_devices) { |
| MMRESULT result = midiOutClose(handle); |
| if (result == MIDIERR_STILLPLAYING) { |
| result = midiOutReset(handle); |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR) |
| << "midiOutReset failed: " << GetOutErrorMessage(result); |
| result = midiOutClose(handle); |
| } |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR) |
| << "midiOutClose failed: " << GetOutErrorMessage(result); |
| } |
| } |
| } |
| sender_thread_.Stop(); |
| task_thread_.Stop(); |
| } |
| |
| // MidiServiceWin overrides: |
| void InitializeAsync(MidiServiceWinDelegate* delegate) final { |
| // Start() and Stop() of the threads, and AddDevicesChangeObserver() and |
| // RemoveDevicesChangeObserver() should be called on the same thread. |
| CHECK(thread_checker_.CalledOnValidThread()); |
| |
| delegate_ = delegate; |
| |
| sender_thread_.Start(); |
| task_thread_.Start(); |
| |
| // Start monitoring device changes. This should start before the |
| // following UpdateDeviceList() call not to miss the event happening |
| // between the call and the observer registration. |
| base::SystemMonitor::Get()->AddDevicesChangedObserver(this); |
| |
| UpdateDeviceList(); |
| |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&MidiServiceWinImpl::CompleteInitializationOnTaskThread, |
| base::Unretained(this), MIDI_OK)); |
| } |
| |
| void SendMidiDataAsync(uint32 port_number, |
| const std::vector<uint8>& data, |
| base::TimeTicks time) final { |
| if (destructor_started) { |
| LOG(ERROR) << "ThreadSafeSendData failed because MidiServiceWinImpl is " |
| "being destructed. port: " << port_number; |
| return; |
| } |
| auto state = GetOutputDeviceFromPort(port_number); |
| if (!state) { |
| LOG(ERROR) << "ThreadSafeSendData failed due to an invalid port number. " |
| << "port: " << port_number; |
| return; |
| } |
| if (state->closed) { |
| LOG(ERROR) |
| << "ThreadSafeSendData failed because target port is already closed." |
| << "port: " << port_number; |
| return; |
| } |
| const auto now = base::TimeTicks::Now(); |
| if (now < time) { |
| sender_thread_.message_loop()->PostDelayedTask( |
| FROM_HERE, base::Bind(&MidiServiceWinImpl::SendOnSenderThread, |
| base::Unretained(this), port_number, |
| state->port_age, data, time), |
| time - now); |
| } else { |
| sender_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&MidiServiceWinImpl::SendOnSenderThread, |
| base::Unretained(this), port_number, |
| state->port_age, data, time)); |
| } |
| } |
| |
| // base::SystemMonitor::DevicesChangedObserver overrides: |
| void OnDevicesChanged(base::SystemMonitor::DeviceType device_type) final { |
| CHECK(thread_checker_.CalledOnValidThread()); |
| if (destructor_started) |
| return; |
| |
| switch (device_type) { |
| case base::SystemMonitor::DEVTYPE_AUDIO_CAPTURE: |
| case base::SystemMonitor::DEVTYPE_VIDEO_CAPTURE: |
| // Add case of other unrelated device types here. |
| return; |
| case base::SystemMonitor::DEVTYPE_UNKNOWN: |
| // Interested in MIDI devices. Try updating the device list. |
| UpdateDeviceList(); |
| break; |
| // No default here to capture new DeviceType by compile time. |
| } |
| } |
| |
| private: |
| scoped_refptr<MidiInputDeviceState> GetInputDeviceFromHandle( |
| HMIDIIN midi_handle) { |
| base::AutoLock auto_lock(input_ports_lock_); |
| const auto it = input_device_map_.find(midi_handle); |
| return (it != input_device_map_.end() ? it->second : nullptr); |
| } |
| |
| scoped_refptr<MidiOutputDeviceState> GetOutputDeviceFromHandle( |
| HMIDIOUT midi_handle) { |
| base::AutoLock auto_lock(output_ports_lock_); |
| const auto it = output_device_map_.find(midi_handle); |
| return (it != output_device_map_.end() ? it->second : nullptr); |
| } |
| |
| scoped_refptr<MidiOutputDeviceState> GetOutputDeviceFromPort( |
| uint32 port_number) { |
| base::AutoLock auto_lock(output_ports_lock_); |
| if (output_ports_.size() <= port_number) |
| return nullptr; |
| return output_ports_[port_number]; |
| } |
| |
| void UpdateDeviceList() { |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&MidiServiceWinImpl::UpdateDeviceListOnTaskThread, |
| base::Unretained(this))); |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // Callbacks on the OS multimedia thread. |
| ///////////////////////////////////////////////////////////////////////////// |
| |
| static void CALLBACK |
| OnMidiInEventOnMainlyMultimediaThread(HMIDIIN midi_in_handle, |
| UINT message, |
| DWORD_PTR instance, |
| DWORD_PTR param1, |
| DWORD_PTR param2) { |
| MidiServiceWinImpl* self = reinterpret_cast<MidiServiceWinImpl*>(instance); |
| if (!self) |
| return; |
| switch (message) { |
| case MIM_OPEN: |
| self->OnMidiInOpen(midi_in_handle); |
| break; |
| case MIM_DATA: |
| self->OnMidiInDataOnMultimediaThread(midi_in_handle, param1, param2); |
| break; |
| case MIM_LONGDATA: |
| self->OnMidiInLongDataOnMultimediaThread(midi_in_handle, param1, |
| param2); |
| break; |
| case MIM_CLOSE: |
| self->OnMidiInCloseOnMultimediaThread(midi_in_handle); |
| break; |
| } |
| } |
| |
| void OnMidiInOpen(HMIDIIN midi_in_handle) { |
| UINT device_id = 0; |
| MMRESULT result = midiInGetID(midi_in_handle, &device_id); |
| if (result != MMSYSERR_NOERROR) { |
| DLOG(ERROR) << "midiInGetID failed: " << GetInErrorMessage(result); |
| return; |
| } |
| MIDIINCAPS2W caps = {}; |
| result = midiInGetDevCaps(device_id, reinterpret_cast<LPMIDIINCAPSW>(&caps), |
| sizeof(caps)); |
| if (result != MMSYSERR_NOERROR) { |
| DLOG(ERROR) << "midiInGetDevCaps failed: " << GetInErrorMessage(result); |
| return; |
| } |
| auto state = |
| make_scoped_refptr(new MidiInputDeviceState(MidiDeviceInfo(caps))); |
| state->midi_handle = midi_in_handle; |
| state->midi_header = CreateMIDIHDR(kBufferLength); |
| const auto& state_device_info = state->device_info; |
| bool add_new_port = false; |
| uint32 port_number = 0; |
| { |
| base::AutoLock auto_lock(input_ports_lock_); |
| const auto it = unused_input_ports_.find(state_device_info); |
| if (it == unused_input_ports_.end()) { |
| port_number = static_cast<uint32>(input_ports_.size()); |
| add_new_port = true; |
| input_ports_.push_back(nullptr); |
| input_ports_ages_.push_back(0); |
| } else { |
| port_number = it->second.top(); |
| it->second.pop(); |
| if (it->second.empty()) { |
| unused_input_ports_.erase(it); |
| } |
| } |
| input_ports_[port_number] = state; |
| |
| input_ports_ages_[port_number] += 1; |
| input_device_map_[input_ports_[port_number]->midi_handle] = |
| input_ports_[port_number]; |
| input_ports_[port_number]->port_index = port_number; |
| input_ports_[port_number]->port_age = input_ports_ages_[port_number]; |
| } |
| // Several initial startup tasks cannot be done in MIM_OPEN handler. |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&MidiServiceWinImpl::StartInputDeviceOnTaskThread, |
| base::Unretained(this), midi_in_handle)); |
| if (add_new_port) { |
| const MidiPortInfo port_info( |
| // TODO(toyoshim): Use a hash ID insted crbug.com/467448 |
| base::IntToString(static_cast<int>(port_number)), |
| GetManufacturerName(state_device_info), |
| base::WideToUTF8(state_device_info.product_name), |
| MmversionToString(state_device_info.driver_version), |
| MIDI_PORT_OPENED); |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&MidiServiceWinImpl::AddInputPortOnTaskThread, |
| base::Unretained(this), port_info)); |
| } else { |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&MidiServiceWinImpl::SetInputPortStateOnTaskThread, |
| base::Unretained(this), port_number, |
| MidiPortState::MIDI_PORT_CONNECTED)); |
| } |
| } |
| |
| void OnMidiInDataOnMultimediaThread(HMIDIIN midi_in_handle, |
| DWORD_PTR param1, |
| DWORD_PTR param2) { |
| auto state = GetInputDeviceFromHandle(midi_in_handle); |
| if (!state) |
| return; |
| const uint8 status_byte = static_cast<uint8>(param1 & 0xff); |
| const uint8 first_data_byte = static_cast<uint8>((param1 >> 8) & 0xff); |
| const uint8 second_data_byte = static_cast<uint8>((param1 >> 16) & 0xff); |
| const DWORD elapsed_ms = param2; |
| const size_t len = GetMidiMessageLength(status_byte); |
| const uint8 kData[] = {status_byte, first_data_byte, second_data_byte}; |
| std::vector<uint8> data; |
| data.assign(kData, kData + len); |
| DCHECK_LE(len, arraysize(kData)); |
| // 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 = |
| state->start_time + base::TimeDelta::FromMilliseconds(elapsed_ms); |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&MidiServiceWinImpl::ReceiveMidiDataOnTaskThread, |
| base::Unretained(this), state->port_index, data, |
| event_time)); |
| } |
| |
| void OnMidiInLongDataOnMultimediaThread(HMIDIIN midi_in_handle, |
| DWORD_PTR param1, |
| DWORD_PTR param2) { |
| auto state = GetInputDeviceFromHandle(midi_in_handle); |
| if (!state) |
| return; |
| MIDIHDR* header = reinterpret_cast<MIDIHDR*>(param1); |
| const DWORD elapsed_ms = param2; |
| MMRESULT result = MMSYSERR_NOERROR; |
| if (destructor_started) { |
| if (state->midi_header && |
| (state->midi_header->dwFlags & MHDR_PREPARED) == MHDR_PREPARED) { |
| result = |
| midiInUnprepareHeader(state->midi_handle, state->midi_header.get(), |
| sizeof(*state->midi_header)); |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR) |
| << "Failed to uninitialize input buffer: " |
| << GetInErrorMessage(result); |
| } |
| return; |
| } |
| if (header->dwBytesRecorded > 0) { |
| const uint8* src = reinterpret_cast<const uint8*>(header->lpData); |
| std::vector<uint8> data; |
| data.assign(src, src + header->dwBytesRecorded); |
| // 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 = |
| state->start_time + base::TimeDelta::FromMilliseconds(elapsed_ms); |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&MidiServiceWinImpl::ReceiveMidiDataOnTaskThread, |
| base::Unretained(this), state->port_index, data, |
| event_time)); |
| } |
| result = midiInAddBuffer(state->midi_handle, header, sizeof(*header)); |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR) |
| << "Failed to attach input buffer: " << GetInErrorMessage(result) |
| << "port number:" << state->port_index; |
| } |
| |
| void OnMidiInCloseOnMultimediaThread(HMIDIIN midi_in_handle) { |
| auto state = GetInputDeviceFromHandle(midi_in_handle); |
| if (!state) |
| return; |
| const uint32 port_number = state->port_index; |
| const auto device_info(state->device_info); |
| { |
| base::AutoLock auto_lock(input_ports_lock_); |
| input_device_map_.erase(state->midi_handle); |
| input_ports_[port_number] = nullptr; |
| input_ports_ages_[port_number] += 1; |
| unused_input_ports_[device_info].push(port_number); |
| } |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&MidiServiceWinImpl::SetInputPortStateOnTaskThread, |
| base::Unretained(this), port_number, |
| MIDI_PORT_DISCONNECTED)); |
| } |
| |
| static void CALLBACK |
| OnMidiOutEventOnMainlyMultimediaThread(HMIDIOUT midi_out_handle, |
| UINT message, |
| DWORD_PTR instance, |
| DWORD_PTR param1, |
| DWORD_PTR param2) { |
| MidiServiceWinImpl* self = reinterpret_cast<MidiServiceWinImpl*>(instance); |
| if (!self) |
| return; |
| switch (message) { |
| case MOM_OPEN: |
| self->OnMidiOutOpen(midi_out_handle, param1, param2); |
| break; |
| case MOM_DONE: |
| self->OnMidiOutDoneOnMultimediaThread(midi_out_handle, param1); |
| break; |
| case MOM_CLOSE: |
| self->OnMidiOutCloseOnMultimediaThread(midi_out_handle); |
| break; |
| } |
| } |
| |
| void OnMidiOutOpen(HMIDIOUT midi_out_handle, |
| DWORD_PTR param1, |
| DWORD_PTR param2) { |
| UINT device_id = 0; |
| MMRESULT result = midiOutGetID(midi_out_handle, &device_id); |
| if (result != MMSYSERR_NOERROR) { |
| DLOG(ERROR) << "midiOutGetID failed: " << GetOutErrorMessage(result); |
| return; |
| } |
| MIDIOUTCAPS2W caps = {}; |
| result = midiOutGetDevCaps( |
| device_id, reinterpret_cast<LPMIDIOUTCAPSW>(&caps), sizeof(caps)); |
| if (result != MMSYSERR_NOERROR) { |
| DLOG(ERROR) << "midiInGetDevCaps failed: " << GetOutErrorMessage(result); |
| return; |
| } |
| auto state = |
| make_scoped_refptr(new MidiOutputDeviceState(MidiDeviceInfo(caps))); |
| state->midi_handle = midi_out_handle; |
| const auto& state_device_info = state->device_info; |
| if (IsUnsupportedDevice(state_device_info)) |
| return; |
| bool add_new_port = false; |
| uint32 port_number = 0; |
| { |
| base::AutoLock auto_lock(output_ports_lock_); |
| const auto it = unused_output_ports_.find(state_device_info); |
| if (it == unused_output_ports_.end()) { |
| port_number = static_cast<uint32>(output_ports_.size()); |
| add_new_port = true; |
| output_ports_.push_back(nullptr); |
| output_ports_ages_.push_back(0); |
| } else { |
| port_number = it->second.top(); |
| it->second.pop(); |
| if (it->second.empty()) |
| unused_output_ports_.erase(it); |
| } |
| output_ports_[port_number] = state; |
| output_ports_ages_[port_number] += 1; |
| output_device_map_[output_ports_[port_number]->midi_handle] = |
| output_ports_[port_number]; |
| output_ports_[port_number]->port_index = port_number; |
| output_ports_[port_number]->port_age = output_ports_ages_[port_number]; |
| } |
| if (add_new_port) { |
| const MidiPortInfo port_info( |
| // TODO(toyoshim): Use a hash ID insted. crbug.com/467448 |
| base::IntToString(static_cast<int>(port_number)), |
| GetManufacturerName(state_device_info), |
| base::WideToUTF8(state_device_info.product_name), |
| MmversionToString(state_device_info.driver_version), |
| MIDI_PORT_OPENED); |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, base::Bind(&MidiServiceWinImpl::AddOutputPortOnTaskThread, |
| base::Unretained(this), port_info)); |
| } else { |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&MidiServiceWinImpl::SetOutputPortStateOnTaskThread, |
| base::Unretained(this), port_number, MIDI_PORT_CONNECTED)); |
| } |
| } |
| |
| void OnMidiOutDoneOnMultimediaThread(HMIDIOUT midi_out_handle, |
| DWORD_PTR param1) { |
| auto state = GetOutputDeviceFromHandle(midi_out_handle); |
| if (!state) |
| return; |
| // Take ownership of the MIDIHDR object. |
| ScopedMIDIHDR header(reinterpret_cast<MIDIHDR*>(param1)); |
| if (!header) |
| return; |
| MMRESULT result = midiOutUnprepareHeader(state->midi_handle, header.get(), |
| sizeof(*header)); |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR) |
| << "Failed to uninitialize output buffer: " |
| << GetOutErrorMessage(result); |
| } |
| |
| void OnMidiOutCloseOnMultimediaThread(HMIDIOUT midi_out_handle) { |
| auto state = GetOutputDeviceFromHandle(midi_out_handle); |
| if (!state) |
| return; |
| const uint32 port_number = state->port_index; |
| const auto device_info(state->device_info); |
| { |
| base::AutoLock auto_lock(output_ports_lock_); |
| output_device_map_.erase(state->midi_handle); |
| output_ports_[port_number] = nullptr; |
| output_ports_ages_[port_number] += 1; |
| unused_output_ports_[device_info].push(port_number); |
| state->closed = true; |
| } |
| task_thread_.message_loop()->PostTask( |
| FROM_HERE, |
| base::Bind(&MidiServiceWinImpl::SetOutputPortStateOnTaskThread, |
| base::Unretained(this), port_number, |
| MIDI_PORT_DISCONNECTED)); |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // Callbacks on the sender thread. |
| ///////////////////////////////////////////////////////////////////////////// |
| |
| void AssertOnSenderThread() { |
| DCHECK_EQ(sender_thread_.thread_id(), base::PlatformThread::CurrentId()); |
| } |
| |
| void SendOnSenderThread(uint32 port_number, |
| uint64 port_age, |
| const std::vector<uint8>& data, |
| base::TimeTicks time) { |
| AssertOnSenderThread(); |
| if (destructor_started) { |
| LOG(ERROR) << "ThreadSafeSendData failed because MidiServiceWinImpl is " |
| "being destructed. port: " << port_number; |
| } |
| auto state = GetOutputDeviceFromPort(port_number); |
| if (!state) { |
| LOG(ERROR) << "ThreadSafeSendData failed due to an invalid port number. " |
| << "port: " << port_number; |
| return; |
| } |
| if (state->closed) { |
| LOG(ERROR) |
| << "ThreadSafeSendData failed because target port is already closed." |
| << "port: " << port_number; |
| return; |
| } |
| if (state->port_age != port_age) { |
| LOG(ERROR) |
| << "ThreadSafeSendData failed because target port is being closed." |
| << "port: " << port_number << "expected port age: " << port_age |
| << "actual port age: " << state->port_age; |
| } |
| |
| // MIDI Running status must be filtered out. |
| MidiMessageQueue message_queue(false); |
| message_queue.Add(data); |
| std::vector<uint8> message; |
| while (true) { |
| if (destructor_started) |
| break; |
| if (state->closed) |
| break; |
| message_queue.Get(&message); |
| if (message.empty()) |
| break; |
| // SendShortMidiMessageInternal can send a MIDI message up to 3 bytes. |
| if (message.size() <= 3) |
| SendShortMidiMessageInternal(state->midi_handle, message); |
| else |
| SendLongMidiMessageInternal(state->midi_handle, message); |
| } |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // Callbacks on the task thread. |
| ///////////////////////////////////////////////////////////////////////////// |
| |
| void AssertOnTaskThread() { |
| DCHECK_EQ(task_thread_.thread_id(), base::PlatformThread::CurrentId()); |
| } |
| |
| void UpdateDeviceListOnTaskThread() { |
| AssertOnTaskThread(); |
| const UINT num_in_devices = midiInGetNumDevs(); |
| for (UINT device_id = 0; device_id < num_in_devices; ++device_id) { |
| // Here we use |CALLBACK_FUNCTION| to subscribe MIM_DATA, MIM_LONGDATA, |
| // MIM_OPEN, 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_OPEN: This event is sent the input device is opened. Note that |
| // this message is called on the caller thread. |
| // - 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. |
| HMIDIIN midi_handle = kInvalidMidiInHandle; |
| const MMRESULT result = midiInOpen( |
| &midi_handle, device_id, |
| reinterpret_cast<DWORD_PTR>(&OnMidiInEventOnMainlyMultimediaThread), |
| reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION); |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR && result != MMSYSERR_ALLOCATED) |
| << "Failed to open output device. " |
| << " id: " << device_id << " message: " << GetInErrorMessage(result); |
| } |
| |
| const UINT num_out_devices = midiOutGetNumDevs(); |
| for (UINT device_id = 0; device_id < num_out_devices; ++device_id) { |
| // Here we use |CALLBACK_FUNCTION| to subscribe MOM_DONE, MOM_OPEN, 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_OPEN: This event is sent the output device is opened. Note that |
| // this message is called on the caller thread. |
| // - 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. |
| HMIDIOUT midi_handle = kInvalidMidiOutHandle; |
| const MMRESULT result = midiOutOpen( |
| &midi_handle, device_id, |
| reinterpret_cast<DWORD_PTR>(&OnMidiOutEventOnMainlyMultimediaThread), |
| reinterpret_cast<DWORD_PTR>(this), CALLBACK_FUNCTION); |
| DLOG_IF(ERROR, result != MMSYSERR_NOERROR && result != MMSYSERR_ALLOCATED) |
| << "Failed to open output device. " |
| << " id: " << device_id << " message: " << GetOutErrorMessage(result); |
| } |
| } |
| |
| void StartInputDeviceOnTaskThread(HMIDIIN midi_in_handle) { |
| AssertOnTaskThread(); |
| auto state = GetInputDeviceFromHandle(midi_in_handle); |
| if (!state) |
| return; |
| MMRESULT result = |
| midiInPrepareHeader(state->midi_handle, state->midi_header.get(), |
| sizeof(*state->midi_header)); |
| if (result != MMSYSERR_NOERROR) { |
| DLOG(ERROR) << "Failed to initialize input buffer: " |
| << GetInErrorMessage(result); |
| return; |
| } |
| result = midiInAddBuffer(state->midi_handle, state->midi_header.get(), |
| sizeof(*state->midi_header)); |
| if (result != MMSYSERR_NOERROR) { |
| DLOG(ERROR) << "Failed to attach input buffer: " |
| << GetInErrorMessage(result); |
| return; |
| } |
| result = midiInStart(state->midi_handle); |
| if (result != MMSYSERR_NOERROR) { |
| DLOG(ERROR) << "Failed to start input port: " |
| << GetInErrorMessage(result); |
| return; |
| } |
| state->start_time = base::TimeTicks::Now(); |
| state->start_time_initialized = true; |
| } |
| |
| void CompleteInitializationOnTaskThread(MidiResult result) { |
| AssertOnTaskThread(); |
| delegate_->OnCompleteInitialization(result); |
| } |
| |
| void ReceiveMidiDataOnTaskThread(uint32 port_index, |
| std::vector<uint8> data, |
| base::TimeTicks time) { |
| AssertOnTaskThread(); |
| delegate_->OnReceiveMidiData(port_index, data, time); |
| } |
| |
| void AddInputPortOnTaskThread(MidiPortInfo info) { |
| AssertOnTaskThread(); |
| delegate_->OnAddInputPort(info); |
| } |
| |
| void AddOutputPortOnTaskThread(MidiPortInfo info) { |
| AssertOnTaskThread(); |
| delegate_->OnAddOutputPort(info); |
| } |
| |
| void SetInputPortStateOnTaskThread(uint32 port_index, MidiPortState state) { |
| AssertOnTaskThread(); |
| delegate_->OnSetInputPortState(port_index, state); |
| } |
| |
| void SetOutputPortStateOnTaskThread(uint32 port_index, MidiPortState state) { |
| AssertOnTaskThread(); |
| delegate_->OnSetOutputPortState(port_index, state); |
| } |
| |
| ///////////////////////////////////////////////////////////////////////////// |
| // Fields: |
| ///////////////////////////////////////////////////////////////////////////// |
| |
| // Does not take ownership. |
| MidiServiceWinDelegate* delegate_; |
| |
| base::ThreadChecker thread_checker_; |
| |
| base::Thread sender_thread_; |
| base::Thread task_thread_; |
| |
| base::Lock input_ports_lock_; |
| base::hash_map<HMIDIIN, scoped_refptr<MidiInputDeviceState>> |
| input_device_map_; // GUARDED_BY(input_ports_lock_) |
| PortNumberCache unused_input_ports_; // GUARDED_BY(input_ports_lock_) |
| std::vector<scoped_refptr<MidiInputDeviceState>> |
| input_ports_; // GUARDED_BY(input_ports_lock_) |
| std::vector<uint64> input_ports_ages_; // GUARDED_BY(input_ports_lock_) |
| |
| base::Lock output_ports_lock_; |
| base::hash_map<HMIDIOUT, scoped_refptr<MidiOutputDeviceState>> |
| output_device_map_; // GUARDED_BY(output_ports_lock_) |
| PortNumberCache unused_output_ports_; // GUARDED_BY(output_ports_lock_) |
| std::vector<scoped_refptr<MidiOutputDeviceState>> |
| output_ports_; // GUARDED_BY(output_ports_lock_) |
| std::vector<uint64> output_ports_ages_; // GUARDED_BY(output_ports_lock_) |
| |
| // True if one thread reached MidiServiceWinImpl::~MidiServiceWinImpl(). Note |
| // that MidiServiceWinImpl::~MidiServiceWinImpl() is blocked until |
| // |sender_thread_|, and |task_thread_| are stopped. |
| // This flag can be used as the signal that when background tasks must be |
| // interrupted. |
| // TODO(toyoshim): Use std::atomic<bool> when it is allowed. |
| volatile bool destructor_started; |
| |
| DISALLOW_COPY_AND_ASSIGN(MidiServiceWinImpl); |
| }; |
| |
| } // namespace |
| |
| MidiManagerWin::MidiManagerWin() { |
| } |
| |
| MidiManagerWin::~MidiManagerWin() { |
| midi_service_.reset(); |
| } |
| |
| void MidiManagerWin::StartInitialization() { |
| midi_service_.reset(new MidiServiceWinImpl); |
| // Note that |CompleteInitialization()| will be called from the callback. |
| midi_service_->InitializeAsync(this); |
| } |
| |
| void MidiManagerWin::DispatchSendMidiData(MidiManagerClient* client, |
| uint32 port_index, |
| const std::vector<uint8>& data, |
| double timestamp) { |
| if (!midi_service_) |
| return; |
| |
| base::TimeTicks time_to_send = base::TimeTicks::Now(); |
| if (timestamp != 0.0) { |
| time_to_send = |
| base::TimeTicks() + base::TimeDelta::FromMicroseconds( |
| timestamp * base::Time::kMicrosecondsPerSecond); |
| } |
| midi_service_->SendMidiDataAsync(port_index, data, time_to_send); |
| |
| // TOOD(toyoshim): This calculation should be done when the date is actually |
| // sent. |
| client->AccumulateMidiBytesSent(data.size()); |
| } |
| |
| void MidiManagerWin::OnCompleteInitialization(MidiResult result) { |
| CompleteInitialization(result); |
| } |
| |
| void MidiManagerWin::OnAddInputPort(MidiPortInfo info) { |
| AddInputPort(info); |
| } |
| |
| void MidiManagerWin::OnAddOutputPort(MidiPortInfo info) { |
| AddOutputPort(info); |
| } |
| |
| void MidiManagerWin::OnSetInputPortState(uint32 port_index, |
| MidiPortState state) { |
| SetInputPortState(port_index, state); |
| } |
| |
| void MidiManagerWin::OnSetOutputPortState(uint32 port_index, |
| MidiPortState state) { |
| SetOutputPortState(port_index, state); |
| } |
| |
| void MidiManagerWin::OnReceiveMidiData(uint32 port_index, |
| const std::vector<uint8>& data, |
| base::TimeTicks time) { |
| ReceiveMidiData(port_index, &data[0], data.size(), time); |
| } |
| |
| MidiManager* MidiManager::Create() { |
| return new MidiManagerWin(); |
| } |
| |
| } // namespace midi |
| } // namespace media |