blob: 2a7265ced4c68bd0219614b8b661bd50c97274ed [file] [log] [blame]
// 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 <stddef.h>
#include <algorithm>
#include <functional>
#include <queue>
#include <string>
#include "base/bind.h"
#include "base/containers/hash_tables.h"
#include "base/feature_list.h"
#include "base/macros.h"
#include "base/message_loop/message_loop.h"
#include "base/single_thread_task_runner.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 "base/win/windows_version.h"
#include "device/usb/usb_ids.h"
#include "media/midi/dynamically_initialized_midi_manager_win.h"
#include "media/midi/message_util.h"
#include "media/midi/midi_manager_winrt.h"
#include "media/midi/midi_message_queue.h"
#include "media/midi/midi_port_info.h"
#include "media/midi/midi_switches.h"
namespace midi {
namespace {
using mojom::PortState;
using mojom::Result;
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));
}
void CloseOutputPortOnTaskThread(HMIDIOUT midi_out_handle) {
midiOutClose(midi_out_handle);
}
class MIDIHDRDeleter {
public:
void operator()(MIDIHDR* header) {
if (!header)
return;
delete[] static_cast<char*>(header->lpData);
header->lpData = NULL;
header->dwBufferLength = 0;
delete header;
}
};
using ScopedMIDIHDR = std::unique_ptr<MIDIHDR, MIDIHDRDeleter>;
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;
}
void SendShortMidiMessageInternal(HMIDIOUT midi_out_handle,
const std::vector<uint8_t>& 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_t>(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_t>& 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 256 KB size
// limit on SysEx message, with hoping that midiOutLongMsg will be blocked at
// most 4 sec or so with a typical USB-MIDI device.
// TODO(crbug.com/383578): This restriction should be removed once Web MIDI
// defines a standardized way to handle large sysex messages.
const size_t kSysExSizeLimit = 256 * 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.
ignore_result(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)),
is_software_synth(false) {}
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)),
is_software_synth(IsSoftwareSynth(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),
is_software_synth(info.is_software_synth) {}
// 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_t manufacturer_id;
const uint16_t product_id;
const uint32_t driver_version;
const base::string16 product_name;
const uint16_t usb_vendor_id;
const uint16_t usb_product_id;
const bool is_usb_device;
const bool is_software_synth;
// 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 bool IsSoftwareSynth(const MIDIOUTCAPS2W& caps) {
return caps.wTechnology == MOD_SWSYNTH;
}
static uint16_t ExtractUsbVendorIdIfExists(const MIDIINCAPS2W& caps) {
if (!IS_COMPATIBLE_USBAUDIO_MID(&caps.ManufacturerGuid))
return 0;
return EXTRACT_USBAUDIO_MID(&caps.ManufacturerGuid);
}
static uint16_t ExtractUsbVendorIdIfExists(const MIDIOUTCAPS2W& caps) {
if (!IS_COMPATIBLE_USBAUDIO_MID(&caps.ManufacturerGuid))
return 0;
return EXTRACT_USBAUDIO_MID(&caps.ManufacturerGuid);
}
static uint16_t ExtractUsbProductIdIfExists(const MIDIINCAPS2W& caps) {
if (!IS_COMPATIBLE_USBAUDIO_PID(&caps.ProductGuid))
return 0;
return EXTRACT_USBAUDIO_PID(&caps.ProductGuid);
}
static uint16_t 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.is_software_synth && 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_t,
std::vector<uint32_t>,
std::greater<uint32_t>>,
MidiDeviceInfo::Hasher>;
struct MidiInputDeviceState final
: base::RefCountedThreadSafe<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_t 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_t 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::RefCountedThreadSafe<MidiInputDeviceState>;
~MidiInputDeviceState() {}
};
struct MidiOutputDeviceState final
: base::RefCountedThreadSafe<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_t 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_t 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::RefCountedThreadSafe<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.
DCHECK(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 (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 (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.
DCHECK(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_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&MidiServiceWinImpl::CompleteInitializationOnTaskThread,
base::Unretained(this), Result::OK));
}
void SendMidiDataAsync(uint32_t port_number,
const std::vector<uint8_t>& 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_.task_runner()->PostDelayedTask(
FROM_HERE, base::Bind(&MidiServiceWinImpl::SendOnSenderThread,
base::Unretained(this), port_number,
state->port_age, data, time),
time - now);
} else {
sender_thread_.task_runner()->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 {
DCHECK(thread_checker_.CalledOnValidThread());
if (destructor_started)
return;
switch (device_type) {
case base::SystemMonitor::DEVTYPE_AUDIO:
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_t 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_.task_runner()->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_t 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_t>(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_.task_runner()->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),
PortState::OPENED);
task_thread_.task_runner()->PostTask(
FROM_HERE, base::Bind(&MidiServiceWinImpl::AddInputPortOnTaskThread,
base::Unretained(this), port_info));
} else {
task_thread_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&MidiServiceWinImpl::SetInputPortStateOnTaskThread,
base::Unretained(this), port_number,
PortState::CONNECTED));
}
}
void OnMidiInDataOnMultimediaThread(HMIDIIN midi_in_handle,
DWORD_PTR param1,
DWORD_PTR param2) {
auto state = GetInputDeviceFromHandle(midi_in_handle);
if (!state)
return;
const uint8_t status_byte = static_cast<uint8_t>(param1 & 0xff);
const uint8_t first_data_byte = static_cast<uint8_t>((param1 >> 8) & 0xff);
const uint8_t second_data_byte =
static_cast<uint8_t>((param1 >> 16) & 0xff);
const DWORD elapsed_ms = param2;
const size_t len = GetMessageLength(status_byte);
const uint8_t kData[] = {status_byte, first_data_byte, second_data_byte};
std::vector<uint8_t> 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_.task_runner()->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_t* src = reinterpret_cast<const uint8_t*>(header->lpData);
std::vector<uint8_t> 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_.task_runner()->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_t 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_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&MidiServiceWinImpl::SetInputPortStateOnTaskThread,
base::Unretained(this), port_number,
PortState::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)) {
task_thread_.task_runner()->PostTask(
FROM_HERE, base::Bind(&CloseOutputPortOnTaskThread, midi_out_handle));
return;
}
bool add_new_port = false;
uint32_t 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_t>(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),
PortState::OPENED);
task_thread_.task_runner()->PostTask(
FROM_HERE, base::Bind(&MidiServiceWinImpl::AddOutputPortOnTaskThread,
base::Unretained(this), port_info));
} else {
task_thread_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&MidiServiceWinImpl::SetOutputPortStateOnTaskThread,
base::Unretained(this), port_number,
PortState::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_t 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_.task_runner()->PostTask(
FROM_HERE,
base::Bind(&MidiServiceWinImpl::SetOutputPortStateOnTaskThread,
base::Unretained(this), port_number,
PortState::DISCONNECTED));
}
/////////////////////////////////////////////////////////////////////////////
// Callbacks on the sender thread.
/////////////////////////////////////////////////////////////////////////////
void AssertOnSenderThread() {
DCHECK_EQ(sender_thread_.GetThreadId(), base::PlatformThread::CurrentId());
}
void SendOnSenderThread(uint32_t port_number,
uint64_t port_age,
const std::vector<uint8_t>& 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_t> 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_.GetThreadId(), 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(Result result) {
AssertOnTaskThread();
delegate_->OnCompleteInitialization(result);
}
void ReceiveMidiDataOnTaskThread(uint32_t port_index,
std::vector<uint8_t> 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_t port_index, PortState state) {
AssertOnTaskThread();
delegate_->OnSetInputPortState(port_index, state);
}
void SetOutputPortStateOnTaskThread(uint32_t port_index, PortState 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_t> 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_t> 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(MidiService* service) : MidiManager(service) {}
MidiManagerWin::~MidiManagerWin() {
}
void MidiManagerWin::StartInitialization() {
midi_service_.reset(new MidiServiceWinImpl);
// Note that |CompleteInitialization()| will be called from the callback.
midi_service_->InitializeAsync(this);
}
void MidiManagerWin::Finalize() {
midi_service_.reset();
}
void MidiManagerWin::DispatchSendMidiData(MidiManagerClient* client,
uint32_t port_index,
const std::vector<uint8_t>& 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(Result result) {
CompleteInitialization(result);
}
void MidiManagerWin::OnAddInputPort(MidiPortInfo info) {
AddInputPort(info);
}
void MidiManagerWin::OnAddOutputPort(MidiPortInfo info) {
AddOutputPort(info);
}
void MidiManagerWin::OnSetInputPortState(uint32_t port_index, PortState state) {
SetInputPortState(port_index, state);
}
void MidiManagerWin::OnSetOutputPortState(uint32_t port_index,
PortState state) {
SetOutputPortState(port_index, state);
}
void MidiManagerWin::OnReceiveMidiData(uint32_t port_index,
const std::vector<uint8_t>& data,
base::TimeTicks time) {
ReceiveMidiData(port_index, &data[0], data.size(), time);
}
MidiManager* MidiManager::Create(MidiService* service) {
if (base::FeatureList::IsEnabled(features::kMidiManagerWinrt) &&
base::win::GetVersion() >= base::win::VERSION_WIN10)
return new MidiManagerWinrt(service);
if (base::FeatureList::IsEnabled(features::kMidiManagerDynamicInstantiation))
return new DynamicallyInitializedMidiManagerWin(service);
return new MidiManagerWin(service);
}
} // namespace midi