blob: 26657d642f9e2b684f39ef0138d2214321934c8e [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>
// 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 <string>
#include "base/bind.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<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 = size;
return header.Pass();
}
void SendShortMidiMessageInternal(HMIDIOUT midi_out_handle,
const std::vector<uint8>& message) {
if (message.size() >= 4)
return;
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 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<char>(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<InDeviceInfo> Create(MidiManagerWin* manager,
UINT device_id) {
scoped_ptr<InDeviceInfo> 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<DWORD_PTR>(&HandleMessage),
reinterpret_cast<DWORD_PTR>(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<InDeviceInfo*>(instance);
if (!self)
return;
if (self->midi_handle() != midi_in_handle)
return;
switch (message) {
case MIM_DATA:
self->OnShortMessageReceived(static_cast<uint8>(param1 & 0xff),
static_cast<uint8>((param1 >> 8) & 0xff),
static_cast<uint8>((param1 >> 16) & 0xff),
param2);
return;
case MIM_LONGDATA:
self->OnLongMessageReceived(reinterpret_cast<MIDIHDR*>(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<const uint8*>(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);
manager_->ReceiveMidiData(port_index_, data, length, event_time);
}
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<OutDeviceInfo> Create(UINT device_id) {
scoped_ptr<OutDeviceInfo> 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<uint8>& 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<uint8> 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<DWORD_PTR>(&HandleMessage),
reinterpret_cast<DWORD_PTR>(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<OutDeviceInfo*>(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<MIDIHDR*>(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") {
}
void MidiManagerWin::StartInitialization() {
const UINT num_in_devices = midiInGetNumDevs();
in_devices_.reserve(num_in_devices);
int inport_index = 0;
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<InDeviceInfo> in_device(InDeviceInfo::Create(this, device_id));
if (!in_device)
continue;
MidiPortInfo info(
base::IntToString(static_cast<int>(device_id)),
"",
base::WideToUTF8(caps.szPname),
base::IntToString(static_cast<int>(caps.vDriverVersion)),
MIDI_PORT_OPENED);
AddInputPort(info);
in_device->set_port_index(inport_index++);
in_devices_.push_back(in_device.release());
}
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<OutDeviceInfo> out_port(OutDeviceInfo::Create(device_id));
if (!out_port)
continue;
MidiPortInfo info(
base::IntToString(static_cast<int>(device_id)),
"",
base::WideToUTF8(caps.szPname),
base::IntToString(static_cast<int>(caps.vDriverVersion)),
MIDI_PORT_OPENED);
AddOutputPort(info);
out_devices_.push_back(out_port.release());
}
CompleteInitialization(MIDI_OK);
}
MidiManagerWin::~MidiManagerWin() {
// Cleanup order is important. |send_thread_| must be stopped before
// |out_devices_| is cleared.
for (auto& device : out_devices_)
device->Quit();
send_thread_.Stop();
out_devices_.clear();
in_devices_.clear();
}
void MidiManagerWin::DispatchSendMidiData(MidiManagerClient* client,
uint32 port_index,
const std::vector<uint8>& 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];
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