// 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 #include #include #include #include #include #include #include #include #include #include "sine-source.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 DEFAULT_TONE_DURATION = 1.5f; static constexpr float MIN_TONE_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 = 20000.0f; static constexpr float DEFAULT_RECORD_DURATION = 30.0f; static constexpr uint32_t DEFAULT_FRAME_RATE = 48000; static constexpr uint32_t DEFAULT_BITS_PER_SAMPLE = 16; static constexpr uint32_t DEFAULT_CHANNELS = 2; static constexpr audio_sample_format_t AUDIO_SAMPLE_FORMAT_UNSIGNED_8BIT = static_cast(AUDIO_SAMPLE_FORMAT_8BIT | AUDIO_SAMPLE_FORMAT_FLAG_UNSIGNED); enum class Command { INVALID, INFO, MUTE, UNMUTE, AGC, GAIN, PLUG_MONITOR, TONE, PLAY, RECORD, }; void usage(const char* prog_name) { printf("usage:\n"); printf("%s [options] \n", prog_name); printf("\nOptions\n"); printf(" When options are specified, they must occur before the command and command\n" " arguments. Valid options include...\n" " -d : Dev node id for the audio device to use. Defaults to 0.\n" " -t : The type of device to open, either input or output. Ignored if\n" " the command given is direction specific (play, record, etc).\n" " Otherwise, defaults to output.\n" " -r : Frame rate to use. Defaults to 48000 Hz\n" " -b : Bits per sample to use. Defaults to 16\n" " -c : Channels to use. Defaults to 2\n"); printf("\nValid command are\n"); printf("info : Fetches capability and status info for the specified stream\n"); printf("mute : Mute the specified stream\n"); printf("unmute : Unmute the specified stream\n"); printf("agc : Params : (on|off)\n"); printf(" Enable or disable AGC for the specified input stream.\n"); printf("gain : Params : \n"); printf(" Set the gain of the stream to the specified level\n"); printf("pmon : Params : []\n" " Monitor the plug state of the specified stream for the\n" " specified amount of time. Duration defaults to %.1fs and is\n" " floored at %u mSec\n", DEFAULT_PLUG_MONITOR_DURATION, static_cast(MIN_PLUG_MONITOR_DURATION * 1000)); printf("tone : Params : [] []\n" " Play a sinusoidal tone of the specified frequency for the\n" " specified duration. Frequency is clamped on the range\n" " [%.1f, %.1f] Hz. Duration is given in seconds and floored\n" " at %d mSec. Default is %.1f Hz for %.1f seconds\n", MIN_TONE_FREQ, MAX_TONE_FREQ, static_cast(MIN_TONE_DURATION * 1000), DEFAULT_TONE_FREQ, DEFAULT_TONE_DURATION); printf("play : Params : \n"); printf(" Play the specified WAV file on the selected output.\n"); printf("record : Params : [duration]\n" " Record to the specified WAV file from the selected input.\n" " Duration defaults to %.1f seconds if unspecified.\n", DEFAULT_RECORD_DURATION); } void dump_format_range(size_t ndx, const audio_stream_format_range_t& range) { printf("[%2zu] Sample Format :", ndx); struct { audio_sample_format_t flag; const char* name; } SF_FLAG_LUT[] = { { AUDIO_SAMPLE_FORMAT_FLAG_UNSIGNED, "Unsigned" }, { AUDIO_SAMPLE_FORMAT_FLAG_INVERT_ENDIAN , "Inv Endian" }, }; for (const auto& sf : SF_FLAG_LUT) { if (range.sample_formats & sf.flag) { printf(" %s", sf.name); } } struct { audio_sample_format_t flag; const char* name; } SF_FORMAT_LUT[] = { { AUDIO_SAMPLE_FORMAT_BITSTREAM, "Bitstream" }, { AUDIO_SAMPLE_FORMAT_8BIT, "8" }, { AUDIO_SAMPLE_FORMAT_16BIT, "16" }, { AUDIO_SAMPLE_FORMAT_20BIT_PACKED, "20-packed" }, { AUDIO_SAMPLE_FORMAT_24BIT_PACKED, "24-packed" }, { AUDIO_SAMPLE_FORMAT_20BIT_IN32, "20-in-32" }, { AUDIO_SAMPLE_FORMAT_24BIT_IN32, "24-in-32" }, { AUDIO_SAMPLE_FORMAT_32BIT, "32" }, { AUDIO_SAMPLE_FORMAT_32BIT_FLOAT, "Float 32" }, }; bool first = true; printf(" ["); for (const auto& sf : SF_FORMAT_LUT) { if (range.sample_formats & sf.flag) { printf("%s%s", first ? "" : ", ", sf.name); first = false; } } printf("]\n"); printf(" Channel Count : [%u, %u]\n", range.min_channels, range.max_channels); printf(" Frame Rates :"); if (range.flags & ASF_RANGE_FLAG_FPS_CONTINUOUS) { printf(" [%u, %u] Hz continuous\n", range.min_frames_per_second, range.max_frames_per_second); } else { audio::utils::FrameRateEnumerator enumerator(range); first = true; for (uint32_t rate : enumerator) { printf("%s%u", first ? " " : ", ", rate); first = false; } printf(" Hz\n"); } } static void FixupStringRequest(audio_stream_cmd_get_string_resp_t* resp, zx_status_t res) { if (res != ZX_OK) { snprintf(reinterpret_cast(resp->str), sizeof(resp->str), "", res); return; } if (resp->strlen > sizeof(resp->str)) { snprintf(reinterpret_cast(resp->str), sizeof(resp->str), "", 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 = fbl::min(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; } 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. audio_stream_cmd_get_gain_resp gain_state; res = stream.GetGain(&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. audio_stream_cmd_plug_detect_resp plug_state; res = stream.GetPlugState(&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. fbl::Vector fmts; res = stream.GetSupportedFormats(&fmts); if (res != ZX_OK) { printf("Failed to fetch supported formats! (res %d)\n", res); return res; } printf("\nStream supports %zu format range%s\n", fmts.size(), fmts.size() == 1 ? "" : "s"); for (size_t i = 0; i < fmts.size(); ++i) dump_format_range(i, fmts[i]); return ZX_OK; } int main(int argc, const char** argv) { bool input = false; uint32_t dev_id = 0; uint32_t frame_rate = DEFAULT_FRAME_RATE; uint32_t bits_per_sample = DEFAULT_BITS_PER_SAMPLE; uint32_t channels = DEFAULT_CHANNELS; Command cmd = Command::INVALID; auto print_usage = fbl::MakeAutoCall([prog_name = argv[0]]() { usage(prog_name); }); int arg = 1; if (arg >= argc) return -1; struct { const char* name; const char* tag; uint32_t* val; } UINT_OPTIONS[] = { { .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 }, }; static const struct { const char* name; Command cmd; bool force_out; bool force_in; } COMMANDS[] = { { "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 }, { "play", Command::PLAY, true, false }, { "record", Command::RECORD, false, true }, }; while (arg < argc) { // Check to see if this is an integer option bool parsed_option = false; for (const auto& o : UINT_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; if (sscanf(argv[arg], "%u", o.val) != 1) { printf("Failed to parse %s option, \"%s\"\n", o.tag, argv[arg]); return -1; } ++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])) { input = true; } else if (!strcmp("output", argv[arg])) { input = false; } 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) input = false; if (entry.force_in) input = true; 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) { 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); return -1; } float tone_freq = 440.0; float duration; const char* 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 = fbl::max(duration, MIN_PLUG_MONITOR_DURATION); } break; case Command::TONE: duration = DEFAULT_TONE_DURATION; if (arg < argc) { if (sscanf(argv[arg], "%f", &tone_freq) != 1) { printf("Failed to parse tone frequency \"%s\"\n", argv[arg]); return -1; } arg++; if (arg < argc) { if (sscanf(argv[arg], "%f", &duration) != 1) { printf("Failed to parse tone duration \"%s\"\n", argv[arg]); return -1; } arg++; } tone_freq = fbl::clamp(tone_freq, 15.0f, 20000.0f); duration = fbl::max(duration, MIN_TONE_DURATION); } break; case Command::PLAY: case Command::RECORD: if (arg >= argc) return -1; wav_filename = argv[arg]; arg++; if (cmd == Command::RECORD) { 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; 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. fbl::unique_ptr stream; if (input) stream = audio::utils::AudioInput::Create(dev_id); else stream = audio::utils::AudioOutput::Create(dev_id); 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; // Execute the chosen command. switch (cmd) { 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); 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, 1.0, duration, frame_rate, channels, sample_format); if (res != ZX_OK) { printf("Failed to initialize sine wav generator (res %d)\n", res); return res; } printf("Playing %.2f Hz tone for %.2f seconds\n", tone_freq, duration); return static_cast(stream.get())->Play(sine_source); } case Command::PLAY: { if (stream->input()) { printf("The \"play\" command can only be used on output streams.\n"); return -1; } WAVSource wav_source; res = wav_source.Initialize(wav_filename); if (res != ZX_OK) return res; return static_cast(stream.get())->Play(wav_source); } case Command::RECORD: { if (!stream->input()) { printf("The \"record\" command can only be used on input streams.\n"); return -1; } res = stream->SetFormat(frame_rate, static_cast(channels), 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(wav_filename); if (res != ZX_OK) return res; return static_cast(stream.get())->Record(wav_sink, duration); } default: ZX_DEBUG_ASSERT(false); return -1; } }