diff --git a/src/audio/audio_decoder.cpp b/src/audio/audio_decoder.cpp index 5a2c75c7..af026262 100644 --- a/src/audio/audio_decoder.cpp +++ b/src/audio/audio_decoder.cpp @@ -98,27 +98,27 @@ auto AudioDecoder::Process(const std::vector& inputs, while (true) { if (has_samples_to_send_) { - if (!current_output_format_) { - auto format = current_codec_->GetOutputFormat(); + auto format = current_codec_->GetOutputFormat(); + if (format.has_value()) { current_output_format_ = StreamInfo::Pcm{ - .channels = format.num_channels, - .bits_per_sample = format.bits_per_sample, - .sample_rate = format.sample_rate_hz, + .channels = format->num_channels, + .bits_per_sample = format->bits_per_sample, + .sample_rate = format->sample_rate_hz, }; - } - if (!output->prepare(*current_output_format_)) { - break; - } + if (!output->prepare(*current_output_format_)) { + break; + } - auto write_res = current_codec_->WriteOutputSamples(output->data()); - output->add(write_res.first); - has_samples_to_send_ = !write_res.second; + auto write_res = current_codec_->WriteOutputSamples(output->data()); + output->add(write_res.first); + has_samples_to_send_ = !write_res.second; - if (has_samples_to_send_) { - // We weren't able to fit all the generated samples into the output - // buffer. Stop trying; we'll finish up during the next pass. - break; + if (has_samples_to_send_) { + // We weren't able to fit all the generated samples into the output + // buffer. Stop trying; we'll finish up during the next pass. + break; + } } } diff --git a/src/audio/audio_fsm.cpp b/src/audio/audio_fsm.cpp index 3ab8b6e8..6c974905 100644 --- a/src/audio/audio_fsm.cpp +++ b/src/audio/audio_fsm.cpp @@ -6,6 +6,7 @@ #include "audio_fsm.hpp" #include +#include #include "audio_decoder.hpp" #include "audio_events.hpp" #include "audio_task.hpp" @@ -16,6 +17,8 @@ namespace audio { +static const char kTag[] = "audio_fsm"; + drivers::GpioExpander* AudioState::sGpioExpander; std::shared_ptr AudioState::sDac; std::shared_ptr AudioState::sPots; @@ -25,6 +28,8 @@ std::unique_ptr AudioState::sFileSource; std::unique_ptr AudioState::sI2SOutput; std::vector> AudioState::sPipeline; +std::deque AudioState::sSongQueue; + auto AudioState::Init(drivers::GpioExpander* gpio_expander, std::weak_ptr database) -> bool { sGpioExpander = gpio_expander; @@ -66,6 +71,33 @@ void Standby::react(const PlayFile& ev) { } } +void Playback::entry() { + ESP_LOGI(kTag, "beginning playback"); + sI2SOutput->SetInUse(true); +} + +void Playback::exit() { + ESP_LOGI(kTag, "finishing playback"); + sI2SOutput->SetInUse(false); +} + +void Playback::react(const InputFileFinished& ev) { + ESP_LOGI(kTag, "finished file"); + if (sSongQueue.empty()) { + return; + } + EnqueuedItem next_item = sSongQueue.front(); + sSongQueue.pop_front(); + + if (std::holds_alternative(next_item)) { + sFileSource->OpenFile(std::get(next_item)); + } +} + +void Playback::react(const AudioPipelineIdle& ev) { + transit(); +} + } // namespace states } // namespace audio diff --git a/src/audio/audio_task.cpp b/src/audio/audio_task.cpp index 46d527b5..9dd7d994 100644 --- a/src/audio/audio_task.cpp +++ b/src/audio/audio_task.cpp @@ -15,6 +15,8 @@ #include #include +#include "audio_events.hpp" +#include "audio_fsm.hpp" #include "audio_sink.hpp" #include "cbor.h" #include "esp_err.h" @@ -60,6 +62,7 @@ void AudioTaskMain(std::unique_ptr pipeline, IAudioSink* sink) { std::vector all_elements = pipeline->GetIterationOrder(); + bool previously_had_work = false; events::EventQueue& event_queue = events::EventQueue::GetInstance(); while (1) { // First, see if we actually have any pipeline work to do in this iteration. @@ -75,6 +78,11 @@ void AudioTaskMain(std::unique_ptr pipeline, IAudioSink* sink) { } } + if (previously_had_work && !has_work) { + events::Dispatch({}); + } + previously_had_work = has_work; + // See if there's any new events. event_queue.ServiceAudio(has_work ? delay_ticks : portMAX_DELAY); @@ -118,6 +126,7 @@ void AudioTaskMain(std::unique_ptr pipeline, IAudioSink* sink) { if (sink_stream.info().bytes_in_stream == 0) { // No new bytes to sink, so skip sinking completely. + ESP_LOGI(kTag, "no bytes to sink"); continue; } @@ -130,6 +139,7 @@ void AudioTaskMain(std::unique_ptr pipeline, IAudioSink* sink) { output_format = sink_stream.info().format; sink->Configure(*output_format); } else { + ESP_LOGI(kTag, "waiting to reconfigure"); continue; } } diff --git a/src/audio/fatfs_audio_input.cpp b/src/audio/fatfs_audio_input.cpp index 8abc7d32..9affcf1a 100644 --- a/src/audio/fatfs_audio_input.cpp +++ b/src/audio/fatfs_audio_input.cpp @@ -13,8 +13,11 @@ #include #include "arena.hpp" +#include "audio_events.hpp" +#include "audio_fsm.hpp" #include "esp_heap_caps.h" #include "esp_log.h" +#include "event_queue.hpp" #include "ff.h" #include "freertos/portmacro.h" @@ -69,6 +72,10 @@ auto FatfsAudioInput::Process(const std::vector& inputs, } std::size_t max_size = output->data().size_bytes(); + if (max_size < output->data().size_bytes() / 2) { + return; + } + std::size_t size = 0; FRESULT result = f_read(¤t_file_, output->data().data(), max_size, &size); @@ -83,6 +90,12 @@ auto FatfsAudioInput::Process(const std::vector& inputs, if (size < max_size || f_eof(¤t_file_)) { f_close(¤t_file_); is_file_open_ = false; + + // TODO(jacqueline): MP3 only + std::fill_n(output->data().begin(), 8, std::byte(0)); + output->add(8); + + events::Dispatch({}); } } diff --git a/src/audio/i2s_audio_output.cpp b/src/audio/i2s_audio_output.cpp index aab011d1..982499a0 100644 --- a/src/audio/i2s_audio_output.cpp +++ b/src/audio/i2s_audio_output.cpp @@ -38,7 +38,6 @@ I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander, attenuation_(pots_->GetMaxAttenuation()) { SetVolume(25); // For testing dac_->SetSource(buffer()); - dac_->Start(); } I2SAudioOutput::~I2SAudioOutput() { @@ -46,6 +45,15 @@ I2SAudioOutput::~I2SAudioOutput() { dac_->SetSource(nullptr); } +auto I2SAudioOutput::SetInUse(bool in_use) -> void { + if (in_use) { + dac_->Start(); + } else { + dac_->Stop(); + } + pots_->SetZeroCrossDetect(in_use); +} + auto I2SAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void { int_fast8_t new_difference = balance - left_difference_; left_difference_ = balance; @@ -124,6 +132,19 @@ auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool { ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %lu Hz", pcm.bits_per_sample, pcm.sample_rate); + drivers::I2SDac::Channels ch; + switch (pcm.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 false; + } + drivers::I2SDac::BitsPerSample bps; switch (pcm.bits_per_sample) { case 16: @@ -153,9 +174,7 @@ auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool { return false; } - // TODO(jacqueline): probs do something with the channel hey - - dac_->Reconfigure(bps, sample_rate); + dac_->Reconfigure(ch, bps, sample_rate); current_config_ = pcm; return true; diff --git a/src/audio/include/audio_events.hpp b/src/audio/include/audio_events.hpp index 765e0899..920b134e 100644 --- a/src/audio/include/audio_events.hpp +++ b/src/audio/include/audio_events.hpp @@ -24,4 +24,7 @@ struct PlaySong : tinyfsm::Event { std::optional tags; }; +struct InputFileFinished : tinyfsm::Event {}; +struct AudioPipelineIdle : tinyfsm::Event {}; + } // namespace audio diff --git a/src/audio/include/audio_fsm.hpp b/src/audio/include/audio_fsm.hpp index 6274041c..5dad87c0 100644 --- a/src/audio/include/audio_fsm.hpp +++ b/src/audio/include/audio_fsm.hpp @@ -6,6 +6,7 @@ #pragma once +#include #include #include @@ -16,6 +17,7 @@ #include "gpio_expander.hpp" #include "i2s_audio_output.hpp" #include "i2s_dac.hpp" +#include "song.hpp" #include "storage.hpp" #include "tinyfsm.hpp" @@ -40,6 +42,9 @@ class AudioState : public tinyfsm::Fsm { virtual void react(const PlaySong&) {} virtual void react(const PlayFile&) {} + virtual void react(const InputFileFinished&) {} + virtual void react(const AudioPipelineIdle&) {} + protected: static drivers::GpioExpander* sGpioExpander; static std::shared_ptr sDac; @@ -49,6 +54,9 @@ class AudioState : public tinyfsm::Fsm { static std::unique_ptr sFileSource; static std::unique_ptr sI2SOutput; static std::vector> sPipeline; + + typedef std::variant EnqueuedItem; + static std::deque sSongQueue; }; namespace states { @@ -68,6 +76,12 @@ class Standby : public AudioState { class Playback : public AudioState { public: + void entry() override; + void exit() override; + + void react(const InputFileFinished&) override; + void react(const AudioPipelineIdle&) override; + using AudioState::react; }; diff --git a/src/audio/include/audio_sink.hpp b/src/audio/include/audio_sink.hpp index 2cc706ff..e6538bda 100644 --- a/src/audio/include/audio_sink.hpp +++ b/src/audio/include/audio_sink.hpp @@ -40,6 +40,8 @@ class IAudioSink { free(metadata_); } + virtual auto SetInUse(bool) -> void {} + virtual auto SetVolumeImbalance(int_fast8_t balance) -> void = 0; virtual auto SetVolume(uint_fast8_t percent) -> void = 0; virtual auto GetVolume() -> uint_fast8_t = 0; diff --git a/src/audio/include/i2s_audio_output.hpp b/src/audio/include/i2s_audio_output.hpp index 20980573..7c125476 100644 --- a/src/audio/include/i2s_audio_output.hpp +++ b/src/audio/include/i2s_audio_output.hpp @@ -29,6 +29,8 @@ class I2SAudioOutput : public IAudioSink { std::weak_ptr pots); ~I2SAudioOutput(); + auto SetInUse(bool) -> void override; + auto SetVolumeImbalance(int_fast8_t balance) -> void override; auto SetVolume(uint_fast8_t percent) -> void override; auto GetVolume() -> uint_fast8_t override; diff --git a/src/audio/include/pipeline.hpp b/src/audio/include/pipeline.hpp index c1f6bf59..bb773006 100644 --- a/src/audio/include/pipeline.hpp +++ b/src/audio/include/pipeline.hpp @@ -36,6 +36,10 @@ class Pipeline { auto GetIterationOrder() -> std::vector; + // Not copyable or movable. + Pipeline(const Pipeline&) = delete; + Pipeline& operator=(const Pipeline&) = delete; + private: IAudioElement* root_; std::vector> subtrees_; diff --git a/src/audio/stream_info.cpp b/src/audio/stream_info.cpp index ee85f35d..8fc31530 100644 --- a/src/audio/stream_info.cpp +++ b/src/audio/stream_info.cpp @@ -22,7 +22,8 @@ namespace audio { void InputStream::consume(std::size_t bytes) const { assert(raw_->info->bytes_in_stream >= bytes); - auto new_data = raw_->data.subspan(bytes); + auto new_data = + raw_->data.subspan(bytes, raw_->info->bytes_in_stream - bytes); std::move(new_data.begin(), new_data.end(), raw_->data.begin()); raw_->info->bytes_in_stream = new_data.size_bytes(); } diff --git a/src/codecs/include/codec.hpp b/src/codecs/include/codec.hpp index 9dd717c9..c8a68ff3 100644 --- a/src/codecs/include/codec.hpp +++ b/src/codecs/include/codec.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -32,7 +33,7 @@ class ICodec { uint32_t sample_rate_hz; }; - virtual auto GetOutputFormat() -> OutputFormat = 0; + virtual auto GetOutputFormat() -> std::optional = 0; enum ProcessingError { MALFORMED_DATA }; diff --git a/src/codecs/include/mad.hpp b/src/codecs/include/mad.hpp index 3b1f5757..ea16cdc8 100644 --- a/src/codecs/include/mad.hpp +++ b/src/codecs/include/mad.hpp @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -24,7 +25,7 @@ class MadMp3Decoder : public ICodec { ~MadMp3Decoder(); auto CanHandleType(StreamType type) -> bool override; - auto GetOutputFormat() -> OutputFormat override; + auto GetOutputFormat() -> std::optional override; auto ResetForNewStream() -> void override; auto SetInput(cpp::span input) -> void override; auto GetInputPosition() -> std::size_t override; @@ -37,9 +38,6 @@ class MadMp3Decoder : public ICodec { mad_frame frame_; mad_synth synth_; - mad_header header_; - bool has_decoded_header_; - int current_sample_; }; diff --git a/src/codecs/mad.cpp b/src/codecs/mad.cpp index 2e55d4c6..5044c22f 100644 --- a/src/codecs/mad.cpp +++ b/src/codecs/mad.cpp @@ -8,6 +8,7 @@ #include #include +#include #include "mad.h" @@ -34,31 +35,29 @@ MadMp3Decoder::MadMp3Decoder() { mad_stream_init(&stream_); mad_frame_init(&frame_); mad_synth_init(&synth_); - mad_header_init(&header_); } MadMp3Decoder::~MadMp3Decoder() { mad_stream_finish(&stream_); mad_frame_finish(&frame_); mad_synth_finish(&synth_); - mad_header_finish(&header_); } auto MadMp3Decoder::CanHandleType(StreamType type) -> bool { return type == STREAM_MP3; } -auto MadMp3Decoder::GetOutputFormat() -> OutputFormat { - return OutputFormat{ +auto MadMp3Decoder::GetOutputFormat() -> std::optional { + if (synth_.pcm.channels == 0 || synth_.pcm.samplerate == 0) { + return {}; + } + return std::optional({ .num_channels = static_cast(synth_.pcm.channels), - .bits_per_sample = 16, - .sample_rate_hz = - synth_.pcm.samplerate == 0 ? 44100 : synth_.pcm.samplerate, - }; + .bits_per_sample = 24, + .sample_rate_hz = synth_.pcm.samplerate, + }); } -auto MadMp3Decoder::ResetForNewStream() -> void { - has_decoded_header_ = false; -} +auto MadMp3Decoder::ResetForNewStream() -> void {} auto MadMp3Decoder::SetInput(cpp::span input) -> void { mad_stream_buffer(&stream_, @@ -71,16 +70,6 @@ auto MadMp3Decoder::GetInputPosition() -> std::size_t { } auto MadMp3Decoder::ProcessNextFrame() -> cpp::result { - if (!has_decoded_header_) { - // The header of any given frame should be representative of the - // entire stream, so only need to read it once. - mad_header_decode(&header_, &stream_); - has_decoded_header_ = true; - - // TODO: Use the info in the header for something. I think the - // duration will help with seeking? - } - // Whatever was last synthesized is now invalid, so ensure we don't try to // send it. current_sample_ = -1; @@ -128,7 +117,6 @@ auto MadMp3Decoder::WriteOutputSamples(cpp::span output) for (int channel = 0; channel < synth_.pcm.channels; channel++) { // TODO(jacqueline): output 24 bit samples when (if?) we have a downmix // step in the pipeline. - /* uint32_t sample_24 = scaleToBits(synth_.pcm.samples[channel][current_sample_], 24); output[output_byte++] = static_cast((sample_24 >> 16) & 0xFF); @@ -136,11 +124,12 @@ auto MadMp3Decoder::WriteOutputSamples(cpp::span output) output[output_byte++] = static_cast((sample_24)&0xFF); // 24 bit samples must still be aligned to 32 bits. The LSB is ignored. output[output_byte++] = static_cast(0); - */ + /* uint16_t sample_16 = scaleToBits(synth_.pcm.samples[channel][current_sample_], 16); output[output_byte++] = static_cast((sample_16 >> 8) & 0xFF); output[output_byte++] = static_cast((sample_16)&0xFF); + */ } current_sample_++; } diff --git a/src/drivers/display.cpp b/src/drivers/display.cpp index dd4cecb8..3c0df978 100644 --- a/src/drivers/display.cpp +++ b/src/drivers/display.cpp @@ -126,7 +126,7 @@ auto Display::Create(GpioExpander* expander, ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0)); ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0)); - ledc_fade_func_install(0); + // ledc_fade_func_install(0); // Next, init the SPI device spi_device_interface_config_t spi_cfg = { @@ -194,8 +194,9 @@ auto Display::SetDisplayOn(bool enabled) -> void { display_on_ = enabled; int new_duty = display_on_ ? brightness_ : 0; - ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, new_duty, 250); - ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT); + // ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, new_duty, + // 250); ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, + // LEDC_FADE_NO_WAIT); } void Display::SendInitialisationSequence(const uint8_t* data) { diff --git a/src/drivers/i2s_dac.cpp b/src/drivers/i2s_dac.cpp index 68e92970..dbbb58f1 100644 --- a/src/drivers/i2s_dac.cpp +++ b/src/drivers/i2s_dac.cpp @@ -116,24 +116,30 @@ auto I2SDac::Stop() -> void { i2s_active_ = false; } -auto I2SDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void { +auto I2SDac::Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate) + -> void { if (i2s_active_) { i2s_channel_disable(i2s_handle_); } - uint8_t bps_bits = 0; + switch (ch) { + case CHANNELS_MONO: + slot_config_.slot_mode = I2S_SLOT_MODE_MONO; + break; + case CHANNELS_STEREO: + slot_config_.slot_mode = I2S_SLOT_MODE_STEREO; + break; + } + switch (bps) { case BPS_16: slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT; - bps_bits = 0; break; case BPS_24: slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_24BIT; - bps_bits = 0b10; break; case BPS_32: slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT; - bps_bits = 0b11; break; } ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(i2s_handle_, &slot_config_)); diff --git a/src/drivers/include/display.hpp b/src/drivers/include/display.hpp index b394dd9e..4b63e1c4 100644 --- a/src/drivers/include/display.hpp +++ b/src/drivers/include/display.hpp @@ -43,6 +43,10 @@ class Display { const lv_area_t* area, lv_color_t* color_map); + // Not copyable or movable. + Display(const Display&) = delete; + Display& operator=(const Display&) = delete; + private: GpioExpander* gpio_; spi_device_handle_t handle_; diff --git a/src/drivers/include/i2s_dac.hpp b/src/drivers/include/i2s_dac.hpp index 42c094b1..388d09fa 100644 --- a/src/drivers/include/i2s_dac.hpp +++ b/src/drivers/include/i2s_dac.hpp @@ -40,6 +40,10 @@ class I2SDac { auto Start() -> void; auto Stop() -> void; + enum Channels { + CHANNELS_MONO, + CHANNELS_STEREO, + }; enum BitsPerSample { BPS_16 = I2S_DATA_BIT_WIDTH_16BIT, BPS_24 = I2S_DATA_BIT_WIDTH_24BIT, @@ -56,7 +60,7 @@ class I2SDac { SAMPLE_RATE_192 = 192000, }; - auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void; + auto Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate) -> void; auto WriteData(const cpp::span& data) -> void; auto SetSource(StreamBufferHandle_t buffer) -> void;