summaryrefslogtreecommitdiff
path: root/src/drivers
diff options
context:
space:
mode:
Diffstat (limited to 'src/drivers')
-rw-r--r--src/drivers/bluetooth.cpp25
-rw-r--r--src/drivers/i2s_dac.cpp20
-rw-r--r--src/drivers/include/drivers/bluetooth.hpp8
-rw-r--r--src/drivers/include/drivers/i2s_dac.hpp7
-rw-r--r--src/drivers/include/drivers/pcm_buffer.hpp24
-rw-r--r--src/drivers/pcm_buffer.cpp40
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);