blob: 484ecd1903592e15b335e3b78b02200fb246f965 [file] [log] [blame]
// Copyright 2014 The Chromium Authors
// 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_alsa.h"
#include <errno.h>
#include <poll.h>
#include <stddef.h>
#include <stdlib.h>
#include <string>
#include <utility>
#include "base/functional/bind.h"
#include "base/json/json_string_value_serializer.h"
#include "base/logging.h"
#include "base/posix/eintr_wrapper.h"
#include "base/posix/safe_strerror.h"
#include "base/ranges/algorithm.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/stringprintf.h"
#include "base/task/single_thread_task_runner.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "crypto/sha2.h"
#include "media/midi/midi_service.h"
#include "media/midi/midi_service.mojom.h"
#include "media/midi/task_service.h"
namespace midi {
namespace {
using mojom::PortState;
using mojom::Result;
enum {
kDefaultRunnerNotUsedOnAlsa = TaskService::kDefaultRunnerId,
kEventTaskRunner,
kSendTaskRunner
};
// Per-output buffer. This can be smaller, but then large sysex messages
// will be (harmlessly) split across multiple seq events. This should
// not have any real practical effect, except perhaps to slightly reorder
// realtime messages with respect to sysex.
constexpr size_t kSendBufferSize = 256;
// Minimum client id for which we will have ALSA card devices for. When we
// are searching for card devices (used to get the path, id, and manufacturer),
// we don't want to get confused by kernel clients that do not have a card.
// See seq_clientmgr.c in the ALSA code for this.
// TODO(agoode): Add proper client -> card export from the kernel to avoid
// hardcoding.
constexpr int kMinimumClientIdForCards = 16;
// ALSA constants.
const char kAlsaHw[] = "hw";
// udev constants.
const char kUdev[] = "udev";
const char kUdevSubsystemSound[] = "sound";
const char kUdevPropertySoundInitialized[] = "SOUND_INITIALIZED";
const char kUdevActionChange[] = "change";
const char kUdevActionRemove[] = "remove";
const char kUdevIdVendor[] = "ID_VENDOR";
const char kUdevIdVendorEnc[] = "ID_VENDOR_ENC";
const char kUdevIdVendorFromDatabase[] = "ID_VENDOR_FROM_DATABASE";
const char kUdevIdVendorId[] = "ID_VENDOR_ID";
const char kUdevIdModelId[] = "ID_MODEL_ID";
const char kUdevIdBus[] = "ID_BUS";
const char kUdevIdPath[] = "ID_PATH";
const char kUdevIdUsbInterfaceNum[] = "ID_USB_INTERFACE_NUM";
const char kUdevIdSerialShort[] = "ID_SERIAL_SHORT";
const char kSysattrVendorName[] = "vendor_name";
const char kSysattrVendor[] = "vendor";
const char kSysattrModel[] = "model";
const char kSysattrGuid[] = "guid";
const char kCardSyspath[] = "/card";
// Constants for the capabilities we search for in inputs and outputs.
// See http://www.alsa-project.org/alsa-doc/alsa-lib/seq.html.
constexpr unsigned int kRequiredInputPortCaps =
SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_SUBS_READ;
constexpr unsigned int kRequiredOutputPortCaps =
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_SUBS_WRITE;
constexpr unsigned int kCreateOutputPortCaps =
SND_SEQ_PORT_CAP_READ | SND_SEQ_PORT_CAP_NO_EXPORT;
constexpr unsigned int kCreateInputPortCaps =
SND_SEQ_PORT_CAP_WRITE | SND_SEQ_PORT_CAP_NO_EXPORT;
constexpr unsigned int kCreatePortType =
SND_SEQ_PORT_TYPE_MIDI_GENERIC | SND_SEQ_PORT_TYPE_APPLICATION;
int AddrToInt(int client, int port) {
return (client << 8) | port;
}
// Returns true if this client has an ALSA card associated with it.
bool IsCardClient(snd_seq_client_type_t type, int client_id) {
return (type == SND_SEQ_KERNEL_CLIENT) &&
(client_id >= kMinimumClientIdForCards);
}
// TODO(agoode): Move this to device/udev_linux.
const std::string UdevDeviceGetPropertyOrSysattr(
struct udev_device* udev_device,
const char* property_key,
const char* sysattr_key) {
// First try the property.
std::string value =
device::UdevDeviceGetPropertyValue(udev_device, property_key);
// If no property, look for sysattrs and walk up the parent devices too.
while (value.empty() && udev_device) {
value = device::UdevDeviceGetSysattrValue(udev_device, sysattr_key);
udev_device = device::udev_device_get_parent(udev_device);
}
return value;
}
int GetCardNumber(udev_device* dev) {
const char* syspath = device::udev_device_get_syspath(dev);
if (!syspath)
return -1;
std::string syspath_str(syspath);
size_t i = syspath_str.rfind(kCardSyspath);
if (i == std::string::npos)
return -1;
int number;
if (!base::StringToInt(syspath_str.substr(i + strlen(kCardSyspath)), &number))
return -1;
return number;
}
std::string GetVendor(udev_device* dev) {
// Try to get the vendor string. Sometimes it is encoded.
std::string vendor = device::UdevDecodeString(
device::UdevDeviceGetPropertyValue(dev, kUdevIdVendorEnc));
// Sometimes it is not encoded.
if (vendor.empty())
vendor =
UdevDeviceGetPropertyOrSysattr(dev, kUdevIdVendor, kSysattrVendorName);
return vendor;
}
void SetStringIfNonEmpty(base::Value::Dict* value,
const std::string& path,
const std::string& in_value) {
if (!in_value.empty())
value->Set(path, in_value);
}
} // namespace
MidiManagerAlsa::MidiManagerAlsa(MidiService* service) : MidiManager(service) {}
MidiManagerAlsa::~MidiManagerAlsa() {
{
base::AutoLock lock(out_client_lock_);
// Close the out client. This will trigger the event thread to stop,
// because of SND_SEQ_EVENT_CLIENT_EXIT.
out_client_.reset();
}
// Ensure that no task is running any more.
if (!service()->task_service()->UnbindInstance())
return;
// |out_client_| should be reset before UnbindInstance() call to avoid
// a deadlock, but other finalization steps should be implemented after the
// UnbindInstance() call above, if we need.
}
void MidiManagerAlsa::StartInitialization() {
if (!service()->task_service()->BindInstance())
return CompleteInitialization(Result::INITIALIZATION_ERROR);
// Create client handles and name the clients.
int err;
{
snd_seq_t* in_seq = nullptr;
err = snd_seq_open(&in_seq, kAlsaHw, SND_SEQ_OPEN_INPUT, SND_SEQ_NONBLOCK);
if (err != 0) {
VLOG(1) << "snd_seq_open fails: " << snd_strerror(err);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
in_client_ = ScopedSndSeqPtr(in_seq);
in_client_id_ = snd_seq_client_id(in_client_.get());
err = snd_seq_set_client_name(in_client_.get(), "Chrome (input)");
if (err != 0) {
VLOG(1) << "snd_seq_set_client_name fails: " << snd_strerror(err);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
}
{
snd_seq_t* out_seq = nullptr;
err = snd_seq_open(&out_seq, kAlsaHw, SND_SEQ_OPEN_OUTPUT, 0);
if (err != 0) {
VLOG(1) << "snd_seq_open fails: " << snd_strerror(err);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
base::AutoLock lock(out_client_lock_);
out_client_ = ScopedSndSeqPtr(out_seq);
out_client_id_ = snd_seq_client_id(out_client_.get());
err = snd_seq_set_client_name(out_client_.get(), "Chrome (output)");
if (err != 0) {
VLOG(1) << "snd_seq_set_client_name fails: " << snd_strerror(err);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
}
// Create input port.
in_port_id_ = snd_seq_create_simple_port(
in_client_.get(), NULL, kCreateInputPortCaps, kCreatePortType);
if (in_port_id_ < 0) {
VLOG(1) << "snd_seq_create_simple_port fails: "
<< snd_strerror(in_port_id_);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
// Subscribe to the announce port.
snd_seq_port_subscribe_t* subs;
snd_seq_port_subscribe_alloca(&subs);
snd_seq_addr_t announce_sender;
snd_seq_addr_t announce_dest;
announce_sender.client = SND_SEQ_CLIENT_SYSTEM;
announce_sender.port = SND_SEQ_PORT_SYSTEM_ANNOUNCE;
announce_dest.client = in_client_id_;
announce_dest.port = in_port_id_;
snd_seq_port_subscribe_set_sender(subs, &announce_sender);
snd_seq_port_subscribe_set_dest(subs, &announce_dest);
err = snd_seq_subscribe_port(in_client_.get(), subs);
if (err != 0) {
VLOG(1) << "snd_seq_subscribe_port on the announce port fails: "
<< snd_strerror(err);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
// Initialize decoder.
decoder_ = CreateScopedSndMidiEventPtr(0);
snd_midi_event_no_status(decoder_.get(), 1);
// Initialize udev and monitor.
udev_ = device::ScopedUdevPtr(device::udev_new());
udev_monitor_ = device::ScopedUdevMonitorPtr(
device::udev_monitor_new_from_netlink(udev_.get(), kUdev));
if (!udev_monitor_.get()) {
VLOG(1) << "udev_monitor_new_from_netlink fails";
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
err = device::udev_monitor_filter_add_match_subsystem_devtype(
udev_monitor_.get(), kUdevSubsystemSound, nullptr);
if (err != 0) {
VLOG(1) << "udev_monitor_add_match_subsystem fails: "
<< base::safe_strerror(-err);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
err = device::udev_monitor_enable_receiving(udev_monitor_.get());
if (err != 0) {
VLOG(1) << "udev_monitor_enable_receiving fails: "
<< base::safe_strerror(-err);
return CompleteInitialization(Result::INITIALIZATION_ERROR);
}
// Generate hotplug events for existing ports.
// TODO(agoode): Check the return value for failure.
EnumerateAlsaPorts();
// Generate hotplug events for existing udev devices. This must be done
// after udev_monitor_enable_receiving() is called. See the algorithm
// at http://www.signal11.us/oss/udev/.
EnumerateUdevCards();
// Start processing events. Don't do this before enumeration of both
// ALSA and udev.
service()->task_service()->PostBoundTask(
kEventTaskRunner,
base::BindOnce(&MidiManagerAlsa::EventLoop, base::Unretained(this)));
CompleteInitialization(Result::OK);
}
void MidiManagerAlsa::DispatchSendMidiData(MidiManagerClient* client,
uint32_t port_index,
const std::vector<uint8_t>& data,
base::TimeTicks timestamp) {
service()->task_service()->PostBoundDelayedTask(
kSendTaskRunner,
base::BindOnce(&MidiManagerAlsa::SendMidiData, base::Unretained(this),
client, port_index, data),
MidiService::TimestampToTimeDeltaDelay(timestamp));
}
MidiManagerAlsa::MidiPort::Id::Id() = default;
MidiManagerAlsa::MidiPort::Id::Id(const std::string& bus,
const std::string& vendor_id,
const std::string& model_id,
const std::string& usb_interface_num,
const std::string& serial)
: bus_(bus),
vendor_id_(vendor_id),
model_id_(model_id),
usb_interface_num_(usb_interface_num),
serial_(serial) {}
MidiManagerAlsa::MidiPort::Id::Id(const Id&) = default;
MidiManagerAlsa::MidiPort::Id::~Id() = default;
bool MidiManagerAlsa::MidiPort::Id::operator==(const Id& rhs) const {
return (bus_ == rhs.bus_) && (vendor_id_ == rhs.vendor_id_) &&
(model_id_ == rhs.model_id_) &&
(usb_interface_num_ == rhs.usb_interface_num_) &&
(serial_ == rhs.serial_);
}
bool MidiManagerAlsa::MidiPort::Id::empty() const {
return bus_.empty() && vendor_id_.empty() && model_id_.empty() &&
usb_interface_num_.empty() && serial_.empty();
}
MidiManagerAlsa::MidiPort::MidiPort(const std::string& path,
const Id& id,
int client_id,
int port_id,
int midi_device,
const std::string& client_name,
const std::string& port_name,
const std::string& manufacturer,
const std::string& version,
Type type)
: id_(id),
midi_device_(midi_device),
type_(type),
path_(path),
client_id_(client_id),
port_id_(port_id),
client_name_(client_name),
port_name_(port_name),
manufacturer_(manufacturer),
version_(version) {}
MidiManagerAlsa::MidiPort::~MidiPort() = default;
// Note: keep synchronized with the MidiPort::Match* methods.
std::unique_ptr<base::Value::Dict> MidiManagerAlsa::MidiPort::Value() const {
std::unique_ptr<base::Value::Dict> value(new base::Value::Dict);
std::string type;
switch (type_) {
case Type::kInput:
type = "input";
break;
case Type::kOutput:
type = "output";
break;
}
value->Set("type", type);
SetStringIfNonEmpty(value.get(), "path", path_);
SetStringIfNonEmpty(value.get(), "clientName", client_name_);
SetStringIfNonEmpty(value.get(), "portName", port_name_);
value->Set("clientId", client_id_);
value->Set("portId", port_id_);
value->Set("midiDevice", midi_device_);
// Flatten id fields.
SetStringIfNonEmpty(value.get(), "bus", id_.bus());
SetStringIfNonEmpty(value.get(), "vendorId", id_.vendor_id());
SetStringIfNonEmpty(value.get(), "modelId", id_.model_id());
SetStringIfNonEmpty(value.get(), "usbInterfaceNum", id_.usb_interface_num());
SetStringIfNonEmpty(value.get(), "serial", id_.serial());
return value;
}
std::string MidiManagerAlsa::MidiPort::JSONValue() const {
std::string json;
JSONStringValueSerializer serializer(&json);
serializer.Serialize(*Value().get());
return json;
}
// TODO(agoode): Do not use SHA256 here. Instead store a persistent
// mapping and just use a UUID or other random string.
// http://crbug.com/465320
std::string MidiManagerAlsa::MidiPort::OpaqueKey() const {
uint8_t hash[crypto::kSHA256Length];
crypto::SHA256HashString(JSONValue(), hash, sizeof(hash));
return base::HexEncode(hash);
}
bool MidiManagerAlsa::MidiPort::MatchConnected(const MidiPort& query) const {
// Matches on:
// connected == true
// type
// path
// id
// client_id
// port_id
// midi_device
// client_name
// port_name
return connected() && (type() == query.type()) && (path() == query.path()) &&
(id() == query.id()) && (client_id() == query.client_id()) &&
(port_id() == query.port_id()) &&
(midi_device() == query.midi_device()) &&
(client_name() == query.client_name()) &&
(port_name() == query.port_name());
}
bool MidiManagerAlsa::MidiPort::MatchCardPass1(const MidiPort& query) const {
// Matches on:
// connected == false
// type
// path
// id
// port_id
// midi_device
return MatchCardPass2(query) && (path() == query.path());
}
bool MidiManagerAlsa::MidiPort::MatchCardPass2(const MidiPort& query) const {
// Matches on:
// connected == false
// type
// id
// port_id
// midi_device
return !connected() && (type() == query.type()) && (id() == query.id()) &&
(port_id() == query.port_id()) &&
(midi_device() == query.midi_device());
}
bool MidiManagerAlsa::MidiPort::MatchNoCardPass1(const MidiPort& query) const {
// Matches on:
// connected == false
// type
// path.empty(), for both this and query
// id.empty(), for both this and query
// client_id
// port_id
// client_name
// port_name
// midi_device == -1, for both this and query
return MatchNoCardPass2(query) && (client_id() == query.client_id());
}
bool MidiManagerAlsa::MidiPort::MatchNoCardPass2(const MidiPort& query) const {
// Matches on:
// connected == false
// type
// path.empty(), for both this and query
// id.empty(), for both this and query
// port_id
// client_name
// port_name
// midi_device == -1, for both this and query
return !connected() && (type() == query.type()) && path().empty() &&
query.path().empty() && id().empty() && query.id().empty() &&
(port_id() == query.port_id()) &&
(client_name() == query.client_name()) &&
(port_name() == query.port_name()) && (midi_device() == -1) &&
(query.midi_device() == -1);
}
MidiManagerAlsa::MidiPortStateBase::~MidiPortStateBase() = default;
MidiManagerAlsa::MidiPortStateBase::iterator
MidiManagerAlsa::MidiPortStateBase::Find(
const MidiManagerAlsa::MidiPort& port) {
auto result = FindConnected(port);
if (result == end())
result = FindDisconnected(port);
return result;
}
MidiManagerAlsa::MidiPortStateBase::iterator
MidiManagerAlsa::MidiPortStateBase::FindConnected(
const MidiManagerAlsa::MidiPort& port) {
// Exact match required for connected ports.
return base::ranges::find_if(ports_, [&port](std::unique_ptr<MidiPort>& p) {
return p->MatchConnected(port);
});
}
MidiManagerAlsa::MidiPortStateBase::iterator
MidiManagerAlsa::MidiPortStateBase::FindDisconnected(
const MidiManagerAlsa::MidiPort& port) {
// Always match on:
// type
// Possible things to match on:
// path
// id
// client_id
// port_id
// midi_device
// client_name
// port_name
if (!port.path().empty()) {
// If path is present, then we have a card-based client.
// Pass 1. Match on path, id, midi_device, port_id.
// This is the best possible match for hardware card-based clients.
// This will also match the empty id correctly for devices without an id.
auto it =
base::ranges::find_if(ports_, [&port](std::unique_ptr<MidiPort>& p) {
return p->MatchCardPass1(port);
});
if (it != ports_.end())
return it;
if (!port.id().empty()) {
// Pass 2. Match on id, midi_device, port_id.
// This will give us a high-confidence match when a user moves a device to
// another USB/Firewire/Thunderbolt/etc port, but only works if the device
// has a hardware id.
it = base::ranges::find_if(ports_, [&port](std::unique_ptr<MidiPort>& p) {
return p->MatchCardPass2(port);
});
if (it != ports_.end())
return it;
}
} else {
// Else, we have a non-card-based client.
// Pass 1. Match on client_id, port_id, client_name, port_name.
// This will give us a reasonably good match.
auto it =
base::ranges::find_if(ports_, [&port](std::unique_ptr<MidiPort>& p) {
return p->MatchNoCardPass1(port);
});
if (it != ports_.end())
return it;
// Pass 2. Match on port_id, client_name, port_name.
// This is weaker but similar to pass 2 in the hardware card-based clients
// match.
it = base::ranges::find_if(ports_, [&port](std::unique_ptr<MidiPort>& p) {
return p->MatchNoCardPass2(port);
});
if (it != ports_.end())
return it;
}
// No match.
return ports_.end();
}
MidiManagerAlsa::MidiPortStateBase::MidiPortStateBase() = default;
MidiManagerAlsa::MidiPortState::MidiPortState() = default;
uint32_t MidiManagerAlsa::MidiPortState::push_back(
std::unique_ptr<MidiPort> port) {
// Add the web midi index.
uint32_t web_port_index = 0;
switch (port->type()) {
case MidiPort::Type::kInput:
web_port_index = num_input_ports_++;
break;
case MidiPort::Type::kOutput:
web_port_index = num_output_ports_++;
break;
}
port->set_web_port_index(web_port_index);
MidiPortStateBase::push_back(std::move(port));
return web_port_index;
}
MidiManagerAlsa::AlsaSeqState::AlsaSeqState() = default;
MidiManagerAlsa::AlsaSeqState::~AlsaSeqState() = default;
void MidiManagerAlsa::AlsaSeqState::ClientStart(int client_id,
const std::string& client_name,
snd_seq_client_type_t type) {
ClientExit(client_id);
clients_.insert(
std::make_pair(client_id, std::make_unique<Client>(client_name, type)));
if (IsCardClient(type, client_id))
++card_client_count_;
}
bool MidiManagerAlsa::AlsaSeqState::ClientStarted(int client_id) {
return clients_.find(client_id) != clients_.end();
}
void MidiManagerAlsa::AlsaSeqState::ClientExit(int client_id) {
auto it = clients_.find(client_id);
if (it != clients_.end()) {
if (IsCardClient(it->second->type(), client_id))
--card_client_count_;
clients_.erase(it);
}
}
void MidiManagerAlsa::AlsaSeqState::PortStart(
int client_id,
int port_id,
const std::string& port_name,
MidiManagerAlsa::AlsaSeqState::PortDirection direction,
bool midi) {
auto it = clients_.find(client_id);
if (it != clients_.end())
it->second->AddPort(port_id,
std::make_unique<Port>(port_name, direction, midi));
}
void MidiManagerAlsa::AlsaSeqState::PortExit(int client_id, int port_id) {
auto it = clients_.find(client_id);
if (it != clients_.end())
it->second->RemovePort(port_id);
}
snd_seq_client_type_t MidiManagerAlsa::AlsaSeqState::ClientType(
int client_id) const {
auto it = clients_.find(client_id);
if (it == clients_.end())
return SND_SEQ_USER_CLIENT;
return it->second->type();
}
std::unique_ptr<MidiManagerAlsa::TemporaryMidiPortState>
MidiManagerAlsa::AlsaSeqState::ToMidiPortState(const AlsaCardMap& alsa_cards) {
std::unique_ptr<MidiManagerAlsa::TemporaryMidiPortState> midi_ports(
new TemporaryMidiPortState);
auto card_it = alsa_cards.begin();
int card_midi_device = -1;
for (const auto& client_pair : clients_) {
int client_id = client_pair.first;
auto* client = client_pair.second.get();
// Get client metadata.
const std::string client_name = client->name();
std::string manufacturer;
std::string driver;
std::string path;
MidiPort::Id id;
std::string card_name;
std::string card_longname;
int midi_device = -1;
if (IsCardClient(client->type(), client_id)) {
auto& card = card_it->second;
if (card_midi_device == -1)
card_midi_device = 0;
manufacturer = card->manufacturer();
path = card->path();
id = MidiPort::Id(card->bus(), card->vendor_id(), card->model_id(),
card->usb_interface_num(), card->serial());
card_name = card->name();
card_longname = card->longname();
midi_device = card_midi_device;
++card_midi_device;
if (card_midi_device >= card->midi_device_count()) {
card_midi_device = -1;
++card_it;
}
}
for (const auto& port_pair : *client) {
int port_id = port_pair.first;
const auto& port = port_pair.second;
if (port->midi()) {
std::string version;
if (!driver.empty()) {
version = driver + " / ";
}
version +=
base::StringPrintf("ALSA library version %d.%d.%d", SND_LIB_MAJOR,
SND_LIB_MINOR, SND_LIB_SUBMINOR);
PortDirection direction = port->direction();
if (direction == PortDirection::kInput ||
direction == PortDirection::kDuplex) {
midi_ports->push_back(std::make_unique<MidiPort>(
path, id, client_id, port_id, midi_device, client->name(),
port->name(), manufacturer, version, MidiPort::Type::kInput));
}
if (direction == PortDirection::kOutput ||
direction == PortDirection::kDuplex) {
midi_ports->push_back(std::make_unique<MidiPort>(
path, id, client_id, port_id, midi_device, client->name(),
port->name(), manufacturer, version, MidiPort::Type::kOutput));
}
}
}
}
return midi_ports;
}
MidiManagerAlsa::AlsaSeqState::Port::Port(
const std::string& name,
MidiManagerAlsa::AlsaSeqState::PortDirection direction,
bool midi)
: name_(name), direction_(direction), midi_(midi) {}
MidiManagerAlsa::AlsaSeqState::Port::~Port() = default;
MidiManagerAlsa::AlsaSeqState::Client::Client(const std::string& name,
snd_seq_client_type_t type)
: name_(name), type_(type) {}
MidiManagerAlsa::AlsaSeqState::Client::~Client() = default;
void MidiManagerAlsa::AlsaSeqState::Client::AddPort(
int addr,
std::unique_ptr<Port> port) {
ports_[addr] = std::move(port);
}
void MidiManagerAlsa::AlsaSeqState::Client::RemovePort(int addr) {
ports_.erase(addr);
}
MidiManagerAlsa::AlsaSeqState::Client::PortMap::const_iterator
MidiManagerAlsa::AlsaSeqState::Client::begin() const {
return ports_.begin();
}
MidiManagerAlsa::AlsaSeqState::Client::PortMap::const_iterator
MidiManagerAlsa::AlsaSeqState::Client::end() const {
return ports_.end();
}
MidiManagerAlsa::AlsaCard::AlsaCard(udev_device* dev,
const std::string& name,
const std::string& longname,
const std::string& driver,
int midi_device_count)
: name_(name),
longname_(longname),
driver_(driver),
path_(device::UdevDeviceGetPropertyValue(dev, kUdevIdPath)),
bus_(device::UdevDeviceGetPropertyValue(dev, kUdevIdBus)),
vendor_id_(
UdevDeviceGetPropertyOrSysattr(dev, kUdevIdVendorId, kSysattrVendor)),
model_id_(
UdevDeviceGetPropertyOrSysattr(dev, kUdevIdModelId, kSysattrModel)),
usb_interface_num_(
device::UdevDeviceGetPropertyValue(dev, kUdevIdUsbInterfaceNum)),
serial_(UdevDeviceGetPropertyOrSysattr(dev,
kUdevIdSerialShort,
kSysattrGuid)),
midi_device_count_(midi_device_count),
manufacturer_(ExtractManufacturerString(
GetVendor(dev),
vendor_id_,
device::UdevDeviceGetPropertyValue(dev, kUdevIdVendorFromDatabase),
name,
longname)) {}
MidiManagerAlsa::AlsaCard::~AlsaCard() = default;
// static
std::string MidiManagerAlsa::AlsaCard::ExtractManufacturerString(
const std::string& udev_id_vendor,
const std::string& udev_id_vendor_id,
const std::string& udev_id_vendor_from_database,
const std::string& alsa_name,
const std::string& alsa_longname) {
// Let's try to determine the manufacturer. Here is the ordered preference
// in extraction:
// 1. Vendor name from the hardware device string, from udev properties
// or sysattrs.
// 2. Vendor name from the udev database (property ID_VENDOR_FROM_DATABASE).
// 3. Heuristic from ALSA.
// Is the vendor string present and not just the vendor hex id?
if (!udev_id_vendor.empty() && (udev_id_vendor != udev_id_vendor_id)) {
return udev_id_vendor;
}
// Is there a vendor string in the hardware database?
if (!udev_id_vendor_from_database.empty()) {
return udev_id_vendor_from_database;
}
// Ok, udev gave us nothing useful, or was unavailable. So try a heuristic.
// We assume that card longname is in the format of
// "<manufacturer> <name> at <bus>". Otherwise, we give up to detect
// a manufacturer name here.
size_t at_index = alsa_longname.rfind(" at ");
if (at_index && at_index != std::string::npos) {
size_t name_index = alsa_longname.rfind(alsa_name, at_index - 1);
if (name_index && name_index != std::string::npos)
return alsa_longname.substr(0, name_index - 1);
}
// Failure.
return "";
}
void MidiManagerAlsa::SendMidiData(MidiManagerClient* client,
uint32_t port_index,
const std::vector<uint8_t>& data) {
ScopedSndMidiEventPtr encoder = CreateScopedSndMidiEventPtr(kSendBufferSize);
for (const auto datum : data) {
snd_seq_event_t event;
snd_seq_ev_clear(&event);
int result = snd_midi_event_encode_byte(encoder.get(), datum, &event);
if (result == 1) {
// Full event, send it.
base::AutoLock ports_lock(out_ports_lock_);
auto it = out_ports_.find(port_index);
if (it != out_ports_.end()) {
base::AutoLock client_lock(out_client_lock_);
if (!out_client_)
return;
snd_seq_ev_set_source(&event, it->second);
snd_seq_ev_set_subs(&event);
snd_seq_ev_set_direct(&event);
snd_seq_event_output_direct(out_client_.get(), &event);
}
}
}
// Acknowledge send.
AccumulateMidiBytesSent(client, data.size());
}
void MidiManagerAlsa::EventLoop() {
bool loop_again = true;
struct pollfd pfd[2];
snd_seq_poll_descriptors(in_client_.get(), &pfd[0], 1, POLLIN);
pfd[1].fd = device::udev_monitor_get_fd(udev_monitor_.get());
pfd[1].events = POLLIN;
int err = HANDLE_EINTR(poll(pfd, std::size(pfd), -1));
if (err < 0) {
VLOG(1) << "poll fails: " << base::safe_strerror(errno);
loop_again = false;
} else {
if (pfd[0].revents & POLLIN) {
// Read available incoming MIDI data.
int remaining;
base::TimeTicks timestamp = base::TimeTicks::Now();
do {
snd_seq_event_t* event;
err = snd_seq_event_input(in_client_.get(), &event);
remaining = snd_seq_event_input_pending(in_client_.get(), 0);
if (err == -ENOSPC) {
// Handle out of space error.
VLOG(1) << "snd_seq_event_input detected buffer overrun";
// We've lost events: check another way to see if we need to shut
// down.
} else if (err == -EAGAIN) {
// We've read all the data.
} else if (err < 0) {
// Handle other errors.
VLOG(1) << "snd_seq_event_input fails: " << snd_strerror(err);
// TODO(agoode): Use RecordAction() or similar to log this.
loop_again = false;
} else if (event->source.client == SND_SEQ_CLIENT_SYSTEM &&
event->source.port == SND_SEQ_PORT_SYSTEM_ANNOUNCE) {
// Handle announce events.
switch (event->type) {
case SND_SEQ_EVENT_PORT_START:
// Don't use SND_SEQ_EVENT_CLIENT_START because the
// client name may not be set by the time we query
// it. It should be set by the time ports are made.
ProcessClientStartEvent(event->data.addr.client);
ProcessPortStartEvent(event->data.addr);
break;
case SND_SEQ_EVENT_CLIENT_EXIT:
// Check for disconnection of our "out" client. This means "shut
// down".
if (event->data.addr.client == out_client_id_) {
loop_again = false;
remaining = 0;
} else
ProcessClientExitEvent(event->data.addr);
break;
case SND_SEQ_EVENT_PORT_EXIT:
ProcessPortExitEvent(event->data.addr);
break;
}
} else {
// Normal operation.
ProcessSingleEvent(event, timestamp);
}
} while (remaining > 0);
}
if (pfd[1].revents & POLLIN) {
device::ScopedUdevDevicePtr dev(
device::udev_monitor_receive_device(udev_monitor_.get()));
if (dev.get())
ProcessUdevEvent(dev.get());
else
VLOG(1) << "udev_monitor_receive_device fails";
}
}
// Do again.
if (loop_again) {
service()->task_service()->PostBoundTask(
kEventTaskRunner,
base::BindOnce(&MidiManagerAlsa::EventLoop, base::Unretained(this)));
}
}
void MidiManagerAlsa::ProcessSingleEvent(snd_seq_event_t* event,
base::TimeTicks timestamp) {
auto source_it =
source_map_.find(AddrToInt(event->source.client, event->source.port));
if (source_it != source_map_.end()) {
uint32_t source = source_it->second;
if (event->type == SND_SEQ_EVENT_SYSEX) {
// Special! Variable-length sysex.
ReceiveMidiData(source, static_cast<const uint8_t*>(event->data.ext.ptr),
event->data.ext.len, timestamp);
} else {
// Otherwise, decode this and send that on.
unsigned char buf[12];
long count =
snd_midi_event_decode(decoder_.get(), buf, sizeof(buf), event);
if (count <= 0) {
if (count != -ENOENT) {
// ENOENT means that it's not a MIDI message, which is not an
// error, but other negative values are errors for us.
VLOG(1) << "snd_midi_event_decoder fails " << snd_strerror(count);
// TODO(agoode): Record this failure.
}
} else {
ReceiveMidiData(source, buf, count, timestamp);
}
}
}
}
void MidiManagerAlsa::ProcessClientStartEvent(int client_id) {
// Ignore if client is already started.
if (alsa_seq_state_.ClientStarted(client_id))
return;
snd_seq_client_info_t* client_info;
snd_seq_client_info_alloca(&client_info);
int err =
snd_seq_get_any_client_info(in_client_.get(), client_id, client_info);
if (err != 0)
return;
// Skip our own clients.
if ((client_id == in_client_id_) || (client_id == out_client_id_))
return;
// Update our view of ALSA seq state.
alsa_seq_state_.ClientStart(client_id,
snd_seq_client_info_get_name(client_info),
snd_seq_client_info_get_type(client_info));
// Generate Web MIDI events.
UpdatePortStateAndGenerateEvents();
}
void MidiManagerAlsa::ProcessPortStartEvent(const snd_seq_addr_t& addr) {
snd_seq_port_info_t* port_info;
snd_seq_port_info_alloca(&port_info);
int err = snd_seq_get_any_port_info(in_client_.get(), addr.client, addr.port,
port_info);
if (err != 0)
return;
unsigned int caps = snd_seq_port_info_get_capability(port_info);
bool input = (caps & kRequiredInputPortCaps) == kRequiredInputPortCaps;
bool output = (caps & kRequiredOutputPortCaps) == kRequiredOutputPortCaps;
AlsaSeqState::PortDirection direction;
if (input && output)
direction = AlsaSeqState::PortDirection::kDuplex;
else if (input)
direction = AlsaSeqState::PortDirection::kInput;
else if (output)
direction = AlsaSeqState::PortDirection::kOutput;
else
return;
// Update our view of ALSA seq state.
alsa_seq_state_.PortStart(
addr.client, addr.port, snd_seq_port_info_get_name(port_info), direction,
snd_seq_port_info_get_type(port_info) & SND_SEQ_PORT_TYPE_MIDI_GENERIC);
// Generate Web MIDI events.
UpdatePortStateAndGenerateEvents();
}
void MidiManagerAlsa::ProcessClientExitEvent(const snd_seq_addr_t& addr) {
// Update our view of ALSA seq state.
alsa_seq_state_.ClientExit(addr.client);
// Generate Web MIDI events.
UpdatePortStateAndGenerateEvents();
}
void MidiManagerAlsa::ProcessPortExitEvent(const snd_seq_addr_t& addr) {
// Update our view of ALSA seq state.
alsa_seq_state_.PortExit(addr.client, addr.port);
// Generate Web MIDI events.
UpdatePortStateAndGenerateEvents();
}
void MidiManagerAlsa::ProcessUdevEvent(udev_device* dev) {
// Only card devices have this property set, and only when they are
// fully initialized.
if (!device::udev_device_get_property_value(dev,
kUdevPropertySoundInitialized))
return;
// Get the action. If no action, then we are doing first time enumeration
// and the device is treated as new.
const char* action = device::udev_device_get_action(dev);
if (!action)
action = kUdevActionChange;
if (strcmp(action, kUdevActionChange) == 0) {
AddCard(dev);
// Generate Web MIDI events.
UpdatePortStateAndGenerateEvents();
} else if (strcmp(action, kUdevActionRemove) == 0) {
RemoveCard(GetCardNumber(dev));
// Generate Web MIDI events.
UpdatePortStateAndGenerateEvents();
}
}
void MidiManagerAlsa::AddCard(udev_device* dev) {
int number = GetCardNumber(dev);
if (number == -1)
return;
RemoveCard(number);
snd_ctl_card_info_t* card;
snd_hwdep_info_t* hwdep;
snd_ctl_card_info_alloca(&card);
snd_hwdep_info_alloca(&hwdep);
const std::string id = base::StringPrintf("hw:CARD=%i", number);
snd_ctl_t* handle;
int err = snd_ctl_open(&handle, id.c_str(), 0);
if (err != 0) {
VLOG(1) << "snd_ctl_open fails: " << snd_strerror(err);
return;
}
err = snd_ctl_card_info(handle, card);
if (err != 0) {
VLOG(1) << "snd_ctl_card_info fails: " << snd_strerror(err);
snd_ctl_close(handle);
return;
}
std::string name = snd_ctl_card_info_get_name(card);
std::string longname = snd_ctl_card_info_get_longname(card);
std::string driver = snd_ctl_card_info_get_driver(card);
// Count rawmidi devices (not subdevices).
int midi_count = 0;
for (int device = -1;
!snd_ctl_rawmidi_next_device(handle, &device) && device >= 0;)
++midi_count;
// Count any hwdep synths that become MIDI devices outside of rawmidi.
//
// Explanation:
// Any kernel driver can create an ALSA client (visible to us).
// With modern hardware, only rawmidi devices do this. Kernel
// drivers create rawmidi devices and the rawmidi subsystem makes
// the seq clients. But the OPL3 driver is special, it does not
// make a rawmidi device but a seq client directly. (This is the
// only one to worry about in the kernel code, as of 2015-03-23.)
//
// OPL3 is very old (but still possible to get in new
// hardware). It is unlikely that new drivers would not use
// rawmidi and defeat our heuristic.
//
// Longer term, support should be added in the kernel to expose a
// direct link from card->client (or client->card) so that all
// these heuristics will be obsolete. Once that is there, we can
// assume our old heuristics will work on old kernels and the new
// robust code will be used on new. Then we will not need to worry
// about changes to kernel internals breaking our code.
// See the TODO above at kMinimumClientIdForCards.
for (int device = -1;
!snd_ctl_hwdep_next_device(handle, &device) && device >= 0;) {
err = snd_ctl_hwdep_info(handle, hwdep);
if (err != 0) {
VLOG(1) << "snd_ctl_hwdep_info fails: " << snd_strerror(err);
continue;
}
snd_hwdep_iface_t iface = snd_hwdep_info_get_iface(hwdep);
if (iface == SND_HWDEP_IFACE_OPL2 || iface == SND_HWDEP_IFACE_OPL3 ||
iface == SND_HWDEP_IFACE_OPL4)
++midi_count;
}
snd_ctl_close(handle);
if (midi_count > 0) {
auto alsa_card =
std::make_unique<AlsaCard>(dev, name, longname, driver, midi_count);
alsa_cards_.insert(std::make_pair(number, std::move(alsa_card)));
alsa_card_midi_count_ += midi_count;
}
}
void MidiManagerAlsa::RemoveCard(int number) {
auto it = alsa_cards_.find(number);
if (it == alsa_cards_.end())
return;
alsa_card_midi_count_ -= it->second->midi_device_count();
alsa_cards_.erase(it);
}
void MidiManagerAlsa::UpdatePortStateAndGenerateEvents() {
// Verify that our information from ALSA and udev are in sync. If
// not, we cannot generate events right now.
if (alsa_card_midi_count_ != alsa_seq_state_.card_client_count())
return;
// Generate new port state.
auto new_port_state = alsa_seq_state_.ToMidiPortState(alsa_cards_);
// Disconnect any connected old ports that are now missing.
for (auto& old_port : port_state_) {
if (old_port->connected() &&
(new_port_state->FindConnected(*old_port) == new_port_state->end())) {
old_port->set_connected(false);
uint32_t web_port_index = old_port->web_port_index();
switch (old_port->type()) {
case MidiPort::Type::kInput:
source_map_.erase(
AddrToInt(old_port->client_id(), old_port->port_id()));
SetInputPortState(web_port_index, PortState::DISCONNECTED);
break;
case MidiPort::Type::kOutput:
DeleteAlsaOutputPort(web_port_index);
SetOutputPortState(web_port_index, PortState::DISCONNECTED);
break;
}
}
}
// Reconnect or add new ports.
auto it = new_port_state->begin();
while (it != new_port_state->end()) {
auto& new_port = *it;
auto old_port = port_state_.Find(*new_port);
if (old_port == port_state_.end()) {
// Add new port.
const auto& opaque_key = new_port->OpaqueKey();
const auto& manufacturer = new_port->manufacturer();
const auto& port_name = new_port->port_name();
const auto& version = new_port->version();
const auto& type = new_port->type();
const auto& client_id = new_port->client_id();
const auto& port_id = new_port->port_id();
uint32_t web_port_index = port_state_.push_back(std::move(new_port));
it = new_port_state->erase(it);
mojom::PortInfo info(opaque_key, manufacturer, port_name, version,
PortState::OPENED);
switch (type) {
case MidiPort::Type::kInput:
if (Subscribe(web_port_index, client_id, port_id))
AddInputPort(info);
break;
case MidiPort::Type::kOutput:
if (CreateAlsaOutputPort(web_port_index, client_id, port_id))
AddOutputPort(info);
break;
}
} else if (!(*old_port)->connected()) {
// Reconnect.
uint32_t web_port_index = (*old_port)->web_port_index();
(*old_port)->Update(new_port->path(), new_port->client_id(),
new_port->port_id(), new_port->client_name(),
new_port->port_name(), new_port->manufacturer(),
new_port->version());
switch ((*old_port)->type()) {
case MidiPort::Type::kInput:
if (Subscribe(web_port_index, (*old_port)->client_id(),
(*old_port)->port_id()))
SetInputPortState(web_port_index, PortState::OPENED);
break;
case MidiPort::Type::kOutput:
if (CreateAlsaOutputPort(web_port_index, (*old_port)->client_id(),
(*old_port)->port_id()))
SetOutputPortState(web_port_index, PortState::OPENED);
break;
}
(*old_port)->set_connected(true);
++it;
} else {
++it;
}
}
}
// TODO(agoode): return false on failure.
void MidiManagerAlsa::EnumerateAlsaPorts() {
snd_seq_client_info_t* client_info;
snd_seq_client_info_alloca(&client_info);
snd_seq_port_info_t* port_info;
snd_seq_port_info_alloca(&port_info);
// Enumerate clients.
snd_seq_client_info_set_client(client_info, -1);
while (!snd_seq_query_next_client(in_client_.get(), client_info)) {
int client_id = snd_seq_client_info_get_client(client_info);
ProcessClientStartEvent(client_id);
// Enumerate ports.
snd_seq_port_info_set_client(port_info, client_id);
snd_seq_port_info_set_port(port_info, -1);
while (!snd_seq_query_next_port(in_client_.get(), port_info)) {
const snd_seq_addr_t* addr = snd_seq_port_info_get_addr(port_info);
ProcessPortStartEvent(*addr);
}
}
}
bool MidiManagerAlsa::EnumerateUdevCards() {
int err;
device::ScopedUdevEnumeratePtr enumerate(
device::udev_enumerate_new(udev_.get()));
if (!enumerate.get()) {
VLOG(1) << "udev_enumerate_new fails";
return false;
}
err = device::udev_enumerate_add_match_subsystem(enumerate.get(),
kUdevSubsystemSound);
if (err) {
VLOG(1) << "udev_enumerate_add_match_subsystem fails: "
<< base::safe_strerror(-err);
return false;
}
err = device::udev_enumerate_scan_devices(enumerate.get());
if (err) {
VLOG(1) << "udev_enumerate_scan_devices fails: "
<< base::safe_strerror(-err);
return false;
}
udev_list_entry* list_entry;
auto* devices = device::udev_enumerate_get_list_entry(enumerate.get());
udev_list_entry_foreach(list_entry, devices) {
const char* path = device::udev_list_entry_get_name(list_entry);
device::ScopedUdevDevicePtr dev(
device::udev_device_new_from_syspath(udev_.get(), path));
if (dev.get())
ProcessUdevEvent(dev.get());
}
return true;
}
bool MidiManagerAlsa::CreateAlsaOutputPort(uint32_t port_index,
int client_id,
int port_id) {
// Create the port.
int out_port;
{
base::AutoLock lock(out_client_lock_);
out_port = snd_seq_create_simple_port(
out_client_.get(), NULL, kCreateOutputPortCaps, kCreatePortType);
if (out_port < 0) {
VLOG(1) << "snd_seq_create_simple_port fails: " << snd_strerror(out_port);
return false;
}
// Activate port subscription.
snd_seq_port_subscribe_t* subs;
snd_seq_port_subscribe_alloca(&subs);
snd_seq_addr_t sender;
sender.client = out_client_id_;
sender.port = out_port;
snd_seq_port_subscribe_set_sender(subs, &sender);
snd_seq_addr_t dest;
dest.client = client_id;
dest.port = port_id;
snd_seq_port_subscribe_set_dest(subs, &dest);
int err = snd_seq_subscribe_port(out_client_.get(), subs);
if (err != 0) {
VLOG(1) << "snd_seq_subscribe_port fails: " << snd_strerror(err);
snd_seq_delete_simple_port(out_client_.get(), out_port);
return false;
}
}
// Update our map.
base::AutoLock lock(out_ports_lock_);
out_ports_[port_index] = out_port;
return true;
}
void MidiManagerAlsa::DeleteAlsaOutputPort(uint32_t port_index) {
int alsa_port;
{
base::AutoLock lock(out_ports_lock_);
auto it = out_ports_.find(port_index);
if (it == out_ports_.end())
return;
alsa_port = it->second;
out_ports_.erase(it);
}
{
base::AutoLock lock(out_client_lock_);
snd_seq_delete_simple_port(out_client_.get(), alsa_port);
}
}
bool MidiManagerAlsa::Subscribe(uint32_t port_index,
int client_id,
int port_id) {
// Activate port subscription.
snd_seq_port_subscribe_t* subs;
snd_seq_port_subscribe_alloca(&subs);
snd_seq_addr_t sender;
sender.client = client_id;
sender.port = port_id;
snd_seq_port_subscribe_set_sender(subs, &sender);
snd_seq_addr_t dest;
dest.client = in_client_id_;
dest.port = in_port_id_;
snd_seq_port_subscribe_set_dest(subs, &dest);
int err = snd_seq_subscribe_port(in_client_.get(), subs);
if (err != 0) {
VLOG(1) << "snd_seq_subscribe_port fails: " << snd_strerror(err);
return false;
}
// Update our map.
source_map_[AddrToInt(client_id, port_id)] = port_index;
return true;
}
MidiManagerAlsa::ScopedSndMidiEventPtr
MidiManagerAlsa::CreateScopedSndMidiEventPtr(size_t size) {
snd_midi_event_t* coder;
snd_midi_event_new(size, &coder);
return ScopedSndMidiEventPtr(coder);
}
MidiManager* MidiManager::Create(MidiService* service) {
return new MidiManagerAlsa(service);
}
} // namespace midi