parent
37041b810f
commit
530fd15e66
@ -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}) |
||||
|
@ -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