blob: 045686065b205fcbd1463482a2de5e6e9a015e02 [file] [log] [blame]
// Copyright 2018 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/tools/signal_generator/signal_generator.h"
#include <fuchsia/ultrasound/cpp/fidl.h>
#include <zircon/syscalls.h>
#include <zircon/syscalls/clock.h>
#include <algorithm>
#include <cmath>
#include <iostream>
#include <fbl/algorithm.h>
#include <fbl/string_printf.h>
#include "src/media/audio/lib/clock/clone_mono.h"
#include "src/media/audio/lib/clock/utils.h"
#include "src/media/audio/lib/logging/cli.h"
#include "src/media/audio/lib/timeline/timeline_function.h"
namespace media::tools {
// The signal's duration, in nanoseconds, must fit into an int64_t. (292 yrs: not a problem)
constexpr double kMaxDurationSecs = std::numeric_limits<int64_t>::max() / ZX_SEC(1);
constexpr auto kPlayStartupDelay = zx::msec(0);
const char* SampleFormatToString(const fuchsia::media::AudioSampleFormat& format) {
switch (format) {
case fuchsia::media::AudioSampleFormat::FLOAT:
return "float";
case fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32:
return "int24";
case fuchsia::media::AudioSampleFormat::SIGNED_16:
return "int16";
default:
return "(unknown)";
}
}
fbl::String RefTimeStrFromZxTime(zx::time zx_time) {
auto time = zx_time.get();
if (time == fuchsia::media::NO_TIMESTAMP) {
return fbl::String(" [NO_TIMESTAMP] ");
}
return fbl::StringPrintf("%07lu'%03lu'%03lu'%03lu", time / ZX_SEC(1),
(time % ZX_SEC(1)) / ZX_MSEC(1), (time % ZX_MSEC(1)) / ZX_USEC(1),
time % ZX_USEC(1));
}
fbl::String RefTimeMsStrFromZxTime(zx::time zx_time) {
auto time = zx_time.get();
if (time == fuchsia::media::NO_TIMESTAMP) {
return fbl::String("[NO_TIMESTAMP] ");
}
return fbl::StringPrintf("%07lu'%03lu.%02lu ms", time / ZX_SEC(1),
(time % ZX_SEC(1)) / ZX_MSEC(1), (time % ZX_MSEC(1)) / ZX_USEC(10));
}
MediaApp::MediaApp(fit::closure quit_callback) : quit_callback_(std::move(quit_callback)) {
CLI_CHECK(quit_callback_, "quit_callback must not be null");
}
// Prepare for playback, submit initial data, start the presentation timeline.
void MediaApp::Run(sys::ComponentContext* app_context) {
AcquireRenderer(app_context);
// Calculate the frame size, number of packets, and shared-buffer size.
SetupPayloadCoefficients();
// Check the cmdline flags; exit if any are invalid or out-of-range.
ParameterRangeChecks();
ConfigureRenderer();
// Set loudness levels. We include app_context so we can interact with AudioCore interface if
// needed (SetRenderUsageGain, BindVolumeControl)
SetLoudnessLevels(app_context);
// Show a summary of all our settings: exactly what we are about to do.
DisplayConfigurationSettings();
// If requested, configure a WavWriter that will concurrently write this signal to a WAV file.
InitializeWavWriter();
// Create VmoMapper(s) that Create+Map a VMO. Send these down via AudioRenderer::AddPayloadBuffer.
CreateMemoryMapping();
// Retrieve the default reference clock for this renderer; once a device is ready, start playback.
GetClockAndStart();
}
// Use ComponentContext to acquire AudioPtr; use that to acquire AudioRendererPtr in turn. Set
// AudioRenderer error handler, in case of channel closure.
void MediaApp::AcquireRenderer(sys::ComponentContext* app_context) {
if (ultrasound_) {
fuchsia::ultrasound::FactorySyncPtr ultrasound_factory;
app_context->svc()->Connect(ultrasound_factory.NewRequest());
zx::clock reference_clock;
fuchsia::media::AudioStreamType stream_type;
ultrasound_factory->CreateRenderer(audio_renderer_.NewRequest(), &reference_clock,
&stream_type);
frame_rate_ = stream_type.frames_per_second;
num_channels_ = stream_type.channels;
sample_format_ = stream_type.sample_format;
} else {
// Audio interface is needed to create AudioRenderer and set routing policy.
fuchsia::media::AudioCorePtr audio_core;
app_context->svc()->Connect(audio_core.NewRequest());
audio_core->CreateAudioRenderer(audio_renderer_.NewRequest());
}
audio_renderer_.set_error_handler([this](zx_status_t status) {
CLI_CHECK(Shutdown(), "Client connection to fuchsia.media.AudioRenderer failed: " << status);
});
}
// Based on the user-specified values for signal frequency and milliseconds per payload, calculate
// the other related coefficients needed for our mapped memory section, and for our series of
// payloads that reference that section.
void MediaApp::SetupPayloadCoefficients() {
// Max duration_secs_(2^33.1) and frame_rate_(192k: 2^17.6) ==> 2^50.7 frames: no overflow risk
total_frames_to_send_ = static_cast<uint64_t>(duration_secs_ * frame_rate_);
num_packets_to_send_ = total_frames_to_send_ / frames_per_packet_;
if (num_packets_to_send_ * frames_per_packet_ < total_frames_to_send_) {
++num_packets_to_send_;
}
// Number of frames in each period of the recurring signal.
frames_per_period_ = frame_rate_ / frequency_;
amplitude_scalar_ = amplitude_;
switch (sample_format_) {
case fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32:
amplitude_scalar_ *= (std::numeric_limits<int32_t>::max() & 0xFFFFFF00);
sample_size_ = sizeof(int32_t);
break;
case fuchsia::media::AudioSampleFormat::SIGNED_16:
amplitude_scalar_ *= std::numeric_limits<int16_t>::max();
sample_size_ = sizeof(int16_t);
break;
case fuchsia::media::AudioSampleFormat::FLOAT:
sample_size_ = sizeof(float);
break;
default:
printf("Unknown AudioSampleFormat: %u\n", static_cast<unsigned int>(sample_format_));
Shutdown();
return;
}
// As mentioned above, for 24-bit audio we use 32-bit samples (low byte 0).
frame_size_ = num_channels_ * sample_size_;
bytes_per_packet_ = frames_per_packet_ * frame_size_;
// From the specified size|number of payload buffers, determine how many packets fit, then trim
// the mapping to what will be used. This size will be split across |num_payload_buffers_|
// buffers, e.g. 2 buffers of 48000 frames each will be large enough hold 200 480-frame packets.
auto total_payload_buffer_space = num_payload_buffers_ * frames_per_payload_buffer_ * frame_size_;
total_mappable_packets_ = total_payload_buffer_space / bytes_per_packet_;
// Shard out the payloads across multiple buffers, ensuring we can hold at least 1 buffer.
packets_per_payload_buffer_ = std::max(1u, total_mappable_packets_ / num_payload_buffers_);
}
void MediaApp::ParameterRangeChecks() {
bool success = true;
if (num_channels_ < fuchsia::media::MIN_PCM_CHANNEL_COUNT) {
std::cerr << "Number of channels must be at least " << fuchsia::media::MIN_PCM_CHANNEL_COUNT
<< std::endl;
success = false;
}
if (num_channels_ > fuchsia::media::MAX_PCM_CHANNEL_COUNT) {
std::cerr << "Number of channels must be no greater than "
<< fuchsia::media::MAX_PCM_CHANNEL_COUNT << std::endl;
success = false;
}
if (frame_rate_ < fuchsia::media::MIN_PCM_FRAMES_PER_SECOND) {
std::cerr << "Frame rate must be at least " << fuchsia::media::MIN_PCM_FRAMES_PER_SECOND
<< std::endl;
success = false;
}
if (frame_rate_ > fuchsia::media::MAX_PCM_FRAMES_PER_SECOND) {
std::cerr << "Frame rate must be no greater than " << fuchsia::media::MAX_PCM_FRAMES_PER_SECOND
<< std::endl;
success = false;
}
if (frequency_ < 0.0) {
std::cerr << "Frequency cannot be negative" << std::endl;
success = false;
}
if (amplitude_ > 1.0) {
std::cerr << "Amplitude must be no greater than 1.0" << std::endl;
success = false;
}
if (amplitude_ < -1.0) {
std::cerr << "Amplitude must be no less than -1.0" << std::endl;
success = false;
}
if (duty_cycle_percent_ >= 100.0f) {
std::cerr << "Duty cycle must be smaller than 100.0%" << std::endl;
success = false;
}
if (duty_cycle_percent_ <= 0.0f) {
std::cerr << "Duty cycle must be greater than 0.0%" << std::endl;
success = false;
}
if (duration_secs_ < 0.0) {
std::cerr << "Duration cannot be negative" << std::endl;
success = false;
}
if (duration_secs_ > kMaxDurationSecs) {
std::cerr << "Duration must not exceed " << kMaxDurationSecs << " seconds ("
<< (kMaxDurationSecs / 86400.0 / 365.25) << " years)" << std::endl;
success = false;
}
if (frames_per_packet_ > (num_payload_buffers_ * frames_per_payload_buffer_ / 2) &&
frames_per_packet_ < num_payload_buffers_ * frames_per_payload_buffer_) {
std::cerr << "Packet size cannot be larger than half the total payload space" << std::endl;
success = false;
}
if (frames_per_packet_ < frame_rate_ / 1000) {
std::cerr << "Packet size must be 1 millisecond or more" << std::endl;
success = false;
}
if (static_cast<uint64_t>(frames_per_payload_buffer_) * frame_size_ >
std::numeric_limits<uint32_t>::max()) {
std::cerr << "Payload buffer cannot exceed " << std::numeric_limits<uint32_t>::max()
<< " bytes (" << (std::numeric_limits<uint32_t>::max() / frame_size_)
<< " frames, for this frame_size)" << std::endl;
success = false;
}
if (clock_rate_adjustment_) {
if (clock_type_ != ClockType::Monotonic) {
clock_type_ = ClockType::Custom;
}
if (clock_rate_adjustment_.value() > ZX_CLOCK_UPDATE_MAX_RATE_ADJUST) {
std::cerr << "Clock adjustment must be " << ZX_CLOCK_UPDATE_MAX_RATE_ADJUST
<< " parts-per-million or less" << std::endl;
success = false;
}
if (clock_rate_adjustment_.value() < ZX_CLOCK_UPDATE_MIN_RATE_ADJUST) {
std::cerr << "Clock rate adjustment must be " << ZX_CLOCK_UPDATE_MIN_RATE_ADJUST
<< " parts-per-million or more" << std::endl;
success = false;
}
}
if (stream_gain_db_.has_value()) {
stream_gain_db_ =
std::clamp<float>(stream_gain_db_.value(), fuchsia::media::audio::MUTED_GAIN_DB,
fuchsia::media::audio::MAX_GAIN_DB);
}
if (usage_gain_db_.has_value()) {
usage_gain_db_ = std::clamp<float>(usage_gain_db_.value(), fuchsia::media::audio::MUTED_GAIN_DB,
kUnityGainDb);
}
if (usage_volume_.has_value()) {
usage_volume_ = std::clamp<float>(usage_volume_.value(), fuchsia::media::audio::MIN_VOLUME,
fuchsia::media::audio::MAX_VOLUME);
}
if (initial_delay_.has_value()) {
if (initial_delay_.value() < zx::nsec(0)) {
std::cerr << "Initial delay cannot be negative" << std::endl;
success = false;
} else {
initial_delay_frames_ = frame_rate_ * initial_delay_->to_nsecs() / 1'000'000'000;
}
}
CLI_CHECK(success, "Exiting.");
}
// Configure the renderer as specified (ultrasound renderers must use the provided clock/format).
void MediaApp::ConfigureRenderer() {
if (!ultrasound_) {
// Set our render stream format, plus other settings as needed: gain, clock
InitializeAudibleRenderer();
// ... now just let the instance of audio go out of scope.
//
// Although we could technically call gain_control_'s SetMute|SetGain|SetGainWithRamp here,
// then disconnect it (like we do for audio_core and audio), we instead maintain our
// GainControl throughout playback, in case we someday want to change gain during playback.
}
if (online_) {
online_send_packet_ref_period_ = (zx::sec(1) * frames_per_packet_) / frame_rate_;
}
SetAudioRendererEvents();
// Set the PTS units and continuity threshold, if specified.
ConfigureAudioRendererPts();
}
// Set the AudioRenderer's audio format, plus other settings requested by command line
void MediaApp::InitializeAudibleRenderer() {
CLI_CHECK(audio_renderer_, "audio_renderer must not be null");
fuchsia::media::AudioStreamType format;
format.sample_format = sample_format_;
format.channels = num_channels_;
format.frames_per_second = frame_rate_;
// To indicate we want a reference clock OTHER than the default, we'll call SetReferenceClock().
if (clock_type_ != ClockType::Default) {
zx::clock reference_clock_to_set;
if (clock_type_ == ClockType::Flexible) {
// To select the Flexible clock maintained by audio_core, we effectively SetRefClock(NULL).
reference_clock_to_set = zx::clock(ZX_HANDLE_INVALID);
} else {
// For Monotonic and Custom, we create and rights-reduce a clock to send to SetRefClock().
zx_status_t status;
zx::clock::update_args args;
args.reset();
if (clock_rate_adjustment_) {
args.set_rate_adjust(clock_rate_adjustment_.value());
}
if (clock_type_ == ClockType::Monotonic) {
// This clock is already started, in lock-step with CLOCK_MONOTONIC.
reference_clock_to_set = audio::clock::AdjustableCloneOfMonotonic();
CLI_CHECK(reference_clock_to_set.is_valid(),
"Invalid clock; could not clone monotonic clock");
} else {
// In custom clock case, set it to start at value zero. Rate-adjust it if specified.
status = zx::clock::create(ZX_CLOCK_OPT_MONOTONIC | ZX_CLOCK_OPT_CONTINUOUS, nullptr,
&reference_clock_to_set);
CLI_CHECK_OK(status, "zx::clock::create failed");
args.set_value(zx::time(0));
}
if (clock_rate_adjustment_ || clock_type_ == ClockType::Custom) {
// update starts our clock
status = reference_clock_to_set.update(args);
CLI_CHECK_OK(status, "zx::clock::update failed");
}
// The clock we send to AudioRenderer cannot have ZX_RIGHT_WRITE. Most clients would
// retain their custom clocks for subsequent rate-adjustment, and thus would use
// 'duplicate' to create the rights-reduced clock. This app doesn't yet allow
// rate-adjustment during playback (we also don't need this clock to read the current ref
// time: we call GetReferenceClock later), so we use 'replace' (not 'duplicate').
auto rights = ZX_RIGHT_DUPLICATE | ZX_RIGHT_TRANSFER | ZX_RIGHT_READ;
status = reference_clock_to_set.replace(rights, &reference_clock_to_set);
CLI_CHECK_OK(status, "zx::clock::duplicate failed");
}
audio_renderer_->SetReferenceClock(std::move(reference_clock_to_set));
}
audio_renderer_->SetUsage(usage_);
audio_renderer_->SetPcmStreamType(format);
}
// Enable audio renderer callbacks
void MediaApp::SetAudioRendererEvents() {
audio_renderer_.events().OnMinLeadTimeChanged = [this](int64_t min_lead_time_nsec) {
min_lead_time_ = zx::duration(min_lead_time_nsec);
if (verbose_) {
printf("- OnMinLeadTimeChanged: %lu at %lu: %s to start playback (%s ref clock)\n",
min_lead_time_nsec, zx::clock::get_monotonic().get(),
(min_lead_time_ >= kRealDeviceMinLeadTime ? "sufficient" : "insufficient"),
(reference_clock_.is_valid() ? "Received" : "Awaiting"));
}
if (min_lead_time_ >= kRealDeviceMinLeadTime && reference_clock_.is_valid() && !playing()) {
Play();
}
};
audio_renderer_->EnableMinLeadTimeEvents(true);
}
void MediaApp::ConfigureAudioRendererPts() {
if (pts_units_numerator_.has_value()) {
audio_renderer_->SetPtsUnits(pts_units_numerator_.value(), pts_units_denominator_.value());
}
if (pts_continuity_threshold_secs_.has_value()) {
audio_renderer_->SetPtsContinuityThreshold(pts_continuity_threshold_secs_.value());
}
if (timestamp_packets_) {
packet_num_to_pts_ = std::make_unique<TimelineFunction>(
first_packet_pts_.value_or(media_start_pts_.value_or(0)), 0,
static_cast<uint64_t>(frames_per_packet_) * pts_units_numerator_.value_or(1'000'000'000),
static_cast<uint64_t>(frame_rate_) * pts_units_denominator_.value_or(1));
}
}
// AudioCore interface is used to change the gain/volume of usages.
void MediaApp::SetLoudnessLevels(sys::ComponentContext* app_context) {
if (usage_gain_db_.has_value() || usage_volume_.has_value()) {
fuchsia::media::AudioCorePtr audio_core;
app_context->svc()->Connect(audio_core.NewRequest());
if (usage_gain_db_.has_value()) {
audio_core->SetRenderUsageGain(usage_, usage_gain_db_.value());
}
if (usage_volume_.has_value()) {
audio_core->BindUsageVolumeControl(
fuchsia::media::Usage::WithRenderUsage(fidl::Clone(usage_)),
usage_volume_control_.NewRequest());
usage_volume_control_.set_error_handler([this](zx_status_t status) {
CLI_CHECK(Shutdown(),
"Client connection to fuchsia.media.audio.VolumeControl failed: " << status);
});
// Set usage volume, if specified.
usage_volume_control_->SetVolume(usage_volume_.value());
}
// ... now just let the instance of audio_core go out of scope.
}
if (stream_mute_.has_value() || stream_gain_db_.has_value() || ramp_target_gain_db_.has_value()) {
audio_renderer_->BindGainControl(gain_control_.NewRequest());
gain_control_.set_error_handler([this](zx_status_t status) {
CLI_CHECK(Shutdown(),
"Client connection to fuchsia.media.audio.GainControl failed: " << status);
});
// Set stream gain and mute, if specified.
if (stream_mute_.has_value()) {
gain_control_->SetMute(stream_mute_.value());
}
if (stream_gain_db_.has_value()) {
gain_control_->SetGain(stream_gain_db_.value());
}
if (ramp_target_gain_db_.has_value()) {
gain_control_->SetGainWithRamp(ramp_target_gain_db_.value(), ramp_duration_nsec_,
fuchsia::media::audio::RampType::SCALE_LINEAR);
}
}
}
void MediaApp::DisplayConfigurationSettings() {
auto it = std::find_if(kRenderUsageOptions.cbegin(), kRenderUsageOptions.cend(),
[usage = usage_](auto usage_string_and_usage) {
return usage == usage_string_and_usage.second;
});
CLI_CHECK(it != kRenderUsageOptions.cend(), "no RenderUsage found");
auto usage_str = ultrasound_ ? "ULTRASOUND" : it->first;
printf("\nAudioRenderer configured for %d-channel %s at %u Hz with the %s usage", num_channels_,
SampleFormatToString(sample_format_), frame_rate_, usage_str);
if (stream_gain_db_.has_value()) {
printf(",\nsetting stream gain to %.3f dB", stream_gain_db_.value());
}
if (ramp_target_gain_db_.has_value()) {
printf(",%s\nramping stream gain to %.3f dB over %.1lf seconds (%ld nanoseconds)",
(stream_gain_db_.has_value() ? " then" : ""), ramp_target_gain_db_.value(),
static_cast<double>(ramp_duration_nsec_) / 1'000'000'000.0, ramp_duration_nsec_);
}
if (stream_mute_.has_value()) {
printf(",\nafter explicitly %s this stream", stream_mute_.value() ? "muting" : "unmuting");
}
if (usage_gain_db_.has_value() || usage_volume_.has_value()) {
printf(",\nafter setting ");
if (usage_gain_db_.has_value()) {
printf("%s gain to %.3f dB%s", usage_str, usage_gain_db_.value(),
(usage_volume_.has_value() ? " and " : ""));
}
if (usage_volume_.has_value()) {
printf("%s volume to %.1f", usage_str, usage_volume_.value());
}
}
printf(".\nContent is ");
if (output_signal_type_ == kOutputTypeNoise) {
printf("white noise");
} else if (output_signal_type_ == kOutputTypePinkNoise) {
printf("pink noise");
} else if (output_signal_type_ == kOutputTypeImpulse) {
printf("a single-frame impulse");
} else {
printf("a %.3f Hz ", frequency_);
if (output_signal_type_ == kOutputTypePulse) {
printf("pulse wave with duty cycle %2.1f%%", duty_cycle_percent_);
} else if (output_signal_type_ == kOutputTypeSine) {
printf("sine wave");
} else if (output_signal_type_ == kOutputTypeSawtooth) {
printf("rising sawtooth wave");
} else if (output_signal_type_ == kOutputTypeTriangle) {
printf("isosceles triangle wave");
}
}
printf(" with amplitude %.4f", amplitude_);
if (initial_delay_.has_value()) {
printf(" after initial delay of %6.5f seconds",
static_cast<double>(initial_delay_.value().to_usecs()) / 1'000'000.0);
}
printf(".\nThe generated signal will play for %.3f seconds", duration_secs_);
if (file_name_) {
printf(" and will be saved to '%s'", file_name_.value().c_str());
}
printf(".\nThe stream's reference clock will be ");
switch (clock_type_) {
case ClockType::Default:
printf("the default clock");
break;
case ClockType::Flexible:
printf("the AudioCore-provided 'flexible' clock");
break;
case ClockType::Monotonic:
printf("a clone of the MONOTONIC clock");
if (clock_rate_adjustment_) {
printf(", rate-adjusted by %i ppm", clock_rate_adjustment_.value());
}
break;
case ClockType::Custom:
printf("a custom clock");
if (clock_rate_adjustment_) {
printf(", rate-adjusted by %i ppm", clock_rate_adjustment_.value());
}
break;
}
printf(
".\nThe renderer will transport data using %u %s buffer sections of %u frames, "
"across %u payload buffers",
total_mappable_packets_, (timestamp_packets_ ? "timestamped" : "non-timestamped"),
frames_per_packet_, num_payload_buffers_);
if (online_) {
printf(",\nusing strict timing for flow control (online mode)");
} else {
printf(",\nusing previous packet completions for flow control (contiguous mode)");
}
auto media_time_str =
RefTimeStrFromZxTime(zx::time(media_start_pts_.value_or(fuchsia::media::NO_TIMESTAMP)));
printf(
".\nSignal will play at %s ref_time, and media_time %s, for %.3f seconds",
specify_ref_start_time_ ? "a specific (to-be-determined)" : "an unspecified ('NO_TIMESTAMP')",
media_time_str.c_str(), duration_secs_);
if (timestamp_packets_ || media_start_pts_.has_value()) {
printf(",\nusing a timestamp unit of (%u / %u) per second",
pts_units_numerator_.value_or(1'000'000'000), pts_units_denominator_.value_or(1));
}
if (pts_continuity_threshold_secs_.has_value()) {
printf(",\nhaving set the PTS continuity threshold to %f seconds",
pts_continuity_threshold_secs_.value());
}
printf(".\n\n");
}
void MediaApp::InitializeWavWriter() {
// 24-bit buffers use 32-bit samples (lowest byte zero), and when this particular utility saves to
// .wav file, we save the entire 32 bits.
if (file_name_) {
wav_writer_initialized_ = wav_writer_.Initialize(
file_name_.value().c_str(), sample_format_, static_cast<uint16_t>(num_channels_),
frame_rate_, static_cast<uint16_t>(sample_size_ * 8));
CLI_CHECK(wav_writer_initialized_, "WavWriter::Initialize() failed");
}
}
// We share a memory section with our AudioRenderer, divided into equally-sized payloads (size
// specified by the user). For now, we trim the end of the memory section, rather than handle the
// occasional irregularly-sized packet.
// TODO(mpuryear): handle end-of-buffer wraparound; make it a true ring buffer.
void MediaApp::CreateMemoryMapping() {
for (uint32_t i = 0; i < num_payload_buffers_; ++i) {
auto& payload_buffer = payload_buffers_.emplace_back();
zx::vmo payload_vmo;
zx_status_t status = payload_buffer.CreateAndMap(
bytes_per_packet_ * packets_per_payload_buffer_, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE,
nullptr, &payload_vmo, ZX_RIGHT_READ | ZX_RIGHT_MAP | ZX_RIGHT_TRANSFER);
CLI_CHECK(status == ZX_OK || Shutdown(), "VmoMapper:::CreateAndMap failed: " << status);
audio_renderer_->AddPayloadBuffer(i, std::move(payload_vmo));
}
}
void MediaApp::GetClockAndStart() {
audio_renderer_->GetReferenceClock([this](zx::clock received_clock) {
reference_clock_ = std::move(received_clock);
if (verbose_) {
audio::clock::GetAndDisplayClockDetails(reference_clock_);
auto mono_now = zx::clock::get_monotonic();
printf("- Received ref clock at %lu. (%s sufficient min_lead_time)\n", mono_now.get(),
(min_lead_time_ >= kRealDeviceMinLeadTime ? "Received" : "Awaiting"));
}
if (min_lead_time_ >= kRealDeviceMinLeadTime && !playing()) {
Play();
}
});
}
// Prime (pre-submit) an initial set of packets, then start playback.
// Called from the GetReferenceClock callback
void MediaApp::Play() {
if (num_packets_to_send_ == 0) {
// No packets to send, so we're done! Shutdown will unwind everything and exit our loop.
Shutdown();
return;
}
zx::time ref_now;
auto status = reference_clock_.read(ref_now.get_address());
CLI_CHECK(status == ZX_OK || Shutdown(), "zx::clock::read failed during init: " << status);
// read current time and use it as our rand48 seed ...
srand48(ref_now.get());
// ... before generating random data to prime our pink noise generator
if (output_signal_type_ == kOutputTypePinkNoise) {
PrimePinkNoiseFilter();
}
target_num_packets_outstanding_ =
online_ ? (total_mappable_packets_ / 2) : total_mappable_packets_;
// std::min must be done at the higher width, but the result is guaranteed to fit into int32
target_num_packets_outstanding_ = static_cast<uint32_t>(
std::min<uint64_t>(target_num_packets_outstanding_, num_packets_to_send_));
auto target_duration_outstanding =
(zx::sec(1) * target_num_packets_outstanding_ * frames_per_packet_) / frame_rate_;
if (target_duration_outstanding < min_lead_time_ &&
target_duration_outstanding < zx::nsec(static_cast<int64_t>(duration_secs_ * ZX_SEC(1)))) {
printf("\nPayload buffer space is too small for the minimum lead time and signal duration.\n");
Shutdown();
return;
}
// We "prime" the audio renderer by submitting an initial set of packets before starting playback.
// We will subsequently send the rest one at a time -- either from a timer (if 'online'), or from
// the completion of a previous packet (if not 'online').
// When priming, we send down only as many packets as concurrently fit into our payload buffer.
// And if online, we send half that much, to provide leeway for the renderer to temporarily
// complete packets too fast OR too slow, because of slight differences in clock rate.
for (uint32_t packet_num = 0; packet_num < target_num_packets_outstanding_; ++packet_num) {
SendPacket();
}
status = reference_clock_.read(ref_now.get_address());
CLI_CHECK(status == ZX_OK || Shutdown(), "zx::clock::read failed during Play(): " << status);
// Extrapolating backwards (to make future calculations easier), this represents when we would
// have sent our first packet. This is our first approximation, we will update this when we
// receive the actual start time.
target_online_send_first_packet_ref_time_ = ref_now - target_duration_outstanding;
reference_start_time_ = ref_now + kPlayStartupDelay + min_lead_time_;
auto media_start_pts = media_start_pts_.value_or(fuchsia::media::NO_TIMESTAMP);
if (verbose_) {
auto mono_time_result = audio::clock::MonotonicTimeFromReferenceTime(reference_clock_, ref_now);
CLI_CHECK(mono_time_result.is_ok(), "Could not convert ref_time to mono_time");
auto mono_now = mono_time_result.take_value();
auto requested_ref_str = RefTimeStrFromZxTime(
specify_ref_start_time_ ? reference_start_time_ : zx::time(fuchsia::media::NO_TIMESTAMP));
auto requested_media_str = RefTimeStrFromZxTime(zx::time{media_start_pts});
auto ref_now_str = RefTimeMsStrFromZxTime(ref_now);
auto mono_now_str = RefTimeMsStrFromZxTime(mono_now);
printf("\nCalling Play (ref %s, media %s) at ref_now %s : mono_now %s\n",
requested_ref_str.c_str(), requested_media_str.c_str(), ref_now_str.c_str(),
mono_now_str.c_str());
}
auto play_completion_func = [this](int64_t actual_ref_start, int64_t actual_media_start) {
if (verbose_) {
zx::time ref_now;
auto status = reference_clock_.read(ref_now.get_address());
CLI_CHECK(status == ZX_OK || Shutdown(),
"zx::clock::read failed during Play callback: " << status);
auto mono_time_result =
audio::clock::MonotonicTimeFromReferenceTime(reference_clock_, ref_now);
CLI_CHECK(mono_time_result.is_ok(), "Could not convert ref_time to mono_time");
auto mono_now = mono_time_result.take_value();
auto actual_ref_str = RefTimeStrFromZxTime(zx::time{actual_ref_start});
auto actual_media_str = RefTimeStrFromZxTime(zx::time{actual_media_start});
auto ref_now_str = RefTimeMsStrFromZxTime(ref_now);
auto mono_now_str = RefTimeMsStrFromZxTime(mono_now);
printf("Play callback(ref %s, media %s) at ref_now %s : mono_now %s\n\n",
actual_ref_str.c_str(), actual_media_str.c_str(), ref_now_str.c_str(),
mono_now_str.c_str());
}
// Now that we have the real start time, update our online "start" value.
target_online_send_first_packet_ref_time_ =
target_online_send_first_packet_ref_time_ +
(zx::time(actual_ref_start) - reference_start_time_);
reference_start_time_ = zx::time(actual_ref_start);
};
audio_renderer_->Play(reference_start_time_.get(), media_start_pts, play_completion_func);
set_playing();
if (online_) {
ScheduleNextSendPacket();
}
}
// We have a set of buffers each backed by its own VMO, with each buffer sub-divided into
// uniformly-sized zones, called payloads.
//
// We round robin packets across each buffer, wrapping around to the start of each buffer once
// the end is encountered. For example, with 2 buffers that can each hold 2 payloads, we would
// send audio packets in the following order:
//
// ------------------------
// | buffer_id | payload_id |
// | (vmo) | (offset) |
// |-----------|------------|
// | buffer 0 | payload 0 |
// | buffer 1 | payload 0 |
// | buffer 0 | payload 1 |
// | buffer 1 | payload 1 |
// | buffer 0 | payload 0 |
// | ... etc ... |
// ------------------------
MediaApp::AudioPacket MediaApp::CreateAudioPacket(uint64_t packet_num) {
fuchsia::media::StreamPacket packet;
packet.payload_buffer_id = packet_num % num_payload_buffers_;
auto buffer_payload_index = packet_num / num_payload_buffers_;
packet.payload_offset = (buffer_payload_index % packets_per_payload_buffer_) * bytes_per_packet_;
// If last payload, send exactly what remains (otherwise send a full payload).
packet.payload_size =
(packet_num + 1 == num_packets_to_send_)
? (total_frames_to_send_ - (packet_num * frames_per_packet_)) * frame_size_
: bytes_per_packet_;
// By default, the packet.pts (media time) field is NO_TIMESTAMP if we do not override it.
if (timestamp_packets_) {
packet.pts = static_cast<uint64_t>(packet_num_to_pts_->Apply(packet_num));
}
return {
.stream_packet = std::move(packet),
.vmo = &payload_buffers_[packet.payload_buffer_id],
};
}
void MediaApp::GenerateAudioForPacket(const AudioPacket& audio_packet, uint64_t packet_num) {
const auto& packet = audio_packet.stream_packet;
auto audio_buff = reinterpret_cast<uint8_t*>(audio_packet.vmo->start()) + packet.payload_offset;
// Recompute payload_frames each time, since the final packet may be 'short'.
//
// TODO(mpuryear): don't recompute this every time; use payload_frames_ (and pre-compute this)
// except for last packet, which we either check for here or pass in as a boolean parameter.
uint32_t payload_frames = static_cast<uint32_t>(packet.payload_size) / frame_size_;
switch (sample_format_) {
case fuchsia::media::AudioSampleFormat::SIGNED_24_IN_32:
WriteAudioIntoBuffer<int32_t>(reinterpret_cast<int32_t*>(audio_buff), payload_frames,
frames_per_packet_ * packet_num);
break;
case fuchsia::media::AudioSampleFormat::SIGNED_16:
WriteAudioIntoBuffer<int16_t>(reinterpret_cast<int16_t*>(audio_buff), payload_frames,
frames_per_packet_ * packet_num);
break;
case fuchsia::media::AudioSampleFormat::FLOAT:
WriteAudioIntoBuffer<float>(reinterpret_cast<float*>(audio_buff), payload_frames,
frames_per_packet_ * packet_num);
break;
default:
CLI_CHECK(false, "Unknown AudioSampleFormat");
}
}
// Allocate memory for history values; advance the filter through its initial transient
void MediaApp::PrimePinkNoiseFilter() {
input_history_ = std::make_unique<HistoryBuffer[]>(num_channels_);
output_history_ = std::make_unique<HistoryBuffer[]>(num_channels_);
// Skip the filter's initial transient response by pre-generating 1430 frames, the filter's T60
// (-60 decay) interval, computed by "T60 = round(log(1000)/(1-max(abs(roots(kFeedBack)))))"
for (auto i = 0u; i < 1430u; ++i) {
AdvancePinkNoiseFrame();
}
}
// Generate a pink-noise frame, using a four-stage filter with kFeedFwd and kFeedBack coefficients.
void MediaApp::AdvancePinkNoiseFrame() {
// For each channel, calculate a new output based on cached vals plus a new random input value
for (uint32_t chan = 0; chan < num_channels_; ++chan) {
(void)NextPinkNoiseSample(chan);
}
}
// Calculate and retrieve the new pink-noise sample value for this channel.
double MediaApp::NextPinkNoiseSample(uint32_t chan) {
//
// First, shift our previous inputs and outputs into the past, by one frame
for (size_t i = 3; i > 0; --i) {
output_history_[chan][i] = output_history_[chan][i - 1];
input_history_[chan][i] = input_history_[chan][i - 1];
}
// (both [chan][0] values are now stale, but we overwrite them immediately)
//
// Second, generate the initial white-noise input, boosting to normalize the result.
input_history_[chan][0] = drand48() * 2.0 - 1.0;
input_history_[chan][0] *= kPinkNoiseSignalBoostFactor;
//
// Finally, apply the filter to {input + cached input/output values} to get the new output val.
output_history_[chan][0] =
(input_history_[chan][0] * kFeedFwd[0] + input_history_[chan][1] * kFeedFwd[1] +
input_history_[chan][2] * kFeedFwd[2] + input_history_[chan][3] * kFeedFwd[3]) -
(output_history_[chan][1] * kFeedBack[1] + output_history_[chan][2] * kFeedBack[2] +
output_history_[chan][3] * kFeedBack[3]);
return output_history_[chan][0];
}
// Write signal into the next section of our buffer. Track how many total frames since playback
// started, to handle arbitrary frequencies of type double. Converting frames_since_start to a
// double running_frame limits precision to 2^53 frames (1487 yrs @ 192kHz: more than adequate!).
template <typename SampleType>
void MediaApp::WriteAudioIntoBuffer(SampleType* audio_buffer, uint32_t num_frames,
int64_t frames_since_start) {
const double rads_per_frame = 2.0 * M_PI / frames_per_period_; // Radians/Frame.
for (uint32_t frame = 0; frame < num_frames; ++frame, ++frames_since_start) {
// Generated signal value, before applying amplitude scaling.
double raw_val;
for (auto chan_num = 0u; chan_num < num_channels_; ++chan_num) {
if (frames_since_start < initial_delay_frames_) {
raw_val = 0.0;
} else {
double running_frame = static_cast<double>(frames_since_start - initial_delay_frames_);
switch (output_signal_type_) {
case kOutputTypeSine:
raw_val = sin(rads_per_frame * running_frame);
break;
case kOutputTypePulse:
raw_val = (fmod(running_frame, frames_per_period_) >=
(static_cast<float>(frames_per_period_) * duty_cycle_percent_) / 100.0f)
? -1.0
: 1.0;
break;
case kOutputTypeSawtooth:
raw_val = (fmod(running_frame / frames_per_period_, 1.0) * 2.0) - 1.0;
break;
case kOutputTypeTriangle:
raw_val = (abs(fmod(running_frame / frames_per_period_, 1.0) - 0.5) * 4.0) - 1.0;
break;
case kOutputTypeNoise:
raw_val = drand48() * 2.0 - 1.0;
break;
case kOutputTypePinkNoise:
raw_val = NextPinkNoiseSample(chan_num);
break;
case kOutputTypeImpulse:
raw_val = (frames_since_start > initial_delay_frames_) ? 0.0 : 1.0;
break;
}
}
// raw_val cannot exceed 1.0; amplitude_scalar_ cannot exceed the SampleType's max.
// Thus, the below static_casts are safe.
SampleType val;
if constexpr (std::is_same_v<SampleType, float>) {
val = static_cast<float>(raw_val * amplitude_scalar_);
} else if constexpr (std::is_same_v<SampleType,
int32_t>) { // 24-bit in 32-bit container:
val = static_cast<int32_t>(
lround(raw_val * amplitude_scalar_ / 256.0)); // round at bit 8, and
val = val << 8; // leave bits 0-7 blank
} else {
val = static_cast<int16_t>(lround(raw_val * amplitude_scalar_));
}
audio_buffer[frame * num_channels_ + chan_num] = val;
}
}
}
constexpr zx::duration kPacketCompleteToleranceDuration = zx::msec(50);
constexpr uint64_t kPacketCompleteTolerance = 5;
bool MediaApp::CheckPayloadSpace() {
if (num_packets_completed_ > 0 && num_packets_sent_ <= num_packets_completed_) {
printf("! Sending: packet %4lu; packet %4lu has already completed - did we underrun?\n",
num_packets_sent_, num_packets_completed_);
return false;
}
if (num_packets_sent_ >= num_packets_completed_ + total_mappable_packets_) {
printf("! Sending: packet %4lu; only %4lu have completed - did we overrun?\n",
num_packets_sent_, num_packets_completed_);
return false;
}
target_num_packets_outstanding_ =
std::min(static_cast<uint32_t>(num_packets_to_send_ - num_packets_completed_),
target_num_packets_outstanding_);
auto actual_packets_outstanding = num_packets_sent_ - num_packets_completed_;
auto target_duration_outstanding =
(zx::sec(1) * target_num_packets_outstanding_ * frames_per_packet_) / frame_rate_;
auto actual_duration_outstanding =
(zx::sec(1) * actual_packets_outstanding * frames_per_packet_) / frame_rate_;
auto elapsed_time_sec = static_cast<double>(num_frames_completed_) / frame_rate_;
// Check whether payload buffer is staying at approx the same fullness.
if (num_packets_completed_ > 0 &&
actual_packets_outstanding + kPacketCompleteTolerance <= target_num_packets_outstanding_ &&
actual_duration_outstanding + kPacketCompleteToleranceDuration <=
target_duration_outstanding) {
printf(
"\n? %4lu packets outstanding (%ld msec); expected %4u (%ld msec); total elapsed %f sec: "
"are we completing faster than sending?\n\n",
actual_packets_outstanding, (actual_duration_outstanding / ZX_MSEC(1)).get(),
target_num_packets_outstanding_, (target_duration_outstanding / ZX_MSEC(1)).get(),
elapsed_time_sec);
return false;
}
if (num_packets_completed_ > 0 &&
target_num_packets_outstanding_ + kPacketCompleteTolerance <= actual_packets_outstanding &&
target_duration_outstanding + kPacketCompleteToleranceDuration <=
actual_duration_outstanding) {
printf(
"\n? %4lu packets outstanding (%ld msec); expected %4u (%ld msec); total elapsed %f sec: "
"are we sending faster than completing?\n\n",
actual_packets_outstanding, (actual_duration_outstanding / ZX_MSEC(1)).get(),
target_num_packets_outstanding_, (target_duration_outstanding / ZX_MSEC(1)).get(),
elapsed_time_sec);
return false;
}
return true;
}
// Calculate the next SendPacket ref_time and mono_time, and Post to our async::TaskClosureMethod
void MediaApp::ScheduleNextSendPacket() {
CLI_CHECK(online_, "Should only call NextSendPacket in online mode");
CLI_CHECK(online_send_packet_ref_period_ > zx::duration(0), "SendPacket period is not set");
if (num_packets_sent_ >= num_packets_to_send_) {
return;
}
target_online_send_packet_ref_time_ = target_online_send_first_packet_ref_time_ +
(online_send_packet_ref_period_ * num_packets_sent_);
auto mono_time_result = audio::clock::MonotonicTimeFromReferenceTime(
reference_clock_, target_online_send_packet_ref_time_);
CLI_CHECK(mono_time_result.is_ok(), "Could not convert ref_time to mono_time");
auto target_mono_time = mono_time_result.take_value();
if (verbose_) {
zx::time ref_now;
auto status = reference_clock_.read(ref_now.get_address());
if (status != ZX_OK) {
Shutdown();
CLI_CHECK_OK(status, "zx::clock::read failed during Play callback");
}
auto mono_time_result = audio::clock::MonotonicTimeFromReferenceTime(reference_clock_, ref_now);
CLI_CHECK(mono_time_result.is_ok(), "Could not convert ref_time to mono_time");
auto mono_now = mono_time_result.take_value();
auto target_ref_str = RefTimeStrFromZxTime(target_online_send_packet_ref_time_);
auto ref_now_str = RefTimeMsStrFromZxTime(ref_now);
auto mono_now_str = RefTimeMsStrFromZxTime(mono_now);
printf("Scheduling packet %4lu (reference %s) : ref_now %s : mono_now %s\n",
num_packets_sent_, target_ref_str.c_str(), ref_now_str.c_str(), mono_now_str.c_str());
}
zx_status_t status =
online_send_packet_timer_.PostForTime(audio_renderer_.dispatcher(), target_mono_time);
if (status != ZX_OK) {
Shutdown();
CLI_CHECK_OK(status, "Failed to schedule SendPacket");
}
}
void MediaApp::OnSendPacketTimer() {
SendPacket();
ScheduleNextSendPacket();
}
// Submit a packet, incrementing our count of packets sent. When it returns:
// a. if there are more packets to send, create and send the next packet;
// b. if all expected packets have completed, begin closing down the system.
void MediaApp::SendPacket() {
CLI_CHECK(CheckPayloadSpace(), "Insufficient payload buffer space -- synchronization issue?");
auto packet = CreateAudioPacket(num_packets_sent_);
GenerateAudioForPacket(packet, num_packets_sent_);
if (file_name_) {
CLI_CHECK(packet.stream_packet.payload_size <= std::numeric_limits<uint32_t>::max(),
"Packet payload too large");
CLI_CHECK(wav_writer_.Write(reinterpret_cast<char*>(packet.vmo->start()) +
packet.stream_packet.payload_offset,
static_cast<uint32_t>(packet.stream_packet.payload_size)) ||
Shutdown(),
"WavWriter::Write() failed");
}
if (verbose_) {
zx::time ref_now;
auto status = reference_clock_.read(ref_now.get_address());
CLI_CHECK((status == ZX_OK) || Shutdown(),
"zx::clock::read failed during SendPacket(): " << status);
auto mono_time_result = audio::clock::MonotonicTimeFromReferenceTime(reference_clock_, ref_now);
CLI_CHECK(mono_time_result.is_ok(), "Could not convert ref_time to mono_time");
auto mono_now = mono_time_result.take_value();
auto pts_str = RefTimeStrFromZxTime(zx::time{packet.stream_packet.pts});
auto ref_now_str = RefTimeMsStrFromZxTime(ref_now);
auto mono_now_str = RefTimeMsStrFromZxTime(mono_now);
printf(" Sending: packet %4lu (media pts %s) : ref_now %s : mono_now %s\n",
num_packets_sent_, pts_str.c_str(), ref_now_str.c_str(), mono_now_str.c_str());
}
++num_packets_sent_;
uint64_t frames_completed = packet.stream_packet.payload_size / frame_size_;
audio_renderer_->SendPacket(
packet.stream_packet, [this, frames_completed]() { OnSendPacketComplete(frames_completed); });
}
void MediaApp::OnSendPacketComplete(uint64_t frames_completed) {
num_frames_completed_ += frames_completed;
if (verbose_) {
zx::time ref_now;
auto status = reference_clock_.read(ref_now.get_address());
CLI_CHECK(status == ZX_OK || Shutdown(),
"zx::clock::read failed during OnSendPacketComplete(): " << status);
auto mono_time_result = audio::clock::MonotonicTimeFromReferenceTime(reference_clock_, ref_now);
CLI_CHECK(mono_time_result.is_ok(), "Could not convert ref_time to mono_time");
auto mono_now = mono_time_result.take_value();
auto ref_now_str = RefTimeMsStrFromZxTime(ref_now);
auto mono_now_str = RefTimeMsStrFromZxTime(mono_now);
printf("Completed: packet %4lu (%5lu frames, up to %8lu ) : ref_now %s : mono_now %s\n",
num_packets_completed_, frames_completed, num_frames_completed_, ref_now_str.c_str(),
mono_now_str.c_str());
}
++num_packets_completed_;
CLI_CHECK(num_packets_completed_ <= num_packets_to_send_,
"packets_completed cannot exceed packets_to_send");
if (num_packets_completed_ >= num_packets_to_send_) {
Shutdown();
} else if (num_packets_sent_ < num_packets_to_send_ && !online_) {
SendPacket();
}
}
// Unmap memory, quit message loop (FIDL interfaces auto-delete upon ~MediaApp).
bool MediaApp::Shutdown() {
online_send_packet_timer_.Cancel();
gain_control_.Unbind();
usage_volume_control_.Unbind();
audio_renderer_.Unbind();
if (wav_writer_initialized_) {
CLI_CHECK(wav_writer_.Close(), "WavWriter::Close() failed");
}
payload_buffers_.clear();
quit_callback_();
return false;
}
} // namespace media::tools