diff options
Diffstat (limited to 'src/drivers')
| -rw-r--r-- | src/drivers/bluetooth.cpp | 25 | ||||
| -rw-r--r-- | src/drivers/i2s_dac.cpp | 20 | ||||
| -rw-r--r-- | src/drivers/include/drivers/bluetooth.hpp | 8 | ||||
| -rw-r--r-- | src/drivers/include/drivers/i2s_dac.hpp | 7 | ||||
| -rw-r--r-- | src/drivers/include/drivers/pcm_buffer.hpp | 24 | ||||
| -rw-r--r-- | src/drivers/pcm_buffer.cpp | 40 |
6 files changed, 88 insertions, 36 deletions
diff --git a/src/drivers/bluetooth.cpp b/src/drivers/bluetooth.cpp index 8ec30395..3da5dd0c 100644 --- a/src/drivers/bluetooth.cpp +++ b/src/drivers/bluetooth.cpp @@ -38,7 +38,7 @@ namespace drivers { [[maybe_unused]] static constexpr char kTag[] = "bluetooth"; -DRAM_ATTR static PcmBuffer* sStream = nullptr; +DRAM_ATTR static OutputBuffers* sStreams = nullptr; DRAM_ATTR static std::atomic<float> sVolumeFactor = 1.f; static tasks::WorkerPool* sBgWorker; @@ -97,13 +97,16 @@ IRAM_ATTR auto a2dp_data_cb(uint8_t* buf, int32_t buf_size) -> int32_t { if (buf == nullptr || buf_size <= 0) { return 0; } - PcmBuffer* stream = sStream; - if (stream == nullptr) { + OutputBuffers* streams = sStreams; + if (streams == nullptr) { return 0; } int16_t* samples = reinterpret_cast<int16_t*>(buf); - stream->receive({samples, static_cast<size_t>(buf_size / 2)}, false); + streams->first.receive({samples, static_cast<size_t>(buf_size / 2)}, false, + false); + streams->second.receive({samples, static_cast<size_t>(buf_size / 2)}, true, + false); // Apply software volume scaling. float factor = sVolumeFactor.load(); @@ -141,14 +144,14 @@ auto Bluetooth::enabled() -> bool { return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>(); } -auto Bluetooth::source(PcmBuffer* src) -> void { - if (src == sStream) { +auto Bluetooth::sources(OutputBuffers* src) -> void { + auto lock = bluetooth::BluetoothState::lock(); + if (src == sStreams) { return; } - auto lock = bluetooth::BluetoothState::lock(); - sStream = src; + sStreams = src; tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch( - bluetooth::events::SourceChanged{}); + bluetooth::events::SourcesChanged{}); } auto Bluetooth::softVolume(float f) -> void { @@ -771,8 +774,8 @@ void Connected::react(const events::PairedDeviceChanged& ev) { } } -void Connected::react(const events::SourceChanged& ev) { - if (sStream != nullptr) { +void Connected::react(const events::SourcesChanged& ev) { + if (sStreams != nullptr) { ESP_LOGI(kTag, "checking source is ready"); esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY); } else { diff --git a/src/drivers/i2s_dac.cpp b/src/drivers/i2s_dac.cpp index 9c9bb793..46bf8e80 100644 --- a/src/drivers/i2s_dac.cpp +++ b/src/drivers/i2s_dac.cpp @@ -52,10 +52,12 @@ extern "C" IRAM_ATTR auto callback(i2s_chan_handle_t handle, assert(event->size % 4 == 0); uint8_t* buf = reinterpret_cast<uint8_t*>(event->dma_buf); - auto* src = reinterpret_cast<PcmBuffer*>(user_ctx); + auto* src = reinterpret_cast<OutputBuffers*>(user_ctx); - BaseType_t ret = - src->receive({reinterpret_cast<int16_t*>(buf), event->size / 2}, true); + BaseType_t ret1 = src->first.receive( + {reinterpret_cast<int16_t*>(buf), event->size / 2}, false, true); + BaseType_t ret2 = src->second.receive( + {reinterpret_cast<int16_t*>(buf), event->size / 2}, true, true); // The ESP32's I2S peripheral has a different endianness to its processors. // ESP-IDF handles this difference for stereo channels, but not for mono @@ -70,10 +72,10 @@ extern "C" IRAM_ATTR auto callback(i2s_chan_handle_t handle, } } - return ret; + return ret1 || ret2; } -auto I2SDac::create(IGpios& expander, PcmBuffer& buf) +auto I2SDac::create(IGpios& expander, OutputBuffers& bufs) -> std::optional<I2SDac*> { i2s_chan_handle_t i2s_handle; i2s_chan_config_t channel_config{ @@ -90,7 +92,7 @@ auto I2SDac::create(IGpios& expander, PcmBuffer& buf) // First, instantiate the instance so it can do all of its power on // configuration. std::unique_ptr<I2SDac> dac = - std::make_unique<I2SDac>(expander, buf, i2s_handle); + std::make_unique<I2SDac>(expander, bufs, i2s_handle); // Whilst we wait for the initial boot, we can work on installing the I2S // driver. @@ -122,14 +124,14 @@ auto I2SDac::create(IGpios& expander, PcmBuffer& buf) .on_sent = callback, .on_send_q_ovf = NULL, }; - i2s_channel_register_event_callback(i2s_handle, &callbacks, &buf); + i2s_channel_register_event_callback(i2s_handle, &callbacks, &bufs); return dac.release(); } -I2SDac::I2SDac(IGpios& gpio, PcmBuffer& buf, i2s_chan_handle_t i2s_handle) +I2SDac::I2SDac(IGpios& gpio, OutputBuffers& bufs, i2s_chan_handle_t i2s_handle) : gpio_(gpio), - buffer_(buf), + buffers_(bufs), i2s_handle_(i2s_handle), i2s_active_(false), clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(48000)), diff --git a/src/drivers/include/drivers/bluetooth.hpp b/src/drivers/include/drivers/bluetooth.hpp index 449812d6..99c71e52 100644 --- a/src/drivers/include/drivers/bluetooth.hpp +++ b/src/drivers/include/drivers/bluetooth.hpp @@ -45,7 +45,7 @@ class Bluetooth { auto enable(bool en) -> void; auto enabled() -> bool; - auto source(PcmBuffer*) -> void; + auto sources(OutputBuffers*) -> void; auto softVolume(float) -> void; enum class ConnectionState { @@ -98,7 +98,7 @@ struct Disable : public tinyfsm::Event {}; struct ConnectTimedOut : public tinyfsm::Event {}; struct PairedDeviceChanged : public tinyfsm::Event {}; -struct SourceChanged : public tinyfsm::Event {}; +struct SourcesChanged : public tinyfsm::Event {}; struct DeviceDiscovered : public tinyfsm::Event { const Device& device; }; @@ -172,7 +172,7 @@ class BluetoothState : public tinyfsm::Fsm<BluetoothState> { virtual void react(const events::Disable& ev) = 0; virtual void react(const events::ConnectTimedOut& ev){}; virtual void react(const events::PairedDeviceChanged& ev){}; - virtual void react(const events::SourceChanged& ev){}; + virtual void react(const events::SourcesChanged& ev){}; virtual void react(const events::DeviceDiscovered&); @@ -243,7 +243,7 @@ class Connected : public BluetoothState { void exit() override; void react(const events::PairedDeviceChanged& ev) override; - void react(const events::SourceChanged& ev) override; + void react(const events::SourcesChanged& ev) override; void react(const events::Disable& ev) override; void react(events::internal::Gap ev) override; diff --git a/src/drivers/include/drivers/i2s_dac.hpp b/src/drivers/include/drivers/i2s_dac.hpp index cf9258c0..891acb56 100644 --- a/src/drivers/include/drivers/i2s_dac.hpp +++ b/src/drivers/include/drivers/i2s_dac.hpp @@ -40,9 +40,10 @@ constexpr size_t kI2SBufferLengthFrames = 1024; */ class I2SDac { public: - static auto create(IGpios& expander, PcmBuffer&) -> std::optional<I2SDac*>; + static auto create(IGpios& expander, OutputBuffers&) + -> std::optional<I2SDac*>; - I2SDac(IGpios& gpio, PcmBuffer&, i2s_chan_handle_t i2s_handle); + I2SDac(IGpios& gpio, OutputBuffers&, i2s_chan_handle_t i2s_handle); ~I2SDac(); auto SetPaused(bool) -> void; @@ -77,7 +78,7 @@ class I2SDac { auto set_channel(bool) -> void; IGpios& gpio_; - PcmBuffer& buffer_; + OutputBuffers& buffers_; i2s_chan_handle_t i2s_handle_; bool i2s_active_; diff --git a/src/drivers/include/drivers/pcm_buffer.hpp b/src/drivers/include/drivers/pcm_buffer.hpp index 8f53317e..6b38be94 100644 --- a/src/drivers/include/drivers/pcm_buffer.hpp +++ b/src/drivers/include/drivers/pcm_buffer.hpp @@ -39,11 +39,17 @@ class PcmBuffer { * Fills the given span with samples. If enough samples are available in * the buffer, then the span will be filled with samples from the buffer. Any * shortfall is made up by padding the given span with zeroes. + * + * If `mix` is set to true then, instead of overwriting the destination span, + * the retrieved samples will be mixed into any existing samples contained + * within the destination. This mixing uses a naive sum approach, and so may + * introduce clipping. */ - auto receive(std::span<int16_t>, bool isr) -> BaseType_t; + auto receive(std::span<int16_t>, bool mix, bool isr) -> BaseType_t; auto clear() -> void; auto isEmpty() -> bool; + auto suspend(bool) -> void; /* * How many samples have been added to this buffer since it was created. This @@ -62,7 +68,7 @@ class PcmBuffer { PcmBuffer& operator=(const PcmBuffer&) = delete; private: - auto readSingle(std::span<int16_t>, bool isr) + auto readSingle(std::span<int16_t>, bool mix, bool isr) -> std::pair<size_t, BaseType_t>; StaticRingbuffer_t meta_; @@ -70,7 +76,21 @@ class PcmBuffer { std::atomic<uint32_t> sent_; std::atomic<uint32_t> received_; + std::atomic<bool> suspended_; + RingbufHandle_t ringbuf_; }; +/* + * Convenience type for a pair of PcmBuffers. Each audio output handles mixing + * streams together to ensure that low-latency sounds in one channel (e.g. a + * system notification bleep) aren't delayed by a large audio buffer in the + * other channel (e.g. a long-running track). + * + * By convention, the first buffer of this pair is used for tracks, whilst the + * second is reserved for 'system sounds'; usually TTS, but potentially maybe + * other informative noises. + */ +using OutputBuffers = std::pair<PcmBuffer, PcmBuffer>; + } // namespace drivers diff --git a/src/drivers/pcm_buffer.cpp b/src/drivers/pcm_buffer.cpp index 071f5cea..bc58d4b9 100644 --- a/src/drivers/pcm_buffer.cpp +++ b/src/drivers/pcm_buffer.cpp @@ -25,7 +25,8 @@ namespace drivers { [[maybe_unused]] static const char kTag[] = "pcmbuf"; -PcmBuffer::PcmBuffer(size_t size_in_samples) : sent_(0), received_(0) { +PcmBuffer::PcmBuffer(size_t size_in_samples) + : sent_(0), received_(0), suspended_(false) { size_t size_in_bytes = size_in_samples * sizeof(int16_t); ESP_LOGI(kTag, "allocating pcm buffer of size %u (%uKiB)", size_in_samples, size_in_bytes / 1024); @@ -49,18 +50,26 @@ auto PcmBuffer::send(std::span<const int16_t> data) -> size_t { return data.size(); } -IRAM_ATTR auto PcmBuffer::receive(std::span<int16_t> dest, bool isr) +IRAM_ATTR auto PcmBuffer::receive(std::span<int16_t> dest, bool mix, bool isr) -> BaseType_t { + if (suspended_) { + if (!mix) { + std::fill_n(dest.begin(), dest.size(), 0); + } + return false; + } + size_t first_read = 0, second_read = 0; BaseType_t ret1 = false, ret2 = false; - std::tie(first_read, ret1) = readSingle(dest, isr); + std::tie(first_read, ret1) = readSingle(dest, mix, isr); if (first_read < dest.size()) { - std::tie(second_read, ret2) = readSingle(dest.subspan(first_read), isr); + std::tie(second_read, ret2) = + readSingle(dest.subspan(first_read), mix, isr); } size_t total_read = first_read + second_read; - if (total_read < dest.size()) { + if (total_read < dest.size() && !mix) { std::fill_n(dest.begin() + total_read, dest.size() - total_read, 0); } @@ -85,6 +94,10 @@ auto PcmBuffer::isEmpty() -> bool { xRingbufferGetCurFreeSize(ringbuf_); } +auto PcmBuffer::suspend(bool s) -> void { + suspended_ = s; +} + auto PcmBuffer::totalSent() -> uint32_t { return sent_; } @@ -93,7 +106,9 @@ auto PcmBuffer::totalReceived() -> uint32_t { return received_; } -IRAM_ATTR auto PcmBuffer::readSingle(std::span<int16_t> dest, bool isr) +IRAM_ATTR auto PcmBuffer::readSingle(std::span<int16_t> dest, + bool mix, + bool isr) -> std::pair<size_t, BaseType_t> { BaseType_t ret; size_t read_bytes = 0; @@ -111,7 +126,18 @@ IRAM_ATTR auto PcmBuffer::readSingle(std::span<int16_t> dest, bool isr) return {read_samples, ret}; } - std::memcpy(dest.data(), data, read_bytes); + if (mix) { + for (size_t i = 0; i < read_samples; i++) { + // Sum the two samples in a 32 bit field so that the addition is always + // safe. + int32_t sum = static_cast<int32_t>(dest[i]) + + static_cast<int32_t>(reinterpret_cast<int16_t*>(data)[i]); + // Clip back into the range of a single sample. + dest[i] = std::clamp<int32_t>(sum, INT16_MIN, INT16_MAX); + } + } else { + std::memcpy(dest.data(), data, read_bytes); + } if (isr) { vRingbufferReturnItem(ringbuf_, data); |
