parent
37041b810f
commit
530fd15e66
@ -1,6 +1,6 @@ |
|||||||
idf_component_register( |
idf_component_register( |
||||||
SRCS "dac.cpp" "gpio-expander.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" |
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" |
INCLUDE_DIRS "include" |
||||||
REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result" "lvgl") |
REQUIRES "esp_adc_cal" "fatfs" "audio_pipeline" "audio_stream" "result" "lvgl") |
||||||
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) |
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) |
||||||
|
@ -0,0 +1,295 @@ |
|||||||
|
#include "audio_playback.hpp" |
||||||
|
|
||||||
|
#include "audio_output.hpp" |
||||||
|
#include "dac.hpp" |
||||||
|
|
||||||
|
#include <algorithm> |
||||||
|
#include <cstdint> |
||||||
|
#include <exception> |
||||||
|
#include <memory> |
||||||
|
#include <string_view> |
||||||
|
|
||||||
|
#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<uintptr_t>(status); |
||||||
|
return static_cast<audio_element_status_t>(as_pointer_int); |
||||||
|
} |
||||||
|
|
||||||
|
auto AudioPlayback::create(std::unique_ptr<IAudioOutput> output) |
||||||
|
-> cpp::result<std::unique_ptr<AudioPlayback>, 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<AudioPlayback>(output, pipeline, fatfs_stream_reader, event_interface |
||||||
|
} |
||||||
|
|
||||||
|
AudioPlayback::AudioPlayback(std::unique_ptr<IAudioOutput> 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
|
@ -0,0 +1,92 @@ |
|||||||
|
#include "i2s_audio_output.hpp" |
||||||
|
#include <algorithm> |
||||||
|
#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<std::unique_ptr<I2SAudioOutput>, 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<AudioDac> 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_t>(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<I2SAudioOutput>(dac, i2s_stream_writer); |
||||||
|
} |
||||||
|
|
||||||
|
I2SAudioOutput(std::unique<AudioDac> 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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,15 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "audio_common.h" |
||||||
|
#include "audio_element.h" |
||||||
|
#include "audio_output.hpp" |
||||||
|
#include <cstdint> |
||||||
|
|
||||||
|
namespace drivers { |
||||||
|
|
||||||
|
class A2DPAudioOutput : IAudioOutput { |
||||||
|
public: |
||||||
|
virtual auto SetVolume(uint8_t volume) -> void; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace drivers
|
@ -0,0 +1,27 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "audio_common.h" |
||||||
|
#include "audio_element.h" |
||||||
|
#include <cstdint> |
||||||
|
|
||||||
|
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
|
@ -0,0 +1,99 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "audio_output.hpp" |
||||||
|
#include "dac.hpp" |
||||||
|
#include "storage.hpp" |
||||||
|
|
||||||
|
#include <cstdint> |
||||||
|
#include <memory> |
||||||
|
#include <string> |
||||||
|
|
||||||
|
#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<IAudioOutput> output) |
||||||
|
-> cpp::result<std::unique_ptr<AudioPlayback>, Error>; |
||||||
|
|
||||||
|
AudioPlayback(std::unqiue_ptr<IAudioOutput> 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<IAudioOutput> 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
|
@ -0,0 +1,30 @@ |
|||||||
|
#pragma once |
||||||
|
|
||||||
|
#include "audio_common.h" |
||||||
|
#include "audio_element.h" |
||||||
|
#include "audio_output.hpp" |
||||||
|
#include "gpio-expander.hpp" |
||||||
|
#include <cstdint> |
||||||
|
#include <memory> |
||||||
|
#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<std::unique_ptr<I2SAudioOutput>, 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<AudioDac> dac_; |
||||||
|
}; |
||||||
|
|
||||||
|
} // namespace drivers
|
@ -1,67 +0,0 @@ |
|||||||
#pragma once |
|
||||||
|
|
||||||
#include "dac.hpp" |
|
||||||
#include "storage.hpp" |
|
||||||
|
|
||||||
#include <cstdint> |
|
||||||
#include <memory> |
|
||||||
#include <string> |
|
||||||
|
|
||||||
#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<std::unique_ptr<DacAudioPlayback>, 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
|
|
@ -1,246 +0,0 @@ |
|||||||
#include "playback.hpp" |
|
||||||
|
|
||||||
#include "dac.hpp" |
|
||||||
|
|
||||||
#include <cstdint> |
|
||||||
|
|
||||||
#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<uintptr_t>(status); |
|
||||||
return static_cast<audio_element_status_t>(as_pointer_int); |
|
||||||
} |
|
||||||
|
|
||||||
auto DacAudioPlayback::create(AudioDac* dac) |
|
||||||
-> cpp::result<std::unique_ptr<DacAudioPlayback>, 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_t>(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<DacAudioPlayback>(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
|
|
Loading…
Reference in new issue