| // 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 <audio-utils/audio-input.h> |
| #include <fbl/algorithm.h> |
| #include <fbl/unique_ptr.h> |
| #include <fuchsia/media/cpp/fidl.h> |
| #include <inttypes.h> |
| #include <lib/async-loop/cpp/loop.h> |
| #include <lib/async/cpp/task.h> |
| #include <lib/async/default.h> |
| #include <lib/fit/defer.h> |
| #include <lib/fit/function.h> |
| #include <lib/fzl/vmo-mapper.h> |
| #include <lib/zx/time.h> |
| #include <lib/zx/vmar.h> |
| #include <lib/zx/vmo.h> |
| #include <stdio.h> |
| #include <zircon/compiler.h> |
| #include <zircon/errors.h> |
| #include <zircon/types.h> |
| #include <limits> |
| #include <utility> |
| |
| #include "lib/component/cpp/connect.h" |
| #include "lib/component/cpp/startup_context.h" |
| #include "lib/fsl/tasks/fd_waiter.h" |
| #include "lib/media/timeline/timeline_function.h" |
| |
| #include "garnet/lib/media/wav_writer/wav_writer.h" |
| |
| constexpr bool kWavWriterEnabled = false; |
| |
| using media::TimelineFunction; |
| |
| constexpr uint32_t NUM_CHANNELS = 2u; |
| constexpr uint32_t INPUT_FRAMES_PER_SEC = 48000u; |
| constexpr uint32_t INPUT_BUFFER_LENGTH_MSEC = 10u; |
| constexpr uint32_t INPUT_BUFFER_MIN_FRAMES = |
| (INPUT_FRAMES_PER_SEC * INPUT_BUFFER_LENGTH_MSEC) / 1000u; |
| constexpr zx_time_t PROCESS_CHUNK_TIME = ZX_MSEC(1); |
| constexpr uint32_t OUTPUT_BUF_MSEC = 1000; |
| constexpr zx_time_t OUTPUT_BUF_TIME = ZX_MSEC(OUTPUT_BUF_MSEC); |
| constexpr zx_time_t OUTPUT_SEND_PACKET_OVERHEAD_NSEC = ZX_MSEC(1); |
| |
| constexpr int32_t MIN_REVERB_DEPTH_MSEC = 1; |
| constexpr int32_t MAX_REVERB_DEPTH_MSEC = OUTPUT_BUF_MSEC - 10; |
| constexpr int32_t SMALL_REVERB_DEPTH_STEP = 1; |
| constexpr int32_t LARGE_REVERB_DEPTH_STEP = 10; |
| constexpr float MIN_REVERB_FEEDBACK_GAIN = -60.0f; |
| constexpr float MAX_REVERB_FEEDBACK_GAIN = -3.0f; |
| constexpr float SMALL_REVERB_GAIN_STEP = 0.5; |
| constexpr float LARGE_REVERB_GAIN_STEP = 2.5; |
| |
| constexpr float MIN_FUZZ_GAIN = 1.0; |
| constexpr float MAX_FUZZ_GAIN = 50.0; |
| constexpr float SMALL_FUZZ_GAIN_STEP = 0.1; |
| constexpr float LARGE_FUZZ_GAIN_STEP = 1.0; |
| constexpr float MIN_FUZZ_MIX = 0.0; |
| constexpr float MAX_FUZZ_MIX = 1.0; |
| constexpr float SMALL_FUZZ_MIX_STEP = 0.01; |
| constexpr float LARGE_FUZZ_MIX_STEP = 0.1; |
| |
| constexpr float MIN_PREAMP_GAIN = -30.0f; |
| constexpr float MAX_PREAMP_GAIN = 20.0f; |
| constexpr float SMALL_PREAMP_GAIN_STEP = 0.1f; |
| constexpr float LARGE_PREAMP_GAIN_STEP = 1.0f; |
| constexpr uint32_t PREAMP_GAIN_FRAC_BITS = 12; |
| |
| constexpr int32_t DEFAULT_REVERB_DEPTH_MSEC = 200; |
| constexpr float DEFAULT_REVERB_FEEDBACK_GAIN = -4.0f; |
| constexpr float DEFAULT_FUZZ_GAIN = 0.0; |
| constexpr float DEFAULT_FUZZ_MIX = 1.0; |
| constexpr float DEFAULT_PREAMP_GAIN = -5.0f; |
| |
| using audio::utils::AudioInput; |
| |
| class FxProcessor { |
| public: |
| FxProcessor(fbl::unique_ptr<AudioInput> input, fit::closure quit_callback) |
| : input_(std::move(input)), quit_callback_(std::move(quit_callback)) { |
| FXL_DCHECK(quit_callback_); |
| } |
| |
| void Startup(fuchsia::media::AudioPtr audio); |
| |
| private: |
| using EffectFn = void (FxProcessor::*)(int16_t* src, int16_t* dst, |
| uint32_t frames); |
| |
| static inline float Norm(int16_t value) { |
| return (value < 0) |
| ? static_cast<float>(value) / std::numeric_limits<int16_t>::min() |
| : static_cast<float>(value) / |
| std::numeric_limits<int16_t>::max(); |
| } |
| |
| static inline float FuzzNorm(float norm_value, float gain) { |
| return 1.0f - expf(-norm_value * gain); |
| } |
| |
| void OnMinLeadTimeChanged(int64_t new_min_lead_time_nsec); |
| void RequestKeystrokeMessage(); |
| void HandleKeystroke(zx_status_t status, uint32_t events); |
| void Shutdown(const char* reason = "unknown"); |
| void ProcessInput(); |
| void ProduceOutputPackets(fuchsia::media::StreamPacket* out_pkt1, |
| fuchsia::media::StreamPacket* out_pkt2); |
| void ApplyEffect(int16_t* src, uint32_t src_offset, uint32_t src_rb_size, |
| int16_t* dst, uint32_t dst_offset, uint32_t dst_rb_size, |
| uint32_t frames, EffectFn effect); |
| |
| void CopyInputEffect(int16_t* src, int16_t* dst, uint32_t frames); |
| void PreampInputEffect(int16_t* src, int16_t* dst, uint32_t frames); |
| void ReverbMixEffect(int16_t* src, int16_t* dst, uint32_t frames); |
| void FuzzEffect(int16_t* src, int16_t* dst, uint32_t frames); |
| void MixedFuzzEffect(int16_t* src, int16_t* dst, uint32_t frames); |
| |
| fsl::FDWaiter::Callback handle_keystroke_thunk_ = [this](zx_status_t status, |
| uint32_t event) { |
| HandleKeystroke(status, event); |
| }; |
| |
| void UpdateReverb(bool enabled, int32_t depth_delta = 0, |
| float gain_delta = 0.0f); |
| void UpdateFuzz(bool enabled, float gain_delta = 0.0f, |
| float mix_delta = 0.0f); |
| void UpdatePreampGain(float delta); |
| |
| fzl::VmoMapper output_buf_; |
| size_t output_buf_sz_ = 0; |
| uint32_t output_buf_frames_ = 0; |
| uint64_t output_buf_wp_ = 0; |
| int64_t input_rp_ = 0; |
| bool shutting_down_ = false; |
| |
| bool reverb_enabled_ = false; |
| int32_t reverb_depth_msec_ = DEFAULT_REVERB_DEPTH_MSEC; |
| float reverb_feedback_gain_ = DEFAULT_REVERB_FEEDBACK_GAIN; |
| uint32_t reverb_depth_frames_; |
| uint16_t reverb_feedback_gain_fixed_; |
| |
| bool fuzz_enabled_ = false; |
| float fuzz_gain_ = DEFAULT_FUZZ_GAIN; |
| float fuzz_mix_ = DEFAULT_FUZZ_MIX; |
| float fuzz_mix_inv_; |
| |
| float preamp_gain_ = DEFAULT_PREAMP_GAIN; |
| uint16_t preamp_gain_fixed_; |
| |
| fbl::unique_ptr<AudioInput> input_; |
| fit::closure quit_callback_; |
| uint32_t input_buffer_frames_ = 0; |
| fuchsia::media::AudioRendererPtr audio_renderer_; |
| media::TimelineFunction clock_mono_to_input_wr_ptr_; |
| fsl::FDWaiter keystroke_waiter_; |
| media::audio::WavWriter<kWavWriterEnabled> wav_writer_; |
| |
| int64_t lead_time_frames_ = 0; |
| bool lead_time_frames_known_ = false; |
| }; |
| |
| void FxProcessor::Startup(fuchsia::media::AudioPtr audio) { |
| auto cleanup = fit::defer([this] { Shutdown("Startup failure"); }); |
| |
| zx_thread_set_priority(24 /* HIGH_PRIORITY in LK */); |
| |
| if (input_->sample_size() != 2) { |
| printf("Invalid input sample size %u\n", input_->sample_size()); |
| return; |
| } |
| |
| FXL_DCHECK((input_->ring_buffer_bytes() % input_->frame_sz()) == 0); |
| input_buffer_frames_ = input_->ring_buffer_bytes() / input_->frame_sz(); |
| |
| if (!wav_writer_.Initialize( |
| "/tmp/fx.wav", fuchsia::media::AudioSampleFormat::SIGNED_16, |
| input_->channel_cnt(), input_->frame_rate(), 16)) { |
| printf("Unable to initialize WAV file for recording.\n"); |
| return; |
| } |
| |
| // Create an AudioRenderer. Setup connection error handlers. |
| audio->CreateAudioRenderer(audio_renderer_.NewRequest()); |
| |
| audio_renderer_.set_error_handler([this](zx_status_t status) { |
| Shutdown("fuchsia::media::AudioRenderer connection closed"); |
| }); |
| |
| // Set the stream_type. |
| fuchsia::media::AudioStreamType stream_type; |
| stream_type.sample_format = fuchsia::media::AudioSampleFormat::SIGNED_16; |
| stream_type.channels = input_->channel_cnt(); |
| stream_type.frames_per_second = input_->frame_rate(); |
| audio_renderer_->SetPcmStreamType(std::move(stream_type)); |
| |
| // Create and map a VMO, to mix and subsequently send data to the renderer. |
| // Fill it with silence, then send a (read-only) VMO handle to the renderer. |
| output_buf_frames_ = static_cast<uint32_t>( |
| (OUTPUT_BUF_TIME * input_->frame_rate()) / 1000000000u); |
| output_buf_sz_ = static_cast<size_t>(input_->frame_sz()) * output_buf_frames_; |
| |
| zx::vmo rend_vmo; |
| zx_status_t res = output_buf_.CreateAndMap( |
| output_buf_sz_, ZX_VM_PERM_READ | ZX_VM_PERM_WRITE, nullptr, &rend_vmo, |
| ZX_RIGHT_READ | ZX_RIGHT_MAP | ZX_RIGHT_TRANSFER); |
| |
| // We use only a single payload buffer, and hence when creating each packet we |
| // can allow its payload_buffer_id to remain the default value of 0. |
| audio_renderer_->AddPayloadBuffer(0, std::move(rend_vmo)); |
| |
| // We work in Audio Frames as our PTS units. Configure this now. |
| audio_renderer_->SetPtsUnits(input_->frame_rate(), 1); |
| |
| // Start the input ring buffer. |
| res = input_->StartRingBuffer(); |
| if (res != ZX_OK) { |
| printf("Failed to start input ring buffer (res %d)\n", res); |
| return; |
| } |
| |
| // Setup the function which will convert from system ticks to the ring |
| // buffer write pointer (in audio frames). Note, we offset by the fifo |
| // depth so that the write pointer we get back will be the safe write |
| // pointer position; IOW - not where the capture currently is, but where the |
| // most recent frame which is guaranteed to be written to system memory is. |
| int64_t fifo_frames = |
| ((input_->fifo_depth() + input_->frame_sz() - 1) / input_->frame_sz()); |
| |
| media::TimelineRate frames_per_nsec; |
| { |
| media::TimelineRate frames_per_sec(input_->frame_rate(), 1); |
| media::TimelineRate sec_per_nsec(1, ZX_SEC(1)); |
| frames_per_nsec = |
| media::TimelineRate::Product(frames_per_sec, sec_per_nsec); |
| } |
| |
| clock_mono_to_input_wr_ptr_ = media::TimelineFunction( |
| -fifo_frames, input_->start_time(), frames_per_nsec); |
| |
| // Request notifications about the minimum clock lead time requirements. We |
| // will be able to start to process the input stream once we know what this |
| // number is. |
| // TODO(johngro): Set the handler here! |
| audio_renderer_.events().OnMinLeadTimeChanged = [this](int64_t nsec) { |
| OnMinLeadTimeChanged(nsec); |
| }; |
| audio_renderer_->EnableMinLeadTimeEvents(true); |
| |
| // Success. Print out the usage message, and force an update of effect |
| // parameters (which will also print their status). |
| printf( |
| "Welcome to FX. Keybindings are as follows.\n" |
| "q : Quit the application.\n" |
| "\n== Pre-amp Gain\n" |
| "] : Increase the pre-amp gain\n" |
| "[ : Decrease the pre-amp gain\n" |
| "\n== Reverb/Echo Effect ==\n" |
| "r : Toggle Reverb\n" |
| "i : Increase reverb feedback gain\n" |
| "k : Decrease reverb feedback gain\n" |
| "l : Increase reverb delay\n" |
| "j : Decrease reverb delay\n" |
| "\n== Fuzz Effect ==\n" |
| "f : Toggle Fuzz\n" |
| "w : Increase the fuzz gain\n" |
| "s : Decrease the fuzz gain\n" |
| "d : Increase the fuzz mix percentage\n" |
| "a : Decrease the fuzz mix percentage\n" |
| "\nUse <shift> when adjusting parameters in order to use the large " |
| "step size for the parameter.\n" |
| "\nCurrent settings are...\n"); |
| UpdatePreampGain(0.0f); |
| UpdateFuzz(fuzz_enabled_); |
| UpdateReverb(reverb_enabled_); |
| |
| // Start to process keystrokes, then cancel the auto-cleanup and get out.. |
| RequestKeystrokeMessage(); |
| cleanup.cancel(); |
| } |
| |
| void FxProcessor::OnMinLeadTimeChanged(int64_t new_min_lead_time_nsec) { |
| const auto& cm_to_frames = clock_mono_to_input_wr_ptr_.rate(); |
| int64_t new_lead_time_frames = cm_to_frames.Scale(new_min_lead_time_nsec); |
| |
| if (new_lead_time_frames > lead_time_frames_) { |
| // Note: If the system is currently running, this discontinuity is going to |
| // put a pop into our presentation. If this is a huge issue, what we would |
| // really want to do is... |
| // |
| // 1) Take manual control of the routing policy. |
| // 2) When outputs get added, decide whether or not we want to make any |
| // routing changes ourselves. |
| // 3) If we do, and these changes would effect our lead time requirements, |
| // we should smoothly ramp down our current presentation, let that play |
| // out, then stop the output, make the routing changes, then start |
| // everything back up again. |
| // |
| // Right now, there are no policy APIs which would allow us to acomplish any |
| // of this, so this is the best we can do for the time being. |
| lead_time_frames_ = new_lead_time_frames; |
| } |
| |
| // If this is the first time we are learning about our lead time requirements, |
| // it is time to process some input data and start the clock. |
| if (!lead_time_frames_known_) { |
| lead_time_frames_known_ = true; |
| |
| // Offset our initial write pointer by a small number of frames (in addition |
| // to our lead time) to allow time for our packet messages to read the mixer |
| // and get noticed by the mixing output loops. |
| output_buf_wp_ = cm_to_frames.Scale(OUTPUT_SEND_PACKET_OVERHEAD_NSEC); |
| |
| // Set up our concept of the input read pointer so that it one |
| // PROCESS_CHUNK_TIME behind the current write pointer. |
| zx_time_t now = zx_clock_get(ZX_CLOCK_MONOTONIC); |
| input_rp_ = clock_mono_to_input_wr_ptr_.Apply(now - PROCESS_CHUNK_TIME); |
| |
| // Process the input to produce some output, then start the clock. Note: |
| // upon start of playback, we choose to explicitly map the reference time |
| // 'now' to the media time (PTS) '0' on our presentation timeline. We will |
| // control our clock lead time by writing explicit timestamps on packets |
| // using the sum of the current output_buf_wp_ and lead_time_frames_. |
| ProcessInput(); |
| audio_renderer_->PlayNoReply(now, 0); |
| } |
| } |
| |
| void FxProcessor::RequestKeystrokeMessage() { |
| keystroke_waiter_.Wait(std::move(handle_keystroke_thunk_), STDIN_FILENO, |
| POLLIN); |
| } |
| |
| void FxProcessor::HandleKeystroke(zx_status_t status, uint32_t events) { |
| if (shutting_down_) { |
| return; |
| } |
| |
| if (status != ZX_OK) { |
| printf("Bad status in HandleKeystroke (status %d)\n", status); |
| Shutdown("Keystroke read error"); |
| } |
| |
| char c; |
| ssize_t res = ::read(STDIN_FILENO, &c, sizeof(c)); |
| if (res != 1) { |
| printf("Error reading keystroke (res %zd, errno %d)\n", res, errno); |
| Shutdown("Keystroke read error"); |
| } |
| |
| switch (c) { |
| case 'q': |
| case 'Q': |
| Shutdown("User requested"); |
| break; |
| |
| case 'r': |
| case 'R': |
| UpdateReverb(!reverb_enabled_); |
| break; |
| case 'i': |
| UpdateReverb(true, 0, SMALL_REVERB_GAIN_STEP); |
| break; |
| case 'I': |
| UpdateReverb(true, 0, LARGE_REVERB_GAIN_STEP); |
| break; |
| case 'k': |
| UpdateReverb(true, 0, -SMALL_REVERB_GAIN_STEP); |
| break; |
| case 'K': |
| UpdateReverb(true, 0, -LARGE_REVERB_GAIN_STEP); |
| break; |
| case 'l': |
| UpdateReverb(true, SMALL_REVERB_DEPTH_STEP, 0.0f); |
| break; |
| case 'L': |
| UpdateReverb(true, LARGE_REVERB_DEPTH_STEP, 0.0f); |
| break; |
| case 'j': |
| UpdateReverb(true, -SMALL_REVERB_DEPTH_STEP, 0.0f); |
| break; |
| case 'J': |
| UpdateReverb(true, -LARGE_REVERB_DEPTH_STEP, 0.0f); |
| break; |
| |
| case '[': |
| UpdatePreampGain(-SMALL_PREAMP_GAIN_STEP); |
| break; |
| case '{': |
| UpdatePreampGain(-LARGE_PREAMP_GAIN_STEP); |
| break; |
| case ']': |
| UpdatePreampGain(SMALL_PREAMP_GAIN_STEP); |
| break; |
| case '}': |
| UpdatePreampGain(LARGE_PREAMP_GAIN_STEP); |
| break; |
| |
| case 'f': |
| case 'F': |
| UpdateFuzz(!fuzz_enabled_); |
| break; |
| case 'd': |
| UpdateFuzz(true, 0.0, SMALL_FUZZ_MIX_STEP); |
| break; |
| case 'D': |
| UpdateFuzz(true, 0.0, LARGE_FUZZ_MIX_STEP); |
| break; |
| case 'a': |
| UpdateFuzz(true, 0.0, -SMALL_FUZZ_MIX_STEP); |
| break; |
| case 'A': |
| UpdateFuzz(true, 0.0, -LARGE_FUZZ_MIX_STEP); |
| break; |
| case 'w': |
| UpdateFuzz(true, SMALL_FUZZ_GAIN_STEP); |
| break; |
| case 'W': |
| UpdateFuzz(true, LARGE_FUZZ_GAIN_STEP); |
| break; |
| case 's': |
| UpdateFuzz(true, -SMALL_FUZZ_GAIN_STEP); |
| break; |
| case 'S': |
| UpdateFuzz(true, -LARGE_FUZZ_GAIN_STEP); |
| break; |
| |
| default: |
| break; |
| } |
| |
| RequestKeystrokeMessage(); |
| } |
| |
| void FxProcessor::Shutdown(const char* reason) { |
| // We're done (for good or bad): flush (save) the headers; close the WAV file. |
| wav_writer_.Close(); |
| |
| printf("Shutting down, reason = \"%s\"\n", reason); |
| shutting_down_ = true; |
| audio_renderer_.Unbind(); |
| input_.reset(); |
| quit_callback_(); |
| } |
| |
| void FxProcessor::ProcessInput() { |
| fuchsia::media::StreamPacket pkt1, pkt2; |
| |
| pkt1.payload_size = 0; |
| pkt2.payload_size = 0; |
| |
| // Produce output packet(s) If we do not produce any packets, something is |
| // very wrong and we are in the process of shutting down, so just get out now. |
| ProduceOutputPackets(&pkt1, &pkt2); |
| if (!pkt1.payload_size) { |
| return; |
| } |
| |
| // Send the packet(s) |
| audio_renderer_->SendPacketNoReply(std::move(pkt1)); |
| if (pkt2.payload_size) { |
| audio_renderer_->SendPacketNoReply(std::move(pkt2)); |
| } |
| |
| // If the input has been closed by the driver, shutdown. |
| if (input_->IsRingBufChannelConnected()) { |
| Shutdown("Input unplugged"); |
| return; |
| } |
| |
| // Save output audio to WAV file (if configured to do so). |
| auto output_base = reinterpret_cast<uint8_t*>(output_buf_.start()); |
| if (pkt1.payload_size) { |
| wav_writer_.Write(output_base + pkt1.payload_offset, pkt1.payload_size); |
| } |
| |
| if (pkt2.payload_size) { |
| wav_writer_.Write(output_base + pkt2.payload_offset, pkt2.payload_size); |
| } |
| |
| // Schedule our next processing callback. |
| async::PostDelayedTask( |
| async_get_default_dispatcher(), [this]() { ProcessInput(); }, |
| zx::nsec(PROCESS_CHUNK_TIME)); |
| } |
| |
| void FxProcessor::ProduceOutputPackets(fuchsia::media::StreamPacket* out_pkt1, |
| fuchsia::media::StreamPacket* out_pkt2) { |
| // Figure out how much input data we have to process. |
| zx_time_t now = zx_clock_get(ZX_CLOCK_MONOTONIC); |
| int64_t input_wp = clock_mono_to_input_wr_ptr_.Apply(now); |
| if (input_wp <= input_rp_) { |
| printf("input wp <= rp (wp %" PRId64 " rp %" PRId64 " now %" PRIu64 ")\n", |
| input_wp, input_rp_, now); |
| Shutdown("Failed to produce output packet"); |
| return; |
| } |
| |
| int64_t todo64 = input_wp - input_rp_; |
| if (todo64 > input_buffer_frames_) { |
| printf( |
| "Fell behind by more than the input buffer size " |
| "(todo %" PRId64 " buflen %u\n", |
| todo64, input_buffer_frames_); |
| Shutdown("Failed to produce output packet"); |
| return; |
| } |
| |
| uint32_t todo = static_cast<uint32_t>(todo64); |
| uint32_t input_start = |
| static_cast<uint32_t>(input_rp_) % input_buffer_frames_; |
| uint32_t output_start = output_buf_wp_ % output_buf_frames_; |
| uint32_t output_space = output_buf_frames_ - output_start; |
| |
| // Create the actual output packet(s) based on the amt of data we need to |
| // send and the current position of the write pointer in the output ring |
| // buffer. |
| uint32_t pkt1_frames = fbl::min<uint32_t>(output_space, todo); |
| out_pkt1->pts = output_buf_wp_ + lead_time_frames_; |
| out_pkt1->payload_offset = output_start * input_->frame_sz(); |
| out_pkt1->payload_size = pkt1_frames * input_->frame_sz(); |
| |
| // Does this job wrap the ring? If so, we need to create 2 packets instead |
| // of 1. |
| if (pkt1_frames < todo) { |
| out_pkt2->pts = out_pkt1->pts + pkt1_frames; |
| out_pkt2->payload_offset = 0; |
| out_pkt2->payload_size = (todo - pkt1_frames) * input_->frame_sz(); |
| } else { |
| out_pkt2->pts = fuchsia::media::NO_TIMESTAMP; |
| out_pkt2->payload_offset = 0; |
| out_pkt2->payload_size = 0; |
| } |
| |
| // Now actually apply effects. Start by just copying the input to the output. |
| auto input_base = reinterpret_cast<int16_t*>(input_->ring_buffer()); |
| auto output_base = reinterpret_cast<int16_t*>(output_buf_.start()); |
| ApplyEffect(input_base, input_start, input_buffer_frames_, output_base, |
| output_start, output_buf_frames_, todo, |
| (preamp_gain_ == 0.0) ? &FxProcessor::CopyInputEffect |
| : &FxProcessor::PreampInputEffect); |
| |
| // If enabled, add some fuzz |
| if (fuzz_enabled_ && (fuzz_mix_ >= 0.01f)) { |
| ApplyEffect(output_base, output_start, output_buf_frames_, output_base, |
| output_start, output_buf_frames_, todo, |
| (fuzz_mix_ <= 0.99f) ? &FxProcessor::MixedFuzzEffect |
| : &FxProcessor::FuzzEffect); |
| } |
| |
| // If enabled, add some reverb. |
| if (reverb_enabled_ && (reverb_feedback_gain_fixed_ > 0)) { |
| uint32_t reverb_start = |
| output_start + (output_buf_frames_ - reverb_depth_frames_); |
| if (reverb_start >= output_buf_frames_) |
| reverb_start -= output_buf_frames_; |
| |
| ApplyEffect(output_base, reverb_start, output_buf_frames_, output_base, |
| output_start, output_buf_frames_, todo, |
| &FxProcessor::ReverbMixEffect); |
| } |
| |
| // Finally, update our input read pointer and our output write pointer. |
| input_rp_ += todo; |
| output_buf_wp_ += todo; |
| } |
| |
| void FxProcessor::ApplyEffect(int16_t* src, uint32_t src_offset, |
| uint32_t src_rb_size, int16_t* dst, |
| uint32_t dst_offset, uint32_t dst_rb_size, |
| uint32_t frames, EffectFn effect) { |
| while (frames) { |
| ZX_DEBUG_ASSERT(src_offset < src_rb_size); |
| ZX_DEBUG_ASSERT(dst_offset < dst_rb_size); |
| |
| uint32_t src_space = src_rb_size - src_offset; |
| uint32_t dst_space = dst_rb_size - dst_offset; |
| uint32_t todo = fbl::min(fbl::min(frames, src_space), dst_space); |
| |
| // TODO(johngro): Either add fbl::invoke to fbl, or use std::invoke |
| // here when we switch to C++17. The syntax for invoking a pointer to |
| // non-static method on an object is ugly and hard to understand, and |
| // people should not be forced to look at it. |
| ((*this).*(effect))(src + (src_offset * NUM_CHANNELS), |
| dst + (dst_offset * NUM_CHANNELS), todo); |
| |
| src_offset = (src_space > todo) ? (src_offset + todo) : 0; |
| dst_offset = (dst_space > todo) ? (dst_offset + todo) : 0; |
| frames -= todo; |
| } |
| } |
| |
| void FxProcessor::CopyInputEffect(int16_t* src, int16_t* dst, uint32_t frames) { |
| ::memcpy(dst, src, frames * sizeof(*dst) * NUM_CHANNELS); |
| } |
| |
| void FxProcessor::PreampInputEffect(int16_t* src, int16_t* dst, |
| uint32_t frames) { |
| for (uint32_t i = 0; i < frames * NUM_CHANNELS; ++i) { |
| int32_t tmp = src[i]; |
| tmp *= preamp_gain_fixed_; |
| tmp >>= PREAMP_GAIN_FRAC_BITS; |
| tmp = fbl::clamp<int32_t>(tmp, std::numeric_limits<int16_t>::min(), |
| std::numeric_limits<int16_t>::max()); |
| dst[i] = static_cast<int16_t>(tmp); |
| } |
| } |
| |
| void FxProcessor::ReverbMixEffect(int16_t* src, int16_t* dst, uint32_t frames) { |
| // TODO(johngro): We should probably process everything into an intermediate |
| // 32 bit (or even 64 bit or float) buffer, and clamp after the fact. |
| for (uint32_t i = frames * NUM_CHANNELS; i > 0;) { |
| --i; |
| |
| int32_t tmp = src[i]; |
| tmp *= reverb_feedback_gain_fixed_; |
| tmp >>= 16; |
| tmp += dst[i]; |
| tmp = fbl::clamp<int32_t>(tmp, std::numeric_limits<int16_t>::min(), |
| std::numeric_limits<int16_t>::max()); |
| dst[i] = static_cast<int16_t>(tmp); |
| } |
| } |
| |
| void FxProcessor::FuzzEffect(int16_t* src, int16_t* dst, uint32_t frames) { |
| for (uint32_t i = 0; i < frames * NUM_CHANNELS; ++i) { |
| float norm = FuzzNorm(Norm(src[i]), fuzz_gain_); |
| dst[i] = |
| (src[i] < 0) |
| ? static_cast<int16_t>(std::numeric_limits<int16_t>::min() * norm) |
| : static_cast<int16_t>(std::numeric_limits<int16_t>::max() * norm); |
| } |
| } |
| |
| void FxProcessor::MixedFuzzEffect(int16_t* src, int16_t* dst, uint32_t frames) { |
| for (uint32_t i = 0; i < frames * NUM_CHANNELS; ++i) { |
| float norm = Norm(src[i]); |
| float fnorm = FuzzNorm(norm, fuzz_gain_); |
| float mixed = ((fnorm * fuzz_mix_) + (norm * fuzz_mix_inv_)); |
| dst[i] = |
| (src[i] < 0) |
| ? static_cast<int16_t>(std::numeric_limits<int16_t>::min() * mixed) |
| : static_cast<int16_t>(std::numeric_limits<int16_t>::max() * mixed); |
| } |
| } |
| |
| void FxProcessor::UpdateReverb(bool enabled, int32_t depth_delta, |
| float gain_delta) { |
| reverb_enabled_ = enabled; |
| |
| reverb_depth_msec_ = |
| fbl::clamp<uint32_t>(reverb_depth_msec_ + depth_delta, |
| MIN_REVERB_DEPTH_MSEC, MAX_REVERB_DEPTH_MSEC); |
| |
| reverb_feedback_gain_ = |
| fbl::clamp(reverb_feedback_gain_ + gain_delta, MIN_REVERB_FEEDBACK_GAIN, |
| MAX_REVERB_FEEDBACK_GAIN); |
| |
| if (enabled) { |
| reverb_depth_frames_ = (input_->frame_rate() * reverb_depth_msec_) / 1000u; |
| |
| double gain_scale = pow(10.0, reverb_feedback_gain_ / 20.0); |
| reverb_feedback_gain_fixed_ = static_cast<uint16_t>(gain_scale * 0x10000); |
| |
| printf("%7s: %u mSec %.1f dB\n", "Reverb", reverb_depth_msec_, |
| reverb_feedback_gain_); |
| } else { |
| printf("%7s: Disabled\n", "Reverb"); |
| } |
| } |
| |
| void FxProcessor::UpdateFuzz(bool enabled, float gain_delta, float mix_delta) { |
| fuzz_enabled_ = enabled; |
| fuzz_gain_ = |
| fbl::clamp(fuzz_gain_ + gain_delta, MIN_FUZZ_GAIN, MAX_FUZZ_GAIN); |
| fuzz_mix_ = fbl::clamp(fuzz_mix_ + mix_delta, MIN_FUZZ_MIX, MAX_FUZZ_MIX); |
| fuzz_mix_inv_ = 1.0f - fuzz_mix_; |
| |
| if (enabled) { |
| printf("%7s: Gain %.1f Mix %.1f%%\n", "Fuzz", fuzz_gain_, |
| fuzz_mix_ * 100.0f); |
| } else { |
| printf("%7s: Disabled\n", "Fuzz"); |
| } |
| } |
| |
| void FxProcessor::UpdatePreampGain(float delta) { |
| preamp_gain_ = |
| fbl::clamp(preamp_gain_ + delta, MIN_PREAMP_GAIN, MAX_PREAMP_GAIN); |
| |
| double gain_scale = pow(10.0, preamp_gain_ / 20.0); |
| preamp_gain_fixed_ = |
| static_cast<uint16_t>(gain_scale * (0x1 << PREAMP_GAIN_FRAC_BITS)); |
| |
| printf("%7s: %.1f dB\n", "PreGain", preamp_gain_); |
| } |
| |
| void usage(const char* prog_name) { |
| printf("usage: %s [input_dev_num]\n", prog_name); |
| } |
| |
| int main(int argc, char** argv) { |
| uint32_t input_num = 0; |
| |
| if (argc >= 2) { |
| if (1 != sscanf(argv[1], "%u", &input_num)) { |
| usage(argv[0]); |
| return -1; |
| } |
| } |
| |
| zx_status_t res; |
| auto input = AudioInput::Create(input_num); |
| |
| res = input->Open(); |
| if (res != ZX_OK) { |
| return res; |
| } |
| |
| // TODO(johngro) : Fetch the supported stream_types from the audio |
| // input itself and select from them, do not hardcode this. |
| res = input->SetFormat(48000u, NUM_CHANNELS, AUDIO_SAMPLE_FORMAT_16BIT); |
| if (res != ZX_OK) { |
| return res; |
| } |
| |
| res = input->GetBuffer(INPUT_BUFFER_MIN_FRAMES, 0u); |
| if (res != ZX_OK) { |
| return res; |
| } |
| |
| async::Loop loop(&kAsyncLoopConfigAttachToThread); |
| |
| std::unique_ptr<component::StartupContext> startup_context = |
| component::StartupContext::CreateFromStartupInfo(); |
| |
| fuchsia::media::AudioPtr audio = |
| startup_context->ConnectToEnvironmentService<fuchsia::media::Audio>(); |
| |
| FxProcessor fx(std::move(input), [&loop]() { |
| async::PostTask(loop.dispatcher(), [&loop]() { loop.Quit(); }); |
| }); |
| fx.Startup(std::move(audio)); |
| |
| loop.Run(); |
| return 0; |
| } |