Merge branch 'main' into daniel/persistent-positions

custom
ailurux 7 months ago
commit 6c1757a176
  1. 2
      lua/main_menu.lua
  2. 2
      src/codecs/wav.cpp
  3. 25
      src/drivers/bluetooth.cpp
  4. 20
      src/drivers/i2s_dac.cpp
  5. 8
      src/drivers/include/drivers/bluetooth.hpp
  6. 7
      src/drivers/include/drivers/i2s_dac.hpp
  7. 24
      src/drivers/include/drivers/pcm_buffer.hpp
  8. 40
      src/drivers/pcm_buffer.cpp
  9. 2
      src/drivers/spi.cpp
  10. 5
      src/tangara/audio/audio_events.hpp
  11. 57
      src/tangara/audio/audio_fsm.cpp
  12. 6
      src/tangara/audio/audio_fsm.hpp
  13. 8
      src/tangara/audio/bt_audio_output.cpp
  14. 4
      src/tangara/audio/bt_audio_output.hpp
  15. 1
      src/tangara/audio/fatfs_stream_factory.cpp
  16. 6
      src/tangara/audio/i2s_audio_output.cpp
  17. 4
      src/tangara/audio/i2s_audio_output.hpp
  18. 33
      src/tangara/audio/processor.cpp
  19. 56
      src/tangara/audio/processor.hpp
  20. 192
      src/tangara/tts/player.cpp
  21. 47
      src/tangara/tts/player.hpp
  22. 31
      src/tangara/tts/provider.cpp
  23. 17
      src/tangara/tts/provider.hpp

@ -155,6 +155,7 @@ return widgets.MenuScreen:new {
}) })
end) end)
files_btn:Image { src = img.files } files_btn:Image { src = img.files }
widgets.Description(files_btn, "File browser")
theme.set_subject(files_btn, "menu_icon") theme.set_subject(files_btn, "menu_icon")
local settings_btn = bottom_bar:Button {} local settings_btn = bottom_bar:Button {}
@ -162,6 +163,7 @@ return widgets.MenuScreen:new {
backstack.push(require("settings"):new()) backstack.push(require("settings"):new())
end) end)
settings_btn:Image { src = img.settings } settings_btn:Image { src = img.settings }
widgets.Description(settings_btn, "Settings")
theme.set_subject(settings_btn, "menu_icon") theme.set_subject(settings_btn, "menu_icon")
end, end,
} }

@ -137,8 +137,6 @@ auto WavDecoder::OpenStream(std::shared_ptr<IStream> input, uint32_t offset)
// uint32_t file_size = bytes_to_u32(buffer_span.subspan(4, 4)) + 8; // uint32_t file_size = bytes_to_u32(buffer_span.subspan(4, 4)) + 8;
std::string fmt_header = bytes_to_str(buffer_span.subspan(12, 4)); std::string fmt_header = bytes_to_str(buffer_span.subspan(12, 4));
ESP_LOGI(kTag, "fmt header found? %s",
(fmt_header.starts_with("fmt")) ? "yes" : "no");
if (!fmt_header.starts_with("fmt")) { if (!fmt_header.starts_with("fmt")) {
ESP_LOGW(kTag, "Could not find format chunk"); ESP_LOGW(kTag, "Could not find format chunk");
return cpp::fail(Error::kMalformedData); return cpp::fail(Error::kMalformedData);

@ -38,7 +38,7 @@ namespace drivers {
[[maybe_unused]] static constexpr char kTag[] = "bluetooth"; [[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; DRAM_ATTR static std::atomic<float> sVolumeFactor = 1.f;
static tasks::WorkerPool* sBgWorker; 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) { if (buf == nullptr || buf_size <= 0) {
return 0; return 0;
} }
PcmBuffer* stream = sStream; OutputBuffers* streams = sStreams;
if (stream == nullptr) { if (streams == nullptr) {
return 0; return 0;
} }
int16_t* samples = reinterpret_cast<int16_t*>(buf); 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. // Apply software volume scaling.
float factor = sVolumeFactor.load(); float factor = sVolumeFactor.load();
@ -141,14 +144,14 @@ auto Bluetooth::enabled() -> bool {
return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>(); return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>();
} }
auto Bluetooth::source(PcmBuffer* src) -> void { auto Bluetooth::sources(OutputBuffers* src) -> void {
if (src == sStream) { auto lock = bluetooth::BluetoothState::lock();
if (src == sStreams) {
return; return;
} }
auto lock = bluetooth::BluetoothState::lock(); sStreams = src;
sStream = src;
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch( tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::SourceChanged{}); bluetooth::events::SourcesChanged{});
} }
auto Bluetooth::softVolume(float f) -> void { auto Bluetooth::softVolume(float f) -> void {
@ -771,8 +774,8 @@ void Connected::react(const events::PairedDeviceChanged& ev) {
} }
} }
void Connected::react(const events::SourceChanged& ev) { void Connected::react(const events::SourcesChanged& ev) {
if (sStream != nullptr) { if (sStreams != nullptr) {
ESP_LOGI(kTag, "checking source is ready"); ESP_LOGI(kTag, "checking source is ready");
esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY); esp_a2d_media_ctrl(ESP_A2D_MEDIA_CTRL_CHECK_SRC_RDY);
} else { } else {

@ -52,10 +52,12 @@ extern "C" IRAM_ATTR auto callback(i2s_chan_handle_t handle,
assert(event->size % 4 == 0); assert(event->size % 4 == 0);
uint8_t* buf = reinterpret_cast<uint8_t*>(event->dma_buf); 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 = BaseType_t ret1 = src->first.receive(
src->receive({reinterpret_cast<int16_t*>(buf), event->size / 2}, true); {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. // The ESP32's I2S peripheral has a different endianness to its processors.
// ESP-IDF handles this difference for stereo channels, but not for mono // 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*> { -> std::optional<I2SDac*> {
i2s_chan_handle_t i2s_handle; i2s_chan_handle_t i2s_handle;
i2s_chan_config_t channel_config{ 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 // First, instantiate the instance so it can do all of its power on
// configuration. // configuration.
std::unique_ptr<I2SDac> dac = 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 // Whilst we wait for the initial boot, we can work on installing the I2S
// driver. // driver.
@ -122,14 +124,14 @@ auto I2SDac::create(IGpios& expander, PcmBuffer& buf)
.on_sent = callback, .on_sent = callback,
.on_send_q_ovf = NULL, .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(); 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), : gpio_(gpio),
buffer_(buf), buffers_(bufs),
i2s_handle_(i2s_handle), i2s_handle_(i2s_handle),
i2s_active_(false), i2s_active_(false),
clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(48000)), clock_config_(I2S_STD_CLK_DEFAULT_CONFIG(48000)),

@ -45,7 +45,7 @@ class Bluetooth {
auto enable(bool en) -> void; auto enable(bool en) -> void;
auto enabled() -> bool; auto enabled() -> bool;
auto source(PcmBuffer*) -> void; auto sources(OutputBuffers*) -> void;
auto softVolume(float) -> void; auto softVolume(float) -> void;
enum class ConnectionState { enum class ConnectionState {
@ -98,7 +98,7 @@ struct Disable : public tinyfsm::Event {};
struct ConnectTimedOut : public tinyfsm::Event {}; struct ConnectTimedOut : public tinyfsm::Event {};
struct PairedDeviceChanged : public tinyfsm::Event {}; struct PairedDeviceChanged : public tinyfsm::Event {};
struct SourceChanged : public tinyfsm::Event {}; struct SourcesChanged : public tinyfsm::Event {};
struct DeviceDiscovered : public tinyfsm::Event { struct DeviceDiscovered : public tinyfsm::Event {
const Device& device; 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::Disable& ev) = 0;
virtual void react(const events::ConnectTimedOut& ev){}; virtual void react(const events::ConnectTimedOut& ev){};
virtual void react(const events::PairedDeviceChanged& 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&); virtual void react(const events::DeviceDiscovered&);
@ -243,7 +243,7 @@ class Connected : public BluetoothState {
void exit() override; void exit() override;
void react(const events::PairedDeviceChanged& ev) 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(const events::Disable& ev) override;
void react(events::internal::Gap ev) override; void react(events::internal::Gap ev) override;

@ -40,9 +40,10 @@ constexpr size_t kI2SBufferLengthFrames = 1024;
*/ */
class I2SDac { class I2SDac {
public: 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(); ~I2SDac();
auto SetPaused(bool) -> void; auto SetPaused(bool) -> void;
@ -77,7 +78,7 @@ class I2SDac {
auto set_channel(bool) -> void; auto set_channel(bool) -> void;
IGpios& gpio_; IGpios& gpio_;
PcmBuffer& buffer_; OutputBuffers& buffers_;
i2s_chan_handle_t i2s_handle_; i2s_chan_handle_t i2s_handle_;
bool i2s_active_; bool i2s_active_;

@ -39,11 +39,17 @@ class PcmBuffer {
* Fills the given span with samples. If enough samples are available in * 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 * 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. * 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 clear() -> void;
auto isEmpty() -> bool; auto isEmpty() -> bool;
auto suspend(bool) -> void;
/* /*
* How many samples have been added to this buffer since it was created. This * 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; PcmBuffer& operator=(const PcmBuffer&) = delete;
private: 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>; -> std::pair<size_t, BaseType_t>;
StaticRingbuffer_t meta_; StaticRingbuffer_t meta_;
@ -70,7 +76,21 @@ class PcmBuffer {
std::atomic<uint32_t> sent_; std::atomic<uint32_t> sent_;
std::atomic<uint32_t> received_; std::atomic<uint32_t> received_;
std::atomic<bool> suspended_;
RingbufHandle_t ringbuf_; 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 } // namespace drivers

@ -25,7 +25,8 @@ namespace drivers {
[[maybe_unused]] static const char kTag[] = "pcmbuf"; [[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); size_t size_in_bytes = size_in_samples * sizeof(int16_t);
ESP_LOGI(kTag, "allocating pcm buffer of size %u (%uKiB)", size_in_samples, ESP_LOGI(kTag, "allocating pcm buffer of size %u (%uKiB)", size_in_samples,
size_in_bytes / 1024); size_in_bytes / 1024);
@ -49,18 +50,26 @@ auto PcmBuffer::send(std::span<const int16_t> data) -> size_t {
return data.size(); 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 { -> BaseType_t {
if (suspended_) {
if (!mix) {
std::fill_n(dest.begin(), dest.size(), 0);
}
return false;
}
size_t first_read = 0, second_read = 0; size_t first_read = 0, second_read = 0;
BaseType_t ret1 = false, ret2 = false; 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()) { 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; 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); std::fill_n(dest.begin() + total_read, dest.size() - total_read, 0);
} }
@ -85,6 +94,10 @@ auto PcmBuffer::isEmpty() -> bool {
xRingbufferGetCurFreeSize(ringbuf_); xRingbufferGetCurFreeSize(ringbuf_);
} }
auto PcmBuffer::suspend(bool s) -> void {
suspended_ = s;
}
auto PcmBuffer::totalSent() -> uint32_t { auto PcmBuffer::totalSent() -> uint32_t {
return sent_; return sent_;
} }
@ -93,7 +106,9 @@ auto PcmBuffer::totalReceived() -> uint32_t {
return received_; 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> { -> std::pair<size_t, BaseType_t> {
BaseType_t ret; BaseType_t ret;
size_t read_bytes = 0; 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}; 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) { if (isr) {
vRingbufferReturnItem(ringbuf_, data); vRingbufferReturnItem(ringbuf_, data);

@ -41,7 +41,7 @@ esp_err_t init_spi(void) {
// manages its own use of DMA-capable memory. // manages its own use of DMA-capable memory.
.max_transfer_sz = 4096, .max_transfer_sz = 4096,
.flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS, .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_IOMUX_PINS,
.isr_cpu_id = ESP_INTR_CPU_AFFINITY_1, .isr_cpu_id = ESP_INTR_CPU_AFFINITY_0,
.intr_flags = ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM, .intr_flags = ESP_INTR_FLAG_LOWMED | ESP_INTR_FLAG_IRAM,
}; };

@ -144,8 +144,11 @@ struct OutputModeChanged : tinyfsm::Event {
std::optional<drivers::NvsStorage::Output> set_to; std::optional<drivers::NvsStorage::Output> set_to;
}; };
namespace internal { struct TtsPlaybackChanged : tinyfsm::Event {
bool is_playing;
};
namespace internal {
struct DecodingStarted : tinyfsm::Event { struct DecodingStarted : tinyfsm::Event {
std::shared_ptr<TrackInfo> track; std::shared_ptr<TrackInfo> track;
}; };

@ -44,6 +44,7 @@
#include "sample.hpp" #include "sample.hpp"
#include "system_fsm/service_locator.hpp" #include "system_fsm/service_locator.hpp"
#include "system_fsm/system_events.hpp" #include "system_fsm/system_events.hpp"
#include "tts/player.hpp"
namespace audio { namespace audio {
@ -60,15 +61,22 @@ std::shared_ptr<IAudioOutput> AudioState::sOutput;
std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput; std::shared_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput; std::shared_ptr<BluetoothAudioOutput> AudioState::sBtOutput;
// Two seconds of samples for two channels, at a representative sample rate. // For tracks, keep about two seconds' worth of samples at 2ch 48kHz. This
constexpr size_t kDrainLatencySamples = 48000 * 2 * 2; // is more headroom than we need for small playback, but it doesn't hurt to
// keep some PSRAM in our pockets for a rainy day.
constexpr size_t kTrackDrainLatencySamples = 48000 * 2 * 2;
std::unique_ptr<drivers::PcmBuffer> AudioState::sDrainBuffer; // For system sounds, we intentionally choose codecs that are very fast to
// decode. This lets us get away with a much smaller drain buffer.
constexpr size_t kSystemDrainLatencySamples = 48000;
std::unique_ptr<drivers::OutputBuffers> AudioState::sDrainBuffers;
std::optional<IAudioOutput::Format> AudioState::sDrainFormat; std::optional<IAudioOutput::Format> AudioState::sDrainFormat;
StreamCues AudioState::sStreamCues; StreamCues AudioState::sStreamCues;
bool AudioState::sIsPaused = true; bool AudioState::sIsPaused = true;
bool AudioState::sIsTtsPlaying = false;
uint8_t AudioState::sUpdateCounter = 0; uint8_t AudioState::sUpdateCounter = 0;
@ -196,6 +204,11 @@ void AudioState::react(const TogglePlayPause& ev) {
} }
} }
void AudioState::react(const TtsPlaybackChanged& ev) {
sIsTtsPlaying = ev.is_playing;
updateOutputMode();
}
void AudioState::react(const internal::DecodingFinished& ev) { void AudioState::react(const internal::DecodingFinished& ev) {
// If we just finished playing whatever's at the front of the queue, then we // If we just finished playing whatever's at the front of the queue, then we
// need to advanve and start playing the next one ASAP in order to continue // need to advanve and start playing the next one ASAP in order to continue
@ -231,7 +244,7 @@ void AudioState::react(const internal::StreamStarted& ev) {
} }
sStreamCues.addCue(ev.track, ev.cue_at_sample); sStreamCues.addCue(ev.track, ev.cue_at_sample);
sStreamCues.update(sDrainBuffer->totalReceived()); sStreamCues.update(sDrainBuffers->first.totalReceived());
if (!sIsPaused && !is_in_state<states::Playback>()) { if (!sIsPaused && !is_in_state<states::Playback>()) {
transit<states::Playback>(); transit<states::Playback>();
@ -374,8 +387,8 @@ void AudioState::react(const OutputModeChanged& ev) {
sOutput = sI2SOutput; sOutput = sI2SOutput;
break; break;
} }
sOutput->mode(IAudioOutput::Modes::kOnPaused);
sSampleProcessor->SetOutput(sOutput); sSampleProcessor->SetOutput(sOutput);
updateOutputMode();
// Bluetooth volume isn't 'changed' until we've connected to a device. // Bluetooth volume isn't 'changed' until we've connected to a device.
if (new_mode == drivers::NvsStorage::Output::kHeadphones) { if (new_mode == drivers::NvsStorage::Output::kHeadphones) {
@ -407,6 +420,14 @@ auto AudioState::updateSavedPosition(std::string uri, uint32_t position)
}); });
} }
auto AudioState::updateOutputMode() -> void {
if (is_in_state<states::Playback>() || sIsTtsPlaying) {
sOutput->mode(IAudioOutput::Modes::kOnPlaying);
} else {
sOutput->mode(IAudioOutput::Modes::kOnPaused);
}
}
auto AudioState::commitVolume() -> void { auto AudioState::commitVolume() -> void {
auto mode = sServices->nvs().OutputMode(); auto mode = sServices->nvs().OutputMode();
auto vol = sOutput->GetVolume(); auto vol = sOutput->GetVolume();
@ -426,13 +447,20 @@ namespace states {
void Uninitialised::react(const system_fsm::BootComplete& ev) { void Uninitialised::react(const system_fsm::BootComplete& ev) {
sServices = ev.services; sServices = ev.services;
sDrainBuffer = std::make_unique<drivers::PcmBuffer>(kDrainLatencySamples); sDrainBuffers = std::make_unique<drivers::OutputBuffers>(
kTrackDrainLatencySamples, kSystemDrainLatencySamples);
sDrainBuffers->first.suspend(true);
sStreamFactory.reset( sStreamFactory.reset(
new FatfsStreamFactory(sServices->database(), sServices->tag_parser())); new FatfsStreamFactory(sServices->database(), sServices->tag_parser()));
sI2SOutput.reset(new I2SAudioOutput(sServices->gpios(), *sDrainBuffer)); sI2SOutput.reset(new I2SAudioOutput(sServices->gpios(), *sDrainBuffers));
sBtOutput.reset(new BluetoothAudioOutput( sBtOutput.reset(new BluetoothAudioOutput(
sServices->bluetooth(), *sDrainBuffer, sServices->bg_worker())); sServices->bluetooth(), *sDrainBuffers, sServices->bg_worker()));
auto& tts_provider = sServices->tts();
auto tts_player = std::make_unique<tts::Player>(
sServices->bg_worker(), sDrainBuffers->second, *sStreamFactory);
tts_provider.player(std::move(tts_player));
auto& nvs = sServices->nvs(); auto& nvs = sServices->nvs();
sI2SOutput->SetMaxVolume(nvs.AmpMaxVolume()); sI2SOutput->SetMaxVolume(nvs.AmpMaxVolume());
@ -463,7 +491,7 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
.left_bias = nvs.AmpLeftBias(), .left_bias = nvs.AmpLeftBias(),
}); });
sSampleProcessor.reset(new SampleProcessor(*sDrainBuffer)); sSampleProcessor.reset(new SampleProcessor(sDrainBuffers->first));
sSampleProcessor->SetOutput(sOutput); sSampleProcessor->SetOutput(sOutput);
sDecoder.reset(Decoder::Start(sSampleProcessor)); sDecoder.reset(Decoder::Start(sSampleProcessor));
@ -474,6 +502,10 @@ void Uninitialised::react(const system_fsm::BootComplete& ev) {
static const char kQueueKey[] = "audio:queue"; static const char kQueueKey[] = "audio:queue";
static const char kCurrentFileKey[] = "audio:current"; static const char kCurrentFileKey[] = "audio:current";
auto Standby::entry() -> void {
updateOutputMode();
}
void Standby::react(const system_fsm::KeyLockChanged& ev) { void Standby::react(const system_fsm::KeyLockChanged& ev) {
if (!ev.locking) { if (!ev.locking) {
return; return;
@ -559,7 +591,8 @@ static void heartbeat(TimerHandle_t) {
void Playback::entry() { void Playback::entry() {
ESP_LOGI(kTag, "audio output resumed"); ESP_LOGI(kTag, "audio output resumed");
sOutput->mode(IAudioOutput::Modes::kOnPlaying); sDrainBuffers->first.suspend(false);
updateOutputMode();
emitPlaybackUpdate(false); emitPlaybackUpdate(false);
if (!sHeartbeatTimer) { if (!sHeartbeatTimer) {
@ -572,7 +605,7 @@ void Playback::entry() {
void Playback::exit() { void Playback::exit() {
ESP_LOGI(kTag, "audio output paused"); ESP_LOGI(kTag, "audio output paused");
xTimerStop(sHeartbeatTimer, portMAX_DELAY); xTimerStop(sHeartbeatTimer, portMAX_DELAY);
sOutput->mode(IAudioOutput::Modes::kOnPaused); sDrainBuffers->first.suspend(true);
emitPlaybackUpdate(true); emitPlaybackUpdate(true);
} }
@ -583,7 +616,7 @@ void Playback::react(const system_fsm::SdStateChanged& ev) {
} }
void Playback::react(const internal::StreamHeartbeat& ev) { void Playback::react(const internal::StreamHeartbeat& ev) {
sStreamCues.update(sDrainBuffer->totalReceived()); sStreamCues.update(sDrainBuffers->first.totalReceived());
if (sStreamCues.hasStream()) { if (sStreamCues.hasStream()) {
emitPlaybackUpdate(false); emitPlaybackUpdate(false);

@ -48,6 +48,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
void react(const PlaySineWave&); void react(const PlaySineWave&);
void react(const SetTrack&); void react(const SetTrack&);
void react(const TogglePlayPause&); void react(const TogglePlayPause&);
void react(const TtsPlaybackChanged&);
void react(const internal::DecodingFinished&); void react(const internal::DecodingFinished&);
void react(const internal::StreamStarted&); void react(const internal::StreamStarted&);
@ -70,6 +71,7 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
virtual void react(const system_fsm::HasPhonesChanged&); virtual void react(const system_fsm::HasPhonesChanged&);
protected: protected:
auto updateOutputMode() -> void;
auto emitPlaybackUpdate(bool paused) -> void; auto emitPlaybackUpdate(bool paused) -> void;
auto commitVolume() -> void; auto commitVolume() -> void;
@ -84,13 +86,14 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::shared_ptr<BluetoothAudioOutput> sBtOutput; static std::shared_ptr<BluetoothAudioOutput> sBtOutput;
static std::shared_ptr<IAudioOutput> sOutput; static std::shared_ptr<IAudioOutput> sOutput;
static std::unique_ptr<drivers::PcmBuffer> sDrainBuffer; static std::unique_ptr<drivers::OutputBuffers> sDrainBuffers;
static StreamCues sStreamCues; static StreamCues sStreamCues;
static std::optional<IAudioOutput::Format> sDrainFormat; static std::optional<IAudioOutput::Format> sDrainFormat;
static bool sIsPaused; static bool sIsPaused;
static uint8_t sUpdateCounter; static uint8_t sUpdateCounter;
static bool sIsTtsPlaying;
}; };
namespace states { namespace states {
@ -105,6 +108,7 @@ class Uninitialised : public AudioState {
class Standby : public AudioState { class Standby : public AudioState {
public: public:
void entry() override;
void react(const system_fsm::KeyLockChanged&) override; void react(const system_fsm::KeyLockChanged&) override;
void react(const system_fsm::SdStateChanged&) override; void react(const system_fsm::SdStateChanged&) override;

@ -36,11 +36,11 @@ static constexpr uint16_t kVolumeRange = 60;
using ConnectionState = drivers::Bluetooth::ConnectionState; using ConnectionState = drivers::Bluetooth::ConnectionState;
BluetoothAudioOutput::BluetoothAudioOutput(drivers::Bluetooth& bt, BluetoothAudioOutput::BluetoothAudioOutput(drivers::Bluetooth& bt,
drivers::PcmBuffer& buffer, drivers::OutputBuffers& bufs,
tasks::WorkerPool& p) tasks::WorkerPool& p)
: IAudioOutput(), : IAudioOutput(),
bluetooth_(bt), bluetooth_(bt),
buffer_(buffer), buffers_(bufs),
bg_worker_(p), bg_worker_(p),
volume_() {} volume_() {}
@ -48,9 +48,9 @@ BluetoothAudioOutput::~BluetoothAudioOutput() {}
auto BluetoothAudioOutput::changeMode(Modes mode) -> void { auto BluetoothAudioOutput::changeMode(Modes mode) -> void {
if (mode == Modes::kOnPlaying) { if (mode == Modes::kOnPlaying) {
bluetooth_.source(&buffer_); bluetooth_.sources(&buffers_);
} else { } else {
bluetooth_.source(nullptr); bluetooth_.sources(nullptr);
} }
} }

@ -25,7 +25,7 @@ namespace audio {
class BluetoothAudioOutput : public IAudioOutput { class BluetoothAudioOutput : public IAudioOutput {
public: public:
BluetoothAudioOutput(drivers::Bluetooth& bt, BluetoothAudioOutput(drivers::Bluetooth& bt,
drivers::PcmBuffer& buf, drivers::OutputBuffers& bufs,
tasks::WorkerPool&); tasks::WorkerPool&);
~BluetoothAudioOutput(); ~BluetoothAudioOutput();
@ -54,7 +54,7 @@ class BluetoothAudioOutput : public IAudioOutput {
private: private:
drivers::Bluetooth& bluetooth_; drivers::Bluetooth& bluetooth_;
drivers::PcmBuffer& buffer_; drivers::OutputBuffers& buffers_;
tasks::WorkerPool& bg_worker_; tasks::WorkerPool& bg_worker_;
uint16_t volume_; uint16_t volume_;

@ -50,7 +50,6 @@ auto FatfsStreamFactory::create(std::string path, uint32_t offset)
-> std::shared_ptr<TaggedStream> { -> std::shared_ptr<TaggedStream> {
auto tags = tag_parser_.ReadAndParseTags(path); auto tags = tag_parser_.ReadAndParseTags(path);
if (!tags) { if (!tags) {
ESP_LOGE(kTag, "failed to read tags");
return {}; return {};
} }

@ -42,10 +42,10 @@ static constexpr uint16_t kLineLevelVolume = 0x13d;
static constexpr uint16_t kDefaultVolume = 0x100; static constexpr uint16_t kDefaultVolume = 0x100;
I2SAudioOutput::I2SAudioOutput(drivers::IGpios& expander, I2SAudioOutput::I2SAudioOutput(drivers::IGpios& expander,
drivers::PcmBuffer& buffer) drivers::OutputBuffers& buffers)
: IAudioOutput(), : IAudioOutput(),
expander_(expander), expander_(expander),
buffer_(buffer), buffers_(buffers),
dac_(), dac_(),
current_mode_(Modes::kOff), current_mode_(Modes::kOff),
current_config_(), current_config_(),
@ -72,7 +72,7 @@ auto I2SAudioOutput::changeMode(Modes mode) -> void {
if (was_off) { if (was_off) {
// Ensure an I2SDac instance actually exists. // Ensure an I2SDac instance actually exists.
if (!dac_) { if (!dac_) {
auto instance = drivers::I2SDac::create(expander_, buffer_); auto instance = drivers::I2SDac::create(expander_, buffers_);
if (!instance) { if (!instance) {
return; return;
} }

@ -21,7 +21,7 @@ namespace audio {
class I2SAudioOutput : public IAudioOutput { class I2SAudioOutput : public IAudioOutput {
public: public:
I2SAudioOutput(drivers::IGpios&, drivers::PcmBuffer&); I2SAudioOutput(drivers::IGpios&, drivers::OutputBuffers&);
auto SetMaxVolume(uint16_t) -> void; auto SetMaxVolume(uint16_t) -> void;
auto SetVolumeDb(uint16_t) -> void; auto SetVolumeDb(uint16_t) -> void;
@ -51,7 +51,7 @@ class I2SAudioOutput : public IAudioOutput {
private: private:
drivers::IGpios& expander_; drivers::IGpios& expander_;
drivers::PcmBuffer& buffer_; drivers::OutputBuffers& buffers_;
std::unique_ptr<drivers::I2SDac> dac_; std::unique_ptr<drivers::I2SDac> dac_;

@ -347,34 +347,39 @@ auto SampleProcessor::discardCommand(Args& command) -> void {
// End of stream commands can just be dropped without further action. // End of stream commands can just be dropped without further action.
} }
SampleProcessor::Buffer::Buffer() Buffer::Buffer(std::span<sample::Sample> storage)
: buffer_(reinterpret_cast<sample::Sample*>( : storage_(nullptr), buffer_(storage), samples_in_buffer_() {}
heap_caps_calloc(kSampleBufferLength,
sizeof(sample::Sample), Buffer::Buffer()
MALLOC_CAP_DMA)), : storage_(reinterpret_cast<sample::Sample*>(
kSampleBufferLength), heap_caps_calloc(kSampleBufferLength,
sizeof(sample::Sample),
MALLOC_CAP_DMA))),
buffer_(storage_, kSampleBufferLength),
samples_in_buffer_() {} samples_in_buffer_() {}
SampleProcessor::Buffer::~Buffer() { Buffer::~Buffer() {
heap_caps_free(buffer_.data()); if (storage_) {
heap_caps_free(storage_);
}
} }
auto SampleProcessor::Buffer::writeAcquire() -> std::span<sample::Sample> { auto Buffer::writeAcquire() -> std::span<sample::Sample> {
return buffer_.subspan(samples_in_buffer_.size()); return buffer_.subspan(samples_in_buffer_.size());
} }
auto SampleProcessor::Buffer::writeCommit(size_t samples) -> void { auto Buffer::writeCommit(size_t samples) -> void {
if (samples == 0) { if (samples == 0) {
return; return;
} }
samples_in_buffer_ = buffer_.first(samples + samples_in_buffer_.size()); samples_in_buffer_ = buffer_.first(samples + samples_in_buffer_.size());
} }
auto SampleProcessor::Buffer::readAcquire() -> std::span<sample::Sample> { auto Buffer::readAcquire() -> std::span<sample::Sample> {
return samples_in_buffer_; return samples_in_buffer_;
} }
auto SampleProcessor::Buffer::readCommit(size_t samples) -> void { auto Buffer::readCommit(size_t samples) -> void {
if (samples == 0) { if (samples == 0) {
return; return;
} }
@ -389,11 +394,11 @@ auto SampleProcessor::Buffer::readCommit(size_t samples) -> void {
} }
} }
auto SampleProcessor::Buffer::isEmpty() -> bool { auto Buffer::isEmpty() -> bool {
return samples_in_buffer_.empty(); return samples_in_buffer_.empty();
} }
auto SampleProcessor::Buffer::clear() -> void { auto Buffer::clear() -> void {
samples_in_buffer_ = {}; samples_in_buffer_ = {};
} }

@ -22,6 +22,35 @@
namespace audio { namespace audio {
/* Utility for managing buffering samples between digital filters. */
class Buffer {
public:
Buffer(std::span<sample::Sample> storage);
Buffer();
~Buffer();
/* Returns a span of the unused space within the buffer. */
auto writeAcquire() -> std::span<sample::Sample>;
/* Signals how many samples were just added to the writeAcquire span. */
auto writeCommit(size_t) -> void;
/* Returns a span of the samples stored within the buffer. */
auto readAcquire() -> std::span<sample::Sample>;
/* Signals how many samples from the readAcquire span were consumed. */
auto readCommit(size_t) -> void;
auto isEmpty() -> bool;
auto clear() -> void;
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
private:
sample::Sample* storage_;
std::span<sample::Sample> buffer_;
std::span<sample::Sample> samples_in_buffer_;
};
/* /*
* Handle to a persistent task that converts samples between formats (sample * Handle to a persistent task that converts samples between formats (sample
* rate, channels, bits per sample), in order to put samples in the preferred * rate, channels, bits per sample), in order to put samples in the preferred
@ -87,33 +116,6 @@ class SampleProcessor {
StreamBufferHandle_t source_; StreamBufferHandle_t source_;
drivers::PcmBuffer& sink_; drivers::PcmBuffer& sink_;
/* Internal utility for managing buffering samples between our filters. */
class Buffer {
public:
Buffer();
~Buffer();
/* Returns a span of the unused space within the buffer. */
auto writeAcquire() -> std::span<sample::Sample>;
/* Signals how many samples were just added to the writeAcquire span. */
auto writeCommit(size_t) -> void;
/* Returns a span of the samples stored within the buffer. */
auto readAcquire() -> std::span<sample::Sample>;
/* Signals how many samples from the readAcquire span were consumed. */
auto readCommit(size_t) -> void;
auto isEmpty() -> bool;
auto clear() -> void;
Buffer(const Buffer&) = delete;
Buffer& operator=(const Buffer&) = delete;
private:
std::span<sample::Sample> buffer_;
std::span<sample::Sample> samples_in_buffer_;
};
Buffer input_buffer_; Buffer input_buffer_;
Buffer resampled_buffer_; Buffer resampled_buffer_;
Buffer output_buffer_; Buffer output_buffer_;

@ -0,0 +1,192 @@
/*
* Copyright 2024 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "tts/player.hpp"
#include <mutex>
#include "audio/audio_events.hpp"
#include "audio/processor.hpp"
#include "audio/resample.hpp"
#include "codec.hpp"
#include "esp_log.h"
#include "events/event_queue.hpp"
#include "freertos/projdefs.h"
#include "portmacro.h"
#include "sample.hpp"
#include "types.hpp"
namespace tts {
[[maybe_unused]] static constexpr char kTag[] = "ttsplay";
Player::Player(tasks::WorkerPool& worker,
drivers::PcmBuffer& output,
audio::FatfsStreamFactory& factory)
: bg_(worker),
stream_factory_(factory),
output_(output),
stream_playing_(false),
stream_cancelled_(false) {}
auto Player::playFile(const std::string& text, const std::string& file)
-> void {
bg_.Dispatch<void>([=, this]() {
{
std::scoped_lock<std::mutex> lock{new_stream_mutex_};
if (stream_playing_) {
stream_cancelled_ = true;
stream_playing_.wait(true);
}
stream_cancelled_ = false;
stream_playing_ = true;
}
openAndDecode(text, file);
if (!stream_cancelled_) {
events::Audio().Dispatch(audio::TtsPlaybackChanged{.is_playing = false});
}
stream_playing_ = false;
stream_playing_.notify_all();
});
}
auto Player::openAndDecode(const std::string& text, const std::string& path)
-> void {
auto stream = stream_factory_.create(path);
if (!stream) {
ESP_LOGW(kTag, "missing '%s' for '%s'", path.c_str(), text.c_str());
return;
}
// FIXME: Rather than hardcoding WAV support only, we should work out a
// proper subset of 'low memory' decoders that can all be used for TTS
// playback.
if (stream->type() != codecs::StreamType::kWav) {
ESP_LOGE(kTag, "'%s' has unsupported encoding", path.c_str());
return;
}
auto decoder = codecs::CreateCodecForType(stream->type());
if (!decoder) {
ESP_LOGE(kTag, "creating decoder failed");
return;
}
std::unique_ptr<codecs::ICodec> codec{*decoder};
auto open_res = codec->OpenStream(stream, 0);
if (open_res.has_error()) {
ESP_LOGE(kTag, "opening stream failed");
return;
}
decodeToSink(*open_res, std::move(codec));
}
auto Player::decodeToSink(const codecs::ICodec::OutputFormat& format,
std::unique_ptr<codecs::ICodec> codec) -> void {
// Set up buffers to hold samples between the intermediary parts of
// processing. We can just use the stack for these, since this method is
// called only from background workers, which have enormous stacks.
sample::Sample decode_storage[4096];
audio::Buffer decode_buf(decode_storage);
sample::Sample resample_storage[4096];
audio::Buffer resample_buf(resample_storage);
sample::Sample stereo_storage[4096];
audio::Buffer stereo_buf(stereo_storage);
// Work out what processing the codec's output needs.
std::unique_ptr<audio::Resampler> resampler;
if (format.sample_rate_hz != 48000) {
resampler = std::make_unique<audio::Resampler>(format.sample_rate_hz, 48000,
format.num_channels);
}
bool double_samples = format.num_channels == 1;
// Start our playback (wait for previous to end?)
events::Audio().Dispatch(audio::TtsPlaybackChanged{.is_playing = true});
// FIXME: This decode-and-process loop is substantially the same as the audio
// processor's filter loop. Ideally we should refactor both of these loops to
// reuse code, however I'm holding off on doing this until we've implemented
// more advanced audio processing features in the audio processor (EQ, tempo
// shifting, etc.) as it's not clear to me yet how much the two codepaths will
// be diverging later anyway.
while ((codec || !decode_buf.isEmpty() || !resample_buf.isEmpty() ||
!stereo_buf.isEmpty()) &&
!stream_cancelled_) {
if (codec) {
auto decode_res = codec->DecodeTo(decode_buf.writeAcquire());
if (decode_res.has_error()) {
ESP_LOGE(kTag, "decoding error");
break;
}
decode_buf.writeCommit(decode_res->samples_written);
if (decode_res->is_stream_finished) {
codec.reset();
}
}
if (!decode_buf.isEmpty()) {
auto resample_input = decode_buf.readAcquire();
auto resample_output = resample_buf.writeAcquire();
size_t read, wrote;
if (resampler) {
std::tie(read, wrote) =
resampler->Process(resample_input, resample_output, false);
} else {
read = wrote = std::min(resample_input.size(), resample_output.size());
std::copy_n(resample_input.begin(), read, resample_output.begin());
}
decode_buf.readCommit(read);
resample_buf.writeCommit(wrote);
}
if (!resample_buf.isEmpty()) {
auto channels_input = resample_buf.readAcquire();
auto channels_output = stereo_buf.writeAcquire();
size_t read, wrote;
if (double_samples) {
wrote = channels_output.size();
read = wrote / 2;
if (read > channels_input.size()) {
read = channels_input.size();
wrote = read * 2;
}
for (size_t i = 0; i < read; i++) {
channels_output[i * 2] = channels_input[i];
channels_output[(i * 2) + 1] = channels_input[i];
}
} else {
read = wrote = std::min(channels_input.size(), channels_output.size());
std::copy_n(channels_input.begin(), read, channels_output.begin());
}
resample_buf.readCommit(read);
stereo_buf.writeCommit(wrote);
}
// The mixin PcmBuffer should almost always be draining, so we can force
// samples into it more aggressively than with the main music PcmBuffer.
while (!stereo_buf.isEmpty()) {
size_t sent = output_.send(stereo_buf.readAcquire());
stereo_buf.readCommit(sent);
}
}
while (!output_.isEmpty()) {
if (stream_cancelled_) {
output_.clear();
} else {
vTaskDelay(pdMS_TO_TICKS(100));
}
}
}
} // namespace tts

@ -0,0 +1,47 @@
/*
* Copyright 2024 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <string>
#include "audio/fatfs_stream_factory.hpp"
#include "codec.hpp"
#include "drivers/pcm_buffer.hpp"
#include "tasks.hpp"
namespace tts {
/*
* A TTS Player is the output stage of the TTS pipeline. It receives a stream
* of filenames that should be played, and handles decoding these files and
* sending them to the output buffer.
*/
class Player {
public:
Player(tasks::WorkerPool&, drivers::PcmBuffer&, audio::FatfsStreamFactory&);
auto playFile(const std::string& text, const std::string& path) -> void;
// Not copyable or movable.
Player(const Player&) = delete;
Player& operator=(const Player&) = delete;
private:
tasks::WorkerPool& bg_;
audio::FatfsStreamFactory& stream_factory_;
drivers::PcmBuffer& output_;
std::mutex new_stream_mutex_;
std::atomic<bool> stream_playing_;
std::atomic<bool> stream_cancelled_;
auto openAndDecode(const std::string& text, const std::string& path) -> void;
auto decodeToSink(const codecs::ICodec::OutputFormat&,
std::unique_ptr<codecs::ICodec>) -> void;
};
} // namespace tts

@ -5,21 +5,39 @@
*/ */
#include "tts/provider.hpp" #include "tts/provider.hpp"
#include <stdint.h>
#include <ios>
#include <optional> #include <optional>
#include <sstream>
#include <string> #include <string>
#include <variant> #include <variant>
#include "drivers/storage.hpp"
#include "esp_log.h" #include "esp_log.h"
#include "komihash.h"
#include "tts/events.hpp" #include "tts/events.hpp"
namespace tts { namespace tts {
[[maybe_unused]] static constexpr char kTag[] = "tts"; [[maybe_unused]] static constexpr char kTag[] = "tts";
static const char* kTtsPath = "/.tangara-tts/";
static auto textToFile(const std::string& text) -> std::optional<std::string> {
uint64_t hash = komihash(text.data(), text.size(), 0);
std::stringstream stream;
stream << kTtsPath << std::hex << hash;
return stream.str();
}
Provider::Provider() {} Provider::Provider() {}
auto Provider::player(std::unique_ptr<Player> p) -> void {
player_ = std::move(p);
}
auto Provider::feed(const Event& e) -> void { auto Provider::feed(const Event& e) -> void {
if (std::holds_alternative<SimpleEvent>(e)) { if (std::holds_alternative<SimpleEvent>(e)) {
// ESP_LOGI(kTag, "context changed"); // ESP_LOGI(kTag, "context changed");
@ -31,6 +49,19 @@ auto Provider::feed(const Event& e) -> void {
// ESP_LOGI(kTag, "new selection: '%s', interactive? %i", // ESP_LOGI(kTag, "new selection: '%s', interactive? %i",
// ev.new_selection->description.value_or("").c_str(), // ev.new_selection->description.value_or("").c_str(),
// ev.new_selection->is_interactive); // ev.new_selection->is_interactive);
auto text = ev.new_selection->description;
if (!text) {
ESP_LOGW(kTag, "missing description for element");
return;
}
auto file = textToFile(*text);
if (!file) {
return;
}
if (player_) {
player_->playFile(*text, *file);
}
} }
} }
} }

@ -6,18 +6,35 @@
#pragma once #pragma once
#include <memory>
#include <optional> #include <optional>
#include <string> #include <string>
#include <variant> #include <variant>
#include "tts/events.hpp" #include "tts/events.hpp"
#include "tts/player.hpp"
namespace tts { namespace tts {
/*
* A TTS Provider is responsible for receiving system events that may be
* relevant to TTS, and digesting them into discrete 'utterances' that can be
* used to generate audio feedback.
*/
class Provider { class Provider {
public: public:
Provider(); Provider();
auto player(std::unique_ptr<Player>) -> void;
auto feed(const Event&) -> void; auto feed(const Event&) -> void;
// Not copyable or movable.
Provider(const Provider&) = delete;
Provider& operator=(const Provider&) = delete;
private:
std::unique_ptr<Player> player_;
}; };
} // namespace tts } // namespace tts

Loading…
Cancel
Save