blob: 8d1904543a47f13ec496652a7f601a7418d116d2 [file] [log] [blame]
// Copyright 2017 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 <ctype.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/fit/defer.h>
#include <poll.h>
#include <stdio.h>
#include <string.h>
#include <zircon/types.h>
#include <algorithm>
#include <limits>
#include <optional>
#include <thread>
#include <audio-proto-utils/format-utils.h>
#include <audio-utils/audio-device-stream.h>
#include <audio-utils/audio-input.h>
#include <audio-utils/audio-output.h>
#include <fbl/algorithm.h>
#include "generated-source.h"
#include "noise-source.h"
#include "sine-source.h"
#include "src/lib/fsl/tasks/fd_waiter.h"
#include "wav-sink.h"
#include "wav-source.h"
static constexpr float DEFAULT_PLUG_MONITOR_DURATION = 10.0f;
static constexpr float MIN_PLUG_MONITOR_DURATION = 0.5f;
static constexpr float MIN_PLAY_AMPLITUDE = 0.1f;
static constexpr float MAX_PLAY_AMPLITUDE = 1.0f;
static constexpr float DEFAULT_PLAY_DURATION = std::numeric_limits<float>::max();
static constexpr float DEFAULT_PLAY_AMPLITUDE = MIN_PLAY_AMPLITUDE;
static constexpr float MIN_PLAY_DURATION = 0.001f;
static constexpr float DEFAULT_TONE_FREQ = 440.0f;
static constexpr float MIN_TONE_FREQ = 15.0f;
static constexpr float MAX_TONE_FREQ = 96'000.0f;
static constexpr float DEFAULT_RECORD_DURATION = std::numeric_limits<float>::max();
static constexpr uint32_t DEFAULT_FRAME_RATE = 48000;
static constexpr uint32_t DEFAULT_BITS_PER_SAMPLE = 16;
static constexpr uint32_t DEFAULT_ACTIVE_CHANNELS = SineSource::kAllChannelsActive;
static constexpr audio_sample_format_t AUDIO_SAMPLE_FORMAT_UNSIGNED_8BIT =
static_cast<audio_sample_format_t>(AUDIO_SAMPLE_FORMAT_8BIT |
AUDIO_SAMPLE_FORMAT_FLAG_UNSIGNED);
enum class Command {
INVALID,
HELP,
INFO,
MUTE,
UNMUTE,
AGC,
GAIN,
PLUG_MONITOR,
TONE,
NOISE,
PLAY,
LOOP,
RECORD,
DUPLEX,
};
enum class Type : uint8_t { INPUT, OUTPUT, DUPLEX };
static std::optional<uint32_t> GetUint32(const char* arg) {
char* end = nullptr;
auto result = strtol(arg, &end, 16);
if (*end != '\0' || result < 0 || (result == 0 && arg == end)) {
return {};
}
return {result};
}
// LINT.IfChange
void usage(const char* prog_name, bool full_usage) {
// clang-format off
printf(
"Usage:\n"
" audio-driver-ctl [-d <id>] [-t (input|output)] agc (on|off)\n\n"
" audio-driver-ctl [-a <mask>] [-b (8|16|20|24|32)] [-c <channels>] \\\n"
" [-d <id>] [-r <hertz>] duplex <playpath> <recordpath>\n\n"
" audio-driver-ctl [-d <id>] [-t (input|output)] gain <decibels>\n\n"
" audio-driver-ctl [-d <id>] [-t (input|output)] info\n\n"
" audio-driver-ctl [-a <mask>] [-b (8|16|20|24|32)] [-c <channels>] \\\n"
" [-d <id>] loop <playpath>\n\n"
" audio-driver-ctl [-d <id>] [-t (input|output)] mute\n\n"
" audio-driver-ctl [-a <mask>] [-b (8|16|20|24|32)] [-c <channels>] \\\n"
" [-d <id>] [-r <hertz>] noise [<seconds>] [<amplitude>]\n\n"
" audio-driver-ctl [-a <mask>] [-b (8|16|20|24|32)] [-c <channels>] \\\n"
" [-d <id>] play <playpath>\n\n"
" audio-driver-ctl [-d <id>] [-t (input|output)] pmon [<seconds>]\n\n"
" audio-driver-ctl [-a <mask>] [-b (8|16|20|24|32)] [-c <channels>] \\\n"
" [-d <id>] [-r <hertz>] record <recordpath> [<seconds>]\n\n"
" audio-driver-ctl [-a <mask>] [-b (8|16|20|24|32)] [-c <channels>] \\\n"
" [-d <id>] [-r <hertz>] tone [<frequency>] [<seconds>] [<amplitude>]\n\n"
" audio-driver-ctl [-d <id>] [-t (input|output)] unmute\n\n"
"Play, record, and configure audio streams.\n\n");
if (!full_usage) {
printf("For full usage help use: audio-driver-ctl help\n");
} else {
printf(
"Options:\n"
" -a <mask> Active channel mask. For example `0xf` or `15` for\n"
" channels 0, 1, 2, and 3. Defaults to all channels.\n"
" -b (8|16|20|24|32) Bits per sample. Defaults to `16`.\n"
" -c <channels> Number of channels to use when recording or generating\n"
" tones/noises. Does not affect playback of WAV files\n"
" because WAV files specify how many channels to use in\n"
" their headers. Defaults to the first driver-reported\n"
" value. Run `audio-driver-ctl info` to see how many\n"
" channels your target Fuchsia device has. The number of\n"
" channels must match what the audio driver expects\n"
" because `audio-driver-ctl` does not do any mixing.\n"
" -d <id> The device node ID of the stream. Defaults to `0`.\n"
" To figure out <id>, run `audio-driver-ctl info`. You'll\n"
" see path values like `/dev/class/audio-input/000`. <id> in\n"
" this example is `000`.\n"
" -t (input|output) The device type. Defaults to `output`. This option is\n"
" ignored for commands like `play` that only make sense\n"
" for one of the types.\n"
" -r <hertz> The frame rate in hertz. Defaults to `%u`.\n\n", DEFAULT_FRAME_RATE);
printf(
"Commands:\n"
" agc Enables or disables automatic gain control for the stream.\n"
" duplex Simultaneously plays the WAV file located at <playpath>\n"
" and records another WAV file into <recordpath>\n"
" in order to analyze the delays in the system. The `-c`\n"
" option if provided applies to the recording side since\n"
" the number of channels for playback is taken from the\n"
" WAV file header.\n"
" gain Sets the gain of the stream in decibels.\n"
" info Gets capability and status info for a stream.\n"
" loop Repeatedly plays the WAV file at <playpath> on the selected\n"
" output until a key is pressed.\n"
" mute Mutes a stream.\n"
" noise Plays pseudo-white noise. <seconds> controls how long\n"
" the noise plays and must be at least %.3f seconds.\n"
" If <seconds> is not provided the noise plays until a\n"
" key is pressed.\n", MIN_PLAY_DURATION);
printf(
" play Plays a WAV file.\n"
" pmon Monitors the plug state of a stream. <seconds> must be\n"
" above %.1f seconds (default: %.1f seconds).\n",
MIN_PLUG_MONITOR_DURATION, DEFAULT_PLUG_MONITOR_DURATION);
printf(
" record Records to the specified WAV file from the selected input.\n"
" If <seconds> is not provided the input is recorded until\n"
" a key is pressed.\n"
" tone Plays a sinusoidal tone. <frequency> must be between %.1f\n"
" and %.1f hertz (default: %.1f hertz). <seconds> must be above\n"
" %.3f seconds. If <seconds> is not provided the tone plays\n"
" until a key is pressed. <amplitude> scales the output\n"
" if provided and must be between %.1f and %.1f.\n", MIN_TONE_FREQ, MAX_TONE_FREQ, DEFAULT_TONE_FREQ, MIN_PLAY_DURATION, MIN_PLAY_AMPLITUDE, MAX_PLAY_AMPLITUDE);
printf(
" unmute Unmutes a stream. Note that the gain of the stream will\n"
" be reset to its default value.\n\n"
"Examples:\n"
" Enable automatic gain control on the default output stream:\n"
" $ audio-driver-ctl agc on\n\n"
" Get info for the default output stream:\n"
" # Equivalent to `audio-driver-ctl -t output -d 000 info`\n"
" $ audio-driver-ctl info\n"
" Info for audio output at \"/dev/class/audio-output/000\"\n"
" Unique ID : 0100000000000000-0000000000000000\n"
" Manufacturer : Spacely Sprockets\n"
" Product : acme\n"
" Current Gain : 0.00 dB (unmuted, AGC on)\n"
" Gain Caps : gain range [-103.00, 24.00] in 0.50 dB steps; can mute; can AGC\n"
" Plug State : plugged\n"
" Plug Time : 12297829382473034410\n"
" PD Caps : hardwired\n"
" Number of channels : 1\n"
" Frame rate : 8000Hz\n"
" Bits per channel : 16\n"
" Valid bits per channel : 16\n"
" ...\n\n"
" Use the `-t` and `-d` options to interact with a stream other than the\n"
" default output stream:\n"
" $ audio-driver-ctl -t input -d 001 info\n"
" ...\n\n"
" Set the gain of the default output stream to -40 decibels:\n"
" $ audio-driver-ctl gain -40\n\n"
" Mute the default output stream:\n"
" $ audio-driver-ctl mute\n\n"
" Repeatedly play a WAV file on the default output stream:\n"
" $ audio-driver-ctl loop /tmp/test.wav\n"
" Looping /tmp/test.wav until a key is pressed\n\n"
" Play a WAV file on the default output stream:\n"
" $ audio-driver-ctl play /tmp/test.wav\n\n"
" Play a 450 hertz tone for 1 second at 50%% amplitude on the default output stream:\n"
" $ audio-driver-ctl tone 450 1 0.5\n"
" Playing 450.00 Hz tone for 1.00 seconds at 0.50 amplitude\n\n"
" Unmute the default output stream:\n"
" $ audio-driver-ctl unmute\n\n"
"Notes:\n"
" Commands that exercise audio streams such as `play` are only supported in diagnostic\n"
" product bundles (https://fuchsia.dev/fuchsia-src/glossary#product-bundle) like `core`.\n"
" In other builds only the informational commands like `info` are supported.\n\n"
" To copy WAV files from your host to your target Fuchsia device or vice versa,\n"
" run `fx cp (--to-target|--to-host) <source> <destination>` on your host.\n"
" <source> is the file you want to copy and <destination> is where you want\n"
" the copied file to be placed:\n"
" # Copy from host to Fuchsia target device.\n"
" $ fx cp --to-target /path/on/host/example.wav /path/on/fuchsia/target/example.wav\n"
" # Copy from Fuchsia target device to host.\n"
" $ fx cp --to-host /path/on/fuchsia/target/example.wav /path/on/host/example.wav\n\n"
" If you get a `Failed to set source format` error like the next example when\n"
" running `play` it means that there's a mismatch between the number of channels\n"
" specified in the WAV file's header and the number of channels on your target\n"
" Fuchsia device. For example the WAV file might be intended for a 2-channel\n"
" system whereas your target Fuchsia device only has 1 channel. The solution is\n"
" to get a WAV file with the same number of channels as your target Fuchsia device.\n"
" $ audio-driver-ctl play /tmp/two_channel.wav\n"
" Failed to set source format [11025 Hz, 1 Chan, 00000000ffffffff Mask, 00000004 fmt] (res -20)\n\n"
" Source code for `audio-driver-ctl`: https://cs.opensource.google/fuchsia/fuchsia/+/main:src/media/audio/tools/audio-driver-ctl/audio.cc\n\n");
// clang-format on
}
}
// LINT.ThenChange(//docs/reference/tools/hardware/audio-driver-ctl.md)
void dump_formats(const audio::utils::AudioDeviceStream& stream) {
stream.GetSupportedFormats([](const fuchsia_hardware_audio::wire::SupportedFormats& formats) {
auto& pcm = formats.pcm_supported_formats();
printf("\nNumber of channels :");
bool has_attributes = false;
for (auto i : pcm.channel_sets()) {
printf(" %zu", i.attributes().count());
for (auto j : i.attributes()) {
if (j.has_min_frequency()) {
has_attributes = true;
}
if (j.has_max_frequency()) {
has_attributes = true;
}
}
}
if (has_attributes) {
printf("\nChannels attributes :");
for (auto i : pcm.channel_sets()) {
for (auto j : i.attributes()) {
printf(" ");
if (j.has_min_frequency()) {
printf("%u", j.min_frequency());
}
printf("/");
if (j.has_max_frequency()) {
printf("%u", j.max_frequency());
}
}
printf(" (min/max Hz for %zu channels)", i.attributes().count());
}
}
printf("\nFrame rate :");
for (auto i : pcm.frame_rates()) {
printf(" %uHz", i);
}
printf("\nBits per channel :");
for (auto i : pcm.bytes_per_sample()) {
printf(" %u", 8 * i);
}
printf("\nValid bits per channel :");
for (auto i : pcm.valid_bits_per_sample()) {
printf(" %u", i);
}
printf("\n");
});
}
static void FixupStringRequest(audio_stream_cmd_get_string_resp_t* resp, zx_status_t res) {
if (res != ZX_OK) {
snprintf(reinterpret_cast<char*>(resp->str), sizeof(resp->str), "<err %d>", res);
return;
}
if (resp->strlen > sizeof(resp->str)) {
snprintf(reinterpret_cast<char*>(resp->str), sizeof(resp->str), "<bad strllen %u>",
resp->strlen);
return;
}
// We are going to display this string using ASCII, but it is encoded using
// UTF8. Go over the string and replace unprintable characters with
// something else. Also replace embedded nulls with a space. Finally,
// ensure that the string is null terminated.
uint32_t len = std::min<uint32_t>(sizeof(resp->str) - 1, resp->strlen);
uint32_t i;
for (i = 0; i < len; ++i) {
if (resp->str[i] == 0) {
resp->str[i] = ' ';
} else if (!isprint(resp->str[i])) {
resp->str[i] = '?';
}
}
resp->str[i] = 0;
}
int Play(std::unique_ptr<audio::utils::AudioDeviceStream> stream, const char* play_wav_filename,
uint32_t active, const audio::utils::Duration& duration_config) {
WAVSource wav_source;
auto res = wav_source.Initialize(play_wav_filename, active, duration_config);
if (res != ZX_OK)
return res;
return static_cast<audio::utils::AudioOutput*>(stream.get())->Play(wav_source);
}
int Record(std::unique_ptr<audio::utils::AudioDeviceStream> stream, const char* record_wav_filename,
uint32_t frame_rate, uint32_t channels, uint32_t active,
audio_sample_format_t sample_format, const audio::utils::Duration& duration_config) {
auto res = stream->SetFormat(frame_rate, static_cast<uint16_t>(channels), active, sample_format);
if (res != ZX_OK) {
printf("Failed to set format (rate %u, chan %u, fmt 0x%08x, res %d)\n", frame_rate, channels,
sample_format, res);
return -1;
}
WAVSink wav_sink;
res = wav_sink.Initialize(record_wav_filename);
if (res != ZX_OK)
return res;
return static_cast<audio::utils::AudioInput*>(stream.get())->Record(wav_sink, duration_config);
}
int Duplex(std::unique_ptr<audio::utils::AudioDeviceStream> play_stream,
std::unique_ptr<audio::utils::AudioDeviceStream> record_stream,
const char* play_wav_filename, const char* record_wav_filename, uint32_t frame_rate,
uint32_t channels, uint32_t active, audio_sample_format_t sample_format) {
// Initialize recording.
auto res =
record_stream->SetFormat(frame_rate, static_cast<uint16_t>(channels), active, sample_format);
if (res != ZX_OK) {
printf("Failed to set format (rate %u, chan %u, fmt 0x%08x, res %d)\n", frame_rate, channels,
sample_format, res);
return -1;
}
WAVSink wav_sink;
res = wav_sink.Initialize(record_wav_filename);
if (res != ZX_OK)
return res;
auto input = static_cast<audio::utils::AudioInput*>(record_stream.get());
res = input->RecordPrepare(wav_sink);
if (res != ZX_OK)
return res;
// Initialize playback.
WAVSource wav_source;
// duration not in loop mode, unused.
float unused_duration = 0.f;
res = wav_source.Initialize(play_wav_filename, active, unused_duration);
if (res != ZX_OK)
return res;
auto output = static_cast<audio::utils::AudioOutput*>(play_stream.get());
res = output->PlayPrepare(wav_source);
if (res != ZX_OK)
return res;
// Start recording and playback.
res = input->StartRingBuffer();
auto res2 = output->StartRingBuffer();
if (res != ZX_OK) {
printf("Failed to start capture (res %d)\n", res);
return res;
}
if (res2 != ZX_OK) {
printf("Failed to start playback (res %d)\n", res);
return res;
}
int64_t record_start = input->start_time();
int64_t playback_start = output->start_time();
// Complete recording and playback.
zx_status_t play_completion_error = ZX_ERR_INTERNAL;
std::atomic<bool> play_done = {};
auto th = std::thread([&]() {
play_completion_error = output->PlayToCompletion(wav_source);
play_done.store(true);
});
res = input->RecordToCompletion(wav_sink, [&]() -> bool { return !play_done.load(); });
if (res != ZX_OK) {
printf("Failed to complete recording (res %d)\n", res);
th.join();
return res;
}
th.join();
if (play_completion_error != ZX_OK) {
printf("Failed to complete playback (res %d)\n", play_completion_error);
return play_completion_error;
}
// Now report known delays.
printf(
"Duplex delays:\n"
" Play start : %ld usecs\n"
" Input external : %ld usecs\n"
" Output external : %ld usecs\n"
" Total : %ld usecs\n",
(playback_start - record_start) / 1000, input->external_delay_nsec() / 1000,
output->external_delay_nsec() / 1000,
(playback_start - record_start + input->external_delay_nsec() +
output->external_delay_nsec()) /
1000);
return res;
}
zx_status_t dump_stream_info(const audio::utils::AudioDeviceStream& stream) {
zx_status_t res;
printf("Info for audio %s at \"%s\"\n", stream.input() ? "input" : "output", stream.name());
// Grab and display some of the interesting properties of the device,
// including its unique ID, its manufacturer name, and its product name.
audio_stream_cmd_get_unique_id_resp_t uid_resp;
res = stream.GetUniqueId(&uid_resp);
if (res != ZX_OK) {
printf("Failed to fetch unique ID! (res %d)\n", res);
return res;
}
const auto& uid = uid_resp.unique_id.data;
static_assert(sizeof(uid) == 16, "Unique ID is not 16 bytes long!\n");
printf(" Unique ID : %02x%02x%02x%02x%02x%02x%02x%02x-%02x%02x%02x%02x%02x%02x%02x%02x\n",
uid[0], uid[1], uid[2], uid[3], uid[4], uid[5], uid[6], uid[7], uid[8], uid[9], uid[10],
uid[11], uid[12], uid[13], uid[14], uid[15]);
audio_stream_cmd_get_string_resp_t str_resp;
res = stream.GetString(AUDIO_STREAM_STR_ID_MANUFACTURER, &str_resp);
FixupStringRequest(&str_resp, res);
printf(" Manufacturer : %s\n", str_resp.str);
res = stream.GetString(AUDIO_STREAM_STR_ID_PRODUCT, &str_resp);
FixupStringRequest(&str_resp, res);
printf(" Product : %s\n", str_resp.str);
// Fetch and print the current gain settings for this audio stream.
// Since we reconnect to the audio stream every time we run this uapp and we are guaranteed by the
// audio driver interface definition that the driver will reply to the first watch request, we
// can get the gain state by issuing a watch FIDL call.
audio_stream_cmd_get_gain_resp gain_state;
res = stream.WatchGain(&gain_state);
if (res != ZX_OK) {
printf("Failed to fetch gain information! (res %d)\n", res);
return res;
}
printf(" Current Gain : %.2f dB (%smuted%s)\n", gain_state.cur_gain,
gain_state.cur_mute ? "" : "un",
gain_state.can_agc ? (gain_state.cur_agc ? ", AGC on" : ", AGC off") : "");
printf(" Gain Caps : ");
if ((gain_state.min_gain == gain_state.max_gain) && (gain_state.min_gain == 0.0f)) {
printf("fixed 0 dB gain");
} else if (gain_state.gain_step == 0.0f) {
printf("gain range [%.2f, %.2f] dB (continuous)", gain_state.min_gain, gain_state.max_gain);
} else {
printf("gain range [%.2f, %.2f] in %.2f dB steps", gain_state.min_gain, gain_state.max_gain,
gain_state.gain_step);
}
printf("; %s mute", gain_state.can_mute ? "can" : "cannot");
printf("; %s AGC\n", gain_state.can_agc ? "can" : "cannot");
// Fetch and print the current pluged/unplugged state for this audio stream.
// Since we reconnect to the audio stream every time we run this uapp and we are guaranteed by the
// audio driver interface definition that the driver will reply to the first watch request, we
// can get the plug state by issuing a watch FIDL call.
audio_stream_cmd_plug_detect_resp plug_state;
res = stream.WatchPlugState(&plug_state);
if (res != ZX_OK) {
printf("Failed to fetch plug state information! (res %d)\n", res);
return res;
}
printf(" Plug State : %splugged\n", plug_state.flags & AUDIO_PDNF_PLUGGED ? "" : "un");
printf(" Plug Time : %lu\n", plug_state.plug_state_time);
printf(" PD Caps : %s\n",
(plug_state.flags & AUDIO_PDNF_HARDWIRED)
? "hardwired"
: ((plug_state.flags & AUDIO_PDNF_CAN_NOTIFY) ? "dynamic (async)"
: "dynamic (synchronous)"));
// Fetch and print the currently supported audio formats for this audio stream.
dump_formats(stream);
return ZX_OK;
}
int main(int argc, const char** argv) {
Type type = Type::OUTPUT;
std::optional<uint32_t> dev_id = 0;
std::optional<uint32_t> frame_rate = DEFAULT_FRAME_RATE;
std::optional<uint32_t> bits_per_sample = DEFAULT_BITS_PER_SAMPLE;
std::optional<uint32_t> channels;
std::optional<uint32_t> active = DEFAULT_ACTIVE_CHANNELS;
Command cmd = Command::INVALID;
auto print_usage = fit::defer([prog_name = argv[0]]() { usage(prog_name, false); });
int arg = 1;
printf(
"Important: audio-driver-ctl is deprecated. Please use ffx audio device \n"
"to interact with audio drivers. For more information, run \n"
"ffx audio device --help and see the README located at \n"
"https://cs.opensource.google/fuchsia/fuchsia/+/main:src/developer/ffx/plugins/audio/README.md\n\n");
if (arg >= argc)
return -1;
struct {
const char* name;
const char* tag;
std::optional<uint32_t>* val;
} OPTIONS[] = {
// clang-format off
{ .name = "-d", .tag = "device ID", .val = &dev_id },
{ .name = "-r", .tag = "frame rate", .val = &frame_rate },
{ .name = "-b", .tag = "bits/sample", .val = &bits_per_sample },
{ .name = "-c", .tag = "channels", .val = &channels },
{ .name = "-a", .tag = "active", .val = &active },
// clang-format on
};
static const struct {
const char* name;
Command cmd;
bool force_out;
bool force_in;
} COMMANDS[] = {
// clang-format off
{ "help", Command::HELP, false, false },
{ "info", Command::INFO, false, false },
{ "mute", Command::MUTE, false, false },
{ "unmute", Command::UNMUTE, false, false },
{ "agc", Command::AGC, false, true },
{ "gain", Command::GAIN, false, false },
{ "pmon", Command::PLUG_MONITOR, false, false },
{ "tone", Command::TONE, true, false },
{ "noise", Command::NOISE, true, false },
{ "play", Command::PLAY, true, false },
{ "loop", Command::LOOP, true, false },
{ "record", Command::RECORD, false, true },
{ "duplex", Command::DUPLEX, false, false },
// clang-format on
};
while (arg < argc) {
// Check to see if this is an integer option
bool parsed_option = false;
for (const auto& o : OPTIONS) {
if (!strcmp(o.name, argv[arg])) {
// Looks like this is an integer argument we care about.
// Attempt to parse it.
if (++arg >= argc)
return -1;
std::optional<uint32_t> value = GetUint32(argv[arg]);
if (!value.has_value()) {
printf("Failed to parse %s option, \"%s\"\n", o.tag, argv[arg]);
return -1;
}
*o.val = value;
++arg;
parsed_option = true;
break;
}
}
// If we successfully parse an integer option, continue on to the next
// argument (if any).
if (parsed_option)
continue;
// Was this the device type flag?
if (!strcmp("-t", argv[arg])) {
if (++arg >= argc)
return -1;
if (!strcmp("input", argv[arg])) {
type = Type::INPUT;
} else if (!strcmp("output", argv[arg])) {
type = Type::OUTPUT;
} else {
printf("Invalid input/output specifier \"%s\".\n", argv[arg]);
return -1;
}
++arg;
continue;
}
// Well, this didn't look like an option we understand, so it must be a
// command. Attempt to figure out what command it was.
for (const auto& entry : COMMANDS) {
if (!strcmp(entry.name, argv[arg])) {
cmd = entry.cmd;
parsed_option = true;
arg++;
if (entry.force_out)
type = Type::OUTPUT;
if (entry.force_in)
type = Type::INPUT;
break;
}
}
if (!parsed_option) {
printf("Failed to parse command ID \"%s\"\n", argv[arg]);
return -1;
}
break;
}
if (cmd == Command::INVALID) {
printf("Failed to find valid command ID.\n");
return -1;
}
audio_sample_format_t sample_format;
switch (bits_per_sample.value()) {
case 8:
sample_format = AUDIO_SAMPLE_FORMAT_UNSIGNED_8BIT;
break;
case 16:
sample_format = AUDIO_SAMPLE_FORMAT_16BIT;
break;
case 20:
sample_format = AUDIO_SAMPLE_FORMAT_20BIT_IN32;
break;
case 24:
sample_format = AUDIO_SAMPLE_FORMAT_24BIT_IN32;
break;
case 32:
sample_format = AUDIO_SAMPLE_FORMAT_32BIT;
break;
default:
printf("Unsupported number of bits per sample (%u)\n", bits_per_sample.value());
return -1;
}
float tone_freq = 440.0;
float duration;
float amplitude = DEFAULT_PLAY_AMPLITUDE;
const char* play_wav_filename = nullptr;
const char* record_wav_filename = nullptr;
float target_gain = -100.0;
bool enb_agc = false;
// Parse any additional arguments
switch (cmd) {
case Command::GAIN:
if (arg >= argc)
return -1;
if (sscanf(argv[arg], "%f", &target_gain) != 1) {
printf("Failed to parse gain \"%s\"\n", argv[arg]);
return -1;
}
arg++;
break;
case Command::AGC:
if (arg >= argc)
return -1;
if (strcasecmp(argv[arg], "on") == 0) {
enb_agc = true;
} else if (strcasecmp(argv[arg], "off") == 0) {
enb_agc = false;
} else {
printf("Failed to parse agc setting \"%s\"\n", argv[arg]);
return -1;
}
arg++;
break;
case Command::PLUG_MONITOR:
duration = DEFAULT_PLUG_MONITOR_DURATION;
if (arg < argc) {
if (sscanf(argv[arg], "%f", &duration) != 1) {
printf("Failed to parse plug monitor duration \"%s\"\n", argv[arg]);
return -1;
}
arg++;
duration = std::max(duration, MIN_PLUG_MONITOR_DURATION);
}
break;
case Command::TONE:
case Command::NOISE:
duration = DEFAULT_PLAY_DURATION;
if (arg < argc) {
if (cmd == Command::TONE) {
if (sscanf(argv[arg], "%f", &tone_freq) != 1) {
printf("Failed to parse tone frequency \"%s\"\n", argv[arg]);
return -1;
}
arg++;
tone_freq = std::clamp(tone_freq, MIN_TONE_FREQ, MAX_TONE_FREQ);
}
if (arg < argc) {
if (sscanf(argv[arg], "%f", &duration) != 1) {
printf("Failed to parse playback duration \"%s\"\n", argv[arg]);
return -1;
}
arg++;
}
if (arg < argc) {
if (sscanf(argv[arg], "%f", &amplitude) != 1) {
printf("Failed to parse playback amplitude \"%s\"\n", argv[arg]);
return -1;
}
arg++;
}
duration = std::max(duration, MIN_PLAY_DURATION);
amplitude = std::min(amplitude, MAX_PLAY_AMPLITUDE);
amplitude = std::max(amplitude, MIN_PLAY_AMPLITUDE);
}
break;
case Command::PLAY:
if (arg >= argc)
return -1;
play_wav_filename = argv[arg];
arg++;
break;
case Command::RECORD:
if (arg >= argc)
return -1;
record_wav_filename = argv[arg];
arg++;
duration = DEFAULT_RECORD_DURATION;
if (arg < argc) {
if (sscanf(argv[arg], "%f", &duration) != 1) {
printf("Failed to parse record duration \"%s\"\n", argv[arg]);
return -1;
}
arg++;
}
break;
case Command::LOOP:
if (arg >= argc)
return -1;
play_wav_filename = argv[arg];
arg++;
break;
case Command::DUPLEX:
if (arg >= argc)
return -1;
play_wav_filename = argv[arg];
arg++;
if (arg >= argc)
return -1;
record_wav_filename = argv[arg];
arg++;
type = Type::DUPLEX;
break;
default:
break;
}
if (arg != argc) {
printf("Invalid number of arguments.\n");
return -1;
}
// Argument parsing is done, we can cancel the usage dump.
print_usage.cancel();
// Open the selected stream.
std::unique_ptr<audio::utils::AudioDeviceStream> stream;
std::unique_ptr<audio::utils::AudioDeviceStream> stream_duplex_record;
switch (type) {
case Type::INPUT:
stream = audio::utils::AudioInput::Create(dev_id.value());
break;
case Type::OUTPUT:
stream = audio::utils::AudioOutput::Create(dev_id.value());
break;
case Type::DUPLEX: {
stream_duplex_record = audio::utils::AudioInput::Create(dev_id.value());
if (stream_duplex_record == nullptr) {
printf("Out of memory!\n");
return ZX_ERR_NO_MEMORY;
}
// No need to log in the case of failure. Open has already done so.
zx_status_t res = stream_duplex_record->Open();
if (res != ZX_OK) {
return res;
}
stream = audio::utils::AudioOutput::Create(dev_id.value());
} break;
}
if (stream == nullptr) {
printf("Out of memory!\n");
return ZX_ERR_NO_MEMORY;
}
// No need to log in the case of failure. Open has already done so.
zx_status_t res = stream->Open();
if (res != ZX_OK)
return res;
auto formats = fidl::WireCall(stream->BorrowStreamChannel())->GetSupportedFormats();
if (!formats.ok()) {
printf("Can't connect to the driver\n");
return ZX_ERR_BAD_STATE;
}
if (!channels.has_value()) {
if (formats.value().supported_formats.count() < 1 ||
formats.value().supported_formats[0].pcm_supported_formats().channel_sets().count() < 1) {
printf("No valid format reported by driver\n");
return ZX_ERR_BAD_STATE;
}
// Use the first number of channels value reported.
auto& pcm = formats.value().supported_formats[0].pcm_supported_formats();
channels = static_cast<uint32_t>(pcm.channel_sets()[0].attributes().count());
}
async::Loop async_loop(&kAsyncLoopConfigNoAttachToCurrentThread);
async_loop.StartThread("audio CLI wait for key");
std::atomic<bool> pressed(false);
fsl::FDWaiter fd_waiter(async_loop.dispatcher());
auto cleanup = fit::defer([&async_loop] { async_loop.Shutdown(); });
fd_waiter.Wait([&pressed](zx_status_t, uint32_t) { pressed.store(true); }, 0, POLLIN);
auto loop_done = [&pressed]() -> bool { return !pressed.load(); };
audio::utils::Duration duration_config = {};
const bool interactive = duration == std::numeric_limits<float>::max();
if (interactive) {
duration_config = loop_done;
} else {
duration_config = duration;
}
// Execute the chosen command.
switch (cmd) {
case Command::HELP: {
usage(argv[0], true);
return ZX_OK;
}
case Command::INFO:
return dump_stream_info(*stream);
case Command::MUTE:
return stream->SetMute(true);
case Command::UNMUTE:
return stream->SetMute(false);
case Command::GAIN:
return stream->SetGain(target_gain);
case Command::AGC:
return stream->SetAgc(enb_agc);
case Command::PLUG_MONITOR:
return stream->PlugMonitor(duration, nullptr);
case Command::TONE: {
if (stream->input()) {
printf("The \"tone\" command can only be used on output streams.\n");
return -1;
}
SineSource sine_source;
res = sine_source.Init(tone_freq, amplitude, duration_config, frame_rate.value(),
channels.value(), active.value(), sample_format);
if (res != ZX_OK) {
printf("Failed to initialize sine wav generator (res %d)\n", res);
return res;
}
if (interactive) {
printf("Playing %.2f Hz tone at %.2f amplitude until a key is pressed\n", tone_freq,
amplitude);
} else {
printf("Playing %.2f Hz tone for %.2f seconds at %.2f amplitude\n", tone_freq,
std::get<float>(duration_config), amplitude);
}
return static_cast<audio::utils::AudioOutput*>(stream.get())->Play(sine_source);
}
case Command::NOISE: {
if (stream->input()) {
printf("The \"noise\" command can only be used on output streams.\n");
return -1;
}
NoiseSource noise_source;
res = noise_source.Init(tone_freq, 1.0, duration_config, frame_rate.value(), channels.value(),
active.value(), sample_format);
if (res != ZX_OK) {
printf("Failed to initialize white noise generator (res %d)\n", res);
return res;
}
if (interactive) {
printf("Playing white noise until a key is pressed\n");
} else {
printf("Playing white noise for %.2f seconds\n", std::get<float>(duration_config));
}
return static_cast<audio::utils::AudioOutput*>(stream.get())->Play(noise_source);
}
case Command::PLAY:
if (stream->input()) {
printf("The \"play\" command can only be used on output streams.\n");
return -1;
}
return Play(std::move(stream), play_wav_filename, active.value(), duration_config);
case Command::LOOP:
if (stream->input()) {
printf("The \"loop\" command can only be used on output streams.\n");
return -1;
}
duration_config = loop_done;
printf("Looping %s until a key is pressed\n", play_wav_filename);
return Play(std::move(stream), play_wav_filename, active.value(), duration_config);
case Command::RECORD:
if (!stream->input()) {
printf("The \"record\" command can only be used on input streams.\n");
return -1;
}
if (interactive) {
printf("Recording until a key is pressed\n");
}
return Record(std::move(stream), record_wav_filename, frame_rate.value(), channels.value(),
active.value(), sample_format, duration_config);
case Command::DUPLEX: {
if (stream->input() || !stream_duplex_record || !stream_duplex_record->input()) {
printf("The \"duplex\" command can only be used on one output and one input stream.\n");
return -1;
}
return Duplex(std::move(stream), std::move(stream_duplex_record), play_wav_filename,
record_wav_filename, frame_rate.value(), channels.value(), active.value(),
sample_format);
}
default:
ZX_DEBUG_ASSERT(false);
return -1;
}
}