blob: 55a41b00f2bcd2ab083c3781f855bad4e542c7dc [file] [log] [blame]
// Copyright 2019 The Fuchsia 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 "src/media/audio/drivers/virtual_audio/virtual_audio.h"
#include <fuchsia/virtualaudio/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fdio/directory.h>
#include <lib/fzl/vmo-mapper.h>
#include <lib/media/cpp/timeline_function.h>
#include <lib/media/cpp/timeline_rate.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/log_settings.h>
#include <lib/syslog/cpp/macros.h>
#include <lib/zx/clock.h>
#include <poll.h>
#include <unistd.h>
#include <zircon/device/audio.h>
#include <zircon/status.h>
#include <zircon/syscalls/clock.h>
#include <iterator>
#include <limits>
#include <optional>
#include <fbl/algorithm.h>
#include "src/lib/fsl/tasks/fd_waiter.h"
#include "src/lib/fxl/command_line.h"
#include "src/lib/fxl/strings/concatenate.h"
#include "src/lib/fxl/strings/string_number_conversions.h"
namespace virtual_audio {
class VirtualAudioUtil {
public:
explicit VirtualAudioUtil(async::Loop* loop) { VirtualAudioUtil::loop_ = loop; }
void Run(fxl::CommandLine* cmdline);
private:
enum class Command {
GET_NUM_VIRTUAL_DEVICES,
SET_DEVICE_NAME,
SET_MANUFACTURER,
SET_PRODUCT_NAME,
SET_UNIQUE_ID,
ADD_FORMAT_RANGE,
CLEAR_FORMAT_RANGES,
SET_CLOCK_DOMAIN,
SET_INITIAL_CLOCK_RATE,
SET_TRANSFER_BYTES,
SET_INTERNAL_DELAY,
SET_EXTERNAL_DELAY,
SET_RING_BUFFER_RESTRICTIONS,
SET_GAIN_PROPS,
SET_PLUG_PROPS,
RESET_CONFIG,
ADD_DEVICE,
REMOVE_DEVICE,
PLUG,
UNPLUG,
GET_GAIN,
GET_FORMAT,
RETRIEVE_BUFFER,
WRITE_BUFFER,
GET_POSITION,
SET_NOTIFICATION_FREQUENCY,
ADJUST_CLOCK_RATE,
SET_CODEC,
SET_COMPOSITE,
SET_DAI,
SET_STREAM_CONFIG,
SET_IN,
SET_OUT,
SET_NO_DIRECTION,
WAIT,
HELP,
INVALID,
};
static constexpr char kNumDevsSwitch[] = "num-devs";
static constexpr char kDeviceNameSwitch[] = "dev";
static constexpr char kManufacturerSwitch[] = "mfg";
static constexpr char kProductNameSwitch[] = "prod";
static constexpr char kUniqueIdSwitch[] = "id";
static constexpr char kAddFormatRangeSwitch[] = "add-format";
static constexpr char kClearFormatRangesSwitch[] = "clear-format";
static constexpr char kClockDomainSwitch[] = "domain";
static constexpr char kInitialRateSwitch[] = "initial-rate";
static constexpr char kTransferBytesSwitch[] = "transfer";
static constexpr char kInternalDelaySwitch[] = "int-delay";
static constexpr char kExternalDelaySwitch[] = "ext-delay";
static constexpr char kBufferRestrictionsSwitch[] = "rb";
static constexpr char kGainPropsSwitch[] = "gain-props";
static constexpr char kPlugPropsSwitch[] = "plug-props";
static constexpr char kResetConfigSwitch[] = "reset";
static constexpr char kAddDeviceSwitch[] = "add";
static constexpr char kRemoveDeviceSwitch[] = "remove";
static constexpr char kPlugSwitch[] = "plug";
static constexpr char kUnplugSwitch[] = "unplug";
static constexpr char kGetGainSwitch[] = "get-gain";
static constexpr char kGetFormatSwitch[] = "get-format";
static constexpr char kRetrieveBufferSwitch[] = "get-rb";
static constexpr char kWriteBufferSwitch[] = "write-rb";
static constexpr char kGetPositionSwitch[] = "get-pos";
static constexpr char kNotificationFrequencySwitch[] = "notifs";
static constexpr char kClockRateSwitch[] = "rate";
static constexpr char kCodecSwitch[] = "codec";
static constexpr char kCompositeSwitch[] = "composite";
static constexpr char kDaiSwitch[] = "dai";
static constexpr char kStreamConfigSwitch[] = "stream";
static constexpr char kDirectionInSwitch[] = "in";
static constexpr char kDirectionOutSwitch[] = "out";
static constexpr char kDirectionlessSwitch[] = "no-direction";
static constexpr char kWaitSwitch[] = "wait";
static constexpr char kHelp1Switch[] = "help";
static constexpr char kHelp2Switch[] = "?";
static constexpr char kDefaultDeviceName[] = "Vertex";
static constexpr char kDefaultManufacturer[] = "Puerile Virtual Functions, Incorporated";
static constexpr char kDefaultProductName[] = "Virgil, version 1.0";
static constexpr uint8_t kDefaultUniqueId[16] = {0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88};
static constexpr int32_t kDefaultClockDomain = 0;
static constexpr int32_t kDefaultInitialClockRatePpm = 0;
static constexpr uint8_t kDefaultFormatRangeOption = 0;
static constexpr uint32_t kDefaultTransferBytes = 0x100;
static constexpr int64_t kDefaultInternalDelayNsec = zx::msec(0).get();
static constexpr int64_t kDefaultExternalDelayNsec = zx::msec(1).get();
static constexpr uint8_t kDefaultRingBufferOption = 0;
// This repeated value can be interpreted various ways, at various sample_sizes and num_chans.
static constexpr uint64_t kDefaultValueToWrite = 0x22446688AACCEE00;
static constexpr uint8_t kDefaultGainPropsOption = 0;
static constexpr uint8_t kDefaultPlugPropsOption = 0;
static constexpr uint32_t kDefaultNotificationFrequency = 4;
static constexpr struct {
const char* name;
Command cmd;
} COMMANDS[] = {
{kNumDevsSwitch, Command::GET_NUM_VIRTUAL_DEVICES},
{kDeviceNameSwitch, Command::SET_DEVICE_NAME},
{kManufacturerSwitch, Command::SET_MANUFACTURER},
{kProductNameSwitch, Command::SET_PRODUCT_NAME},
{kUniqueIdSwitch, Command::SET_UNIQUE_ID},
{kAddFormatRangeSwitch, Command::ADD_FORMAT_RANGE},
{kClearFormatRangesSwitch, Command::CLEAR_FORMAT_RANGES},
{kClockDomainSwitch, Command::SET_CLOCK_DOMAIN},
{kInitialRateSwitch, Command::SET_INITIAL_CLOCK_RATE},
{kTransferBytesSwitch, Command::SET_TRANSFER_BYTES},
{kInternalDelaySwitch, Command::SET_INTERNAL_DELAY},
{kExternalDelaySwitch, Command::SET_EXTERNAL_DELAY},
{kBufferRestrictionsSwitch, Command::SET_RING_BUFFER_RESTRICTIONS},
{kGainPropsSwitch, Command::SET_GAIN_PROPS},
{kPlugPropsSwitch, Command::SET_PLUG_PROPS},
{kResetConfigSwitch, Command::RESET_CONFIG},
{kAddDeviceSwitch, Command::ADD_DEVICE},
{kRemoveDeviceSwitch, Command::REMOVE_DEVICE},
{kPlugSwitch, Command::PLUG},
{kUnplugSwitch, Command::UNPLUG},
{kGetGainSwitch, Command::GET_GAIN},
{kGetFormatSwitch, Command::GET_FORMAT},
{kRetrieveBufferSwitch, Command::RETRIEVE_BUFFER},
{kWriteBufferSwitch, Command::WRITE_BUFFER},
{kGetPositionSwitch, Command::GET_POSITION},
{kNotificationFrequencySwitch, Command::SET_NOTIFICATION_FREQUENCY},
{kClockRateSwitch, Command::ADJUST_CLOCK_RATE},
{kCodecSwitch, Command::SET_CODEC},
{kCompositeSwitch, Command::SET_COMPOSITE},
{kDaiSwitch, Command::SET_DAI},
{kStreamConfigSwitch, Command::SET_STREAM_CONFIG},
{kDirectionInSwitch, Command::SET_IN},
{kDirectionOutSwitch, Command::SET_OUT},
{kDirectionlessSwitch, Command::SET_NO_DIRECTION},
{kWaitSwitch, Command::WAIT},
{kHelp1Switch, Command::HELP},
{kHelp2Switch, Command::HELP},
};
static async::Loop* loop_;
static bool received_callback_;
static void QuitLoop();
static bool RunForDuration(zx::duration duration);
static bool WaitForNoCallback();
static bool WaitForCallback();
void RegisterKeyWaiter();
bool WaitForKey();
bool ConnectToController();
bool ConnectToDevice();
void SetUpEvents();
void ParseAndExecute(fxl::CommandLine* cmdline);
bool ExecuteCommand(Command cmd, const std::string& value);
static void Usage();
// Methods using the FIDL Service interface
bool GetNumDevices();
bool AddDevice();
// Methods using the FIDL Configuration interface
bool SetDeviceName(const std::string& name);
bool SetManufacturer(const std::string& name);
bool SetProductName(const std::string& name);
bool SetUniqueId(const std::string& unique_id);
bool AddFormatRange(const std::string& format_range_str);
bool ClearFormatRanges();
bool SetClockDomain(const std::string& clock_domain_str);
bool SetInitialClockRate(const std::string& initial_clock_rate_str);
bool SetTransferBytes(const std::string& transfer_bytes_str);
bool SetInternalDelay(const std::string& delay_str);
bool SetExternalDelay(const std::string& delay_str);
bool SetRingBufferRestrictions(const std::string& rb_restr_str);
bool SetGainProps(const std::string& gain_props_str);
bool SetPlugProps(const std::string& plug_props_str);
bool ResetConfiguration(fuchsia::virtualaudio::DeviceType device_type,
std::optional<bool> is_input);
bool ResetAllConfigurations();
// Methods using the FIDL Device interface
bool RemoveDevice();
bool ChangePlugState(const std::string& plug_time_str, bool plugged);
bool GetGain();
bool GetFormat();
bool GetBuffer();
bool WriteBuffer(const std::string& write_value_str);
bool GetPosition();
bool SetNotificationFrequency(const std::string& override_notifs_str);
bool AdjustClockRate(const std::string& clock_adjust_str);
bool SetDirection(std::optional<bool> is_input);
// Convenience method that allows us to set configuration without having to check
// that some FIDL table members have been defined.
void EnsureTypesExist();
std::unique_ptr<sys::ComponentContext> component_context_;
fsl::FDWaiter keystroke_waiter_;
bool key_quit_ = false;
fuchsia::virtualaudio::ControlSyncPtr controller_ = nullptr;
fuchsia::virtualaudio::DevicePtr codec_ = nullptr;
fuchsia::virtualaudio::DevicePtr codec_input_ = nullptr;
fuchsia::virtualaudio::DevicePtr codec_output_ = nullptr;
fuchsia::virtualaudio::DevicePtr composite_ = nullptr;
fuchsia::virtualaudio::DevicePtr dai_input_ = nullptr;
fuchsia::virtualaudio::DevicePtr dai_output_ = nullptr;
fuchsia::virtualaudio::DevicePtr stream_config_input_ = nullptr;
fuchsia::virtualaudio::DevicePtr stream_config_output_ = nullptr;
fuchsia::virtualaudio::Configuration codec_config_;
fuchsia::virtualaudio::Configuration codec_input_config_;
fuchsia::virtualaudio::Configuration codec_output_config_;
fuchsia::virtualaudio::Configuration composite_config_;
fuchsia::virtualaudio::Configuration dai_input_config_;
fuchsia::virtualaudio::Configuration dai_output_config_;
fuchsia::virtualaudio::Configuration stream_config_input_config_;
fuchsia::virtualaudio::Configuration stream_config_output_config_;
std::optional<bool> configuring_input_; // Not applicable for Composite devices.
static zx::vmo ring_buffer_vmo_;
static uint32_t BytesPerSample(uint32_t format);
static void UpdateRunningPosition(uint32_t ring_position, bool is_output_);
static size_t rb_size_[2];
static uint32_t last_rb_position_[2];
static uint64_t running_position_[2];
public:
static uint32_t frame_size_[2];
static media::TimelineRate ref_time_to_running_position_rate_[2];
static media::TimelineFunction ref_time_to_running_position_[2];
private:
fuchsia::virtualaudio::DevicePtr* device() {
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return configuring_input_ ? (*configuring_input_ ? &codec_input_ : &codec_output_)
: &codec_;
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite:
return &composite_;
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai:
return configuring_input_.value_or(false) ? &dai_input_ : &dai_output_;
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig:
return configuring_input_.value_or(false) ? &stream_config_input_ : &stream_config_output_;
default:
ZX_ASSERT_MSG(0, "Unknown device type");
}
}
fuchsia::virtualaudio::Configuration* ConfigForDevice(
std::optional<bool> is_input, fuchsia::virtualaudio::DeviceType device_type) {
switch (device_type) {
case fuchsia::virtualaudio::DeviceType::CODEC:
return is_input ? (*is_input ? &codec_input_config_ : &codec_output_config_)
: &codec_config_;
case fuchsia::virtualaudio::DeviceType::COMPOSITE:
return &composite_config_;
case fuchsia::virtualaudio::DeviceType::DAI:
return is_input.value_or(false) ? &dai_input_config_ : &dai_output_config_;
case fuchsia::virtualaudio::DeviceType::STREAM_CONFIG:
return is_input.value_or(false) ? &stream_config_input_config_
: &stream_config_output_config_;
default:
ZX_ASSERT_MSG(0, "Unknown device type");
}
}
fuchsia::virtualaudio::Configuration* config() {
return ConfigForDevice(configuring_input_, device_type_);
}
static void CallbackReceived();
template <bool is_out>
static void FormatNotification(uint32_t fps, uint32_t fmt, uint32_t chans, zx_duration_t delay);
template <bool is_out>
static void FormatCallback(fuchsia::virtualaudio::Device_GetFormat_Result result);
template <bool is_out>
static void GainNotification(bool current_mute, bool current_agc, float gain_db);
template <bool is_out>
static void GainCallback(fuchsia::virtualaudio::Device_GetGain_Result result);
template <bool is_out>
static void BufferNotification(zx::vmo ring_buffer_vmo, uint32_t num_ring_buffer_frames,
uint32_t notifications_per_ring);
template <bool is_out>
static void BufferCallback(fuchsia::virtualaudio::Device_GetBuffer_Result result);
template <bool is_out>
static void StartNotification(zx_time_t start_time);
template <bool is_out>
static void StopNotification(zx_time_t stop_time, uint32_t ring_position);
template <bool is_out>
static void PositionNotification(zx_time_t monotonic_time_for_position, uint32_t ring_position);
template <bool is_out>
static void PositionCallback(fuchsia::virtualaudio::Device_GetPosition_Result result);
fuchsia::virtualaudio::DeviceType device_type_ = fuchsia::virtualaudio::DeviceType::STREAM_CONFIG;
};
::async::Loop* VirtualAudioUtil::loop_;
bool VirtualAudioUtil::received_callback_;
zx::vmo VirtualAudioUtil::ring_buffer_vmo_;
size_t VirtualAudioUtil::rb_size_[2];
uint32_t VirtualAudioUtil::last_rb_position_[2];
uint64_t VirtualAudioUtil::running_position_[2];
uint32_t VirtualAudioUtil::frame_size_[2];
media::TimelineRate VirtualAudioUtil::ref_time_to_running_position_rate_[2];
media::TimelineFunction VirtualAudioUtil::ref_time_to_running_position_[2];
enum DeviceDirection { kOutput = 0u, kInput = 1u };
uint32_t VirtualAudioUtil::BytesPerSample(uint32_t format_bitfield) {
if (format_bitfield & (AUDIO_SAMPLE_FORMAT_20BIT_IN32 | AUDIO_SAMPLE_FORMAT_24BIT_IN32 |
AUDIO_SAMPLE_FORMAT_32BIT | AUDIO_SAMPLE_FORMAT_32BIT_FLOAT)) {
return 4;
}
if (format_bitfield & AUDIO_SAMPLE_FORMAT_24BIT_PACKED) {
return 3;
}
if (format_bitfield & AUDIO_SAMPLE_FORMAT_16BIT) {
return 2;
}
if (format_bitfield & AUDIO_SAMPLE_FORMAT_8BIT) {
return 1;
}
printf("\n--Unknown format, could not determine bytes per sample. Exiting.\n");
return 0;
}
// VirtualAudioUtil implementation
//
void VirtualAudioUtil::Run(fxl::CommandLine* cmdline) {
ParseAndExecute(cmdline);
// We are done! Disconnect any error handlers.
codec_.set_error_handler(nullptr);
codec_input_.set_error_handler(nullptr);
codec_output_.set_error_handler(nullptr);
composite_.set_error_handler(nullptr);
dai_input_.set_error_handler(nullptr);
dai_output_.set_error_handler(nullptr);
stream_config_input_.set_error_handler(nullptr);
stream_config_output_.set_error_handler(nullptr);
// If any lingering callbacks were queued, let them drain.
if (!WaitForNoCallback()) {
printf("Received unexpected callback!\n");
}
}
void VirtualAudioUtil::QuitLoop() {
async::PostTask(loop_->dispatcher(), [loop = loop_]() { loop->Quit(); });
}
// Below was borrowed from gtest, as-is
bool VirtualAudioUtil::RunForDuration(zx::duration duration) {
auto canceled = std::make_shared<bool>(false);
bool timed_out = false;
async::PostDelayedTask(
loop_->dispatcher(),
[loop = loop_, canceled, &timed_out] {
if (*canceled) {
return;
}
timed_out = true;
loop->Quit();
},
duration);
loop_->Run();
loop_->ResetQuit();
if (!timed_out) {
*canceled = true;
}
return timed_out;
}
// Above was borrowed from gtest, as-is
bool VirtualAudioUtil::WaitForNoCallback() {
received_callback_ = false;
bool timed_out = RunForDuration(zx::msec(5));
// If all is well, we DIDN'T get a disconnect callback and are still bound.
if (received_callback_) {
printf(" ... received unexpected callback\n");
}
return (timed_out && !received_callback_);
}
bool VirtualAudioUtil::WaitForCallback() {
received_callback_ = false;
bool timed_out = RunForDuration(zx::msec(2000));
if (!received_callback_) {
printf(" ... expected a callback; none was received\n");
}
return (!timed_out && received_callback_);
}
void VirtualAudioUtil::RegisterKeyWaiter() {
keystroke_waiter_.Wait(
[this](zx_status_t, uint32_t) {
int c = std::tolower(getc(stdin));
if (c == 'q') {
key_quit_ = true;
}
QuitLoop();
},
STDIN_FILENO, POLLIN);
}
bool VirtualAudioUtil::WaitForKey() {
printf("\tPress Q to cancel, or any other key to continue...\n");
setbuf(stdin, nullptr);
RegisterKeyWaiter();
while (RunForDuration(zx::sec(1))) {
}
return !key_quit_;
}
bool VirtualAudioUtil::ConnectToController() {
const std::string kControlNodePath =
fxl::Concatenate({"/dev/", fuchsia::virtualaudio::CONTROL_NODE_NAME});
zx_status_t status = fdio_service_connect(kControlNodePath.c_str(),
controller_.NewRequest().TakeChannel().release());
if (status != ZX_OK) {
printf("ERROR: failed to connect to '%s', status = %d\n", kControlNodePath.c_str(), status);
return false;
}
// let VirtualAudio disconnect if all is not well.
bool success = (WaitForNoCallback() && controller_.is_bound());
if (!success) {
printf("Failed to establish channel to async controller\n");
}
return success;
}
void VirtualAudioUtil::SetUpEvents() {
if (configuring_input_) {
device()->events().OnSetFormat = FormatNotification<false>;
device()->events().OnSetGain = GainNotification<false>;
device()->events().OnBufferCreated = BufferNotification<false>;
device()->events().OnStart = StartNotification<false>;
device()->events().OnStop = StopNotification<false>;
device()->events().OnPositionNotify = PositionNotification<false>;
} else {
device()->events().OnSetFormat = FormatNotification<true>;
device()->events().OnSetGain = GainNotification<true>;
device()->events().OnBufferCreated = BufferNotification<true>;
device()->events().OnStart = StartNotification<true>;
device()->events().OnStop = StopNotification<true>;
device()->events().OnPositionNotify = PositionNotification<true>;
}
}
bool VirtualAudioUtil::ResetAllConfigurations() {
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::CODEC, true)) {
return false;
}
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::CODEC, false)) {
return false;
}
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::CODEC, std::nullopt)) {
return false;
}
// Composite drivers do not have a direction (is_input is undefined); just use `true`.
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::COMPOSITE, true)) {
return false;
}
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::DAI, true)) {
return false;
}
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::DAI, false)) {
return false;
}
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::STREAM_CONFIG, true)) {
return false;
}
if (!ResetConfiguration(fuchsia::virtualaudio::DeviceType::STREAM_CONFIG, false)) {
return false;
}
return true;
}
void VirtualAudioUtil::ParseAndExecute(fxl::CommandLine* cmdline) {
if (!cmdline->has_argv0() || cmdline->options().empty()) {
printf("No commands provided; no action taken\n");
return;
}
// Looks like we will interact with the service; get ready to connect to it.
component_context_ = sys::ComponentContext::CreateAndServeOutgoingDirectory();
if (!ConnectToController()) {
return;
}
if (!ResetAllConfigurations()) {
return;
}
// Defaults are StreamConfig output.
device_type_ = fuchsia::virtualaudio::DeviceType::STREAM_CONFIG;
configuring_input_ = false;
for (const auto& option : cmdline->options()) {
bool success = false;
Command cmd = Command::INVALID;
for (const auto& entry : COMMANDS) {
if (option.name == entry.name) {
cmd = entry.cmd;
success = true;
break;
}
}
if (!success) {
printf("Failed to parse command ID `--%s'\n", option.name.c_str());
Usage();
return;
}
printf("Executing `--%s' command...\n", option.name.c_str());
success = ExecuteCommand(cmd, option.value);
if (!success) {
printf(" ... `--%s' command was unsuccessful\n", option.name.c_str());
return;
}
} // while (cmdline args) without default
}
bool VirtualAudioUtil::ExecuteCommand(Command cmd, const std::string& value) {
bool success;
switch (cmd) {
// FIDL Service methods
case Command::GET_NUM_VIRTUAL_DEVICES:
success = GetNumDevices();
break;
// FIDL Configuration/Device methods
case Command::SET_DEVICE_NAME:
success = SetDeviceName(value);
break;
case Command::SET_MANUFACTURER:
success = SetManufacturer(value);
break;
case Command::SET_PRODUCT_NAME:
success = SetProductName(value);
break;
case Command::SET_UNIQUE_ID:
success = SetUniqueId(value);
break;
case Command::SET_CLOCK_DOMAIN:
success = SetClockDomain(value);
break;
case Command::SET_INITIAL_CLOCK_RATE:
success = SetInitialClockRate(value);
break;
case Command::ADD_FORMAT_RANGE:
success = AddFormatRange(value);
break;
case Command::CLEAR_FORMAT_RANGES:
success = ClearFormatRanges();
break;
case Command::SET_TRANSFER_BYTES:
success = SetTransferBytes(value);
break;
case Command::SET_INTERNAL_DELAY:
success = SetInternalDelay(value);
break;
case Command::SET_EXTERNAL_DELAY:
success = SetExternalDelay(value);
break;
case Command::SET_RING_BUFFER_RESTRICTIONS:
success = SetRingBufferRestrictions(value);
break;
case Command::SET_GAIN_PROPS:
success = SetGainProps(value);
break;
case Command::SET_PLUG_PROPS:
success = SetPlugProps(value);
break;
case Command::RESET_CONFIG:
success = ResetConfiguration(device_type_, configuring_input_);
break;
case Command::ADD_DEVICE:
success = AddDevice();
break;
case Command::REMOVE_DEVICE:
success = RemoveDevice();
break;
case Command::PLUG:
success = ChangePlugState(value, true);
break;
case Command::UNPLUG:
success = ChangePlugState(value, false);
break;
case Command::GET_GAIN:
success = GetGain();
break;
case Command::GET_FORMAT:
success = GetFormat();
break;
case Command::RETRIEVE_BUFFER:
success = GetBuffer();
break;
case Command::WRITE_BUFFER:
success = WriteBuffer(value);
break;
case Command::GET_POSITION:
success = GetPosition();
break;
case Command::SET_NOTIFICATION_FREQUENCY:
success = SetNotificationFrequency(value);
break;
case Command::ADJUST_CLOCK_RATE:
success = AdjustClockRate(value);
break;
case Command::SET_CODEC:
device_type_ = fuchsia::virtualaudio::DeviceType::CODEC;
success = true;
break;
case Command::SET_COMPOSITE:
device_type_ = fuchsia::virtualaudio::DeviceType::COMPOSITE;
success = true;
break;
case Command::SET_DAI:
device_type_ = fuchsia::virtualaudio::DeviceType::DAI;
success = true;
break;
case Command::SET_STREAM_CONFIG:
device_type_ = fuchsia::virtualaudio::DeviceType::STREAM_CONFIG;
success = true;
break;
case Command::SET_IN:
success = SetDirection(true);
break;
case Command::SET_OUT:
success = SetDirection(false);
break;
case Command::SET_NO_DIRECTION:
success = SetDirection(std::nullopt);
break;
case Command::WAIT:
success = WaitForKey();
break;
case Command::HELP:
Usage();
success = true;
break;
case Command::INVALID:
success = false;
break;
// Intentionally omitting default, so new enums are not forgotten here.
}
return success;
}
void VirtualAudioUtil::Usage() {
printf("\nUsage: virtual_audio [options]\n");
printf("Interactively configure and control virtual audio devices.\n");
printf("\nValid options:\n");
printf("\n By default, a virtual device of type StreamConfig and direction Output is used\n");
printf(" --%s \t\t Switch to a Codec configuration with the same direction\n", kCodecSwitch);
printf(" --%s\t\t Switch to a Composite configuration with the same direction\n",
kCompositeSwitch);
printf(" --%s \t\t Switch to a Dai configuration with the same direction\n", kDaiSwitch);
printf(" --%s \t\t Switch to a StreamConfig configuration with the same direction\n",
kStreamConfigSwitch);
printf(" --%s\t\t\t Switch to an Input configuration (same device type)\n", kDirectionInSwitch);
printf(" --%s\t\t\t Switch to an Output configuration (same device type)\n",
kDirectionOutSwitch);
printf(" --%s\t Switch to a direction-less configuration (same device type)\n",
kDirectionlessSwitch);
printf("\n The following commands customize a device configuration, before it is added\n");
printf(" --%s[=<DEVICE_NAME>]\t Set the device name (default '%s')\n", kDeviceNameSwitch,
kDefaultDeviceName);
printf(" --%s[=<MANUFACTURER>] Set the manufacturer name (default '%s')\n", kManufacturerSwitch,
kDefaultManufacturer);
printf(" --%s[=<PRODUCT>]\t Set the product name (default '%s')\n", kProductNameSwitch,
kDefaultProductName);
printf(
" --%s[=<UINT128>]\t Set the unique ID (default %02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X%02X)\n",
kUniqueIdSwitch, kDefaultUniqueId[0], kDefaultUniqueId[1], kDefaultUniqueId[2],
kDefaultUniqueId[3], kDefaultUniqueId[4], kDefaultUniqueId[5], kDefaultUniqueId[6],
kDefaultUniqueId[7], kDefaultUniqueId[8], kDefaultUniqueId[9], kDefaultUniqueId[10],
kDefaultUniqueId[11], kDefaultUniqueId[12], kDefaultUniqueId[13], kDefaultUniqueId[14],
kDefaultUniqueId[15]);
printf(" --%s[=<NUM>]\t Add format range [0,6] (default 8-44.1 Mono/Stereo 24-32)\n",
kAddFormatRangeSwitch);
printf(" --%s\t Clear any format ranges (including the built-in default)\n",
kClearFormatRangesSwitch);
printf(" --%s[=<NUM>]\t Set device clock domain (default %d)\n", kClockDomainSwitch,
kDefaultClockDomain);
printf(" --%s[=<NUM>] Set initial device clock rate in PPM [-1000, 1000] (default %d)\n",
kInitialRateSwitch, kDefaultInitialClockRatePpm);
printf(" --%s[=<BYTES>]\t Set the transfer bytes, in bytes (default %u)\n",
kTransferBytesSwitch, kDefaultTransferBytes);
printf(" --%s[=<NSEC>]\t Set internal delay (default %zd ns)\n", kInternalDelaySwitch,
kDefaultInternalDelayNsec);
printf(" --%s[=<NSEC>]\t Set external delay (default %zd ns)\n", kExternalDelaySwitch,
kDefaultExternalDelayNsec);
printf(" --%s[=<NUM>]\t\t Set ring-buffer restrictions [0,2] (default 48k-72k frames mod 6k)\n",
kBufferRestrictionsSwitch);
printf(" --%s[=<NUM>]\t Set gain properties [0,3] (default [-60, 0] -2dB mute)\n",
kGainPropsSwitch);
printf(" --%s[=<NUM>]\t Set plug properties [0,5] (default plugged notifiable)\n",
kPlugPropsSwitch);
printf(" --%s\t\t Clear any customizations; return this configuration to the default\n",
kResetConfigSwitch);
printf("\n --%s\t\t\t Activate the current configuration (AddDevice)\n", kAddDeviceSwitch);
printf("\n Subsequent commands require an activated (added) virtual audio device\n");
printf(" --%s\t\t Retrieve the client-selected ring-buffer format\n", kGetFormatSwitch);
printf(" --%s\t\t Retrieve the current device gain\n", kGetGainSwitch);
printf(" --%s\t\t Return a mapping of the ring buffer\n", kRetrieveBufferSwitch);
printf(
" --%s[=<UINT64>]\t Fill the ring-buffer with this uint64 (in hex, default "
"0x%zX)\n",
kWriteBufferSwitch, kDefaultValueToWrite);
printf(" --%s\t\t Retrieve the current ring-buffer position and corresponding ref time\n",
kGetPositionSwitch);
printf(" --%s[=<FREQ>]\t Set an alternate notifications-per-ring frequency (default %u).\n",
kNotificationFrequencySwitch, kDefaultNotificationFrequency);
printf("\t\t\t (Don't receive the same position notifications sent to the client)\n");
printf(" --%s=<DELTA PPM>\t Adjust the rate of the device clock, in parts-per-million\n",
kClockRateSwitch);
printf("\t\t\t This is reflected in position notification delivery timing and timestamps.\n");
printf(" --%s\t\t Change the device's plug-state to Plugged\n", kPlugSwitch);
printf(" --%s\t\t Change the device's plug-state to Unplugged\n", kUnplugSwitch);
printf("\n --%s\t\t Deactivate the current device configuration (RemoveDevice)\n",
kRemoveDeviceSwitch);
printf("\n The following commands are on the virtualaudio::Control protocol:\n");
printf(" --%s\t\t Retrieve the number of currently active virtual audio devices\n",
kNumDevsSwitch);
printf("\n --%s\t\t Wait for a key press before executing subsequent commands\n", kWaitSwitch);
printf(" --%s, --%s\t\t Show this message\n", kHelp1Switch, kHelp2Switch);
printf("\n");
}
bool VirtualAudioUtil::GetNumDevices() {
uint32_t num_inputs;
uint32_t num_outputs;
uint32_t num_unspecified_direction;
zx_status_t status =
controller_->GetNumDevices(&num_inputs, &num_outputs, &num_unspecified_direction);
if (status != ZX_OK) {
printf("ERROR: GetNumDevices failed, status = %d", status);
return false;
}
printf("--Received NumDevices (%u inputs, %u outputs, %u unspecified direction)\n", num_inputs,
num_outputs, num_unspecified_direction);
return true;
}
bool VirtualAudioUtil::SetDeviceName(const std::string& name) {
config()->set_device_name(name);
return true;
}
bool VirtualAudioUtil::SetManufacturer(const std::string& name) {
config()->set_manufacturer_name(name);
return true;
}
bool VirtualAudioUtil::SetProductName(const std::string& name) {
config()->set_product_name(name);
return true;
}
bool VirtualAudioUtil::SetUniqueId(const std::string& unique_id_str) {
std::array<uint8_t, 16> unique_id;
bool use_default = (unique_id_str.empty());
for (size_t index = 0; index < 16; ++index) {
unique_id[index] =
use_default ? kDefaultUniqueId[index]
: unique_id_str.size() <= (2 * index + 1)
? 0
: fxl::StringToNumber<uint8_t>(unique_id_str.substr(index * 2, 2), fxl::Base::k16);
}
memcpy(config()->mutable_unique_id()->data(), unique_id.data(), sizeof(unique_id));
return true;
}
bool VirtualAudioUtil::SetClockDomain(const std::string& clock_domain_str) {
int32_t clock_domain =
(clock_domain_str.empty() ? kDefaultClockDomain
: fxl::StringToNumber<int32_t>(clock_domain_str));
EnsureTypesExist();
fuchsia::virtualaudio::ClockProperties* clock_properties = nullptr;
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here.
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite:
clock_properties =
config()->mutable_device_specific()->composite().mutable_clock_properties();
break;
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai:
clock_properties = config()->mutable_device_specific()->dai().mutable_clock_properties();
break;
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig:
clock_properties =
config()->mutable_device_specific()->stream_config().mutable_clock_properties();
break;
default:
return false;
}
clock_properties->set_domain(clock_domain);
if (clock_domain == 0 && (clock_properties->has_rate_adjustment_ppm() &&
clock_properties->rate_adjustment_ppm() != 0)) {
printf("WARNING: by definition, a clock in domain 0 should never have rate variance!\n");
}
return true;
}
bool VirtualAudioUtil::SetInitialClockRate(const std::string& initial_clock_rate_str) {
int32_t clock_adjustment_ppm =
(initial_clock_rate_str.empty() ? kDefaultInitialClockRatePpm
: fxl::StringToNumber<int32_t>(initial_clock_rate_str));
EnsureTypesExist();
fuchsia::virtualaudio::ClockProperties* clock_properties = nullptr;
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here.
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite:
clock_properties =
config()->mutable_device_specific()->composite().mutable_clock_properties();
break;
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai:
clock_properties = config()->mutable_device_specific()->dai().mutable_clock_properties();
break;
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig:
clock_properties =
config()->mutable_device_specific()->stream_config().mutable_clock_properties();
break;
default:
return false;
}
clock_properties->set_rate_adjustment_ppm(clock_adjustment_ppm);
if (clock_adjustment_ppm < ZX_CLOCK_UPDATE_MIN_RATE_ADJUST ||
clock_adjustment_ppm > ZX_CLOCK_UPDATE_MAX_RATE_ADJUST) {
printf("ERROR: Clock rate adjustment must be within [%d, %d].\n",
ZX_CLOCK_UPDATE_MIN_RATE_ADJUST, ZX_CLOCK_UPDATE_MAX_RATE_ADJUST);
return false;
}
if ((clock_properties->has_domain() && clock_properties->domain() == 0) &&
clock_adjustment_ppm != 0) {
printf("WARNING: by definition, a clock in domain 0 should never have rate variance!\n");
}
return true;
}
struct Format {
uint32_t flags;
uint32_t min_rate;
uint32_t max_rate;
uint8_t min_chans;
uint8_t max_chans;
uint16_t rate_family_flags;
};
// These formats exercise various scenarios:
// 0: full range of rates in both families (but not 48k), both 1-2 chans
// 1: float-only, 48k family extends to 96k, 2 or 4 chan
// 2: fixed 48k 2-chan 16b
// 3: 16k 2-chan 16b
// 4: 96k and 48k, 2-chan 16b
// 5: 3-chan device at 48k 16b
// 6: 1-chan device at 8k 16b
// 7: 1-chan device at 48k 16b
// 8: 2-chan device at 96k 16b
//
// Going forward, it would be best to have chans, rate and bitdepth specifiable individually.
constexpr Format kFormatSpecs[9] = {
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT | AUDIO_SAMPLE_FORMAT_24BIT_IN32,
.min_rate = 8000,
.max_rate = 44100,
.min_chans = 1,
.max_chans = 2,
.rate_family_flags = ASF_RANGE_FLAG_FPS_44100_FAMILY | ASF_RANGE_FLAG_FPS_48000_FAMILY,
},
{
.flags = AUDIO_SAMPLE_FORMAT_32BIT_FLOAT,
.min_rate = 32000,
.max_rate = 96000,
.min_chans = 2,
.max_chans = 4,
.rate_family_flags = ASF_RANGE_FLAG_FPS_48000_FAMILY,
},
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT,
.min_rate = 48000,
.max_rate = 48000,
.min_chans = 2,
.max_chans = 2,
.rate_family_flags = ASF_RANGE_FLAG_FPS_CONTINUOUS,
},
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT,
.min_rate = 16000,
.max_rate = 16000,
.min_chans = 2,
.max_chans = 2,
.rate_family_flags = ASF_RANGE_FLAG_FPS_48000_FAMILY,
},
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT,
.min_rate = 48000,
.max_rate = 96000,
.min_chans = 2,
.max_chans = 2,
.rate_family_flags = ASF_RANGE_FLAG_FPS_48000_FAMILY,
},
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT,
.min_rate = 48000,
.max_rate = 48000,
.min_chans = 3,
.max_chans = 3,
.rate_family_flags = ASF_RANGE_FLAG_FPS_48000_FAMILY,
},
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT,
.min_rate = 8000,
.max_rate = 8000,
.min_chans = 1,
.max_chans = 1,
.rate_family_flags = ASF_RANGE_FLAG_FPS_CONTINUOUS,
},
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT,
.min_rate = 48000,
.max_rate = 48000,
.min_chans = 1,
.max_chans = 1,
.rate_family_flags = ASF_RANGE_FLAG_FPS_48000_FAMILY,
},
{
.flags = AUDIO_SAMPLE_FORMAT_16BIT,
.min_rate = 96000,
.max_rate = 96000,
.min_chans = 2,
.max_chans = 2,
.rate_family_flags = ASF_RANGE_FLAG_FPS_CONTINUOUS,
},
};
bool VirtualAudioUtil::AddFormatRange(const std::string& format_range_str) {
uint8_t format_option =
(format_range_str.empty() ? kDefaultFormatRangeOption
: fxl::StringToNumber<uint8_t>(format_range_str));
if (format_option >= std::size(kFormatSpecs)) {
printf("ERROR: Format range option must be %lu or less.\n", std::size(kFormatSpecs) - 1);
return false;
}
fuchsia::virtualaudio::FormatRange range = {
.sample_format_flags = kFormatSpecs[format_option].flags,
.min_frame_rate = kFormatSpecs[format_option].min_rate,
.max_frame_rate = kFormatSpecs[format_option].max_rate,
.min_channels = kFormatSpecs[format_option].min_chans,
.max_channels = kFormatSpecs[format_option].max_chans,
.rate_family_flags = kFormatSpecs[format_option].rate_family_flags,
};
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here (no RingBuffer).
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
// Set formats for all ring buffers.
for (auto& i : *composite.mutable_ring_buffers()) {
i.mutable_ring_buffer()->mutable_supported_formats()->push_back(range);
}
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
auto& dai = config()->mutable_device_specific()->dai();
dai.mutable_ring_buffer()->mutable_supported_formats()->push_back(range);
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
stream_config.mutable_ring_buffer()->mutable_supported_formats()->push_back(range);
return true;
}
default:
return false;
}
}
bool VirtualAudioUtil::ClearFormatRanges() {
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here (no RingBuffer).
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
// Clear format ranges for all ring buffers.
for (auto& i : *composite.mutable_ring_buffers()) {
i.mutable_ring_buffer()->mutable_supported_formats()->clear();
}
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
config()
->mutable_device_specific()
->dai()
.mutable_ring_buffer()
->mutable_supported_formats()
->clear();
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
config()
->mutable_device_specific()
->stream_config()
.mutable_ring_buffer()
->mutable_supported_formats()
->clear();
return true;
}
default:
return false;
}
}
bool VirtualAudioUtil::SetTransferBytes(const std::string& transfer_bytes_str) {
uint32_t driver_transfer_bytes = transfer_bytes_str.empty()
? kDefaultTransferBytes
: fxl::StringToNumber<uint32_t>(transfer_bytes_str);
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here (no RingBuffer).
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
// Set driver transfer bytes for all ring buffers.
for (auto& i : *composite.mutable_ring_buffers()) {
i.mutable_ring_buffer()->set_driver_transfer_bytes(driver_transfer_bytes);
}
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
auto& dai = config()->mutable_device_specific()->dai();
dai.mutable_ring_buffer()->set_driver_transfer_bytes(driver_transfer_bytes);
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
stream_config.mutable_ring_buffer()->set_driver_transfer_bytes(driver_transfer_bytes);
return true;
}
default:
return false;
}
}
bool VirtualAudioUtil::SetInternalDelay(const std::string& delay_str) {
zx_duration_t internal_delay =
delay_str.empty() ? kDefaultInternalDelayNsec : fxl::StringToNumber<zx_duration_t>(delay_str);
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here (no RingBuffer).
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
// For now, set internal delay for all ring buffers.
for (auto& i : *composite.mutable_ring_buffers()) {
i.mutable_ring_buffer()->set_internal_delay(internal_delay);
}
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
auto& dai = config()->mutable_device_specific()->dai();
dai.mutable_ring_buffer()->set_internal_delay(internal_delay);
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
stream_config.mutable_ring_buffer()->set_internal_delay(internal_delay);
return true;
}
default:
return false;
}
}
bool VirtualAudioUtil::SetExternalDelay(const std::string& delay_str) {
zx_duration_t external_delay =
delay_str.empty() ? kDefaultExternalDelayNsec : fxl::StringToNumber<zx_duration_t>(delay_str);
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here (no RingBuffer).
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
// Set external delay for all ring buffers.
for (auto& i : *composite.mutable_ring_buffers()) {
i.mutable_ring_buffer()->set_external_delay(external_delay);
}
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
auto& dai = config()->mutable_device_specific()->dai();
dai.mutable_ring_buffer()->set_external_delay(external_delay);
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
stream_config.mutable_ring_buffer()->set_external_delay(external_delay);
return true;
}
default:
return false;
}
}
struct BufferSpec {
uint32_t min_frames;
uint32_t max_frames;
uint32_t mod_frames;
};
// Buffer sizes (at default 48kHz rate): [0] 1.0-1.5 sec, in steps of 0.125;
// [1] 0.2-0.6 sec, in steps of 0.01; [2] exactly 2 secs; [3] exactly 6 secs.
constexpr BufferSpec kBufferSpecs[4] = {
{.min_frames = 48000, .max_frames = 72000, .mod_frames = 6000},
{.min_frames = 9600, .max_frames = 28800, .mod_frames = 480},
{.min_frames = 96000, .max_frames = 96000, .mod_frames = 96000},
{.min_frames = 288000, .max_frames = 288000, .mod_frames = 288000},
};
bool VirtualAudioUtil::SetRingBufferRestrictions(const std::string& rb_restr_str) {
uint8_t rb_option = (rb_restr_str.empty() ? kDefaultRingBufferOption
: fxl::StringToNumber<uint8_t>(rb_restr_str));
if (rb_option >= std::size(kBufferSpecs)) {
printf("ERROR: Ring buffer option must be %lu or less.\n", std::size(kBufferSpecs) - 1);
return false;
}
fuchsia::virtualaudio::RingBufferConstraints ring_buffer_constraints;
ring_buffer_constraints.min_frames = kBufferSpecs[rb_option].min_frames;
ring_buffer_constraints.max_frames = kBufferSpecs[rb_option].max_frames;
ring_buffer_constraints.modulo_frames = kBufferSpecs[rb_option].mod_frames;
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here (no RingBuffer).
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
// Set ring buffer constrains for all ring buffers.
for (auto& i : *composite.mutable_ring_buffers()) {
i.mutable_ring_buffer()->set_ring_buffer_constraints(ring_buffer_constraints);
}
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
auto& dai = config()->mutable_device_specific()->dai();
dai.mutable_ring_buffer()->set_ring_buffer_constraints(ring_buffer_constraints);
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
stream_config.mutable_ring_buffer()->set_ring_buffer_constraints(ring_buffer_constraints);
return true;
}
default:
return false;
}
}
struct GainSpec {
bool muted;
bool agc_enabled;
float gain_db;
bool can_mute;
bool can_agc;
float min_gain_db;
float max_gain_db;
float gain_step_db;
};
// The utility defines two preset groups of gain options. Although arbitrarily chosen, they exercise
// the available range through SetGainProperties:
// 0.Can and is mute. Cannot AGC. Gain -2, range [-60, 0] in 2.0dB.
// 1.Can but isn't mute. Can AGC, enabled. Gain -7.5,range [-30,+2] in 0.5db.
// 2.Cannot mute. Cannot AGC. Gain 0, range [0,0] in 0db.
constexpr GainSpec kGainSpecs[] = {
{
.muted = true,
.agc_enabled = false,
.gain_db = -2.0,
.can_mute = true,
.can_agc = false,
.min_gain_db = -60.0,
.max_gain_db = 0.0,
.gain_step_db = 2.0,
},
{
.muted = false,
.agc_enabled = true,
.gain_db = -7.5,
.can_mute = true,
.can_agc = true,
.min_gain_db = -30.0,
.max_gain_db = 2.0,
.gain_step_db = 0.5,
},
{
.muted = false,
.agc_enabled = false,
.gain_db = 0.0,
.can_mute = false,
.can_agc = false,
.min_gain_db = 0.0,
.max_gain_db = 0.0,
.gain_step_db = 0.0,
},
};
bool VirtualAudioUtil::SetGainProps(const std::string& gain_props_str) {
uint8_t gain_props_option =
(gain_props_str.empty() ? kDefaultGainPropsOption
: fxl::StringToNumber<uint8_t>(gain_props_str));
if (gain_props_option >= std::size(kGainSpecs)) {
printf("ERROR: Gain properties option must be %lu or less.\n", std::size(kGainSpecs));
return false;
}
fuchsia::virtualaudio::GainProperties props;
props.set_min_gain_db(kGainSpecs[gain_props_option].min_gain_db);
props.set_max_gain_db(kGainSpecs[gain_props_option].max_gain_db);
props.set_gain_step_db(kGainSpecs[gain_props_option].gain_step_db);
props.set_can_mute(kGainSpecs[gain_props_option].can_mute);
props.set_can_agc(kGainSpecs[gain_props_option].can_agc);
fuchsia::hardware::audio::GainState gain_state;
gain_state.set_gain_db(kGainSpecs[gain_props_option].gain_db);
gain_state.set_muted(kGainSpecs[gain_props_option].muted);
gain_state.set_agc_enabled(kGainSpecs[gain_props_option].agc_enabled);
props.set_gain_state(std::move(gain_state));
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
*stream_config.mutable_gain_properties() = std::move(props);
return true;
}
default:
return false;
}
}
// These preset options represent the following common configurations:
// 0.(Default) Hot-pluggable; 1.Hardwired; 2.Hot-pluggable, unplugged;
// 3.Plugged (synch: detected only by polling); 4.Unplugged (synch)
constexpr audio_pd_notify_flags_t kPlugFlags[] = {
AUDIO_PDNF_PLUGGED /*AUDIO_PDNF_HARDWIRED*/ | AUDIO_PDNF_CAN_NOTIFY,
AUDIO_PDNF_PLUGGED | AUDIO_PDNF_HARDWIRED /* AUDIO_PDNF_CAN_NOTIFY*/,
/*AUDIO_PDNF_PLUGGED AUDIO_PDNF_HARDWIRED */ AUDIO_PDNF_CAN_NOTIFY,
AUDIO_PDNF_PLUGGED /*AUDIO_PDNF_HARDWIRED AUDIO_PDNF_CAN_NOTIFY*/,
0 /*AUDIO_PDNF_PLUGGED AUDIO_PDNF_HARDWIRED AUDIO_PDNF_CAN_NOTIFY*/
};
constexpr zx_time_t kPlugTime[] = {0, -1, -1, ZX_SEC(1), ZX_SEC(2)};
static_assert(std::size(kPlugFlags) == std::size(kPlugTime));
bool VirtualAudioUtil::SetPlugProps(const std::string& plug_props_str) {
uint8_t plug_props_option =
(plug_props_str.empty() ? kDefaultPlugPropsOption
: fxl::StringToNumber<uint8_t>(plug_props_str));
if (plug_props_option >= std::size(kPlugFlags)) {
printf("ERROR: Plug properties option must be %lu or less.\n", std::size(kPlugFlags) - 1);
return false;
}
fuchsia::virtualaudio::PlugProperties props;
fuchsia::hardware::audio::PlugState plug_state;
plug_state.set_plugged(kPlugFlags[plug_props_option] & AUDIO_PDNF_PLUGGED);
plug_state.set_plug_state_time(kPlugTime[plug_props_option]);
props.set_plug_state(std::move(plug_state));
if (kPlugFlags[plug_props_option] & AUDIO_PDNF_HARDWIRED) {
props.set_plug_detect_capabilities(fuchsia::hardware::audio::PlugDetectCapabilities::HARDWIRED);
} else if (kPlugFlags[plug_props_option] & AUDIO_PDNF_CAN_NOTIFY) {
props.set_plug_detect_capabilities(
fuchsia::hardware::audio::PlugDetectCapabilities::CAN_ASYNC_NOTIFY);
}
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
*stream_config.mutable_plug_properties() = std::move(props);
return true;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec: {
auto& codec = config()->mutable_device_specific()->codec();
*codec.mutable_plug_properties() = std::move(props);
return true;
}
default:
return false;
}
}
bool VirtualAudioUtil::AdjustClockRate(const std::string& clock_adjust_str) {
int32_t clock_domain = 0;
auto rate_adjustment_ppm = fxl::StringToNumber<int32_t>(clock_adjust_str);
if (rate_adjustment_ppm < ZX_CLOCK_UPDATE_MIN_RATE_ADJUST ||
rate_adjustment_ppm > ZX_CLOCK_UPDATE_MAX_RATE_ADJUST) {
printf("ERROR: Clock rate adjustment must be within [%d, %d].\n",
ZX_CLOCK_UPDATE_MIN_RATE_ADJUST, ZX_CLOCK_UPDATE_MAX_RATE_ADJUST);
return false;
}
EnsureTypesExist();
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
return false; // Nothing to do here.
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
if (composite.has_clock_properties() && composite.clock_properties().has_domain()) {
clock_domain = composite.clock_properties().domain();
}
break;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
auto& dai = config()->mutable_device_specific()->dai();
if (dai.has_clock_properties() && dai.clock_properties().has_domain()) {
clock_domain = dai.clock_properties().domain();
}
break;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
if (stream_config.has_clock_properties() && stream_config.clock_properties().has_domain()) {
clock_domain = stream_config.clock_properties().domain();
}
break;
}
default:
return false;
}
if (clock_domain == 0 && rate_adjustment_ppm != 0) {
printf("WARNING: by definition, a clock in domain 0 should never have rate variance!\n");
}
(*device())->AdjustClockRate(
rate_adjustment_ppm,
[](fuchsia::virtualaudio::Device_AdjustClockRate_Result result) { CallbackReceived(); });
return WaitForCallback();
}
bool VirtualAudioUtil::SetDirection(std::optional<bool> is_input) {
configuring_input_ = is_input;
switch (device_type_) {
case fuchsia::virtualaudio::DeviceType::CODEC:
// `is_input` is optional for a codec device.
if (is_input.has_value()) {
config()->mutable_device_specific()->codec().set_is_input(*is_input);
} else {
config()->mutable_device_specific()->codec().clear_is_input();
}
return true;
case fuchsia::virtualaudio::DeviceType::COMPOSITE:
// Can't set a direction on a composite device.
return false;
case fuchsia::virtualaudio::DeviceType::DAI:
// Note: although `is_input` is a required DaiProperties field, a badly-behaved driver might
// still fail to set it. That said, this incorrect behavior isn't possible in VAD yet.
// So (for now at least) it is required for all virtual_audio DAI instances.
if (is_input.has_value()) {
config()->mutable_device_specific()->dai().set_is_input(*is_input);
return true;
}
return false;
case fuchsia::virtualaudio::DeviceType::STREAM_CONFIG:
// Note: `is_input` is a required StreamProperties field, however a badly-behaved driver might
// still fail to set it. That said, this incorrect behavior isn't possible in VAD yet: it uses
// this bool when registering the stream_config in devfs (`audio-input` vs. `audio-output`).
// So (for now at least) it is required for all virtual_audio StreamConfig instances.
if (is_input.has_value()) {
config()->mutable_device_specific()->stream_config().set_is_input(*is_input);
return true;
}
return false;
default:
printf("ERROR: Unknown device type\n");
return false;
}
}
bool VirtualAudioUtil::ResetConfiguration(fuchsia::virtualaudio::DeviceType device_type,
std::optional<bool> is_input) {
zx_status_t status = ZX_OK;
fuchsia::virtualaudio::Direction direction;
if (is_input) {
direction.set_is_input(*is_input);
}
fuchsia::virtualaudio::Control_GetDefaultConfiguration_Result config_result;
status = controller_->GetDefaultConfiguration(device_type, std::move(direction), &config_result);
if (status == ZX_OK) {
if (config_result.is_err()) {
status = config_result.err();
}
}
if (status != ZX_OK) {
printf("ERROR: Failed to get default config for device, status = %s\n",
zx_status_get_string(status));
QuitLoop();
return false;
}
*ConfigForDevice(is_input, device_type) = std::move(config_result.response().config);
return true;
}
bool VirtualAudioUtil::AddDevice() {
fuchsia::virtualaudio::Configuration cfg;
zx_status_t status = config()->Clone(&cfg);
FX_CHECK(status == ZX_OK);
auto request = (*device()).NewRequest();
fuchsia::virtualaudio::Control_AddDevice_Result result;
if ((status = controller_->AddDevice(std::move(cfg), std::move(request), &result)) == ZX_OK) {
if (result.is_err()) {
status = result.err();
}
}
if (status != ZX_OK) {
printf("ERROR: Failed to add %s device, status = %d\n", configuring_input_ ? "input" : "output",
status);
QuitLoop();
return false;
}
device()->set_error_handler([is_input = configuring_input_](zx_status_t error) {
printf("%s device disconnected (%d)!\n",
is_input ? (*is_input ? "input" : "output") : "directionless", error);
QuitLoop();
});
SetUpEvents();
// let VirtualAudio disconnect if all is not well.
bool success = (WaitForNoCallback() && device()->is_bound());
if (!success) {
printf("ERROR: Failed to establish channel to %s device\n",
configuring_input_ ? "input" : "output");
}
return success;
}
bool VirtualAudioUtil::RemoveDevice() {
device()->Unbind();
return WaitForNoCallback();
}
bool VirtualAudioUtil::ChangePlugState(const std::string& plug_time_str, bool plugged) {
if (!device()->is_bound()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
auto plug_change_time = (plug_time_str.empty() ? zx::clock::get_monotonic().get()
: fxl::StringToNumber<zx_time_t>(plug_time_str));
(*device())->ChangePlugState(
plug_change_time, plugged,
[](fuchsia::virtualaudio::Device_ChangePlugState_Result result) { CallbackReceived(); });
return WaitForCallback();
}
bool VirtualAudioUtil::GetFormat() {
if (!device()->is_bound()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
if (configuring_input_) {
(*device())->GetFormat(FormatCallback<false>);
} else {
(*device())->GetFormat(FormatCallback<true>);
}
return WaitForCallback();
}
bool VirtualAudioUtil::GetGain() {
if (!device()->is_bound()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
if (configuring_input_) {
(*device())->GetGain(GainCallback<false>);
} else {
(*device())->GetGain(GainCallback<true>);
}
return WaitForCallback();
}
bool VirtualAudioUtil::GetBuffer() {
if (!device()->is_bound()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
if (configuring_input_) {
(*device())->GetBuffer(BufferCallback<false>);
} else {
(*device())->GetBuffer(BufferCallback<true>);
}
return WaitForCallback() && ring_buffer_vmo_.is_valid();
}
bool VirtualAudioUtil::WriteBuffer(const std::string& write_value_str) {
size_t value_to_write =
(write_value_str.empty() ? kDefaultValueToWrite
: fxl::StringToNumber<size_t>(write_value_str, fxl::Base::k16));
if (!ring_buffer_vmo_.is_valid()) {
if (!GetBuffer()) {
printf("ERROR: Failed to retrieve RingBuffer for writing.\n");
return false;
}
}
auto rb_size = rb_size_[configuring_input_ ? kInput : kOutput];
for (size_t offset = 0; offset < rb_size; offset += sizeof(value_to_write)) {
zx_status_t status = ring_buffer_vmo_.write(&value_to_write, offset, sizeof(value_to_write));
if (status != ZX_OK) {
printf("ERROR: Writing %16ld (0x%016zX) to rb_vmo[%zu] failed (%d)\n", value_to_write,
value_to_write, offset, status);
return false;
}
}
printf("--Wrote %16ld (0x%016zX) across the ring buffer\n", value_to_write, value_to_write);
return WaitForNoCallback();
}
bool VirtualAudioUtil::GetPosition() {
if (!device()->is_bound()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
if (configuring_input_) {
(*device())->GetPosition(PositionCallback<false>);
} else {
(*device())->GetPosition(PositionCallback<true>);
}
return WaitForCallback();
}
bool VirtualAudioUtil::SetNotificationFrequency(const std::string& notifs_str) {
if (!device()->is_bound()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
uint32_t notifications_per_ring =
(notifs_str.empty() ? kDefaultNotificationFrequency
: fxl::StringToNumber<uint32_t>(notifs_str));
(*device())->SetNotificationFrequency(
notifications_per_ring,
[](fuchsia::virtualaudio::Device_SetNotificationFrequency_Result result) {
CallbackReceived();
});
return WaitForCallback();
}
void VirtualAudioUtil::CallbackReceived() {
VirtualAudioUtil::received_callback_ = true;
VirtualAudioUtil::loop_->Quit();
}
template <bool is_out>
void VirtualAudioUtil::FormatNotification(uint32_t fps, uint32_t fmt, uint32_t chans,
zx_duration_t delay) {
printf("--Received Format (%u fps, %x fmt, %u chan, %zu delay) for %s\n", fps, fmt, chans, delay,
(is_out ? "output" : "input"));
DeviceDirection dev_type = is_out ? kOutput : kInput;
frame_size_[dev_type] = chans * BytesPerSample(fmt);
ref_time_to_running_position_rate_[dev_type] =
media::TimelineRate(fps * frame_size_[dev_type], ZX_SEC(1));
}
template <bool is_out>
void VirtualAudioUtil::FormatCallback(fuchsia::virtualaudio::Device_GetFormat_Result result) {
VirtualAudioUtil::CallbackReceived();
if (!result.is_response()) {
printf("GetFormatfailed with error %d\n", static_cast<int32_t>(result.err()));
return;
}
VirtualAudioUtil::FormatNotification<is_out>(
result.response().frames_per_second, result.response().sample_format,
result.response().num_channels, result.response().external_delay);
}
template <bool is_out>
void VirtualAudioUtil::GainNotification(bool mute, bool agc, float gain_db) {
printf("--Received Gain (mute: %u, agc: %u, gain: %.5f dB) for %s\n", mute, agc, gain_db,
(is_out ? "output" : "input"));
}
template <bool is_out>
void VirtualAudioUtil::GainCallback(fuchsia::virtualaudio::Device_GetGain_Result result) {
VirtualAudioUtil::CallbackReceived();
if (result.is_err()) {
printf("ERROR: Get gain failed\n");
return;
}
VirtualAudioUtil::GainNotification<is_out>(result.response().current_mute,
result.response().current_agc,
result.response().current_gain_db);
}
template <bool is_out>
void VirtualAudioUtil::BufferNotification(zx::vmo ring_buffer_vmo, uint32_t num_ring_buffer_frames,
uint32_t notifications_per_ring) {
ring_buffer_vmo_ = std::move(ring_buffer_vmo);
uint64_t vmo_size;
ring_buffer_vmo_.get_size(&vmo_size);
DeviceDirection dev_type = is_out ? kOutput : kInput;
rb_size_[dev_type] = static_cast<uint32_t>(num_ring_buffer_frames * frame_size_[dev_type]);
printf("--Received SetBuffer (vmo size: %zu, ring size: %zu, frames: %u, notifs: %u) for %s\n",
vmo_size, rb_size_[dev_type], num_ring_buffer_frames, notifications_per_ring,
(is_out ? "output" : "input"));
}
template <bool is_out>
void VirtualAudioUtil::BufferCallback(fuchsia::virtualaudio::Device_GetBuffer_Result result) {
VirtualAudioUtil::CallbackReceived();
if (!result.is_response()) {
printf("GetBuffer failed with error %d\n", static_cast<int32_t>(result.err()));
return;
}
VirtualAudioUtil::BufferNotification<is_out>(std::move(result.response().ring_buffer),
result.response().num_ring_buffer_frames,
result.response().notifications_per_ring);
}
void VirtualAudioUtil::UpdateRunningPosition(uint32_t ring_position, bool is_output) {
auto dev_type = is_output ? kOutput : kInput;
if (ring_position <= last_rb_position_[dev_type]) {
running_position_[dev_type] += rb_size_[dev_type];
}
running_position_[dev_type] -= last_rb_position_[dev_type];
running_position_[dev_type] += ring_position;
last_rb_position_[dev_type] = ring_position;
}
template <bool is_out>
void VirtualAudioUtil::StartNotification(zx_time_t start_time) {
printf("--Received Start (time: %zu) for %s\n", start_time, (is_out ? "output" : "input"));
DeviceDirection dev_type = is_out ? kOutput : kInput;
ref_time_to_running_position_[dev_type] =
media::TimelineFunction(0, start_time, ref_time_to_running_position_rate_[dev_type]);
running_position_[dev_type] = 0;
last_rb_position_[dev_type] = 0;
}
template <bool is_out>
void VirtualAudioUtil::StopNotification(zx_time_t stop_time, uint32_t ring_position) {
DeviceDirection dev_type = is_out ? kOutput : kInput;
auto expected_running_position = ref_time_to_running_position_[dev_type].Apply(stop_time);
UpdateRunningPosition(ring_position, is_out);
printf("--Received Stop (time: %zu, pos: %u) for %s\n", stop_time, ring_position,
(is_out ? "output" : "input"));
printf("--Stop at position: expected %zu; actual %zu\n", expected_running_position,
running_position_[dev_type]);
running_position_[dev_type] = 0;
last_rb_position_[dev_type] = 0;
}
template <bool is_out>
void VirtualAudioUtil::PositionNotification(zx_time_t monotonic_time_for_position,
uint32_t ring_position) {
printf("--Received Position (time: %13zu, pos: %6u) for %s", monotonic_time_for_position,
ring_position, (is_out ? "output" : "input "));
DeviceDirection dev_type = is_out ? kOutput : kInput;
if (monotonic_time_for_position > ref_time_to_running_position_[dev_type].reference_time()) {
int64_t expected_running_position =
ref_time_to_running_position_[dev_type].Apply(monotonic_time_for_position);
UpdateRunningPosition(ring_position, is_out);
FX_CHECK(running_position_[dev_type] <= std::numeric_limits<int64_t>::max());
int64_t delta = expected_running_position - static_cast<int64_t>(running_position_[dev_type]);
printf(" - running byte position: expect %8zu actual %8zu delta %6zd",
expected_running_position, running_position_[dev_type], delta);
}
printf("\n");
}
template <bool is_out>
void VirtualAudioUtil::PositionCallback(fuchsia::virtualaudio::Device_GetPosition_Result result) {
VirtualAudioUtil::CallbackReceived();
if (!result.is_response()) {
printf("GetPosition failed with error %d\n", static_cast<int32_t>(result.err()));
return;
}
VirtualAudioUtil::PositionNotification<is_out>(result.response().monotonic_time,
result.response().ring_position);
}
// Convenience method that allows us to set configuration without having to check that some FIDL
// table members have been defined.
void VirtualAudioUtil::EnsureTypesExist() {
switch (config()->device_specific().Which()) {
case fuchsia::virtualaudio::DeviceSpecific::Tag::kCodec:
break; // Nothing for Codec to set up ahead of time (no RingBuffer).
case fuchsia::virtualaudio::DeviceSpecific::Tag::kComposite: {
auto& composite = config()->mutable_device_specific()->composite();
// Not all composite drivers have a ring buffer, this convenience code sets a composite
// driver's first ring buffer related FIDL table members, so user of this method do not
// have to check for their existence.
if (!composite.has_ring_buffers()) {
composite.set_ring_buffers(std::vector<fuchsia::virtualaudio::CompositeRingBuffer>(1));
}
for (auto& i : *composite.mutable_ring_buffers()) {
if (!i.has_ring_buffer()) {
i.set_ring_buffer(fuchsia::virtualaudio::RingBuffer{});
}
}
break;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kDai: {
auto& dai = config()->mutable_device_specific()->dai();
if (!dai.has_ring_buffer()) {
dai.set_ring_buffer(fuchsia::virtualaudio::RingBuffer{});
}
break;
}
case fuchsia::virtualaudio::DeviceSpecific::Tag::kStreamConfig: {
auto& stream_config = config()->mutable_device_specific()->stream_config();
if (!stream_config.has_ring_buffer()) {
stream_config.set_ring_buffer(fuchsia::virtualaudio::RingBuffer{});
}
break;
}
default:
break;
}
}
} // namespace virtual_audio
int main(int argc, const char** argv) {
fuchsia_logging::SetTags({"virtual_audio_util"});
fxl::CommandLine command_line = fxl::CommandLineFromArgcArgv(argc, argv);
async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
virtual_audio::VirtualAudioUtil util(&loop);
util.Run(&command_line);
return 0;
}