WIP audio play and pause
This commit is contained in:
@@ -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
|
||||
+23
-22
@@ -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<drivers::SdStorage> 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());
|
||||
return;
|
||||
}
|
||||
std::unique_ptr<drivers::AudioDac> dac = std::move(dac_res.value());
|
||||
|
||||
ESP_LOGI(TAG, "Init Audio Pipeline");
|
||||
auto playback_res = drivers::DacAudioPlayback::create(dac.get());
|
||||
if (playback_res.has_error()) {
|
||||
ESP_LOGE(TAG, "Failed: %d", playback_res.error());
|
||||
return;
|
||||
}
|
||||
std::unique_ptr<drivers::DacAudioPlayback> 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);
|
||||
|
||||
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<drivers::AudioOutput> sink = std::move(sink_res.value());
|
||||
|
||||
ESP_LOGI(TAG, "Init Audio Pipeline");
|
||||
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<drivers::AudioPlayback> playback =
|
||||
std::move(playback_res.value());
|
||||
|
||||
ESP_LOGI(TAG, "Everything looks good! Waiting a mo for debugger.");
|
||||
vTaskDelay(pdMS_TO_TICKS(1500));
|
||||
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user