Flesh out audio state machine for playback

Also fix mono playback
custom
jacqueline 2 years ago
parent 2a568846bd
commit 1f903accd9
  1. 32
      src/audio/audio_decoder.cpp
  2. 32
      src/audio/audio_fsm.cpp
  3. 10
      src/audio/audio_task.cpp
  4. 13
      src/audio/fatfs_audio_input.cpp
  5. 27
      src/audio/i2s_audio_output.cpp
  6. 3
      src/audio/include/audio_events.hpp
  7. 14
      src/audio/include/audio_fsm.hpp
  8. 2
      src/audio/include/audio_sink.hpp
  9. 2
      src/audio/include/i2s_audio_output.hpp
  10. 4
      src/audio/include/pipeline.hpp
  11. 3
      src/audio/stream_info.cpp
  12. 3
      src/codecs/include/codec.hpp
  13. 6
      src/codecs/include/mad.hpp
  14. 35
      src/codecs/mad.cpp
  15. 7
      src/drivers/display.cpp
  16. 16
      src/drivers/i2s_dac.cpp
  17. 4
      src/drivers/include/display.hpp
  18. 6
      src/drivers/include/i2s_dac.hpp

@ -98,27 +98,27 @@ auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
while (true) {
if (has_samples_to_send_) {
if (!current_output_format_) {
auto format = current_codec_->GetOutputFormat();
auto format = current_codec_->GetOutputFormat();
if (format.has_value()) {
current_output_format_ = StreamInfo::Pcm{
.channels = format.num_channels,
.bits_per_sample = format.bits_per_sample,
.sample_rate = format.sample_rate_hz,
.channels = format->num_channels,
.bits_per_sample = format->bits_per_sample,
.sample_rate = format->sample_rate_hz,
};
}
if (!output->prepare(*current_output_format_)) {
break;
}
if (!output->prepare(*current_output_format_)) {
break;
}
auto write_res = current_codec_->WriteOutputSamples(output->data());
output->add(write_res.first);
has_samples_to_send_ = !write_res.second;
auto write_res = current_codec_->WriteOutputSamples(output->data());
output->add(write_res.first);
has_samples_to_send_ = !write_res.second;
if (has_samples_to_send_) {
// We weren't able to fit all the generated samples into the output
// buffer. Stop trying; we'll finish up during the next pass.
break;
if (has_samples_to_send_) {
// We weren't able to fit all the generated samples into the output
// buffer. Stop trying; we'll finish up during the next pass.
break;
}
}
}

@ -6,6 +6,7 @@
#include "audio_fsm.hpp"
#include <memory>
#include <variant>
#include "audio_decoder.hpp"
#include "audio_events.hpp"
#include "audio_task.hpp"
@ -16,6 +17,8 @@
namespace audio {
static const char kTag[] = "audio_fsm";
drivers::GpioExpander* AudioState::sGpioExpander;
std::shared_ptr<drivers::I2SDac> AudioState::sDac;
std::shared_ptr<drivers::DigitalPot> AudioState::sPots;
@ -25,6 +28,8 @@ std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
std::deque<AudioState::EnqueuedItem> AudioState::sSongQueue;
auto AudioState::Init(drivers::GpioExpander* gpio_expander,
std::weak_ptr<database::Database> database) -> bool {
sGpioExpander = gpio_expander;
@ -66,6 +71,33 @@ void Standby::react(const PlayFile& ev) {
}
}
void Playback::entry() {
ESP_LOGI(kTag, "beginning playback");
sI2SOutput->SetInUse(true);
}
void Playback::exit() {
ESP_LOGI(kTag, "finishing playback");
sI2SOutput->SetInUse(false);
}
void Playback::react(const InputFileFinished& ev) {
ESP_LOGI(kTag, "finished file");
if (sSongQueue.empty()) {
return;
}
EnqueuedItem next_item = sSongQueue.front();
sSongQueue.pop_front();
if (std::holds_alternative<std::string>(next_item)) {
sFileSource->OpenFile(std::get<std::string>(next_item));
}
}
void Playback::react(const AudioPipelineIdle& ev) {
transit<Standby>();
}
} // namespace states
} // namespace audio

@ -15,6 +15,8 @@
#include <memory>
#include <variant>
#include "audio_events.hpp"
#include "audio_fsm.hpp"
#include "audio_sink.hpp"
#include "cbor.h"
#include "esp_err.h"
@ -60,6 +62,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
std::vector<Pipeline*> all_elements = pipeline->GetIterationOrder();
bool previously_had_work = false;
events::EventQueue& event_queue = events::EventQueue::GetInstance();
while (1) {
// First, see if we actually have any pipeline work to do in this iteration.
@ -75,6 +78,11 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
}
}
if (previously_had_work && !has_work) {
events::Dispatch<AudioPipelineIdle, AudioState>({});
}
previously_had_work = has_work;
// See if there's any new events.
event_queue.ServiceAudio(has_work ? delay_ticks : portMAX_DELAY);
@ -118,6 +126,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
if (sink_stream.info().bytes_in_stream == 0) {
// No new bytes to sink, so skip sinking completely.
ESP_LOGI(kTag, "no bytes to sink");
continue;
}
@ -130,6 +139,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
output_format = sink_stream.info().format;
sink->Configure(*output_format);
} else {
ESP_LOGI(kTag, "waiting to reconfigure");
continue;
}
}

@ -13,8 +13,11 @@
#include <variant>
#include "arena.hpp"
#include "audio_events.hpp"
#include "audio_fsm.hpp"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "event_queue.hpp"
#include "ff.h"
#include "freertos/portmacro.h"
@ -69,6 +72,10 @@ auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
}
std::size_t max_size = output->data().size_bytes();
if (max_size < output->data().size_bytes() / 2) {
return;
}
std::size_t size = 0;
FRESULT result =
f_read(&current_file_, output->data().data(), max_size, &size);
@ -83,6 +90,12 @@ auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
if (size < max_size || f_eof(&current_file_)) {
f_close(&current_file_);
is_file_open_ = false;
// TODO(jacqueline): MP3 only
std::fill_n(output->data().begin(), 8, std::byte(0));
output->add(8);
events::Dispatch<InputFileFinished, AudioState>({});
}
}

@ -38,7 +38,6 @@ I2SAudioOutput::I2SAudioOutput(drivers::GpioExpander* expander,
attenuation_(pots_->GetMaxAttenuation()) {
SetVolume(25); // For testing
dac_->SetSource(buffer());
dac_->Start();
}
I2SAudioOutput::~I2SAudioOutput() {
@ -46,6 +45,15 @@ I2SAudioOutput::~I2SAudioOutput() {
dac_->SetSource(nullptr);
}
auto I2SAudioOutput::SetInUse(bool in_use) -> void {
if (in_use) {
dac_->Start();
} else {
dac_->Stop();
}
pots_->SetZeroCrossDetect(in_use);
}
auto I2SAudioOutput::SetVolumeImbalance(int_fast8_t balance) -> void {
int_fast8_t new_difference = balance - left_difference_;
left_difference_ = balance;
@ -124,6 +132,19 @@ auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool {
ESP_LOGI(kTag, "incoming audio stream: %u bpp @ %lu Hz", pcm.bits_per_sample,
pcm.sample_rate);
drivers::I2SDac::Channels ch;
switch (pcm.channels) {
case 1:
ch = drivers::I2SDac::CHANNELS_MONO;
break;
case 2:
ch = drivers::I2SDac::CHANNELS_STEREO;
break;
default:
ESP_LOGE(kTag, "dropping stream with out of bounds channels");
return false;
}
drivers::I2SDac::BitsPerSample bps;
switch (pcm.bits_per_sample) {
case 16:
@ -153,9 +174,7 @@ auto I2SAudioOutput::Configure(const StreamInfo::Format& format) -> bool {
return false;
}
// TODO(jacqueline): probs do something with the channel hey
dac_->Reconfigure(bps, sample_rate);
dac_->Reconfigure(ch, bps, sample_rate);
current_config_ = pcm;
return true;

@ -24,4 +24,7 @@ struct PlaySong : tinyfsm::Event {
std::optional<database::SongTags> tags;
};
struct InputFileFinished : tinyfsm::Event {};
struct AudioPipelineIdle : tinyfsm::Event {};
} // namespace audio

@ -6,6 +6,7 @@
#pragma once
#include <deque>
#include <memory>
#include <vector>
@ -16,6 +17,7 @@
#include "gpio_expander.hpp"
#include "i2s_audio_output.hpp"
#include "i2s_dac.hpp"
#include "song.hpp"
#include "storage.hpp"
#include "tinyfsm.hpp"
@ -40,6 +42,9 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
virtual void react(const PlaySong&) {}
virtual void react(const PlayFile&) {}
virtual void react(const InputFileFinished&) {}
virtual void react(const AudioPipelineIdle&) {}
protected:
static drivers::GpioExpander* sGpioExpander;
static std::shared_ptr<drivers::I2SDac> sDac;
@ -49,6 +54,9 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::unique_ptr<FatfsAudioInput> sFileSource;
static std::unique_ptr<I2SAudioOutput> sI2SOutput;
static std::vector<std::unique_ptr<IAudioElement>> sPipeline;
typedef std::variant<database::SongId, std::string> EnqueuedItem;
static std::deque<EnqueuedItem> sSongQueue;
};
namespace states {
@ -68,6 +76,12 @@ class Standby : public AudioState {
class Playback : public AudioState {
public:
void entry() override;
void exit() override;
void react(const InputFileFinished&) override;
void react(const AudioPipelineIdle&) override;
using AudioState::react;
};

@ -40,6 +40,8 @@ class IAudioSink {
free(metadata_);
}
virtual auto SetInUse(bool) -> void {}
virtual auto SetVolumeImbalance(int_fast8_t balance) -> void = 0;
virtual auto SetVolume(uint_fast8_t percent) -> void = 0;
virtual auto GetVolume() -> uint_fast8_t = 0;

@ -29,6 +29,8 @@ class I2SAudioOutput : public IAudioSink {
std::weak_ptr<drivers::DigitalPot> pots);
~I2SAudioOutput();
auto SetInUse(bool) -> void override;
auto SetVolumeImbalance(int_fast8_t balance) -> void override;
auto SetVolume(uint_fast8_t percent) -> void override;
auto GetVolume() -> uint_fast8_t override;

@ -36,6 +36,10 @@ class Pipeline {
auto GetIterationOrder() -> std::vector<Pipeline*>;
// Not copyable or movable.
Pipeline(const Pipeline&) = delete;
Pipeline& operator=(const Pipeline&) = delete;
private:
IAudioElement* root_;
std::vector<std::unique_ptr<Pipeline>> subtrees_;

@ -22,7 +22,8 @@ namespace audio {
void InputStream::consume(std::size_t bytes) const {
assert(raw_->info->bytes_in_stream >= bytes);
auto new_data = raw_->data.subspan(bytes);
auto new_data =
raw_->data.subspan(bytes, raw_->info->bytes_in_stream - bytes);
std::move(new_data.begin(), new_data.end(), raw_->data.begin());
raw_->info->bytes_in_stream = new_data.size_bytes();
}

@ -11,6 +11,7 @@
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
@ -32,7 +33,7 @@ class ICodec {
uint32_t sample_rate_hz;
};
virtual auto GetOutputFormat() -> OutputFormat = 0;
virtual auto GetOutputFormat() -> std::optional<OutputFormat> = 0;
enum ProcessingError { MALFORMED_DATA };

@ -8,6 +8,7 @@
#include <cstddef>
#include <cstdint>
#include <optional>
#include <string>
#include <utility>
@ -24,7 +25,7 @@ class MadMp3Decoder : public ICodec {
~MadMp3Decoder();
auto CanHandleType(StreamType type) -> bool override;
auto GetOutputFormat() -> OutputFormat override;
auto GetOutputFormat() -> std::optional<OutputFormat> override;
auto ResetForNewStream() -> void override;
auto SetInput(cpp::span<const std::byte> input) -> void override;
auto GetInputPosition() -> std::size_t override;
@ -37,9 +38,6 @@ class MadMp3Decoder : public ICodec {
mad_frame frame_;
mad_synth synth_;
mad_header header_;
bool has_decoded_header_;
int current_sample_;
};

@ -8,6 +8,7 @@
#include <stdint.h>
#include <cstdint>
#include <optional>
#include "mad.h"
@ -34,31 +35,29 @@ MadMp3Decoder::MadMp3Decoder() {
mad_stream_init(&stream_);
mad_frame_init(&frame_);
mad_synth_init(&synth_);
mad_header_init(&header_);
}
MadMp3Decoder::~MadMp3Decoder() {
mad_stream_finish(&stream_);
mad_frame_finish(&frame_);
mad_synth_finish(&synth_);
mad_header_finish(&header_);
}
auto MadMp3Decoder::CanHandleType(StreamType type) -> bool {
return type == STREAM_MP3;
}
auto MadMp3Decoder::GetOutputFormat() -> OutputFormat {
return OutputFormat{
auto MadMp3Decoder::GetOutputFormat() -> std::optional<OutputFormat> {
if (synth_.pcm.channels == 0 || synth_.pcm.samplerate == 0) {
return {};
}
return std::optional<OutputFormat>({
.num_channels = static_cast<uint8_t>(synth_.pcm.channels),
.bits_per_sample = 16,
.sample_rate_hz =
synth_.pcm.samplerate == 0 ? 44100 : synth_.pcm.samplerate,
};
.bits_per_sample = 24,
.sample_rate_hz = synth_.pcm.samplerate,
});
}
auto MadMp3Decoder::ResetForNewStream() -> void {
has_decoded_header_ = false;
}
auto MadMp3Decoder::ResetForNewStream() -> void {}
auto MadMp3Decoder::SetInput(cpp::span<const std::byte> input) -> void {
mad_stream_buffer(&stream_,
@ -71,16 +70,6 @@ auto MadMp3Decoder::GetInputPosition() -> std::size_t {
}
auto MadMp3Decoder::ProcessNextFrame() -> cpp::result<bool, ProcessingError> {
if (!has_decoded_header_) {
// The header of any given frame should be representative of the
// entire stream, so only need to read it once.
mad_header_decode(&header_, &stream_);
has_decoded_header_ = true;
// TODO: Use the info in the header for something. I think the
// duration will help with seeking?
}
// Whatever was last synthesized is now invalid, so ensure we don't try to
// send it.
current_sample_ = -1;
@ -128,7 +117,6 @@ auto MadMp3Decoder::WriteOutputSamples(cpp::span<std::byte> output)
for (int channel = 0; channel < synth_.pcm.channels; channel++) {
// TODO(jacqueline): output 24 bit samples when (if?) we have a downmix
// step in the pipeline.
/*
uint32_t sample_24 =
scaleToBits(synth_.pcm.samples[channel][current_sample_], 24);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 16) & 0xFF);
@ -136,11 +124,12 @@ auto MadMp3Decoder::WriteOutputSamples(cpp::span<std::byte> output)
output[output_byte++] = static_cast<std::byte>((sample_24)&0xFF);
// 24 bit samples must still be aligned to 32 bits. The LSB is ignored.
output[output_byte++] = static_cast<std::byte>(0);
*/
/*
uint16_t sample_16 =
scaleToBits(synth_.pcm.samples[channel][current_sample_], 16);
output[output_byte++] = static_cast<std::byte>((sample_16 >> 8) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_16)&0xFF);
*/
}
current_sample_++;
}

@ -126,7 +126,7 @@ auto Display::Create(GpioExpander* expander,
ESP_ERROR_CHECK(ledc_set_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, 0));
ESP_ERROR_CHECK(ledc_update_duty(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0));
ledc_fade_func_install(0);
// ledc_fade_func_install(0);
// Next, init the SPI device
spi_device_interface_config_t spi_cfg = {
@ -194,8 +194,9 @@ auto Display::SetDisplayOn(bool enabled) -> void {
display_on_ = enabled;
int new_duty = display_on_ ? brightness_ : 0;
ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, new_duty, 250);
ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, LEDC_FADE_NO_WAIT);
// ledc_set_fade_with_time(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0, new_duty,
// 250); ledc_fade_start(LEDC_LOW_SPEED_MODE, LEDC_CHANNEL_0,
// LEDC_FADE_NO_WAIT);
}
void Display::SendInitialisationSequence(const uint8_t* data) {

@ -116,24 +116,30 @@ auto I2SDac::Stop() -> void {
i2s_active_ = false;
}
auto I2SDac::Reconfigure(BitsPerSample bps, SampleRate rate) -> void {
auto I2SDac::Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate)
-> void {
if (i2s_active_) {
i2s_channel_disable(i2s_handle_);
}
uint8_t bps_bits = 0;
switch (ch) {
case CHANNELS_MONO:
slot_config_.slot_mode = I2S_SLOT_MODE_MONO;
break;
case CHANNELS_STEREO:
slot_config_.slot_mode = I2S_SLOT_MODE_STEREO;
break;
}
switch (bps) {
case BPS_16:
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_16BIT;
bps_bits = 0;
break;
case BPS_24:
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_24BIT;
bps_bits = 0b10;
break;
case BPS_32:
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT;
bps_bits = 0b11;
break;
}
ESP_ERROR_CHECK(i2s_channel_reconfig_std_slot(i2s_handle_, &slot_config_));

@ -43,6 +43,10 @@ class Display {
const lv_area_t* area,
lv_color_t* color_map);
// Not copyable or movable.
Display(const Display&) = delete;
Display& operator=(const Display&) = delete;
private:
GpioExpander* gpio_;
spi_device_handle_t handle_;

@ -40,6 +40,10 @@ class I2SDac {
auto Start() -> void;
auto Stop() -> void;
enum Channels {
CHANNELS_MONO,
CHANNELS_STEREO,
};
enum BitsPerSample {
BPS_16 = I2S_DATA_BIT_WIDTH_16BIT,
BPS_24 = I2S_DATA_BIT_WIDTH_24BIT,
@ -56,7 +60,7 @@ class I2SDac {
SAMPLE_RATE_192 = 192000,
};
auto Reconfigure(BitsPerSample bps, SampleRate rate) -> void;
auto Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate) -> void;
auto WriteData(const cpp::span<const std::byte>& data) -> void;
auto SetSource(StreamBufferHandle_t buffer) -> void;

Loading…
Cancel
Save