| // 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/fzl/vmo-mapper.h> |
| #include <lib/sys/cpp/component_context.h> |
| #include <lib/zx/clock.h> |
| #include <poll.h> |
| #include <unistd.h> |
| #include <zircon/device/audio.h> |
| |
| #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" |
| #include "src/lib/syslog/cpp/logger.h" |
| |
| namespace virtual_audio { |
| |
| class VirtualAudioUtil { |
| public: |
| VirtualAudioUtil(async::Loop* loop) { VirtualAudioUtil::loop_ = loop; } |
| |
| void Run(fxl::CommandLine* cmdline); |
| |
| private: |
| enum class Command { |
| ENABLE_VIRTUAL_AUDIO, |
| DISABLE_VIRTUAL_AUDIO, |
| GET_NUM_VIRTUAL_DEVICES, |
| |
| SET_DEVICE_NAME, |
| SET_MANUFACTURER, |
| SET_PRODUCT_NAME, |
| SET_UNIQUE_ID, |
| SET_CLOCK_DOMAIN, |
| ADD_FORMAT_RANGE, |
| CLEAR_FORMAT_RANGES, |
| SET_FIFO_DEPTH, |
| 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, |
| |
| SET_IN, |
| SET_OUT, |
| WAIT, |
| INVALID, |
| }; |
| |
| static constexpr struct { |
| const char* name; |
| Command cmd; |
| } COMMANDS[] = { |
| {"enable", Command::ENABLE_VIRTUAL_AUDIO}, |
| {"disable", Command::DISABLE_VIRTUAL_AUDIO}, |
| {"num-devs", Command::GET_NUM_VIRTUAL_DEVICES}, |
| |
| {"dev", Command::SET_DEVICE_NAME}, |
| {"mfg", Command::SET_MANUFACTURER}, |
| {"prod", Command::SET_PRODUCT_NAME}, |
| {"id", Command::SET_UNIQUE_ID}, |
| {"clk-domain", Command::SET_CLOCK_DOMAIN}, |
| {"add-format", Command::ADD_FORMAT_RANGE}, |
| {"clear-format", Command::CLEAR_FORMAT_RANGES}, |
| {"fifo", Command::SET_FIFO_DEPTH}, |
| {"delay", Command::SET_EXTERNAL_DELAY}, |
| {"rb", Command::SET_RING_BUFFER_RESTRICTIONS}, |
| {"gain-props", Command::SET_GAIN_PROPS}, |
| {"plug-props", Command::SET_PLUG_PROPS}, |
| {"reset", Command::RESET_CONFIG}, |
| |
| {"add", Command::ADD_DEVICE}, |
| {"remove", Command::REMOVE_DEVICE}, |
| |
| {"plug", Command::PLUG}, |
| {"unplug", Command::UNPLUG}, |
| {"get-gain", Command::GET_GAIN}, |
| {"get-format", Command::GET_FORMAT}, |
| {"get-rb", Command::RETRIEVE_BUFFER}, |
| {"write-rb", Command::WRITE_BUFFER}, |
| {"get-pos", Command::GET_POSITION}, |
| {"notifs", Command::SET_NOTIFICATION_FREQUENCY}, |
| |
| {"in", Command::SET_IN}, |
| {"out", Command::SET_OUT}, |
| {"wait", Command::WAIT}, |
| }; |
| 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 = 1; |
| |
| static constexpr uint8_t kDefaultFormatRangeOption = 0; |
| |
| static constexpr uint32_t kDefaultFifoDepth = 0x100; |
| static constexpr uint64_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 = 0x0000765400009ABC; |
| |
| static constexpr uint8_t kDefaultGainPropsOption = 0; |
| static constexpr uint8_t kDefaultPlugPropsOption = 0; |
| static constexpr uint32_t kDefaultNotificationFrequency = 4; |
| |
| static async::Loop* loop_; |
| static bool received_callback_; |
| |
| void QuitLoop(); |
| bool RunForDuration(zx::duration duration); |
| bool WaitForNoCallback(); |
| bool WaitForCallback(); |
| |
| void RegisterKeyWaiter(); |
| bool WaitForKey(); |
| |
| bool ConnectToController(); |
| bool ConnectToDevice(); |
| bool ConnectToInput(); |
| bool ConnectToOutput(); |
| void SetUpEvents(); |
| |
| void ParseAndExecute(fxl::CommandLine* cmdline); |
| bool ExecuteCommand(Command cmd, const std::string& value); |
| |
| // Methods using the FIDL Service interface |
| bool Enable(bool enable); |
| bool GetNumDevices(); |
| |
| // 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 SetClockDomain(const std::string& clock_domain); |
| bool AddFormatRange(const std::string& format_str); |
| bool ClearFormatRanges(); |
| bool SetFifoDepth(const std::string& fifo_str); |
| bool SetExternalDelay(const std::string& delay_str); |
| bool SetRingBufferRestrictions(const std::string& rb_restr_str); |
| bool SetGainProperties(const std::string& gain_props_str); |
| bool SetPlugProperties(const std::string& plug_props_str); |
| bool ResetConfiguration(); |
| |
| bool AddDevice(); |
| |
| // 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_val_str); |
| bool GetPosition(); |
| bool SetNotificationFrequency(const std::string& override_notifs_str); |
| |
| std::unique_ptr<sys::ComponentContext> component_context_; |
| fsl::FDWaiter keystroke_waiter_; |
| bool key_quit_ = false; |
| |
| fuchsia::virtualaudio::ControlPtr controller_ = nullptr; |
| fuchsia::virtualaudio::InputPtr input_ = nullptr; |
| fuchsia::virtualaudio::OutputPtr output_ = nullptr; |
| |
| bool configuring_output_ = true; |
| static zx::vmo ring_buffer_vmo_; |
| static uint64_t ring_buffer_size_; |
| |
| static void CallbackReceived(); |
| static void EnableCallback(); |
| static void DisableCallback(); |
| static void NumDevicesCallback(uint32_t num_inputs, uint32_t num_outputs); |
| 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(uint32_t fps, uint32_t fmt, uint32_t chans, zx_duration_t delay); |
| |
| template <bool is_out> |
| static void GainNotification(bool current_mute, bool current_agc, float gain_db); |
| template <bool is_out> |
| static void GainCallback(bool current_mute, bool current_agc, float gain_db); |
| |
| template <bool is_out> |
| static void BufferNotification(zx::vmo buff, uint32_t rb_frames, uint32_t notifs); |
| template <bool is_out> |
| static void BufferCallback(zx::vmo buff, uint32_t rb_frames, uint32_t notifs); |
| |
| 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, uint32_t ring_position); |
| template <bool is_out> |
| static void PositionCallback(zx_time_t monotonic_time, uint32_t ring_position); |
| }; |
| |
| ::async::Loop* VirtualAudioUtil::loop_; |
| bool VirtualAudioUtil::received_callback_; |
| zx::vmo VirtualAudioUtil::ring_buffer_vmo_; |
| uint64_t VirtualAudioUtil::ring_buffer_size_ = 0; |
| |
| // VirtualAudioUtil implementation |
| // |
| void VirtualAudioUtil::Run(fxl::CommandLine* cmdline) { |
| ParseAndExecute(cmdline); |
| |
| // We are done! Disconnect any error handlers. |
| if (input_.is_bound()) { |
| input_.set_error_handler(nullptr); |
| } |
| if (output_.is_bound()) { |
| 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() { |
| if (controller_.is_bound()) { |
| return true; |
| } |
| |
| component_context_->svc()->Connect(controller_.NewRequest()); |
| |
| controller_.set_error_handler([this](zx_status_t error) { |
| printf("controller_ disconnected (%d)!\n", error); |
| QuitLoop(); |
| }); |
| |
| // 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; |
| } |
| |
| bool VirtualAudioUtil::ConnectToDevice() { |
| return (configuring_output_ ? ConnectToOutput() : ConnectToInput()); |
| } |
| |
| bool VirtualAudioUtil::ConnectToInput() { |
| if (input_.is_bound()) { |
| return true; |
| } |
| |
| component_context_->svc()->Connect(input_.NewRequest()); |
| |
| input_.set_error_handler([this](zx_status_t error) { |
| printf("input_ disconnected (%d)!\n", error); |
| QuitLoop(); |
| }); |
| |
| SetUpEvents(); |
| |
| // let VirtualAudio disconnect if all is not well. |
| bool success = (WaitForNoCallback() && input_.is_bound()); |
| |
| if (!success) { |
| printf("Failed to establish channel to input\n"); |
| } |
| return success; |
| } |
| |
| bool VirtualAudioUtil::ConnectToOutput() { |
| if (output_.is_bound()) { |
| return true; |
| } |
| |
| component_context_->svc()->Connect(output_.NewRequest()); |
| |
| output_.set_error_handler([this](zx_status_t error) { |
| printf("output_ disconnected (%d)!\n", error); |
| QuitLoop(); |
| }); |
| |
| SetUpEvents(); |
| |
| // let VirtualAudio disconnect if all is not well. |
| bool success = (WaitForNoCallback() && output_.is_bound()); |
| |
| if (!success) { |
| printf("Failed to establish channel to output\n"); |
| } |
| return success; |
| } |
| |
| void VirtualAudioUtil::SetUpEvents() { |
| if (configuring_output_) { |
| output_.events().OnSetFormat = FormatNotification<true>; |
| output_.events().OnSetGain = GainNotification<true>; |
| output_.events().OnBufferCreated = BufferNotification<true>; |
| output_.events().OnStart = StartNotification<true>; |
| output_.events().OnStop = StopNotification<true>; |
| output_.events().OnPositionNotify = PositionNotification<true>; |
| } else { |
| input_.events().OnSetFormat = FormatNotification<false>; |
| input_.events().OnSetGain = GainNotification<false>; |
| input_.events().OnBufferCreated = BufferNotification<false>; |
| input_.events().OnStart = StartNotification<false>; |
| input_.events().OnStop = StopNotification<false>; |
| input_.events().OnPositionNotify = PositionNotification<false>; |
| } |
| } |
| |
| void VirtualAudioUtil::ParseAndExecute(fxl::CommandLine* cmdline) { |
| if (!cmdline->has_argv0() || cmdline->options().size() == 0) { |
| 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(); |
| |
| for (auto option : cmdline->options()) { |
| bool success = false; |
| Command cmd = Command::INVALID; |
| |
| for (const auto& entry : COMMANDS) { |
| if (!option.name.compare(entry.name)) { |
| cmd = entry.cmd; |
| success = true; |
| |
| break; |
| } |
| } |
| |
| if (!success) { |
| printf("Failed to parse command ID `--%s'\n", option.name.c_str()); |
| 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::ENABLE_VIRTUAL_AUDIO: |
| success = Enable(true); |
| break; |
| case Command::DISABLE_VIRTUAL_AUDIO: |
| success = Enable(false); |
| break; |
| 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::ADD_FORMAT_RANGE: |
| success = AddFormatRange(value); |
| break; |
| case Command::CLEAR_FORMAT_RANGES: |
| success = ClearFormatRanges(); |
| break; |
| case Command::SET_FIFO_DEPTH: |
| success = SetFifoDepth(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 = SetGainProperties(value); |
| break; |
| case Command::SET_PLUG_PROPS: |
| success = SetPlugProperties(value); |
| break; |
| case Command::RESET_CONFIG: |
| success = ResetConfiguration(); |
| 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::SET_IN: |
| configuring_output_ = false; |
| success = true; |
| break; |
| case Command::SET_OUT: |
| configuring_output_ = true; |
| success = true; |
| break; |
| case Command::WAIT: |
| success = WaitForKey(); |
| break; |
| case Command::INVALID: |
| success = false; |
| break; |
| |
| // Intentionally omitting default so new enums are not forgotten here. |
| } |
| return success; |
| } |
| |
| bool VirtualAudioUtil::Enable(bool enable) { |
| if (!ConnectToController()) { |
| return false; |
| } |
| |
| if (enable) { |
| controller_->Enable(EnableCallback); |
| } else { |
| controller_->Disable(DisableCallback); |
| } |
| |
| return WaitForCallback(); |
| } |
| |
| bool VirtualAudioUtil::GetNumDevices() { |
| if (!ConnectToController()) { |
| return false; |
| } |
| |
| controller_->GetNumDevices(NumDevicesCallback); |
| |
| return WaitForCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetDeviceName(const std::string& name) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->SetDeviceName(name == "" ? kDefaultDeviceName : name); |
| } else { |
| input_->SetDeviceName(name == "" ? kDefaultDeviceName : name); |
| } |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetManufacturer(const std::string& name) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->SetManufacturer(name == "" ? kDefaultManufacturer : name); |
| } else { |
| input_->SetManufacturer(name == "" ? kDefaultManufacturer : name); |
| } |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetProductName(const std::string& name) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->SetProduct(name == "" ? kDefaultProductName : name); |
| } else { |
| input_->SetProduct(name == "" ? kDefaultProductName : name); |
| } |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetUniqueId(const std::string& unique_id_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| std::array<uint8_t, 16> unique_id; |
| bool use_default = (unique_id_str == ""); |
| |
| for (uint8_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); |
| } |
| if (configuring_output_) { |
| output_->SetUniqueId(unique_id); |
| } else { |
| input_->SetUniqueId(unique_id); |
| } |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetClockDomain(const std::string& clk_domain_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| int32_t clock_domain = |
| (clk_domain_str == "" ? kDefaultClockDomain : fxl::StringToNumber<int32_t>(clk_domain_str)); |
| if (configuring_output_) { |
| output_->SetClockDomain(clock_domain); |
| } else { |
| input_->SetClockDomain(clock_domain); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| struct Format { |
| uint32_t flags; |
| uint32_t min_rate; |
| uint32_t max_rate; |
| uint32_t min_chans; |
| uint32_t max_chans; |
| uint32_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 |
| // |
| // Going forward, it would be best to have chans, rate and bitdepth specifiable individually. |
| constexpr Format kFormatSpecs[7] = { |
| {.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}}; |
| |
| bool VirtualAudioUtil::AddFormatRange(const std::string& format_range_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| uint8_t format_option = (format_range_str == "" ? kDefaultFormatRangeOption |
| : fxl::StringToNumber<uint8_t>(format_range_str)); |
| if (format_option >= fbl::count_of(kFormatSpecs)) { |
| printf("Format range option must be %lu or less.\n", fbl::count_of(kFormatSpecs) - 1); |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->AddFormatRange( |
| kFormatSpecs[format_option].flags, kFormatSpecs[format_option].min_rate, |
| kFormatSpecs[format_option].max_rate, kFormatSpecs[format_option].min_chans, |
| kFormatSpecs[format_option].max_chans, kFormatSpecs[format_option].rate_family_flags); |
| } else { |
| input_->AddFormatRange( |
| kFormatSpecs[format_option].flags, kFormatSpecs[format_option].min_rate, |
| kFormatSpecs[format_option].max_rate, kFormatSpecs[format_option].min_chans, |
| kFormatSpecs[format_option].max_chans, kFormatSpecs[format_option].rate_family_flags); |
| } |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::ClearFormatRanges() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->ClearFormatRanges(); |
| } else { |
| input_->ClearFormatRanges(); |
| } |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetFifoDepth(const std::string& fifo_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| uint32_t fifo_depth = |
| (fifo_str == "" ? kDefaultFifoDepth : fxl::StringToNumber<uint32_t>(fifo_str)); |
| if (configuring_output_) { |
| output_->SetFifoDepth(fifo_depth); |
| } else { |
| input_->SetFifoDepth(fifo_depth); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetExternalDelay(const std::string& delay_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| zx_duration_t external_delay = |
| (delay_str == "" ? kDefaultExternalDelayNsec : fxl::StringToNumber<zx_duration_t>(delay_str)); |
| if (configuring_output_) { |
| output_->SetExternalDelay(external_delay); |
| } else { |
| input_->SetExternalDelay(external_delay); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| 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 6 secs. |
| constexpr BufferSpec kBufferSpecs[] = { |
| {.min_frames = 48000, .max_frames = 72000, .mod_frames = 6000}, |
| {.min_frames = 9600, .max_frames = 28800, .mod_frames = 480}, |
| {.min_frames = 288000, .max_frames = 288000, .mod_frames = 288000}}; |
| |
| bool VirtualAudioUtil::SetRingBufferRestrictions(const std::string& rb_restr_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| uint8_t rb_option = |
| (rb_restr_str == "" ? kDefaultRingBufferOption : fxl::StringToNumber<uint8_t>(rb_restr_str)); |
| if (rb_option >= fbl::count_of(kBufferSpecs)) { |
| printf("Ring buffer option must be %lu or less.\n", fbl::count_of(kBufferSpecs) - 1); |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->SetRingBufferRestrictions(kBufferSpecs[rb_option].min_frames, |
| kBufferSpecs[rb_option].max_frames, |
| kBufferSpecs[rb_option].mod_frames); |
| } else { |
| input_->SetRingBufferRestrictions(kBufferSpecs[rb_option].min_frames, |
| kBufferSpecs[rb_option].max_frames, |
| kBufferSpecs[rb_option].mod_frames); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| struct GainSpec { |
| bool cur_mute; |
| bool cur_agc; |
| float cur_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 and above represent invalid combinations. |
| constexpr GainSpec kGainSpecs[4] = {{.cur_mute = true, |
| .cur_agc = false, |
| .cur_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}, |
| {.cur_mute = false, |
| .cur_agc = true, |
| .cur_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}, |
| {.cur_mute = true, |
| .cur_agc = true, |
| .cur_gain_db = -12.0, |
| .can_mute = false, |
| .can_agc = false, |
| .min_gain_db = -96.0, |
| .max_gain_db = 0.0, |
| .gain_step_db = 1.0}, |
| {.cur_mute = false, |
| .cur_agc = false, |
| .cur_gain_db = 50.0, |
| .can_mute = true, |
| .can_agc = false, |
| .min_gain_db = 20.0, |
| .max_gain_db = -20.0, |
| .gain_step_db = -3.0}}; |
| |
| bool VirtualAudioUtil::SetGainProperties(const std::string& gain_props_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| uint8_t gain_props_option = (gain_props_str == "" ? kDefaultGainPropsOption |
| : fxl::StringToNumber<uint8_t>(gain_props_str)); |
| if (gain_props_option >= fbl::count_of(kGainSpecs)) { |
| printf("Gain properties option must be %lu or less.\n", fbl::count_of(kGainSpecs)); |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->SetGainProperties( |
| kGainSpecs[gain_props_option].min_gain_db, kGainSpecs[gain_props_option].max_gain_db, |
| kGainSpecs[gain_props_option].gain_step_db, kGainSpecs[gain_props_option].cur_gain_db, |
| kGainSpecs[gain_props_option].can_mute, kGainSpecs[gain_props_option].cur_mute, |
| kGainSpecs[gain_props_option].can_agc, kGainSpecs[gain_props_option].cur_agc); |
| } else { |
| input_->SetGainProperties( |
| kGainSpecs[gain_props_option].min_gain_db, kGainSpecs[gain_props_option].max_gain_db, |
| kGainSpecs[gain_props_option].gain_step_db, kGainSpecs[gain_props_option].cur_gain_db, |
| kGainSpecs[gain_props_option].can_mute, kGainSpecs[gain_props_option].cur_mute, |
| kGainSpecs[gain_props_option].can_agc, kGainSpecs[gain_props_option].cur_agc); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| // 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(fbl::count_of(kPlugFlags) == fbl::count_of(kPlugTime)); |
| |
| bool VirtualAudioUtil::SetPlugProperties(const std::string& plug_props_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| uint8_t plug_props_option = (plug_props_str == "" ? kDefaultPlugPropsOption |
| : fxl::StringToNumber<uint8_t>(plug_props_str)); |
| |
| if (plug_props_option >= fbl::count_of(kPlugFlags)) { |
| printf("Plug properties option must be %lu or less.\n", fbl::count_of(kPlugFlags) - 1); |
| return false; |
| } |
| |
| auto plug_change_time = (kPlugTime[plug_props_option] == -1 ? zx::clock::get_monotonic().get() |
| : kPlugTime[plug_props_option]); |
| bool plugged = (kPlugFlags[plug_props_option] & AUDIO_PDNF_PLUGGED); |
| bool hardwired = (kPlugFlags[plug_props_option] & AUDIO_PDNF_HARDWIRED); |
| bool can_notify = (kPlugFlags[plug_props_option] & AUDIO_PDNF_CAN_NOTIFY); |
| |
| if (configuring_output_) { |
| output_->SetPlugProperties(plug_change_time, plugged, hardwired, can_notify); |
| } else { |
| input_->SetPlugProperties(plug_change_time, plugged, hardwired, can_notify); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::ResetConfiguration() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->ResetConfiguration(); |
| } else { |
| input_->ResetConfiguration(); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::AddDevice() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->Add(); |
| } else { |
| input_->Add(); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::RemoveDevice() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->Remove(); |
| } else { |
| input_->Remove(); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::ChangePlugState(const std::string& plug_time_str, bool plugged) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| auto plug_change_time = (plug_time_str == "" ? zx::clock::get_monotonic().get() |
| : fxl::StringToNumber<zx_time_t>(plug_time_str)); |
| |
| if (configuring_output_) { |
| output_->ChangePlugState(plug_change_time, plugged); |
| } else { |
| input_->ChangePlugState(plug_change_time, plugged); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::GetFormat() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->GetFormat(FormatCallback<true>); |
| } else { |
| input_->GetFormat(FormatCallback<false>); |
| } |
| |
| return WaitForCallback(); |
| } |
| |
| bool VirtualAudioUtil::GetGain() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->GetGain(GainCallback<true>); |
| } else { |
| input_->GetGain(GainCallback<false>); |
| } |
| |
| return WaitForCallback(); |
| } |
| |
| bool VirtualAudioUtil::GetBuffer() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->GetBuffer(BufferCallback<true>); |
| } else { |
| input_->GetBuffer(BufferCallback<false>); |
| } |
| |
| return WaitForCallback() && ring_buffer_vmo_.is_valid(); |
| } |
| |
| bool VirtualAudioUtil::WriteBuffer(const std::string& write_value_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| size_t value_to_write = |
| (write_value_str == "" ? kDefaultValueToWrite : fxl::StringToNumber<size_t>(write_value_str)); |
| |
| if (!ring_buffer_vmo_.is_valid()) { |
| if (!GetBuffer()) { |
| return false; |
| } |
| } |
| |
| for (size_t offset = 0; offset < ring_buffer_size_; offset += sizeof(value_to_write)) { |
| auto status = ring_buffer_vmo_.write(&value_to_write, offset, sizeof(value_to_write)); |
| if (status != ZX_OK) { |
| printf("Writing 0x%016zX to rb_vmo[%zu] failed (%d)\n", value_to_write, offset, status); |
| return false; |
| } |
| } |
| return WaitForNoCallback(); |
| } |
| |
| bool VirtualAudioUtil::GetPosition() { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| if (configuring_output_) { |
| output_->GetPosition(PositionCallback<true>); |
| } else { |
| input_->GetPosition(PositionCallback<false>); |
| } |
| |
| return WaitForCallback(); |
| } |
| |
| bool VirtualAudioUtil::SetNotificationFrequency(const std::string& notifs_str) { |
| if (!ConnectToDevice()) { |
| return false; |
| } |
| |
| uint32_t notifications_per_ring = (notifs_str == "" ? kDefaultNotificationFrequency |
| : fxl::StringToNumber<uint32_t>(notifs_str)); |
| if (configuring_output_) { |
| output_->SetNotificationFrequency(notifications_per_ring); |
| } else { |
| input_->SetNotificationFrequency(notifications_per_ring); |
| } |
| |
| return WaitForNoCallback(); |
| } |
| |
| void VirtualAudioUtil::CallbackReceived() { |
| VirtualAudioUtil::received_callback_ = true; |
| VirtualAudioUtil::loop_->Quit(); |
| } |
| |
| void VirtualAudioUtil::EnableCallback() { |
| VirtualAudioUtil::CallbackReceived(); |
| |
| printf("--Received Enable callback\n"); |
| } |
| |
| void VirtualAudioUtil::DisableCallback() { |
| VirtualAudioUtil::CallbackReceived(); |
| |
| printf("--Received Disable callback\n"); |
| } |
| |
| void VirtualAudioUtil::NumDevicesCallback(uint32_t num_inputs, uint32_t num_outputs) { |
| VirtualAudioUtil::CallbackReceived(); |
| |
| printf("--Received NumDevices (%u inputs, %u outputs)\n", num_inputs, num_outputs); |
| } |
| |
| 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")); |
| } |
| template <bool is_out> |
| void VirtualAudioUtil::FormatCallback(uint32_t fps, uint32_t fmt, uint32_t chans, |
| zx_duration_t delay) { |
| VirtualAudioUtil::CallbackReceived(); |
| VirtualAudioUtil::FormatNotification<is_out>(fps, fmt, chans, delay); |
| } |
| |
| template <bool is_out> |
| void VirtualAudioUtil::GainNotification(bool mute, bool agc, float gain_db) { |
| printf("--Received Gain (mute: %u, agc: %u, gain: %f dB) for %s\n", mute, agc, gain_db, |
| (is_out ? "output" : "input")); |
| } |
| template <bool is_out> |
| void VirtualAudioUtil::GainCallback(bool mute, bool agc, float gain_db) { |
| VirtualAudioUtil::CallbackReceived(); |
| VirtualAudioUtil::GainNotification<is_out>(mute, agc, 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); |
| ring_buffer_vmo_.get_size(&ring_buffer_size_); |
| |
| printf("--Received SetBuffer (size: %zu, frames: %u, notifs: %u) for %s\n", ring_buffer_size_, |
| num_ring_buffer_frames, notifications_per_ring, (is_out ? "output" : "input")); |
| } |
| template <bool is_out> |
| void VirtualAudioUtil::BufferCallback(zx::vmo buff, uint32_t rb_frames, uint32_t notifs) { |
| VirtualAudioUtil::CallbackReceived(); |
| VirtualAudioUtil::BufferNotification<is_out>(std::move(buff), rb_frames, notifs); |
| } |
| |
| 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")); |
| } |
| |
| template <bool is_out> |
| void VirtualAudioUtil::StopNotification(zx_time_t stop_time, uint32_t rb_pos) { |
| printf("--Received Stop (time: %zu, pos: %u) for %s\n", stop_time, rb_pos, |
| (is_out ? "output" : "input")); |
| } |
| |
| template <bool is_out> |
| void VirtualAudioUtil::PositionNotification(zx_time_t time_for_pos, uint32_t rb_pos) { |
| printf("--Received Position (time: %zu, pos: %u) for %s\n", time_for_pos, rb_pos, |
| (is_out ? "output" : "input")); |
| } |
| template <bool is_out> |
| void VirtualAudioUtil::PositionCallback(zx_time_t time_for_pos, uint32_t rb_pos) { |
| VirtualAudioUtil::CallbackReceived(); |
| VirtualAudioUtil::PositionNotification<is_out>(time_for_pos, rb_pos); |
| } |
| |
| } // namespace virtual_audio |
| |
| int main(int argc, const char** argv) { |
| syslog::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; |
| } |