diff options
Diffstat (limited to 'src/tangara/audio/i2s_audio_output.cpp')
| -rw-r--r-- | src/tangara/audio/i2s_audio_output.cpp | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/src/tangara/audio/i2s_audio_output.cpp b/src/tangara/audio/i2s_audio_output.cpp new file mode 100644 index 00000000..bf1c3e5e --- /dev/null +++ b/src/tangara/audio/i2s_audio_output.cpp @@ -0,0 +1,232 @@ +/* + * Copyright 2023 jacqueline <me@jacqueline.id.au> + * + * SPDX-License-Identifier: GPL-3.0-only + */ + +#include "i2s_audio_output.hpp" +#include <stdint.h> + +#include <algorithm> +#include <cstddef> +#include <cstdint> +#include <memory> +#include <variant> + +#include "esp_err.h" +#include "esp_heap_caps.h" +#include "freertos/portmacro.h" +#include "freertos/projdefs.h" + +#include "audio_sink.hpp" +#include "gpios.hpp" +#include "i2c.hpp" +#include "i2s_dac.hpp" +#include "result.hpp" +#include "wm8523.hpp" + +[[maybe_unused]] static const char* kTag = "I2SOUT"; + +namespace audio { + +// Consumer line level = 0.316 VRMS = -10db = 61 +// Professional line level = 1.228 VRMS = +4dB = 111 +// Cliipping level = 2.44 VRMS = 133? +// all into 650 ohms + +static constexpr uint16_t kMaxVolume = 0x1ff; +static constexpr uint16_t kMinVolume = 0b0; +static constexpr uint16_t kMaxVolumeBeforeClipping = 0x185; +static constexpr uint16_t kLineLevelVolume = 0x13d; +static constexpr uint16_t kDefaultVolume = 0x100; + +static constexpr size_t kDrainBufferSize = 8 * 1024; + +I2SAudioOutput::I2SAudioOutput(StreamBufferHandle_t s, + drivers::IGpios& expander) + : IAudioOutput(s), + expander_(expander), + dac_(), + current_mode_(Modes::kOff), + current_config_(), + left_difference_(0), + current_volume_(kDefaultVolume), + max_volume_(0) {} + +I2SAudioOutput::~I2SAudioOutput() { + dac_->Stop(); + dac_->SetSource(nullptr); +} + +auto I2SAudioOutput::changeMode(Modes mode) -> void { + if (mode == current_mode_) { + return; + } + if (mode == Modes::kOff) { + if (dac_) { + dac_->Stop(); + dac_.reset(); + } + return; + } + if (current_mode_ == Modes::kOff) { + if (!dac_) { + auto instance = drivers::I2SDac::create(expander_); + if (!instance) { + return; + } + dac_.reset(*instance); + } + SetVolume(GetVolume()); + dac_->SetSource(stream()); + dac_->Start(); + } + current_mode_ = mode; + dac_->SetPaused(mode == Modes::kOnPaused); +} + +auto I2SAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void { + left_difference_ = balance; + SetVolume(GetVolume()); +} + +auto I2SAudioOutput::SetMaxVolume(uint16_t max) -> void { + max_volume_ = std::clamp(max, drivers::wm8523::kAbsoluteMinVolume, + drivers::wm8523::kAbsoluteMaxVolume); + SetVolume(GetVolume()); +} + +auto I2SAudioOutput::SetVolume(uint16_t vol) -> void { + current_volume_ = std::clamp(vol, kMinVolume, max_volume_); + + int32_t left_unclamped = current_volume_ + left_difference_; + uint16_t left = std::clamp<int32_t>(left_unclamped, kMinVolume, max_volume_); + + using drivers::wm8523::Register; + drivers::wm8523::WriteRegister(Register::kDacGainLeft, left); + drivers::wm8523::WriteRegister(Register::kDacGainRight, + current_volume_ | 0x200); +} + +auto I2SAudioOutput::GetVolume() -> uint16_t { + return current_volume_; +} + +auto I2SAudioOutput::GetVolumePct() -> uint_fast8_t { + return (current_volume_ - kMinVolume) * 100 / (max_volume_ - kMinVolume); +} + +auto I2SAudioOutput::SetVolumePct(uint_fast8_t val) -> bool { + if (val > 100) { + return false; + } + uint16_t vol = (val * (max_volume_ - kMinVolume))/100 + kMinVolume; + SetVolume(vol); + return true; +} + +auto I2SAudioOutput::GetVolumeDb() -> int_fast16_t { + // Add two before dividing in order to round correctly. + return (static_cast<int>(current_volume_) - + static_cast<int>(drivers::wm8523::kLineLevelReferenceVolume) + 2) / + 4; +} + +auto I2SAudioOutput::SetVolumeDb(int_fast16_t val) -> bool { + SetVolume(val * 4 + static_cast<int>(drivers::wm8523::kLineLevelReferenceVolume) - 2); + return true; +} + +auto I2SAudioOutput::AdjustVolumeUp() -> bool { + if (GetVolume() >= max_volume_) { + return false; + } + SetVolume(GetVolume() + 1); + return true; +} + +auto I2SAudioOutput::AdjustVolumeDown() -> bool { + if (GetVolume() == kMinVolume) { + return false; + } + if (GetVolume() <= kMinVolume + 1) { + SetVolume(0); + } else { + SetVolume(GetVolume() - 1); + } + return true; +} + +auto I2SAudioOutput::PrepareFormat(const Format& orig) -> Format { + return Format{ + .sample_rate = std::clamp<uint32_t>(orig.sample_rate, 8000, 96000), + .num_channels = std::min<uint8_t>(orig.num_channels, 2), + .bits_per_sample = std::clamp<uint8_t>(orig.bits_per_sample, 16, 32), + }; +} + +auto I2SAudioOutput::Configure(const Format& fmt) -> void { + if (current_config_ && fmt == *current_config_) { + ESP_LOGI(kTag, "ignoring unchanged format"); + return; + } + + drivers::I2SDac::Channels ch; + switch (fmt.num_channels) { + case 1: + ch = drivers::I2SDac::CHANNELS_MONO; + break; + case 2: + ch = drivers::I2SDac::CHANNELS_STEREO; + break; + default: + ESP_LOGE(kTag, "dropping stream with out of bounds channels"); + return; + } + + drivers::I2SDac::BitsPerSample bps; + switch (fmt.bits_per_sample) { + case 16: + bps = drivers::I2SDac::BPS_16; + break; + case 24: + bps = drivers::I2SDac::BPS_24; + break; + case 32: + bps = drivers::I2SDac::BPS_32; + break; + default: + ESP_LOGE(kTag, "dropping stream with unknown bps"); + return; + } + + drivers::I2SDac::SampleRate sample_rate; + switch (fmt.sample_rate) { + case 8000: + sample_rate = drivers::I2SDac::SAMPLE_RATE_8; + break; + case 32000: + sample_rate = drivers::I2SDac::SAMPLE_RATE_32; + break; + case 44100: + sample_rate = drivers::I2SDac::SAMPLE_RATE_44_1; + break; + case 48000: + sample_rate = drivers::I2SDac::SAMPLE_RATE_48; + break; + case 88200: + sample_rate = drivers::I2SDac::SAMPLE_RATE_88_2; + break; + case 96000: + sample_rate = drivers::I2SDac::SAMPLE_RATE_96; + break; + default: + ESP_LOGE(kTag, "dropping stream with unknown rate"); + return; + } + + dac_->Reconfigure(ch, bps, sample_rate); + current_config_ = fmt; +} + +} // namespace audio |
