blob: 6041adcb26ae3df3d0d468e341ce630347a74db0 [file] [log] [blame]
// Copyright (c) 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_mac.h"
#include <stddef.h>
#include <algorithm>
#include <iterator>
#include <string>
#include <CoreAudio/HostTime.h>
#include "base/bind.h"
#include "base/single_thread_task_runner.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/sys_string_conversions.h"
#include "media/midi/midi_service.h"
#include "media/midi/task_service.h"
using base::NumberToString;
using base::SysCFStringRefToUTF8;
using midi::mojom::PortState;
using midi::mojom::Result;
// NB: System MIDI types are pointer types in 32-bit and integer types in
// 64-bit. Therefore, the initialization is the simplest one that satisfies both
// (if possible).
namespace midi {
namespace {
// Maximum buffer size that CoreMIDI can handle for MIDIPacketList.
const size_t kCoreMIDIMaxPacketListSize = 65536;
// Pessimistic estimation on available data size of MIDIPacketList.
const size_t kEstimatedMaxPacketDataSize = kCoreMIDIMaxPacketListSize / 2;
enum {
kSessionTaskRunner = TaskService::kDefaultRunnerId,
kClientTaskRunner,
};
mojom::PortInfo GetPortInfoFromEndpoint(MIDIEndpointRef endpoint) {
std::string manufacturer;
CFStringRef manufacturer_ref = NULL;
OSStatus result = MIDIObjectGetStringProperty(
endpoint, kMIDIPropertyManufacturer, &manufacturer_ref);
if (result == noErr) {
manufacturer = SysCFStringRefToUTF8(manufacturer_ref);
} else {
// kMIDIPropertyManufacturer is not supported in IAC driver providing
// endpoints, and the result will be kMIDIUnknownProperty (-10835).
DLOG(WARNING) << "Failed to get kMIDIPropertyManufacturer with status "
<< result;
}
std::string name;
CFStringRef name_ref = NULL;
result = MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName,
&name_ref);
if (result == noErr) {
name = SysCFStringRefToUTF8(name_ref);
} else {
DLOG(WARNING) << "Failed to get kMIDIPropertyDisplayName with status "
<< result;
}
std::string version;
SInt32 version_number = 0;
result = MIDIObjectGetIntegerProperty(
endpoint, kMIDIPropertyDriverVersion, &version_number);
if (result == noErr) {
version = NumberToString(version_number);
} else {
// kMIDIPropertyDriverVersion is not supported in IAC driver providing
// endpoints, and the result will be kMIDIUnknownProperty (-10835).
DLOG(WARNING) << "Failed to get kMIDIPropertyDriverVersion with status "
<< result;
}
std::string id;
SInt32 id_number = 0;
result = MIDIObjectGetIntegerProperty(
endpoint, kMIDIPropertyUniqueID, &id_number);
if (result == noErr) {
id = NumberToString(id_number);
} else {
// On connecting some devices, e.g., nano KONTROL2, unknown endpoints
// appear and disappear quickly and they fail on queries.
// Let's ignore such ghost devices.
// Same problems will happen if the device is disconnected before finishing
// all queries.
DLOG(WARNING) << "Failed to get kMIDIPropertyUniqueID with status "
<< result;
}
const PortState state = PortState::OPENED;
return mojom::PortInfo(id, manufacturer, name, version, state);
}
base::TimeTicks MIDITimeStampToTimeTicks(MIDITimeStamp timestamp) {
UInt64 nanoseconds = AudioConvertHostTimeToNanos(timestamp);
return base::TimeTicks() + base::TimeDelta::FromNanoseconds(nanoseconds);
}
MIDITimeStamp TimeTicksToMIDITimeStamp(base::TimeTicks ticks) {
return AudioConvertNanosToHostTime(ticks.since_origin().InNanoseconds());
}
} // namespace
MidiManager* MidiManager::Create(MidiService* service) {
return new MidiManagerMac(service);
}
MidiManagerMac::MidiManagerMac(MidiService* service) : MidiManager(service) {}
MidiManagerMac::~MidiManagerMac() {
if (!service()->task_service()->UnbindInstance())
return;
// Finalization steps should be implemented after the UnbindInstance() call.
// Do not need to dispose |coremidi_input_| and |coremidi_output_| explicitly.
// CoreMIDI automatically disposes them on the client disposal.
base::AutoLock lock(midi_client_lock_);
if (midi_client_)
MIDIClientDispose(midi_client_);
}
void MidiManagerMac::StartInitialization() {
if (!service()->task_service()->BindInstance())
return CompleteInitialization(Result::INITIALIZATION_ERROR);
service()->task_service()->PostBoundTask(
kClientTaskRunner, base::BindOnce(&MidiManagerMac::InitializeCoreMIDI,
base::Unretained(this)));
}
void MidiManagerMac::DispatchSendMidiData(MidiManagerClient* client,
uint32_t port_index,
const std::vector<uint8_t>& data,
base::TimeTicks timestamp) {
service()->task_service()->PostBoundTask(
kClientTaskRunner,
base::BindOnce(&MidiManagerMac::SendMidiData, base::Unretained(this),
client, port_index, data, timestamp));
}
void MidiManagerMac::InitializeCoreMIDI() {
DCHECK(service()->task_service()->IsOnTaskRunner(kClientTaskRunner));
// CoreMIDI registration.
MIDIClientRef client = 0u;
OSStatus result = MIDIClientCreate(CFSTR("Chrome"), ReceiveMidiNotifyDispatch,
this, &client);
if (result != noErr || client == 0u)
return CompleteCoreMIDIInitialization(Result::INITIALIZATION_ERROR);
{
base::AutoLock lock(midi_client_lock_);
midi_client_ = client;
}
// Create input and output port. These MIDIPortRef references are not needed
// to be disposed explicitly. CoreMIDI automatically disposes them on the
// client disposal.
result = MIDIInputPortCreate(client, CFSTR("MIDI Input"), ReadMidiDispatch,
this, &midi_input_);
if (result != noErr || midi_input_ == 0u)
return CompleteCoreMIDIInitialization(Result::INITIALIZATION_ERROR);
result = MIDIOutputPortCreate(client, CFSTR("MIDI Output"), &midi_output_);
if (result != noErr || midi_output_ == 0u)
return CompleteCoreMIDIInitialization(Result::INITIALIZATION_ERROR);
// Following loop may miss some newly attached devices, but such device will
// be captured by ReceiveMidiNotifyDispatch callback.
destinations_.resize(MIDIGetNumberOfDestinations());
for (size_t i = 0u; i < destinations_.size(); ++i) {
MIDIEndpointRef destination = MIDIGetDestination(i);
DCHECK_NE(0u, destination);
// Keep track of all destinations (known as outputs by the Web MIDI API).
destinations_[i] = destination;
AddOutputPort(GetPortInfoFromEndpoint(destination));
}
// Allocate maximum size of buffer that CoreMIDI can handle.
midi_buffer_.resize(kCoreMIDIMaxPacketListSize);
// Open connections from all sources. This loop also may miss new devices.
sources_.resize(MIDIGetNumberOfSources());
for (size_t i = 0u; i < sources_.size(); ++i) {
MIDIEndpointRef source = MIDIGetSource(i);
DCHECK_NE(0u, source);
// Keep track of all sources (known as inputs by the Web MIDI API).
sources_[i] = source;
AddInputPort(GetPortInfoFromEndpoint(source));
}
// Start listening.
for (size_t i = 0u; i < sources_.size(); ++i)
MIDIPortConnectSource(midi_input_, sources_[i], reinterpret_cast<void*>(i));
CompleteCoreMIDIInitialization(Result::OK);
}
void MidiManagerMac::CompleteCoreMIDIInitialization(mojom::Result result) {
service()->task_service()->PostBoundTask(
kSessionTaskRunner,
base::BindOnce(&MidiManagerMac::CompleteInitialization,
base::Unretained(this), result));
}
// static
void MidiManagerMac::ReceiveMidiNotifyDispatch(const MIDINotification* message,
void* refcon) {
// This callback function is invoked on |kClientTaskRunner|.
// |manager| should be valid because we can ensure |midi_client_| is still
// alive here.
MidiManagerMac* manager = static_cast<MidiManagerMac*>(refcon);
manager->ReceiveMidiNotify(message);
}
void MidiManagerMac::ReceiveMidiNotify(const MIDINotification* message) {
DCHECK(service()->task_service()->IsOnTaskRunner(kClientTaskRunner));
if (kMIDIMsgObjectAdded == message->messageID) {
// New device is going to be attached.
const MIDIObjectAddRemoveNotification* notification =
reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
MIDIEndpointRef endpoint =
static_cast<MIDIEndpointRef>(notification->child);
if (notification->childType == kMIDIObjectType_Source) {
// Attaching device is an input device.
auto it = std::find(sources_.begin(), sources_.end(), endpoint);
if (it == sources_.end()) {
mojom::PortInfo info = GetPortInfoFromEndpoint(endpoint);
// If the device disappears before finishing queries, mojom::PortInfo
// becomes incomplete. Skip and do not cache such information here.
// On kMIDIMsgObjectRemoved, the entry will be ignored because it
// will not be found in the pool.
if (!info.id.empty()) {
sources_.push_back(endpoint);
AddInputPort(info);
MIDIPortConnectSource(midi_input_, endpoint,
reinterpret_cast<void*>(sources_.size() - 1));
}
} else {
SetInputPortState(it - sources_.begin(), PortState::OPENED);
}
} else if (notification->childType == kMIDIObjectType_Destination) {
// Attaching device is an output device.
auto it = std::find(destinations_.begin(), destinations_.end(), endpoint);
if (it == destinations_.end()) {
mojom::PortInfo info = GetPortInfoFromEndpoint(endpoint);
// Skip cases that queries are not finished correctly.
if (!info.id.empty()) {
destinations_.push_back(endpoint);
AddOutputPort(info);
}
} else {
SetOutputPortState(it - destinations_.begin(), PortState::OPENED);
}
}
} else if (kMIDIMsgObjectRemoved == message->messageID) {
// Existing device is going to be detached.
const MIDIObjectAddRemoveNotification* notification =
reinterpret_cast<const MIDIObjectAddRemoveNotification*>(message);
MIDIEndpointRef endpoint =
static_cast<MIDIEndpointRef>(notification->child);
if (notification->childType == kMIDIObjectType_Source) {
// Detaching device is an input device.
auto it = std::find(sources_.begin(), sources_.end(), endpoint);
if (it != sources_.end())
SetInputPortState(it - sources_.begin(), PortState::DISCONNECTED);
} else if (notification->childType == kMIDIObjectType_Destination) {
// Detaching device is an output device.
auto it = std::find(destinations_.begin(), destinations_.end(), endpoint);
if (it != destinations_.end())
SetOutputPortState(it - destinations_.begin(), PortState::DISCONNECTED);
}
}
}
// static
void MidiManagerMac::ReadMidiDispatch(const MIDIPacketList* packet_list,
void* read_proc_refcon,
void* src_conn_refcon) {
// This method is called on a separate high-priority thread owned by CoreMIDI.
// |manager| should be valid because we can ensure |midi_client_| is still
// alive here.
MidiManagerMac* manager = static_cast<MidiManagerMac*>(read_proc_refcon);
DCHECK(manager);
uint32_t port_index = reinterpret_cast<uintptr_t>(src_conn_refcon);
// Go through each packet and process separately.
const MIDIPacket* packet = &packet_list->packet[0];
for (size_t i = 0u; i < packet_list->numPackets; i++) {
// Each packet contains MIDI data for one or more messages (like note-on).
base::TimeTicks timestamp = MIDITimeStampToTimeTicks(packet->timeStamp);
manager->ReceiveMidiData(port_index, packet->data, packet->length,
timestamp);
packet = MIDIPacketNext(packet);
}
}
void MidiManagerMac::SendMidiData(MidiManagerClient* client,
uint32_t port_index,
const std::vector<uint8_t>& data,
base::TimeTicks timestamp) {
DCHECK(service()->task_service()->IsOnTaskRunner(kClientTaskRunner));
// Lookup the destination based on the port index.
if (static_cast<size_t>(port_index) >= destinations_.size())
return;
MIDITimeStamp coremidi_timestamp = TimeTicksToMIDITimeStamp(timestamp);
MIDIEndpointRef destination = destinations_[port_index];
size_t send_size;
for (size_t sent_size = 0u; sent_size < data.size(); sent_size += send_size) {
MIDIPacketList* packet_list =
reinterpret_cast<MIDIPacketList*>(midi_buffer_.data());
MIDIPacket* midi_packet = MIDIPacketListInit(packet_list);
// Limit the maximum payload size to kEstimatedMaxPacketDataSize that is
// half of midi_buffer data size. MIDIPacketList and MIDIPacket consume
// extra buffer areas for meta information, and available size is smaller
// than buffer size. Here, we simply assume that at least half size is
// available for data payload.
send_size = std::min(data.size() - sent_size, kEstimatedMaxPacketDataSize);
midi_packet = MIDIPacketListAdd(
packet_list,
kCoreMIDIMaxPacketListSize,
midi_packet,
coremidi_timestamp,
send_size,
&data[sent_size]);
DCHECK(midi_packet);
MIDISend(midi_output_, destination, packet_list);
}
AccumulateMidiBytesSent(client, data.size());
}
} // namespace midi