blob: c2427139df1cc22891da4b56cc7bc273142bc24e [file]
// 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 <fidl/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 <cstddef>
#include <iterator>
#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/string_number_conversions.h"
namespace virtual_audio {
namespace {
class VirtualAudioUtil;
class DeviceEventHandler : public fidl::AsyncEventHandler<fuchsia_virtualaudio::Device> {
public:
void OnSetFormat(fidl::Event<fuchsia_virtualaudio::Device::OnSetFormat>& event) override;
void OnBufferCreated(fidl::Event<fuchsia_virtualaudio::Device::OnBufferCreated>& event) override;
void OnStart(fidl::Event<fuchsia_virtualaudio::Device::OnStart>& event) override;
void OnStop(fidl::Event<fuchsia_virtualaudio::Device::OnStop>& event) override;
void OnPositionNotify(
fidl::Event<fuchsia_virtualaudio::Device::OnPositionNotify>& event) override;
void on_fidl_error(fidl::UnbindInfo info) override;
};
class VirtualAudioUtil {
friend class DeviceEventHandler;
public:
explicit VirtualAudioUtil(async::Loop* loop) { VirtualAudioUtil::loop_ = loop; }
void Run(fxl::CommandLine* cmdline);
private:
enum class Command : uint8_t {
GET_NUM_VIRTUAL_DEVICES,
SET_DEVICE_NAME,
SET_MANUFACTURER,
SET_PRODUCT_NAME,
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,
RESET_CONFIG,
ADD_DEVICE,
REMOVE_DEVICE,
GET_FORMAT,
RETRIEVE_BUFFER,
WRITE_BUFFER,
GET_POSITION,
SET_NOTIFICATION_FREQUENCY,
ADJUST_CLOCK_RATE,
SET_COMPOSITE,
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 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 kResetConfigSwitch[] = "reset";
static constexpr char kAddDeviceSwitch[] = "add";
static constexpr char kRemoveDeviceSwitch[] = "remove";
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 kCompositeSwitch[] = "composite";
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 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 uint32_t kDefaultNotificationFrequency = 4;
static constexpr struct {
const char* name;
Command cmd;
} COMMANDS[] = {
{.name = kNumDevsSwitch, .cmd = Command::GET_NUM_VIRTUAL_DEVICES},
{.name = kDeviceNameSwitch, .cmd = Command::SET_DEVICE_NAME},
{.name = kManufacturerSwitch, .cmd = Command::SET_MANUFACTURER},
{.name = kProductNameSwitch, .cmd = Command::SET_PRODUCT_NAME},
{.name = kAddFormatRangeSwitch, .cmd = Command::ADD_FORMAT_RANGE},
{.name = kClearFormatRangesSwitch, .cmd = Command::CLEAR_FORMAT_RANGES},
{.name = kClockDomainSwitch, .cmd = Command::SET_CLOCK_DOMAIN},
{.name = kInitialRateSwitch, .cmd = Command::SET_INITIAL_CLOCK_RATE},
{.name = kTransferBytesSwitch, .cmd = Command::SET_TRANSFER_BYTES},
{.name = kInternalDelaySwitch, .cmd = Command::SET_INTERNAL_DELAY},
{.name = kExternalDelaySwitch, .cmd = Command::SET_EXTERNAL_DELAY},
{.name = kBufferRestrictionsSwitch, .cmd = Command::SET_RING_BUFFER_RESTRICTIONS},
{.name = kResetConfigSwitch, .cmd = Command::RESET_CONFIG},
{.name = kAddDeviceSwitch, .cmd = Command::ADD_DEVICE},
{.name = kRemoveDeviceSwitch, .cmd = Command::REMOVE_DEVICE},
{.name = kGetFormatSwitch, .cmd = Command::GET_FORMAT},
{.name = kRetrieveBufferSwitch, .cmd = Command::RETRIEVE_BUFFER},
{.name = kWriteBufferSwitch, .cmd = Command::WRITE_BUFFER},
{.name = kGetPositionSwitch, .cmd = Command::GET_POSITION},
{.name = kNotificationFrequencySwitch, .cmd = Command::SET_NOTIFICATION_FREQUENCY},
{.name = kClockRateSwitch, .cmd = Command::ADJUST_CLOCK_RATE},
{.name = kCompositeSwitch, .cmd = Command::SET_COMPOSITE},
{.name = kWaitSwitch, .cmd = Command::WAIT},
{.name = kHelp1Switch, .cmd = Command::HELP},
{.name = kHelp2Switch, .cmd = 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 ConnectToControllers();
bool ConnectToDevice();
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 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);
zx_status_t ResetConfiguration();
// Methods using the FIDL Device interface
bool RemoveDevice();
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);
fidl::Client<fuchsia_virtualaudio::Control>& controller() { return controller_; }
std::unique_ptr<sys::ComponentContext> component_context_;
fsl::FDWaiter keystroke_waiter_;
bool key_quit_ = false;
fidl::Client<fuchsia_virtualaudio::Control> controller_;
fidl::Client<fuchsia_virtualaudio::Device> composite_;
DeviceEventHandler event_handler_;
fuchsia_virtualaudio::Configuration composite_config_;
static zx::vmo ring_buffer_vmo_;
static uint32_t BytesPerSample(uint32_t format);
static void UpdateRunningPosition(uint32_t ring_position);
static size_t rb_size_;
static uint32_t last_rb_position_;
static uint64_t running_position_;
public:
static uint32_t frame_size_;
static media::TimelineRate ref_time_to_running_position_rate_;
static media::TimelineFunction ref_time_to_running_position_;
private:
static void CallbackReceived();
static void FormatNotification(uint32_t fps, uint32_t fmt, uint32_t chans, zx_duration_t delay);
static void BufferNotification(zx::vmo ring_buffer_vmo, uint32_t num_ring_buffer_frames,
uint32_t notifications_per_ring);
static void StartNotification(zx_time_t start_time);
static void StopNotification(zx_time_t stop_time, uint32_t ring_position);
static void PositionNotification(zx_time_t monotonic_time_for_position, uint32_t ring_position);
};
void DeviceEventHandler::OnSetFormat(
fidl::Event<fuchsia_virtualaudio::Device::OnSetFormat>& event) {
VirtualAudioUtil::FormatNotification(event.frames_per_second(), event.sample_format(),
event.num_channels(), event.external_delay());
}
void DeviceEventHandler::OnBufferCreated(
fidl::Event<fuchsia_virtualaudio::Device::OnBufferCreated>& event) {
VirtualAudioUtil::BufferNotification(std::move(event.ring_buffer()),
event.num_ring_buffer_frames(),
event.notifications_per_ring());
}
void DeviceEventHandler::OnStart(fidl::Event<fuchsia_virtualaudio::Device::OnStart>& event) {
VirtualAudioUtil::StartNotification(event.start_time());
}
void DeviceEventHandler::OnStop(fidl::Event<fuchsia_virtualaudio::Device::OnStop>& event) {
VirtualAudioUtil::StopNotification(event.stop_time(), event.ring_position());
}
void DeviceEventHandler::OnPositionNotify(
fidl::Event<fuchsia_virtualaudio::Device::OnPositionNotify>& event) {
VirtualAudioUtil::PositionNotification(event.monotonic_time(), event.ring_position());
}
void DeviceEventHandler::on_fidl_error(fidl::UnbindInfo info) {
printf("device disconnected: %s\n", info.FormatDescription().c_str());
VirtualAudioUtil::loop_->Quit();
}
::async::Loop* VirtualAudioUtil::loop_;
bool VirtualAudioUtil::received_callback_;
zx::vmo VirtualAudioUtil::ring_buffer_vmo_;
size_t VirtualAudioUtil::rb_size_;
uint32_t VirtualAudioUtil::last_rb_position_;
uint64_t VirtualAudioUtil::running_position_;
uint32_t VirtualAudioUtil::frame_size_;
media::TimelineRate VirtualAudioUtil::ref_time_to_running_position_rate_;
media::TimelineFunction VirtualAudioUtil::ref_time_to_running_position_;
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);
// 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");
setvbuf(stdin, nullptr, _IONBF, 0); // Turn off buffering; immediately receive keypresses.
RegisterKeyWaiter();
while (RunForDuration(zx::sec(1))) {
}
return !key_quit_;
}
bool VirtualAudioUtil::ConnectToControllers() {
const std::string kControlNodePath =
std::string{"/dev/"} + fuchsia_virtualaudio::kControlNodeName;
auto endpoints = fidl::CreateEndpoints<fuchsia_virtualaudio::Control>();
if (endpoints.is_error()) {
printf("ERROR: CreateEndpoints failed\n");
return false;
}
zx_status_t status =
fdio_service_connect(kControlNodePath.c_str(), endpoints->server.TakeChannel().release());
if (status != ZX_OK) {
printf("ERROR: failed to connect to '%s', status = %d\n", kControlNodePath.c_str(), status);
return false;
}
controller_.Bind(std::move(endpoints->client), loop_->dispatcher());
// let VirtualAudio disconnect if all is not well.
bool success = (WaitForNoCallback() && controller_.is_valid());
if (!success) {
printf("Failed to establish channel to async controller\n");
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 (!ConnectToControllers()) {
return;
}
if (ResetConfiguration() != ZX_OK) {
QuitLoop();
return;
}
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_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::RESET_CONFIG:
success = (ResetConfiguration() == ZX_OK);
break;
case Command::ADD_DEVICE:
success = AddDevice();
break;
case Command::REMOVE_DEVICE:
success = RemoveDevice();
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_COMPOSITE:
success = true;
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 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[=<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\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 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("\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() {
bool success = false;
controller_->GetNumDevices().Then(
[&](fidl::Result<fuchsia_virtualaudio::Control::GetNumDevices>& result) {
if (result.is_ok()) {
printf("--Received NumDevices (%u inputs, %u outputs, %u unspecified direction)\n",
result->num_input_devices(), result->num_output_devices(),
result->num_unspecified_direction_devices());
success = true;
} else {
printf("ERROR: GetNumDevices failed: %s\n",
result.error_value().FormatDescription().c_str());
}
CallbackReceived();
});
return WaitForCallback() && success;
}
bool VirtualAudioUtil::SetDeviceName(const std::string& name) {
composite_config_.device_name() = name;
return true;
}
bool VirtualAudioUtil::SetManufacturer(const std::string& name) {
composite_config_.manufacturer_name() = name;
return true;
}
bool VirtualAudioUtil::SetProductName(const std::string& name) {
composite_config_.product_name() = name;
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));
auto composite = composite_config_.device_specific()->composite();
composite->clock_properties()->domain() = clock_domain;
if (clock_domain == 0 && composite->clock_properties()->rate_adjustment_ppm().has_value() &&
composite->clock_properties()->rate_adjustment_ppm().value() != 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));
auto composite = composite_config_.device_specific()->composite();
auto props = composite->clock_properties();
props->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 ((props->domain().has_value() && props->domain().value() == 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;
range.sample_format_flags() = kFormatSpecs[format_option].flags;
range.min_frame_rate() = kFormatSpecs[format_option].min_rate;
range.max_frame_rate() = kFormatSpecs[format_option].max_rate;
range.min_channels() = kFormatSpecs[format_option].min_chans;
range.max_channels() = kFormatSpecs[format_option].max_chans;
range.rate_family_flags() = kFormatSpecs[format_option].rate_family_flags;
auto composite = composite_config_.device_specific()->composite();
// Set formats for all ring buffers.
for (auto& i : *composite->ring_buffers()) {
i.ring_buffer()->supported_formats()->emplace_back(std::move(range));
}
return true;
}
bool VirtualAudioUtil::ClearFormatRanges() {
auto composite = composite_config_.device_specific()->composite();
// Clear format ranges for all ring buffers.
for (auto& i : *composite->ring_buffers()) {
i.ring_buffer()->supported_formats()->clear();
}
return true;
}
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);
auto composite = composite_config_.device_specific()->composite();
// Set driver transfer bytes for all ring buffers.
for (auto& i : *composite->ring_buffers()) {
i.ring_buffer()->driver_transfer_bytes() = driver_transfer_bytes;
}
return true;
}
bool VirtualAudioUtil::SetInternalDelay(const std::string& delay_str) {
zx_duration_t internal_delay =
delay_str.empty() ? kDefaultInternalDelayNsec : fxl::StringToNumber<zx_duration_t>(delay_str);
auto composite = composite_config_.device_specific()->composite();
// For now, set internal delay for all ring buffers.
for (auto& i : *composite->ring_buffers()) {
i.ring_buffer()->internal_delay() = internal_delay;
}
return true;
}
bool VirtualAudioUtil::SetExternalDelay(const std::string& delay_str) {
zx_duration_t external_delay =
delay_str.empty() ? kDefaultExternalDelayNsec : fxl::StringToNumber<zx_duration_t>(delay_str);
auto composite = composite_config_.device_specific()->composite();
// Set external delay for all ring buffers.
for (auto& i : *composite->ring_buffers()) {
i.ring_buffer()->external_delay() = external_delay;
}
return true;
}
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;
auto composite = composite_config_.device_specific()->composite();
// Set ring buffer constraints for all ring buffers.
for (auto& i : *composite->ring_buffers()) {
i.ring_buffer()->ring_buffer_constraints() = ring_buffer_constraints;
}
return true;
}
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;
}
auto composite = composite_config_.device_specific()->composite();
if (composite->clock_properties().has_value() &&
composite->clock_properties()->domain().has_value()) {
clock_domain = composite->clock_properties()->domain().value();
}
if (clock_domain == 0 && rate_adjustment_ppm != 0) {
printf("WARNING: by definition, a clock in domain 0 should never have rate variance!\n");
}
composite_->AdjustClockRate({rate_adjustment_ppm})
.Then([](fidl::Result<fuchsia_virtualaudio::Device::AdjustClockRate>& result) {
CallbackReceived();
});
return WaitForCallback();
}
zx_status_t VirtualAudioUtil::ResetConfiguration() {
bool success = false;
zx_status_t status = ZX_OK;
controller()
->GetDefaultConfiguration(
{fuchsia_virtualaudio::DeviceType::kComposite, fuchsia_virtualaudio::Direction()})
.Then([&](fidl::Result<fuchsia_virtualaudio::Control::GetDefaultConfiguration>& result) {
if (result.is_error()) {
printf("ERROR: GetDefaultConfiguration failed: %s\n",
result.error_value().FormatDescription().c_str());
status = ZX_ERR_INTERNAL;
} else {
composite_config_ = std::move(result.value().config());
auto composite = composite_config_.device_specific()->composite();
if (!composite->ring_buffers().has_value()) {
composite->ring_buffers().emplace(1);
}
for (auto& i : *composite->ring_buffers()) {
if (!i.ring_buffer().has_value()) {
i.ring_buffer().emplace();
}
}
success = true;
}
CallbackReceived();
});
if (!WaitForCallback() || !success) {
return status != ZX_OK ? status : ZX_ERR_INTERNAL;
}
return ZX_OK;
}
bool VirtualAudioUtil::AddDevice() {
auto endpoints = fidl::CreateEndpoints<fuchsia_virtualaudio::Device>();
if (endpoints.is_error()) {
printf("ERROR: CreateEndpoints failed\n");
return false;
}
bool success = false;
zx_status_t status = ZX_OK;
controller()
->AddDevice({std::move(composite_config_), std::move(endpoints->server)})
.Then([&](fidl::Result<fuchsia_virtualaudio::Control::AddDevice>& result) {
if (result.is_error()) {
printf("ERROR: AddDevice failed: %s\n", result.error_value().FormatDescription().c_str());
status = ZX_ERR_INTERNAL;
} else {
success = true;
}
CallbackReceived();
});
if (!WaitForCallback() || !success) {
printf("ERROR: Failed to add device\n");
QuitLoop();
return false;
}
composite_.Bind(std::move(endpoints->client), loop_->dispatcher(), &event_handler_);
// let VirtualAudio disconnect if all is not well.
success = (WaitForNoCallback() && composite_.is_valid());
if (!success) {
printf("ERROR: Failed to establish channel to device\n");
}
return success;
}
bool VirtualAudioUtil::RemoveDevice() {
composite_ = {};
return WaitForNoCallback();
}
bool VirtualAudioUtil::GetFormat() {
if (!composite_.is_valid()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
composite_->GetFormat().Then([](fidl::Result<fuchsia_virtualaudio::Device::GetFormat>& result) {
CallbackReceived();
if (result.is_error()) {
printf("GetFormat failed: %s\n", result.error_value().FormatDescription().c_str());
return;
}
FormatNotification(result.value().frames_per_second(), result.value().sample_format(),
result.value().num_channels(), result.value().external_delay());
});
return WaitForCallback();
}
bool VirtualAudioUtil::GetBuffer() {
if (!composite_.is_valid()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
composite_->GetBuffer().Then([](fidl::Result<fuchsia_virtualaudio::Device::GetBuffer>& result) {
CallbackReceived();
if (result.is_error()) {
printf("GetBuffer failed: %s\n", result.error_value().FormatDescription().c_str());
return;
}
BufferNotification(std::move(result.value().ring_buffer()),
result.value().num_ring_buffer_frames(),
result.value().notifications_per_ring());
});
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_;
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 (!composite_.is_valid()) {
printf("ERROR: Device not bound - you must add the device before using this flag.\n");
return false;
}
composite_->GetPosition().Then(
[](fidl::Result<fuchsia_virtualaudio::Device::GetPosition>& result) {
CallbackReceived();
if (result.is_error()) {
printf("GetPosition failed: %s\n", result.error_value().FormatDescription().c_str());
return;
}
PositionNotification(result.value().monotonic_time(), result.value().ring_position());
});
return WaitForCallback();
}
bool VirtualAudioUtil::SetNotificationFrequency(const std::string& notifs_str) {
if (!composite_.is_valid()) {
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));
composite_->SetNotificationFrequency({notifications_per_ring})
.Then([](fidl::Result<fuchsia_virtualaudio::Device::SetNotificationFrequency>& result) {
CallbackReceived();
if (result.is_error()) {
printf("SetNotificationFrequency failed: %s\n",
result.error_value().FormatDescription().c_str());
}
});
return WaitForCallback();
}
void VirtualAudioUtil::CallbackReceived() {
VirtualAudioUtil::received_callback_ = true;
VirtualAudioUtil::loop_->Quit();
}
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)\n", fps, fmt, chans, delay);
frame_size_ = chans * BytesPerSample(fmt);
ref_time_to_running_position_rate_ = media::TimelineRate(fps * frame_size_, ZX_SEC(1));
}
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);
rb_size_ = (static_cast<size_t>(num_ring_buffer_frames * frame_size_));
printf("--Received SetBuffer (vmo size: %zu, ring size: %zu, frames: %u, notifs: %u)\n", vmo_size,
rb_size_, num_ring_buffer_frames, notifications_per_ring);
}
void VirtualAudioUtil::UpdateRunningPosition(uint32_t ring_position) {
if (ring_position <= last_rb_position_) {
running_position_ += rb_size_;
}
running_position_ -= last_rb_position_;
running_position_ += ring_position;
last_rb_position_ = ring_position;
}
void VirtualAudioUtil::StartNotification(zx_time_t start_time) {
printf("--Received Start (time: %zu)\n", start_time);
ref_time_to_running_position_ =
media::TimelineFunction(0, start_time, ref_time_to_running_position_rate_);
running_position_ = 0;
last_rb_position_ = 0;
}
void VirtualAudioUtil::StopNotification(zx_time_t stop_time, uint32_t ring_position) {
auto expected_running_position = ref_time_to_running_position_.Apply(stop_time);
UpdateRunningPosition(ring_position);
printf("--Received Stop (time: %zu, pos: %u)\n", stop_time, ring_position);
printf("--Stop at position: expected %zu; actual %zu\n", expected_running_position,
running_position_);
running_position_ = 0;
last_rb_position_ = 0;
}
void VirtualAudioUtil::PositionNotification(zx_time_t monotonic_time_for_position,
uint32_t ring_position) {
printf("--Received Position (time: %13zu, pos: %6u)", monotonic_time_for_position, ring_position);
if (monotonic_time_for_position > ref_time_to_running_position_.reference_time()) {
int64_t expected_running_position =
ref_time_to_running_position_.Apply(monotonic_time_for_position);
UpdateRunningPosition(ring_position);
FX_CHECK(running_position_ <= std::numeric_limits<int64_t>::max());
int64_t delta = expected_running_position - static_cast<int64_t>(running_position_);
printf(" - running byte position: expect %8zu actual %8zu delta %6zd",
expected_running_position, running_position_, delta);
}
printf("\n");
}
} // namespace
} // namespace virtual_audio
int main(int argc, const char** argv) {
fuchsia_logging::LogSettingsBuilder builder;
builder.WithTags({"virtual_audio_util"}).BuildAndInitialize();
fxl::CommandLine command_line = fxl::CommandLineFromArgcArgv(argc, argv);
async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
virtual_audio::VirtualAudioUtil util(&loop);
util.Run(&command_line);
return 0;
}