diff --git a/src/drivers/CMakeLists.txt b/src/drivers/CMakeLists.txt index 3b3c4a65..899fcf79 100644 --- a/src/drivers/CMakeLists.txt +++ b/src/drivers/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( SRCS "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" - "playback.cpp" "display.cpp" "display-init.cpp" "spi.cpp" + "audio_playback.cpp" "i2s_audio_output.cpp" "display.cpp" "display-init.cpp" "spi.cpp" INCLUDE_DIRS "include" REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result" "lvgl") target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) diff --git a/src/drivers/audio_playback.cpp b/src/drivers/audio_playback.cpp new file mode 100644 index 00000000..b38d0bd3 --- /dev/null +++ b/src/drivers/audio_playback.cpp @@ -0,0 +1,295 @@ +#include "audio_playback.hpp" + +#include "audio_output.hpp" +#include "dac.hpp" + +#include +#include +#include +#include +#include + +#include "audio_element.h" +#include "audio_event_iface.h" +#include "audio_pipeline.h" +#include "driver/i2s.h" +#include "esp_err.h" +#include "freertos/portmacro.h" +#include "mp3_decoder.h" + +static const char* kTag = "PLAYBACK"; +static const char* kSource = "src"; +static const char* kEncoder = "enc"; +static const char* kSink = "sink"; + +static bool endsWith(std::string_view str, std::string_view suffix) { + return str.size() >= suffix.size() && 0 == str.compare(str.size()-suffix.size(), suffix.size(), suffix); +} + +static void toLower(std::string &str) { + std::transform(str.begin(), str.end(), str.begin(), + [](unsigned char c) { return std::tolower(c); }); +} + +namespace drivers { + +static audio_element_status_t status_from_the_void(void* status) { + uintptr_t as_pointer_int = reinterpret_cast(status); + return static_cast(as_pointer_int); +} + +auto AudioPlayback::create(std::unique_ptr output) + -> cpp::result, Error> { + audio_pipeline_handle_t pipeline; + audio_element_handle_t fatfs_stream_reader; + audio_event_iface_handle_t event_interface; + + audio_pipeline_cfg_t pipeline_config = + audio_pipeline_cfg_t(DEFAULT_AUDIO_PIPELINE_CONFIG()); + pipeline = audio_pipeline_init(&pipeline_config); + if (pipeline == NULL) { + return cpp::fail(Error::PIPELINE_INIT); + } + + fatfs_stream_cfg_t fatfs_stream_config = + fatfs_stream_cfg_t(FATFS_STREAM_CFG_DEFAULT()); + fatfs_stream_config.type = AUDIO_STREAM_READER; + fatfs_stream_reader = fatfs_stream_init(&fatfs_stream_config); + if (fatfs_stream_reader == NULL) { + return cpp::fail(Error::FATFS_INIT); + } + + audio_event_iface_cfg_t event_config = AUDIO_EVENT_IFACE_DEFAULT_CFG(); + event_interface = audio_event_iface_init(&event_config); + + audio_pipeline_set_listener(pipeline, event_interface); + audio_element_msg_set_listener(fatfs_stream_reader, event_interface); + audio_element_msg_set_listener(output->GetAudioElement(), event_interface); + + audio_pipeline_register(pipeline, fatfs_stream_reader, kSource); + audio_pipeline_register(pipeline, outut->GetAudioElement(), kSink); + + + return std::make_unique(output, pipeline, fatfs_stream_reader, event_interface +} + +AudioPlayback::AudioPlayback(std::unique_ptr output, + audio_pipeline_handle_t pipeline, + audio_element_handle_t source_element, + audio_event_iface_handle_t event_interface, + audio_element_handle_t mp3_decoder) + : output_(std::move(outout)), + pipeline_(pipeline), + source_element_(source_element), + event_interface_(event_interface) {} + +AudioPlayback::~AudioPlayback() { + audio_pipeline_remove_listener(pipeline_); + audio_element_msg_remove_listener(source_element_, event_interface_); + audio_element_msg_remove_listener(output_->GetAudioElement(), event_interface_); + + audio_pipeline_stop(pipeline_); + audio_pipeline_wait_for_stop(pipeline_); + audio_pipeline_terminate(pipeline_); + + ReconfigurePipeline(NONE); + + audio_pipeline_unregister(pipeline_, source_element_); + audio_pipeline_unregister(pipeline_, output_->GetAudioElement()); + + audio_event_iface_destroy(event_interface_); + + audio_pipeline_deinit(pipeline_); + audio_element_deinit(source_element_); +} + +void AudioPlayback::Play(const std::string& filename) { + if (GetPlaybackState() != STOPPED) { + audio_pipeline_stop(pipeline_); + audio_pipeline_wait_for_stop(pipeline_); + audio_pipeline_terminate(pipeline_); + } + + current_state_ = PLAYING; + Decoder decoder = GetDecoderForFilename(filename); + ReconfigurePipeline(decoder); + audio_element_set_uri(source_element_, filename.c_str()); + audio_pipeline_reset_ringbuffer(pipeline_); + audio_pipeline_reset_elements(pipeline_); + audio_pipeline_run(pipeline_); + dac_->WriteVolume(volume_); +} + +void AudioPlayback::Resume() { + if (GetPlaybackState() == PAUSED) { + current_state_ = PLAYING; + audio_pipeline_resume(pipeline_); + } +} +void AudioPlayback::Pause() { + if (GetPlaybackState() == PLAYING) { + current_state_ = PAUSED; + audio_pipeline_pause(pipeline_); + } +} + +auto AudioPlayback::GetPlaybackState() -> PlaybackState { + return current_state_; +} + +void AudioPlayback::ProcessEvents(uint16_t max_time_ms) { + if (current_state_ == STOPPED) { + return; + } + while (1) { + audio_event_iface_msg_t event; + esp_err_t err = + audio_event_iface_listen(event_interface_, &event, pdMS_TO_TICKS(max_time_ms)); + if (err != ESP_OK) { + ESP_LOGE(kTag, "error listening for event:%x", err); + continue; + } + + if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && + event.source == (void*)decoder_ && + event.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) { + audio_element_info_t music_info; + audio_element_getinfo(decoder_, &music_info); + ESP_LOGI(kTag, "sample_rate=%d, bits=%d, ch=%d", music_info.sample_rates, + music_info.bits, music_info.channels); + } + + if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && + event.source == (void*)source_element_ && + event.cmd == AEL_MSG_CMD_REPORT_STATUS) { + audio_element_status_t status = status_from_the_void(event.data); + if (status == AEL_STATUS_STATE_FINISHED) { + // TODO: Could we change the uri here? hmm. + } + } + + if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && + event.source == (void*)output_->GetAudioElement() && + event.cmd == AEL_MSG_CMD_REPORT_STATUS) { + audio_element_status_t status = status_from_the_void(event.data); + if (status == AEL_STATUS_STATE_FINISHED) { + if (next_filename_ != "") { + Decoder decoder = GetDecoderForFilename(next_filename_); + if (decoder == decoder_type_) { + audio_element_set_uri(source_element_, next_filename_); + audio_pipeline_reset_ringbuffer(pipeline_); + audio_pipeline_reset_elements(pipeline_); + audio_pipeline_change_state(pipeline_, AEL_STATE_INIT); + audio_pipeline_run(pipeline_); + } else { + Play(next_filename_); + } + next_filename_ = ""; + } else { + audio_pipeline_stop(pipeline_); + audio_pipeline_wait_for_stop(pipeline_); + audio_pipeline_terminate(pipeline_); + current_state_ = STOPPED; + } + return; + } + } + + if (event.need_free_data) { + ESP_LOGI(kTag, "freeing event data"); + free(event.data); + } + } +} + +void AudioPlayback::set_next_file(const std::string& filename) { + next_filename_ = filename; +} + +void AudioPlayback::set_volume(uint8_t volume) { + volume_ = volume; + // TODO: don't write immediately if we're muted to change track or similar. + output_->SetVolume(volume); +} + +auto AudioPlayback::volume() -> uint8_t { + return volume_; +} + +auto AudioPlayback::GetDecoderForFilename(std::string filename) -> Decoder { + toLower(filename); + if (endsWith(filename, "mp3")) { + return MP3; + } + if (endsWith(filename, "amr") || endsWith(filename, "wamr") { + return AMR; + } + if (endsWith(filename, "opus")) { + return OPUS; + } + if (endsWith(filename, "ogg")) { + return OGG; + } + if (endsWith(filename, "flac")) { + return FLAC; + } + if (endsWith(filename, "wav")) { + return WAV; + } + if (endsWith(filename, "aac") || endsWith(filename, "m4a") || endsWith(filename, "ts") || endsWith(filename, "mp4")) { + return AAC; + } + return NONE; +} + +auto AudioPlayback::CreateDecoder(Decoder decoder) -> audio_element_handle_t { + switch (decoder) { + case MP3: + mp3_decoder_cfg_t config = DEFAULT_MP3_DECODER_CONFIG(); + return mp3_decoder_init(&config); + case AMR: + amr_decoder_cfg_t config = DEFAULT_AMR_DECODER_CONFIG(); + return amr_decoder_init(&config); + case OPUS: + opus_decoder_cfg_t config = DEFAULT_OPUS_DECODER_CONFIG(); + return decoder_opus_init(&config); + case OGG: + ogg_decoder_cfg_t config = DEFAULT_OGG_DECODER_CONFIG(); + return ogg_decoder_init(&config); + case FLAC: + flac_decoder_cfg_t config = DEFAULT_FLAC_DECODER_CONFIG(); + return flac_decoder_init(&config); + case WAV: + wav_decoder_cfg_t config = DEFAULT_WAV_DECODER_CONFIG(); + return wav_decoder_init(&config); + case AAC: + aac_decoder_cfg_t aac_dec_cfg = DEFAULT_AAC_DECODER_CONFIG(); + return aac_decoder_init(&aac_dec_cfg); + default: + return nullptr; + } +} + +void AudioPlayback::ReconfigurePipeline(Decoder decoder) { + if (decoder_type_ == decoder) { + return; + } + + if (decoder_type_ != NONE) { + audio_pipeline_unlink(pipeline); + audio_element_msg_remove_listener(decoder_, event_interface_); + audio_pipeline_unregister(pipeline_, decoder_); + audio_element_deinit(decoder_); + } + + if (decoder != NONE) { + decoder_ = CreateDecoder(decoder); + decoder_type_ = decoder; + audio_pipeline_register(pipeline_, decoder_, kDecoder); + audio_element_msg_set_listener(decoder_, event_interface_); + static const char* link_tag[3] = {kSource, kDecoder, kSink}; + audio_pipeline_link(pipeline, &link_tag[0], 3); + } +} + +} // namespace drivers diff --git a/src/drivers/i2s_audio_output.cpp b/src/drivers/i2s_audio_output.cpp new file mode 100644 index 00000000..6b231f0e --- /dev/null +++ b/src/drivers/i2s_audio_output.cpp @@ -0,0 +1,92 @@ +#include "i2s_audio_output.hpp" +#include +#include "audio_output.hpp" +#include "gpio-expander.hpp" + +static const i2s_port_t kI2SPort = I2S_NUM_0; + +namespace drivers { + +auto I2SAudioOutput::create(GpioExpander *expander) + -> cpp::result, Error> { + + // First, we need to perform initial configuration of the DAC chip. + auto dac_result = drivers::AudioDac::create(expander); + if (dac_result.has_error()) { + ESP_LOGE(TAG, "failed to init dac: %d", dac_result.error()); + return cpp::fail(DAC_CONFIG); + } + std::unique_ptr dac = std::move(dac_result.value()); + + // Soft mute immediately, in order to minimise any clicks and pops caused by + // the initial output element and pipeline configuration. + dac->WriteVolume(255); + + i2s_stream_cfg_t i2s_stream_config = i2s_stream_cfg_t{ + .type = AUDIO_STREAM_WRITER, + .i2s_config = + { + // static_cast bc esp-adf uses enums incorrectly + .mode = static_cast(I2S_MODE_MASTER | I2S_MODE_TX), + .sample_rate = 44100, + .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, + .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, + .communication_format = I2S_COMM_FORMAT_STAND_I2S, + .intr_alloc_flags = ESP_INTR_FLAG_LOWMED, + .dma_buf_count = 8, + .dma_buf_len = 64, + .use_apll = false, + .tx_desc_auto_clear = false, + .fixed_mclk = 0, + .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, + .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, + }, + .i2s_port = kI2SPort, + .use_alc = false, + .volume = 0, // Does nothing; use AudioDac to change this. + .out_rb_size = I2S_STREAM_RINGBUFFER_SIZE, + .task_stack = I2S_STREAM_TASK_STACK, + .task_core = I2S_STREAM_TASK_CORE, + .task_prio = I2S_STREAM_TASK_PRIO, + .stack_in_ext = false, + .multi_out_num = 0, + .uninstall_drv = true, + .need_expand = false, + .expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT, + }; + i2s_stream_writer = i2s_stream_init(&i2s_stream_config); + if (i2s_stream_writer == NULL) { + return cpp::fail(Error::STREAM_INIT); + } + + // NOTE: i2s_stream_init does some additional setup that hardcodes MCK as + // GPIO0. This happens to work fine for us, but be careful if changing. + i2s_pin_config_t pin_config = {.mck_io_num = GPIO_NUM_0, + .bck_io_num = GPIO_NUM_26, + .ws_io_num = GPIO_NUM_27, + .data_out_num = GPIO_NUM_5, + .data_in_num = I2S_PIN_NO_CHANGE}; + if (esp_err_t err = i2s_set_pin(kI2SPort, &pin_config) != ESP_OK) { + ESP_LOGE(kTag, "failed to configure i2s pins %x", err); + return cpp::fail(Error::I2S_CONFIG); + } + + return std::make_unique(dac, i2s_stream_writer); +} + +I2SAudioOutput(std::unique dac, audio_element_handle_t element) : IAudioOutput(element), dac_(dac) {} +~I2SAudioOutput() { + // TODO: power down the DAC. +} + +auto I2SAudioOutput::SetVolume(uint8_t volume) -> void { + dac_->WriteVolume(255); +} + +auto I2SAudioOutput::Configure(audio_element_info_t info) -> void { + audio_element_setinfo(element_, &music_info); + i2s_stream_set_clk(element_, music_info.sample_rates, + music_info.bits, music_info.channels); +} + +} diff --git a/src/drivers/include/a2dp_audio_output.hpp b/src/drivers/include/a2dp_audio_output.hpp new file mode 100644 index 00000000..43b55956 --- /dev/null +++ b/src/drivers/include/a2dp_audio_output.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include "audio_common.h" +#include "audio_element.h" +#include "audio_output.hpp" +#include + +namespace drivers { + +class A2DPAudioOutput : IAudioOutput { + public: + virtual auto SetVolume(uint8_t volume) -> void; +}; + +} // namespace drivers diff --git a/src/drivers/include/audio_output.hpp b/src/drivers/include/audio_output.hpp new file mode 100644 index 00000000..63cba465 --- /dev/null +++ b/src/drivers/include/audio_output.hpp @@ -0,0 +1,27 @@ +#pragma once + +#include "audio_common.h" +#include "audio_element.h" +#include + +namespace drivers { + +class IAudioOutput { + public: + IAudioOutput(audio_element_handle_t element) : element_(element) {} + virtual ~IAudioOutput() { + audio_element_deinit(element_); + } + + auto GetAudioElement() -> audio_element_handle_t { + return element_; + } + + virtual auto SetVolume(uint8_t volume) -> void = 0; + virtual auto Configure(audio_element_info_t info) -> void = 0; + + protected: + audio_element_handle_t element_; +}; + +} // namespace drivers diff --git a/src/drivers/include/audio_playback.hpp b/src/drivers/include/audio_playback.hpp new file mode 100644 index 00000000..dd0f7f7a --- /dev/null +++ b/src/drivers/include/audio_playback.hpp @@ -0,0 +1,99 @@ +#pragma once + +#include "audio_output.hpp" +#include "dac.hpp" +#include "storage.hpp" + +#include +#include +#include + +#include "audio_common.h" +#include "audio_element.h" +#include "audio_event_iface.h" +#include "audio_pipeline.h" +#include "esp_err.h" +#include "fatfs_stream.h" +#include "i2s_stream.h" +#include "mp3_decoder.h" +#include "result.hpp" + +namespace drivers { + +/* + * Sends an I2S audio stream to the DAC. Includes basic controls for pausing + * and resuming the stream, as well as support for gapless playback of the next + * queued song, but does not implement any kind of sophisticated queing or + * playback control; these should be handled at a higher level. + */ +class AudioPlayback { + public: + enum Error { FATFS_INIT, I2S_INIT, PIPELINE_INIT }; + static auto create(std::unique_ptr output) + -> cpp::result, Error>; + + AudioPlayback(std::unqiue_ptr output, + audio_pipeline_handle_t pipeline, + audio_element_handle_t source_element, + audio_event_iface_handle_t event_interface); + ~AudioPlayback(); + + /* + * Replaces any currently playing file with the one given, and begins + * playback. + * + * Any value set in `set_next_file` is cleared by this method. + */ + void Play(const std::string& filename); + /* Toogle between resumed and paused. */ + void Toggle(); + void Resume(); + void Pause(); + + enum PlaybackState { PLAYING, PAUSED, STOPPED }; + auto GetPlaybackState() -> PlaybackState; + + /* + * Handles any pending events from the underlying audio pipeline. This must + * be called regularly in order to handle configuring the I2S stream for + * different audio types (e.g. sample rate, bit depth), and for gapless + * playback. + */ + void ProcessEvents(uint16_t max_time_ms); + + /* + * Sets the file that should be played immediately after the current file + * finishes. This is used for gapless playback + */ + void set_next_file(const std::string& filename); + + void set_volume(uint8_t volume); + auto volume() -> uint8_t; + + // Not copyable or movable. + AudioPlayback(const AudioPlayback&) = delete; + AudioPlayback& operator=(const AudioPlayback&) = delete; + + private: + PlaybackState current_state_; + + enum Decoder {NONE, MP3, AMR, OPUS, OGG, FLAC, WAV, AAC}; + auto GetDecoderForFilename(std::string filename) -> Decoder; + auto CreateDecoder(Decoder decoder) -> audio_element_handle_t; + void ReconfigurePipeline(); + + std::unique_ptr output_; + std::mutex playback_lock_; + + std::string next_filename_ = ""; + uint8_t volume_; + + audio_pipeline_handle_t pipeline_; + audio_element_handle_t source_element_; + audio_event_iface_handle_t event_interface_; + + audio_element_handle_t decoder_ = nullptr; + Decoder decoder_type_ = NONE; +}; + +} // namespace drivers diff --git a/src/drivers/include/i2s_audio_output.hpp b/src/drivers/include/i2s_audio_output.hpp new file mode 100644 index 00000000..531bddbc --- /dev/null +++ b/src/drivers/include/i2s_audio_output.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "audio_common.h" +#include "audio_element.h" +#include "audio_output.hpp" +#include "gpio-expander.hpp" +#include +#include +#include "result.hpp" +#include "dac.hpp" + +namespace drivers { + +class I2SAudioOutput : public IAudioOutput { + public: + enum Error { DAC_CONFIG, I2S_CONFIG, STREAM_INIT }; + static auto create(GpioExpander* expander) + -> cpp::result, Error>; + + I2SAudioOutput(AudioDac* dac, audio_element_handle_t element); + ~I2SAudioOutput(); + + virtual auto SetVolume(uint8_t volume) -> void; + virtual auto Configure(audio_element_info_t info) -> void; + + private: + std::unique_ptr dac_; +}; + +} // namespace drivers diff --git a/src/drivers/include/playback.hpp b/src/drivers/include/playback.hpp deleted file mode 100644 index 5fa7ab38..00000000 --- a/src/drivers/include/playback.hpp +++ /dev/null @@ -1,67 +0,0 @@ -#pragma once - -#include "dac.hpp" -#include "storage.hpp" - -#include -#include -#include - -#include "audio_common.h" -#include "audio_element.h" -#include "audio_event_iface.h" -#include "audio_pipeline.h" -#include "esp_err.h" -#include "fatfs_stream.h" -#include "i2s_stream.h" -#include "mp3_decoder.h" -#include "result.hpp" - -namespace drivers { - -class DacAudioPlayback { - public: - enum Error { PIPELINE_INIT }; - static auto create(AudioDac* dac) - -> cpp::result, Error>; - - DacAudioPlayback(AudioDac* dac, - audio_pipeline_handle_t pipeline, - audio_element_handle_t fatfs_stream_reader, - audio_element_handle_t i2s_stream_writer, - audio_event_iface_handle_t event_interface, - audio_element_handle_t mp3_decoder); - ~DacAudioPlayback(); - - void Play(const std::string& filename); - void Resume(); - void Pause(); - - void ProcessEvents(); - - /* for gapless */ - void set_next_file(const std::string& filename); - - void set_volume(uint8_t volume); - auto volume() -> uint8_t; - - // Not copyable or movable. - DacAudioPlayback(const DacAudioPlayback&) = delete; - DacAudioPlayback& operator=(const DacAudioPlayback&) = delete; - - private: - AudioDac* dac_; - std::mutex playback_lock_; - - std::string next_filename_; - uint8_t volume_; - - audio_pipeline_handle_t pipeline_; - audio_element_handle_t fatfs_stream_reader_; - audio_element_handle_t i2s_stream_writer_; - audio_event_iface_handle_t event_interface_; - - audio_element_handle_t mp3_decoder_; -}; - -} // namespace drivers diff --git a/src/drivers/playback.cpp b/src/drivers/playback.cpp deleted file mode 100644 index a9290613..00000000 --- a/src/drivers/playback.cpp +++ /dev/null @@ -1,246 +0,0 @@ -#include "playback.hpp" - -#include "dac.hpp" - -#include - -#include "audio_element.h" -#include "audio_event_iface.h" -#include "audio_pipeline.h" -#include "driver/i2s.h" -#include "esp_err.h" -#include "freertos/portmacro.h" -#include "mp3_decoder.h" - -static const char* kTag = "PLAYBACK"; -static const i2s_port_t kI2SPort = I2S_NUM_0; - -namespace drivers { - -static audio_element_status_t status_from_the_void(void* status) { - uintptr_t as_pointer_int = reinterpret_cast(status); - return static_cast(as_pointer_int); -} - -auto DacAudioPlayback::create(AudioDac* dac) - -> cpp::result, Error> { - // Ensure we're soft-muted before initialising, in order to reduce protential - // clicks and pops. - dac->WriteVolume(255); - - audio_pipeline_handle_t pipeline; - audio_element_handle_t fatfs_stream_reader; - audio_element_handle_t i2s_stream_writer; - audio_event_iface_handle_t event_interface; - - audio_pipeline_cfg_t pipeline_config = - audio_pipeline_cfg_t(DEFAULT_AUDIO_PIPELINE_CONFIG()); - pipeline = audio_pipeline_init(&pipeline_config); - if (pipeline == NULL) { - return cpp::fail(Error::PIPELINE_INIT); - } - - fatfs_stream_cfg_t fatfs_stream_config = - fatfs_stream_cfg_t(FATFS_STREAM_CFG_DEFAULT()); - fatfs_stream_config.type = AUDIO_STREAM_READER; - fatfs_stream_reader = fatfs_stream_init(&fatfs_stream_config); - if (fatfs_stream_reader == NULL) { - return cpp::fail(Error::PIPELINE_INIT); - } - - i2s_stream_cfg_t i2s_stream_config = i2s_stream_cfg_t{ - .type = AUDIO_STREAM_WRITER, - .i2s_config = - { - // static_cast bc esp-adf uses enums incorrectly - .mode = static_cast(I2S_MODE_MASTER | I2S_MODE_TX), - .sample_rate = 44100, - .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, - .channel_format = I2S_CHANNEL_FMT_RIGHT_LEFT, - .communication_format = I2S_COMM_FORMAT_STAND_I2S, - .intr_alloc_flags = ESP_INTR_FLAG_LOWMED, - .dma_buf_count = 8, - .dma_buf_len = 64, - .use_apll = false, - .tx_desc_auto_clear = false, - .fixed_mclk = 0, - .mclk_multiple = I2S_MCLK_MULTIPLE_DEFAULT, - .bits_per_chan = I2S_BITS_PER_CHAN_DEFAULT, - }, - .i2s_port = kI2SPort, - .use_alc = false, - .volume = 0, // Does nothing; use AudioDac to change this. - .out_rb_size = I2S_STREAM_RINGBUFFER_SIZE, - .task_stack = I2S_STREAM_TASK_STACK, - .task_core = I2S_STREAM_TASK_CORE, - .task_prio = I2S_STREAM_TASK_PRIO, - .stack_in_ext = false, - .multi_out_num = 0, - .uninstall_drv = true, - .need_expand = false, - .expand_src_bits = I2S_BITS_PER_SAMPLE_16BIT, - }; - i2s_stream_writer = i2s_stream_init(&i2s_stream_config); - if (i2s_stream_writer == NULL) { - return cpp::fail(Error::PIPELINE_INIT); - } - - // NOTE: i2s_stream_init does some additional setup that hardcodes MCK as - // GPIO0. This happens to work fine for us, but be careful if changing. - i2s_pin_config_t pin_config = {.mck_io_num = GPIO_NUM_0, - .bck_io_num = GPIO_NUM_26, - .ws_io_num = GPIO_NUM_27, - .data_out_num = GPIO_NUM_5, - .data_in_num = I2S_PIN_NO_CHANGE}; - if (esp_err_t err = i2s_set_pin(kI2SPort, &pin_config) != ESP_OK) { - ESP_LOGE(kTag, "failed to configure i2s pins %x", err); - return cpp::fail(Error::PIPELINE_INIT); - } - - // TODO: Create encoders dynamically when we need them. - audio_element_handle_t mp3_decoder; - mp3_decoder_cfg_t mp3_config = - mp3_decoder_cfg_t(DEFAULT_MP3_DECODER_CONFIG()); - mp3_decoder = mp3_decoder_init(&mp3_config); - assert(mp3_decoder != NULL); - - audio_event_iface_cfg_t event_config = AUDIO_EVENT_IFACE_DEFAULT_CFG(); - event_interface = audio_event_iface_init(&event_config); - - audio_pipeline_set_listener(pipeline, event_interface); - audio_element_msg_set_listener(fatfs_stream_reader, event_interface); - audio_element_msg_set_listener(mp3_decoder, event_interface); - audio_element_msg_set_listener(i2s_stream_writer, event_interface); - - // TODO: most of this is likely post-init, since it involves a decoder. - // All the elements of our pipeline have been initialised. Now switch them - // together. - audio_pipeline_register(pipeline, fatfs_stream_reader, "file"); - audio_pipeline_register(pipeline, mp3_decoder, "dec"); - audio_pipeline_register(pipeline, i2s_stream_writer, "i2s"); - - const char* link_tag[3] = {"file", "dec", "i2s"}; - audio_pipeline_link(pipeline, &link_tag[0], 3); - - return std::make_unique(dac, pipeline, fatfs_stream_reader, - i2s_stream_writer, event_interface, - mp3_decoder); -} - -DacAudioPlayback::DacAudioPlayback(AudioDac* dac, - audio_pipeline_handle_t pipeline, - audio_element_handle_t fatfs_stream_reader, - audio_element_handle_t i2s_stream_writer, - audio_event_iface_handle_t event_interface, - audio_element_handle_t mp3_decoder) - : dac_(dac), - pipeline_(pipeline), - fatfs_stream_reader_(fatfs_stream_reader), - i2s_stream_writer_(i2s_stream_writer), - event_interface_(event_interface), - mp3_decoder_(mp3_decoder) {} - -DacAudioPlayback::~DacAudioPlayback() { - dac_->WriteVolume(255); - - audio_pipeline_remove_listener(pipeline_); - audio_element_msg_remove_listener(fatfs_stream_reader_, event_interface_); - audio_element_msg_remove_listener(mp3_decoder_, event_interface_); - audio_element_msg_remove_listener(i2s_stream_writer_, event_interface_); - - audio_pipeline_stop(pipeline_); - audio_pipeline_wait_for_stop(pipeline_); - audio_pipeline_terminate(pipeline_); - - audio_pipeline_unregister(pipeline_, fatfs_stream_reader_); - audio_pipeline_unregister(pipeline_, mp3_decoder_); - audio_pipeline_unregister(pipeline_, i2s_stream_writer_); - - audio_event_iface_destroy(event_interface_); - - audio_pipeline_deinit(pipeline_); - audio_element_deinit(fatfs_stream_reader_); - audio_element_deinit(i2s_stream_writer_); - audio_element_deinit(mp3_decoder_); -} - -void DacAudioPlayback::Play(const std::string& filename) { - dac_->WriteVolume(255); - // TODO: handle reconfiguring the pipeline if needed. - audio_element_set_uri(fatfs_stream_reader_, filename.c_str()); - audio_pipeline_run(pipeline_); - dac_->WriteVolume(volume_); -} - -void DacAudioPlayback::Resume() { - // TODO. -} -void DacAudioPlayback::Pause() { - // TODO. -} - -void DacAudioPlayback::ProcessEvents() { - while (1) { - audio_event_iface_msg_t event; - esp_err_t err = - audio_event_iface_listen(event_interface_, &event, portMAX_DELAY); - if (err != ESP_OK) { - ESP_LOGI(kTag, "error listening for event:%x", err); - continue; - } - ESP_LOGI(kTag, "received event, cmd %i", event.cmd); - - if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && - event.source == (void*)mp3_decoder_ && - event.cmd == AEL_MSG_CMD_REPORT_MUSIC_INFO) { - audio_element_info_t music_info; - audio_element_getinfo(mp3_decoder_, &music_info); - ESP_LOGI(kTag, "sample_rate=%d, bits=%d, ch=%d", music_info.sample_rates, - music_info.bits, music_info.channels); - audio_element_setinfo(i2s_stream_writer_, &music_info); - i2s_stream_set_clk(i2s_stream_writer_, music_info.sample_rates, - music_info.bits, music_info.channels); - } - - if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && - event.source == (void*)fatfs_stream_reader_ && - event.cmd == AEL_MSG_CMD_REPORT_STATUS) { - audio_element_status_t status = status_from_the_void(event.data); - if (status == AEL_STATUS_STATE_FINISHED) { - // TODO: enqueue next track? - } - } - - if (event.source_type == AUDIO_ELEMENT_TYPE_ELEMENT && - event.source == (void*)i2s_stream_writer_ && - event.cmd == AEL_MSG_CMD_REPORT_STATUS) { - audio_element_status_t status = status_from_the_void(event.data); - if (status == AEL_STATUS_STATE_FINISHED) { - // TODO. - return; - } - } - - if (event.need_free_data) { - ESP_LOGI(kTag, "freeing event data"); - free(event.data); - } - } -} - -/* for gapless */ -void DacAudioPlayback::set_next_file(const std::string& filename) { - next_filename_ = filename; -} - -void DacAudioPlayback::set_volume(uint8_t volume) { - volume_ = volume; - // TODO: don't write immediately if we're muting to change track or similar. - dac_->WriteVolume(volume); -} - -auto DacAudioPlayback::volume() -> uint8_t { - return volume_; -} - -} // namespace drivers diff --git a/src/main/main.cpp b/src/main/main.cpp index d13a7407..8233b15f 100644 --- a/src/main/main.cpp +++ b/src/main/main.cpp @@ -8,6 +8,7 @@ #include "freertos/portmacro.h" #include "gpio-expander.hpp" #include "i2c.hpp" +#include "i2s_audio_output.hpp" #include "misc/lv_color.h" #include "misc/lv_timer.h" #include "playback.hpp" @@ -106,34 +107,34 @@ extern "C" void app_main(void) { } std::unique_ptr storage = std::move(storage_res.value()); - ESP_LOGI(TAG, "Init DAC"); - auto dac_res = drivers::AudioDac::create(expander); - if (storage_res.has_error()) { - ESP_LOGE(TAG, "Failed: %d", dac_res.error()); + LvglArgs* lvglArgs = (LvglArgs*)calloc(1, sizeof(LvglArgs)); + lvglArgs->gpio_expander = expander; + xTaskCreateStaticPinnedToCore(&lvgl_main, "LVGL", kLvglStackSize, + (void*)lvglArgs, 1, sLvglStack, + &sLvglTaskBuffer, 1); + + ESP_LOGI(TAG, "Init Audio Output (I2S)"); + auto sink_res = drivers::I2SAudioOutput::create(expander); + if (sink_res.has_error()) { + ESP_LOGE(TAG, "Failed: %d", sink_res.error()); return; } - std::unique_ptr dac = std::move(dac_res.value()); + std::unique_ptr sink = std::move(sink_res.value()); ESP_LOGI(TAG, "Init Audio Pipeline"); - auto playback_res = drivers::DacAudioPlayback::create(dac.get()); + auto playback_res = drivers::AudioPlayback::create(std::move(sink)); if (playback_res.has_error()) { ESP_LOGE(TAG, "Failed: %d", playback_res.error()); return; } - std::unique_ptr playback = + std::unique_ptr playback = std::move(playback_res.value()); ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger."); vTaskDelay(pdMS_TO_TICKS(1500)); - LvglArgs* lvglArgs = (LvglArgs*)calloc(1, sizeof(LvglArgs)); - lvglArgs->gpio_expander = expander; - xTaskCreateStaticPinnedToCore(&lvgl_main, "LVGL", kLvglStackSize, - (void*)lvglArgs, 1, sLvglStack, - &sLvglTaskBuffer, 1); - while (1) { - // TODO: Find owners for everything so we can quit this task safely. - vTaskDelay(pdMS_TO_TICKS(1000)); + playback->ProcessEvents(5); + vTaskDelay(pdMS_TO_TICKS(100)); } }