blob: 4e321a271b67046128264a27f4937f1ce52affc1 [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 "garnet/bin/media/wav_recorder/wav_recorder.h"
#include <lib/async-loop/loop.h>
#include <lib/fit/defer.h>
#include "garnet/lib/media/wav_writer/wav_writer.h"
#include "lib/fxl/logging.h"
#include "lib/media/audio/types.h"
namespace media::tools {
// TODO(mpuryear): make these constexpr char[] and eliminate c_str() usage later
static const std::string kLoopbackOption = "loopback";
static const std::string kChannelsOption = "chans";
static const std::string kFrameRateOption = "rate";
static const std::string kFloatFormatOption = "float";
static const std::string k24In32FormatOption = "int24";
static const std::string kPacked24FormatOption = "packed24";
static const std::string kGainOption = "gain";
static const std::string kMuteOption = "mute";
static const std::string kAsyncModeOption = "async";
static const std::string kVerboseOption = "v";
static const std::string kShowUsageOption1 = "help";
static const std::string kShowUsageOption2 = "?";
constexpr zx_duration_t kCaptureChunkDuration = ZX_MSEC(100);
constexpr size_t kCaptureChunkCount = 10;
WavRecorder::~WavRecorder() {
if (payload_buf_virt_ != nullptr) {
FXL_DCHECK(payload_buf_size_ != 0);
FXL_DCHECK(bytes_per_frame_ != 0);
zx::vmar::root_self()->unmap(reinterpret_cast<uintptr_t>(payload_buf_virt_),
payload_buf_size_);
}
}
void WavRecorder::Run(component::StartupContext* app_context) {
auto cleanup = fit::defer([this]() { Shutdown(); });
const auto& pos_args = cmd_line_.positional_args();
// Parse our args.
if (cmd_line_.HasOption(kShowUsageOption1) ||
cmd_line_.HasOption(kShowUsageOption2)) {
Usage();
return;
}
verbose_ = cmd_line_.HasOption(kVerboseOption);
loopback_ = cmd_line_.HasOption(kLoopbackOption);
if (pos_args.size() < 1) {
Usage();
return;
}
filename_ = pos_args[0].c_str();
// Connect to the audio service and obtain AudioCapturer and Gain interfaces.
fuchsia::media::AudioPtr audio =
app_context->ConnectToEnvironmentService<fuchsia::media::Audio>();
audio->CreateAudioCapturer(audio_capturer_.NewRequest(), loopback_);
audio_capturer_->BindGainControl(gain_control_.NewRequest());
audio_capturer_.set_error_handler([this](zx_status_t status) {
FXL_LOG(ERROR)
<< "AudioCapturer connection lost unexpectedly, shutting down.";
Shutdown();
});
gain_control_.set_error_handler([this](zx_status_t status) {
FXL_LOG(ERROR)
<< "GainControl connection lost unexpectedly, shutting down.";
Shutdown();
});
// Fetch the initial media type and figure out what we need to do from there.
audio_capturer_->GetStreamType([this](fuchsia::media::StreamType type) {
OnDefaultFormatFetched(std::move(type));
});
// Quit if someone hits a key.
keystroke_waiter_.Wait([this](zx_status_t, uint32_t) { OnQuit(); },
STDIN_FILENO, POLLIN);
cleanup.cancel();
}
void WavRecorder::Usage() {
printf("\nUsage: %s [options] <filename>\n", cmd_line_.argv0().c_str());
printf("Record an audio signal from the specified source, to a .wav file.\n");
printf("\nValid options:\n");
printf("\n By default, use the preferred input device\n");
printf(" --%s\t\tCapture final-mix output from the preferred output device\n",
kLoopbackOption.c_str());
printf(
"\n By default, use device-preferred channel count and frame rate, in "
"16-bit integer samples\n");
printf(" --%s=<NUM_CHANS>\tSpecify the number of channels (in [%u, %u])\n",
kChannelsOption.c_str(), fuchsia::media::MIN_PCM_CHANNEL_COUNT,
fuchsia::media::MAX_PCM_CHANNEL_COUNT);
printf(" --%s=<rate>\t\tSpecify the capture frame rate (Hz in [%u, %u])\n",
kFrameRateOption.c_str(), fuchsia::media::MIN_PCM_FRAMES_PER_SECOND,
fuchsia::media::MAX_PCM_FRAMES_PER_SECOND);
printf(" --%s\t\tRecord and save as 32-bit float\n",
kFloatFormatOption.c_str());
printf(
" --%s\t\tRecord and save as left-justified 24-in-32 int ('padded-24')\n",
k24In32FormatOption.c_str());
printf(" --%s\t\tRecord as 24-in-32 'padded-24'; save as 'packed-24'\n",
kPacked24FormatOption.c_str());
printf(
"\n By default, don't set AudioCapturer gain and mute (unity 0 dB, "
"unmuted)\n");
printf(
" --%s[=<GAIN_DB>]\tSet stream gain (dB in [%.1f, +%.1f]; 0.0 if only "
"'--%s' is provided)\n",
kGainOption.c_str(), fuchsia::media::MUTED_GAIN_DB,
fuchsia::media::MAX_GAIN_DB, kGainOption.c_str());
printf(
" --%s[=<0|1>]\t\tSet stream mute (0=Unmute or 1=Mute; Mute if only "
"'--%s' is provided)\n",
kMuteOption.c_str(), kMuteOption.c_str());
printf("\n By default, use packet-by-packet ('synchronous') mode\n");
printf(" --%s\t\tCapture using sequential-buffer ('asynchronous') mode\n",
kAsyncModeOption.c_str());
printf("\n --%s\t\t\tBe verbose; display per-packet info\n",
kVerboseOption.c_str());
printf(" --%s, --%s\t\tShow this message\n", kShowUsageOption1.c_str(),
kShowUsageOption2.c_str());
printf("\n");
}
void WavRecorder::Shutdown() {
if (gain_control_.is_bound()) {
gain_control_.set_error_handler(nullptr);
gain_control_.Unbind();
}
if (audio_capturer_.is_bound()) {
audio_capturer_.set_error_handler(nullptr);
audio_capturer_.Unbind();
}
if (clean_shutdown_) {
if (wav_writer_.Close()) {
printf("done.\n");
} else {
printf("file close failed.\n");
}
} else {
if (!wav_writer_.Delete()) {
printf("Could not delete WAV file.\n");
}
}
quit_callback_();
}
bool WavRecorder::SetupPayloadBuffer() {
capture_frames_per_chunk_ =
(kCaptureChunkDuration * frames_per_second_) / ZX_SEC(1);
payload_buf_frames_ = capture_frames_per_chunk_ * kCaptureChunkCount;
payload_buf_size_ = payload_buf_frames_ * bytes_per_frame_;
zx_status_t res;
res = zx::vmo::create(payload_buf_size_, 0, &payload_buf_vmo_);
if (res != ZX_OK) {
FXL_LOG(ERROR) << "Failed to create " << payload_buf_size_
<< " byte payload buffer (res " << res << ")";
return false;
}
uintptr_t tmp;
res = zx::vmar::root_self()->map(0, payload_buf_vmo_, 0, payload_buf_size_,
ZX_VM_PERM_READ, &tmp);
if (res != ZX_OK) {
FXL_LOG(ERROR) << "Failed to map " << payload_buf_size_
<< " byte payload buffer (res " << res << ")";
return false;
}
payload_buf_virt_ = reinterpret_cast<void*>(tmp);
return true;
}
void WavRecorder::SendCaptureJob() {
FXL_DCHECK(capture_frame_offset_ < payload_buf_frames_);
FXL_DCHECK((capture_frame_offset_ + capture_frames_per_chunk_) <=
payload_buf_frames_);
++outstanding_capture_jobs_;
// clang-format off
audio_capturer_->CaptureAt(0,
capture_frame_offset_,
capture_frames_per_chunk_,
[this](fuchsia::media::StreamPacket packet) {
OnPacketProduced(std::move(packet));
});
// clang-format on
capture_frame_offset_ += capture_frames_per_chunk_;
if (capture_frame_offset_ >= payload_buf_frames_) {
capture_frame_offset_ = 0u;
}
}
// Once we receive the default format, we don't need to wait for anything else.
// We open our .wav file for recording, set our capture format, set input gain,
// setup our VMO and add it as a payload buffer, send a series of empty packets
void WavRecorder::OnDefaultFormatFetched(fuchsia::media::StreamType type) {
auto cleanup = fit::defer([this]() { Shutdown(); });
zx_status_t res;
if (!type.medium_specific.is_audio()) {
FXL_LOG(ERROR) << "Default format is not audio!";
return;
}
const auto& fmt = type.medium_specific.audio();
// If user erroneously specifies float AND 24-in-32, prefer float.
if (cmd_line_.HasOption(kFloatFormatOption)) {
sample_format_ = fuchsia::media::AudioSampleFormat::FLOAT;
} else if (cmd_line_.HasOption(kPacked24FormatOption)) {
pack_24bit_samples_ = true;
sample_format_ = fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32;
} else if (cmd_line_.HasOption(k24In32FormatOption)) {
sample_format_ = fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32;
} else {
sample_format_ = fuchsia::media::AudioSampleFormat::SIGNED_16;
}
channel_count_ = fmt.channels;
frames_per_second_ = fmt.frames_per_second;
bool change_format = false;
bool change_gain = false;
bool set_mute = false;
if (fmt.sample_format != sample_format_) {
change_format = true;
}
std::string opt;
if (cmd_line_.GetOptionValue(kFrameRateOption, &opt)) {
uint32_t rate;
if (::sscanf(opt.c_str(), "%u", &rate) != 1) {
Usage();
return;
}
if ((rate < fuchsia::media::MIN_PCM_FRAMES_PER_SECOND) ||
(rate > fuchsia::media::MAX_PCM_FRAMES_PER_SECOND)) {
printf("Frame rate (%u) must be within range [%u, %u]\n", rate,
fuchsia::media::MIN_PCM_FRAMES_PER_SECOND,
fuchsia::media::MAX_PCM_FRAMES_PER_SECOND);
return;
}
if (frames_per_second_ != rate) {
frames_per_second_ = rate;
change_format = true;
}
}
if (cmd_line_.HasOption(kGainOption)) {
stream_gain_db_ = 0.0f;
if (cmd_line_.GetOptionValue(kGainOption, &opt)) {
if (::sscanf(opt.c_str(), "%f", &stream_gain_db_) != 1) {
Usage();
return;
}
if ((stream_gain_db_ < fuchsia::media::MUTED_GAIN_DB) ||
(stream_gain_db_ > fuchsia::media::MAX_GAIN_DB)) {
printf("Gain (%.3f dB) must be within range [%.1f, %.1f]\n",
stream_gain_db_, fuchsia::media::MUTED_GAIN_DB,
fuchsia::media::MAX_GAIN_DB);
return;
}
}
change_gain = true;
}
if (cmd_line_.HasOption(kMuteOption)) {
stream_mute_ = true;
if (cmd_line_.GetOptionValue(kMuteOption, &opt)) {
uint32_t mute_val;
if (::sscanf(opt.c_str(), "%u", &mute_val) != 1) {
Usage();
return;
}
stream_mute_ = (mute_val > 0);
}
set_mute = true;
}
if (cmd_line_.GetOptionValue(kChannelsOption, &opt)) {
uint32_t count;
if (::sscanf(opt.c_str(), "%u", &count) != 1) {
Usage();
return;
}
if ((count < fuchsia::media::MIN_PCM_CHANNEL_COUNT) ||
(count > fuchsia::media::MAX_PCM_CHANNEL_COUNT)) {
printf("Channel count (%u) must be within range [%u, %u]\n", count,
fuchsia::media::MIN_PCM_CHANNEL_COUNT,
fuchsia::media::MAX_PCM_CHANNEL_COUNT);
return;
}
if (channel_count_ != count) {
channel_count_ = count;
change_format = true;
}
}
uint32_t bytes_per_sample =
(sample_format_ == fuchsia::media::AudioSampleFormat::FLOAT)
? sizeof(float)
: (sample_format_ ==
fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32)
? sizeof(int32_t)
: sizeof(int16_t);
bytes_per_frame_ = channel_count_ * bytes_per_sample;
uint32_t bits_per_sample = bytes_per_sample * 8;
if (sample_format_ == fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32 &&
pack_24bit_samples_ == true) {
bits_per_sample = 24;
}
// Write the inital WAV header
if (!wav_writer_.Initialize(filename_, sample_format_, channel_count_,
frames_per_second_, bits_per_sample)) {
return;
}
// If desired format differs from default capturer format, change formats now.
if (change_format) {
audio_capturer_->SetPcmStreamType(media::CreateAudioStreamType(
sample_format_, channel_count_, frames_per_second_));
}
// Set the specified gain (if specified) for the recording.
if (change_gain) {
gain_control_->SetGain(stream_gain_db_);
}
if (set_mute) {
gain_control_->SetMute(stream_mute_);
}
// Create our shared payload buffer, map it into place, then dup the handle
// and pass it on to the capturer to fill.
if (!SetupPayloadBuffer()) {
return;
}
zx::vmo audio_capturer_vmo;
res = payload_buf_vmo_.duplicate(
ZX_RIGHT_TRANSFER | ZX_RIGHT_READ | ZX_RIGHT_WRITE | ZX_RIGHT_MAP,
&audio_capturer_vmo);
if (res != ZX_OK) {
FXL_LOG(ERROR) << "Failed to duplicate VMO handle (res " << res << ")";
return;
}
audio_capturer_->AddPayloadBuffer(0, std::move(audio_capturer_vmo));
// Will we operate in synchronous or asynchronous mode? If synchronous, queue
// all our capture buffers to get the ball rolling. If asynchronous, set an
// event handler for position notification, and start operating in async mode.
if (!cmd_line_.HasOption(kAsyncModeOption)) {
for (size_t i = 0; i < kCaptureChunkCount; ++i) {
SendCaptureJob();
}
} else {
FXL_DCHECK(payload_buf_frames_);
FXL_DCHECK(capture_frames_per_chunk_);
FXL_DCHECK((payload_buf_frames_ % capture_frames_per_chunk_) == 0);
audio_capturer_.events().OnPacketProduced =
[this](fuchsia::media::StreamPacket pkt) {
OnPacketProduced(std::move(pkt));
};
audio_capturer_->StartAsyncCapture(capture_frames_per_chunk_);
}
if (sample_format_ == fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32) {
FXL_DCHECK(bits_per_sample == (pack_24bit_samples_ ? 24 : 32));
if (pack_24bit_samples_ == true) {
compress_32_24_buff_ =
std::make_unique<uint8_t[]>(payload_buf_size_ * 3 / 4);
}
}
printf(
"Recording %s, %u Hz, %u-channel linear PCM\n",
sample_format_ == fuchsia::media::AudioSampleFormat::FLOAT
? "32-bit float"
: sample_format_ == fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32
? (pack_24bit_samples_ ? "packed 24-bit signed int"
: "24-bit-in-32-bit signed int")
: "16-bit signed int",
frames_per_second_, channel_count_);
printf("from %s into '%s'", loopback_ ? "loopback" : "default input",
filename_);
if (change_gain) {
printf(", applying gain of %.2f dB", stream_gain_db_);
}
if (set_mute) {
printf(", after setting stream Mute to %s",
stream_mute_ ? "TRUE" : "FALSE");
}
printf("\n");
cleanup.cancel();
}
// A packet containing captured audio data was just returned to us -- handle it.
void WavRecorder::OnPacketProduced(fuchsia::media::StreamPacket pkt) {
if (verbose_) {
printf("PACKET [%6lu, %6lu] flags 0x%02x : ts %ld\n", pkt.payload_offset,
pkt.payload_size, pkt.flags, pkt.pts);
}
// If operating in sync-mode, track how many submitted packets are pending.
if (audio_capturer_.events().OnPacketProduced == nullptr) {
--outstanding_capture_jobs_;
}
FXL_DCHECK((pkt.payload_offset + pkt.payload_size) <=
(payload_buf_frames_ * bytes_per_frame_));
if (pkt.payload_size) {
FXL_DCHECK(payload_buf_virt_);
auto tgt =
reinterpret_cast<uint8_t*>(payload_buf_virt_) + pkt.payload_offset;
uint32_t write_size = pkt.payload_size;
// If 24_in_32, write as packed-24, skipping the first, least-significant of
// each four bytes). Assuming Write does not buffer, compress locally and
// call Write just once, to avoid potential performance problems.
if (sample_format_ == fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32 &&
pack_24bit_samples_) {
uint32_t write_idx = 0;
uint32_t read_idx = 0;
while (read_idx < pkt.payload_size) {
++read_idx;
compress_32_24_buff_[write_idx++] = tgt[read_idx++];
compress_32_24_buff_[write_idx++] = tgt[read_idx++];
compress_32_24_buff_[write_idx++] = tgt[read_idx++];
}
write_size = write_idx;
tgt = compress_32_24_buff_.get();
}
if (!wav_writer_.Write(reinterpret_cast<void* const>(tgt), write_size)) {
printf("File write failed. Trying to save any already-written data.\n");
if (!wav_writer_.Close()) {
printf("File close failed as well.\n");
}
Shutdown();
}
}
// In sync-mode, we send/track packets as they are sent/returned.
if (audio_capturer_.events().OnPacketProduced == nullptr) {
// If not shutting down, then send another capture job to keep things going.
if (!clean_shutdown_) {
SendCaptureJob();
}
// ...else (if shutting down) wait for pending capture jobs, then Shutdown.
else if (outstanding_capture_jobs_ == 0) {
Shutdown();
}
}
}
// On receiving the key-press to quit, start the sequence of unwinding.
void WavRecorder::OnQuit() {
printf("Shutting down...\n");
clean_shutdown_ = true;
// If async-mode, we can shutdown now (need not wait for packets to return).
if (audio_capturer_.events().OnPacketProduced != nullptr) {
audio_capturer_->StopAsyncCaptureNoReply();
Shutdown();
}
// If operating in sync-mode, wait for all packets to return, then Shutdown.
else {
audio_capturer_->DiscardAllPacketsNoReply();
}
}
} // namespace media::tools