Merge branch 'main' into opus

custom
jacqueline 2 years ago
commit e1181fbe59
  1. 1
      .gitignore
  2. 8
      lib/stb_vorbis/CMakeLists.txt
  3. 418
      lib/stb_vorbis/include/stb_vorbis.h
  4. 5584
      lib/stb_vorbis/stb_vorbis.c
  5. 23
      sdkconfig.common
  6. 116
      src/app_console/app_console.cpp
  7. 2
      src/audio/CMakeLists.txt
  8. 91
      src/audio/audio_task.cpp
  9. 9
      src/audio/fatfs_audio_input.cpp
  10. 38
      src/audio/i2s_audio_output.cpp
  11. 5
      src/audio/include/audio_sink.hpp
  12. 7
      src/audio/include/audio_task.hpp
  13. 131
      src/audio/include/fir.h
  14. 4
      src/audio/include/i2s_audio_output.hpp
  15. 44
      src/audio/include/resample.hpp
  16. 71
      src/audio/include/sink_mixer.hpp
  17. 37
      src/audio/include/stream_info.hpp
  18. 205
      src/audio/resample.cpp
  19. 224
      src/audio/sink_mixer.cpp
  20. 7
      src/audio/stream_info.cpp
  21. 4
      src/codecs/CMakeLists.txt
  22. 3
      src/codecs/codec.cpp
  23. 6
      src/codecs/foxenflac.cpp
  24. 6
      src/codecs/include/codec.hpp
  25. 3
      src/codecs/include/foxenflac.hpp
  26. 3
      src/codecs/include/mad.hpp
  27. 65
      src/codecs/include/sample.hpp
  28. 40
      src/codecs/mad.cpp
  29. 3
      src/drivers/CMakeLists.txt
  30. 253
      src/drivers/bluetooth.cpp
  31. 1
      src/drivers/i2s_dac.cpp
  32. 108
      src/drivers/include/bluetooth.hpp
  33. 6
      src/drivers/include/i2s_dac.hpp
  34. 27
      src/drivers/include/nvs.hpp
  35. 73
      src/drivers/nvs.cpp
  36. 18
      src/drivers/storage.cpp
  37. 4
      src/system_fsm/booting.cpp
  38. 2
      src/system_fsm/include/system_fsm.hpp
  39. 1
      src/system_fsm/system_fsm.cpp
  40. 16
      src/tasks/tasks.cpp
  41. 14
      src/tasks/tasks.hpp
  42. 19
      src/ui/lvgl_task.cpp
  43. 1
      tools/cmake/common.cmake

1
.gitignore vendored

@ -4,6 +4,7 @@
.cache/ .cache/
.clangd .clangd
.helix/
build/ build/
build.clang/ build.clang/
sdkconfig sdkconfig

@ -1,8 +0,0 @@
# Copyright 2023 jacqueline <me@jacqueline.id.au>
#
# SPDX-License-Identifier: GPL-3.0-only
idf_component_register(
SRCS "stb_vorbis.c"
INCLUDE_DIRS "include"
)

@ -1,418 +0,0 @@
// Ogg Vorbis audio decoder - v1.22 - public domain
// http://nothings.org/stb_vorbis/
//
// Original version written by Sean Barrett in 2007.
//
// Originally sponsored by RAD Game Tools. Seeking implementation
// sponsored by Phillip Bennefall, Marc Andersen, Aaron Baker,
// Elias Software, Aras Pranckevicius, and Sean Barrett.
//
// LICENSE
//
// See end of file for license information.
//
// Limitations:
//
// - floor 0 not supported (used in old ogg vorbis files pre-2004)
// - lossless sample-truncation at beginning ignored
// - cannot concatenate multiple vorbis streams
// - sample positions are 32-bit, limiting seekable 192Khz
// files to around 6 hours (Ogg supports 64-bit)
//
// Feature contributors:
// Dougall Johnson (sample-exact seeking)
//
// Bugfix/warning contributors:
// Terje Mathisen Niklas Frykholm Andy Hill
// Casey Muratori John Bolton Gargaj
// Laurent Gomila Marc LeBlanc Ronny Chevalier
// Bernhard Wodo Evan Balster github:alxprd
// Tom Beaumont Ingo Leitgeb Nicolas Guillemot
// Phillip Bennefall Rohit Thiago Goulart
// github:manxorist Saga Musix github:infatum
// Timur Gagiev Maxwell Koo Peter Waller
// github:audinowho Dougall Johnson David Reid
// github:Clownacy Pedro J. Estebanez Remi Verschelde
// AnthoFoxo github:morlat Gabriel Ravier
//
// Partial history:
// 1.22 - 2021-07-11 - various small fixes
// 1.21 - 2021-07-02 - fix bug for files with no comments
// 1.20 - 2020-07-11 - several small fixes
// 1.19 - 2020-02-05 - warnings
// 1.18 - 2020-02-02 - fix seek bugs; parse header comments; misc warnings etc.
// 1.17 - 2019-07-08 - fix CVE-2019-13217..CVE-2019-13223 (by ForAllSecure)
// 1.16 - 2019-03-04 - fix warnings
// 1.15 - 2019-02-07 - explicit failure if Ogg Skeleton data is found
// 1.14 - 2018-02-11 - delete bogus dealloca usage
// 1.13 - 2018-01-29 - fix truncation of last frame (hopefully)
// 1.12 - 2017-11-21 - limit residue begin/end to blocksize/2 to avoid large temp allocs in bad/corrupt files
// 1.11 - 2017-07-23 - fix MinGW compilation
// 1.10 - 2017-03-03 - more robust seeking; fix negative ilog(); clear error in open_memory
// 1.09 - 2016-04-04 - back out 'truncation of last frame' fix from previous version
// 1.08 - 2016-04-02 - warnings; setup memory leaks; truncation of last frame
// 1.07 - 2015-01-16 - fixes for crashes on invalid files; warning fixes; const
// 1.06 - 2015-08-31 - full, correct support for seeking API (Dougall Johnson)
// some crash fixes when out of memory or with corrupt files
// fix some inappropriately signed shifts
// 1.05 - 2015-04-19 - don't define __forceinline if it's redundant
// 1.04 - 2014-08-27 - fix missing const-correct case in API
// 1.03 - 2014-08-07 - warning fixes
// 1.02 - 2014-07-09 - declare qsort comparison as explicitly _cdecl in Windows
// 1.01 - 2014-06-18 - fix stb_vorbis_get_samples_float (interleaved was correct)
// 1.0 - 2014-05-26 - fix memory leaks; fix warnings; fix bugs in >2-channel;
// (API change) report sample rate for decode-full-file funcs
//
// See end of file for full version history.
//////////////////////////////////////////////////////////////////////////////
//
// HEADER BEGINS HERE
//
#ifndef STB_VORBIS_INCLUDE_STB_VORBIS_H
#define STB_VORBIS_INCLUDE_STB_VORBIS_H
#if defined(STB_VORBIS_NO_CRT) && !defined(STB_VORBIS_NO_STDIO)
#define STB_VORBIS_NO_STDIO 1
#endif
#ifndef STB_VORBIS_NO_STDIO
#include <stdio.h>
#endif
#ifdef __cplusplus
extern "C" {
#endif
/////////// THREAD SAFETY
// Individual stb_vorbis* handles are not thread-safe; you cannot decode from
// them from multiple threads at the same time. However, you can have multiple
// stb_vorbis* handles and decode from them independently in multiple thrads.
/////////// MEMORY ALLOCATION
// normally stb_vorbis uses malloc() to allocate memory at startup,
// and alloca() to allocate temporary memory during a frame on the
// stack. (Memory consumption will depend on the amount of setup
// data in the file and how you set the compile flags for speed
// vs. size. In my test files the maximal-size usage is ~150KB.)
//
// You can modify the wrapper functions in the source (setup_malloc,
// setup_temp_malloc, temp_malloc) to change this behavior, or you
// can use a simpler allocation model: you pass in a buffer from
// which stb_vorbis will allocate _all_ its memory (including the
// temp memory). "open" may fail with a VORBIS_outofmem if you
// do not pass in enough data; there is no way to determine how
// much you do need except to succeed (at which point you can
// query get_info to find the exact amount required. yes I know
// this is lame).
//
// If you pass in a non-NULL buffer of the type below, allocation
// will occur from it as described above. Otherwise just pass NULL
// to use malloc()/alloca()
typedef struct
{
char *alloc_buffer;
int alloc_buffer_length_in_bytes;
} stb_vorbis_alloc;
/////////// FUNCTIONS USEABLE WITH ALL INPUT MODES
typedef struct stb_vorbis stb_vorbis;
typedef struct
{
unsigned int sample_rate;
int channels;
unsigned int setup_memory_required;
unsigned int setup_temp_memory_required;
unsigned int temp_memory_required;
int max_frame_size;
} stb_vorbis_info;
typedef struct
{
char *vendor;
int comment_list_length;
char **comment_list;
} stb_vorbis_comment;
// get general information about the file
extern stb_vorbis_info stb_vorbis_get_info(stb_vorbis *f);
// get ogg comments
extern stb_vorbis_comment stb_vorbis_get_comment(stb_vorbis *f);
// get the last error detected (clears it, too)
extern int stb_vorbis_get_error(stb_vorbis *f);
// close an ogg vorbis file and free all memory in use
extern void stb_vorbis_close(stb_vorbis *f);
// this function returns the offset (in samples) from the beginning of the
// file that will be returned by the next decode, if it is known, or -1
// otherwise. after a flush_pushdata() call, this may take a while before
// it becomes valid again.
// NOT WORKING YET after a seek with PULLDATA API
extern int stb_vorbis_get_sample_offset(stb_vorbis *f);
// returns the current seek point within the file, or offset from the beginning
// of the memory buffer. In pushdata mode it returns 0.
extern unsigned int stb_vorbis_get_file_offset(stb_vorbis *f);
/////////// PUSHDATA API
#ifndef STB_VORBIS_NO_PUSHDATA_API
// this API allows you to get blocks of data from any source and hand
// them to stb_vorbis. you have to buffer them; stb_vorbis will tell
// you how much it used, and you have to give it the rest next time;
// and stb_vorbis may not have enough data to work with and you will
// need to give it the same data again PLUS more. Note that the Vorbis
// specification does not bound the size of an individual frame.
extern stb_vorbis *stb_vorbis_open_pushdata(
const unsigned char * datablock, int datablock_length_in_bytes,
int *datablock_memory_consumed_in_bytes,
int *error,
const stb_vorbis_alloc *alloc_buffer);
// create a vorbis decoder by passing in the initial data block containing
// the ogg&vorbis headers (you don't need to do parse them, just provide
// the first N bytes of the file--you're told if it's not enough, see below)
// on success, returns an stb_vorbis *, does not set error, returns the amount of
// data parsed/consumed on this call in *datablock_memory_consumed_in_bytes;
// on failure, returns NULL on error and sets *error, does not change *datablock_memory_consumed
// if returns NULL and *error is VORBIS_need_more_data, then the input block was
// incomplete and you need to pass in a larger block from the start of the file
extern int stb_vorbis_decode_frame_pushdata(
stb_vorbis *f,
const unsigned char *datablock, int datablock_length_in_bytes,
int *channels, // place to write number of float * buffers
float ***output, // place to write float ** array of float * buffers
int *samples // place to write number of output samples
);
// decode a frame of audio sample data if possible from the passed-in data block
//
// return value: number of bytes we used from datablock
//
// possible cases:
// 0 bytes used, 0 samples output (need more data)
// N bytes used, 0 samples output (resynching the stream, keep going)
// N bytes used, M samples output (one frame of data)
// note that after opening a file, you will ALWAYS get one N-bytes,0-sample
// frame, because Vorbis always "discards" the first frame.
//
// Note that on resynch, stb_vorbis will rarely consume all of the buffer,
// instead only datablock_length_in_bytes-3 or less. This is because it wants
// to avoid missing parts of a page header if they cross a datablock boundary,
// without writing state-machiney code to record a partial detection.
//
// The number of channels returned are stored in *channels (which can be
// NULL--it is always the same as the number of channels reported by
// get_info). *output will contain an array of float* buffers, one per
// channel. In other words, (*output)[0][0] contains the first sample from
// the first channel, and (*output)[1][0] contains the first sample from
// the second channel.
//
// *output points into stb_vorbis's internal output buffer storage; these
// buffers are owned by stb_vorbis and application code should not free
// them or modify their contents. They are transient and will be overwritten
// once you ask for more data to get decoded, so be sure to grab any data
// you need before then.
extern void stb_vorbis_flush_pushdata(stb_vorbis *f);
// inform stb_vorbis that your next datablock will not be contiguous with
// previous ones (e.g. you've seeked in the data); future attempts to decode
// frames will cause stb_vorbis to resynchronize (as noted above), and
// once it sees a valid Ogg page (typically 4-8KB, as large as 64KB), it
// will begin decoding the _next_ frame.
//
// if you want to seek using pushdata, you need to seek in your file, then
// call stb_vorbis_flush_pushdata(), then start calling decoding, then once
// decoding is returning you data, call stb_vorbis_get_sample_offset, and
// if you don't like the result, seek your file again and repeat.
#endif
////////// PULLING INPUT API
#ifndef STB_VORBIS_NO_PULLDATA_API
// This API assumes stb_vorbis is allowed to pull data from a source--
// either a block of memory containing the _entire_ vorbis stream, or a
// FILE * that you or it create, or possibly some other reading mechanism
// if you go modify the source to replace the FILE * case with some kind
// of callback to your code. (But if you don't support seeking, you may
// just want to go ahead and use pushdata.)
#if !defined(STB_VORBIS_NO_STDIO) && !defined(STB_VORBIS_NO_INTEGER_CONVERSION)
extern int stb_vorbis_decode_filename(const char *filename, int *channels, int *sample_rate, short **output);
#endif
#if !defined(STB_VORBIS_NO_INTEGER_CONVERSION)
extern int stb_vorbis_decode_memory(const unsigned char *mem, int len, int *channels, int *sample_rate, short **output);
#endif
// decode an entire file and output the data interleaved into a malloc()ed
// buffer stored in *output. The return value is the number of samples
// decoded, or -1 if the file could not be opened or was not an ogg vorbis file.
// When you're done with it, just free() the pointer returned in *output.
extern stb_vorbis * stb_vorbis_open_memory(const unsigned char *data, int len,
int *error, const stb_vorbis_alloc *alloc_buffer);
// create an ogg vorbis decoder from an ogg vorbis stream in memory (note
// this must be the entire stream!). on failure, returns NULL and sets *error
#ifndef STB_VORBIS_NO_STDIO
extern stb_vorbis * stb_vorbis_open_filename(const char *filename,
int *error, const stb_vorbis_alloc *alloc_buffer);
// create an ogg vorbis decoder from a filename via fopen(). on failure,
// returns NULL and sets *error (possibly to VORBIS_file_open_failure).
extern stb_vorbis * stb_vorbis_open_file(FILE *f, int close_handle_on_close,
int *error, const stb_vorbis_alloc *alloc_buffer);
// create an ogg vorbis decoder from an open FILE *, looking for a stream at
// the _current_ seek point (ftell). on failure, returns NULL and sets *error.
// note that stb_vorbis must "own" this stream; if you seek it in between
// calls to stb_vorbis, it will become confused. Moreover, if you attempt to
// perform stb_vorbis_seek_*() operations on this file, it will assume it
// owns the _entire_ rest of the file after the start point. Use the next
// function, stb_vorbis_open_file_section(), to limit it.
extern stb_vorbis * stb_vorbis_open_file_section(FILE *f, int close_handle_on_close,
int *error, const stb_vorbis_alloc *alloc_buffer, unsigned int len);
// create an ogg vorbis decoder from an open FILE *, looking for a stream at
// the _current_ seek point (ftell); the stream will be of length 'len' bytes.
// on failure, returns NULL and sets *error. note that stb_vorbis must "own"
// this stream; if you seek it in between calls to stb_vorbis, it will become
// confused.
#endif
extern int stb_vorbis_seek_frame(stb_vorbis *f, unsigned int sample_number);
extern int stb_vorbis_seek(stb_vorbis *f, unsigned int sample_number);
// these functions seek in the Vorbis file to (approximately) 'sample_number'.
// after calling seek_frame(), the next call to get_frame_*() will include
// the specified sample. after calling stb_vorbis_seek(), the next call to
// stb_vorbis_get_samples_* will start with the specified sample. If you
// do not need to seek to EXACTLY the target sample when using get_samples_*,
// you can also use seek_frame().
extern int stb_vorbis_seek_start(stb_vorbis *f);
// this function is equivalent to stb_vorbis_seek(f,0)
extern unsigned int stb_vorbis_stream_length_in_samples(stb_vorbis *f);
extern float stb_vorbis_stream_length_in_seconds(stb_vorbis *f);
// these functions return the total length of the vorbis stream
extern int stb_vorbis_get_frame_float(stb_vorbis *f, int *channels, float ***output);
// decode the next frame and return the number of samples. the number of
// channels returned are stored in *channels (which can be NULL--it is always
// the same as the number of channels reported by get_info). *output will
// contain an array of float* buffers, one per channel. These outputs will
// be overwritten on the next call to stb_vorbis_get_frame_*.
//
// You generally should not intermix calls to stb_vorbis_get_frame_*()
// and stb_vorbis_get_samples_*(), since the latter calls the former.
#ifndef STB_VORBIS_NO_INTEGER_CONVERSION
extern int stb_vorbis_get_frame_short_interleaved(stb_vorbis *f, int num_c, short *buffer, int num_shorts);
extern int stb_vorbis_get_frame_short (stb_vorbis *f, int num_c, short **buffer, int num_samples);
#endif
// decode the next frame and return the number of *samples* per channel.
// Note that for interleaved data, you pass in the number of shorts (the
// size of your array), but the return value is the number of samples per
// channel, not the total number of samples.
//
// The data is coerced to the number of channels you request according to the
// channel coercion rules (see below). You must pass in the size of your
// buffer(s) so that stb_vorbis will not overwrite the end of the buffer.
// The maximum buffer size needed can be gotten from get_info(); however,
// the Vorbis I specification implies an absolute maximum of 4096 samples
// per channel.
// Channel coercion rules:
// Let M be the number of channels requested, and N the number of channels present,
// and Cn be the nth channel; let stereo L be the sum of all L and center channels,
// and stereo R be the sum of all R and center channels (channel assignment from the
// vorbis spec).
// M N output
// 1 k sum(Ck) for all k
// 2 * stereo L, stereo R
// k l k > l, the first l channels, then 0s
// k l k <= l, the first k channels
// Note that this is not _good_ surround etc. mixing at all! It's just so
// you get something useful.
extern int stb_vorbis_get_samples_float_interleaved(stb_vorbis *f, int channels, float *buffer, int num_floats);
extern int stb_vorbis_get_samples_float(stb_vorbis *f, int channels, float **buffer, int num_samples);
// gets num_samples samples, not necessarily on a frame boundary--this requires
// buffering so you have to supply the buffers. DOES NOT APPLY THE COERCION RULES.
// Returns the number of samples stored per channel; it may be less than requested
// at the end of the file. If there are no more samples in the file, returns 0.
#ifndef STB_VORBIS_NO_INTEGER_CONVERSION
extern int stb_vorbis_get_samples_short_interleaved(stb_vorbis *f, int channels, short *buffer, int num_shorts);
extern int stb_vorbis_get_samples_short(stb_vorbis *f, int channels, short **buffer, int num_samples);
#endif
// gets num_samples samples, not necessarily on a frame boundary--this requires
// buffering so you have to supply the buffers. Applies the coercion rules above
// to produce 'channels' channels. Returns the number of samples stored per channel;
// it may be less than requested at the end of the file. If there are no more
// samples in the file, returns 0.
#endif
//////// ERROR CODES
enum STBVorbisError
{
VORBIS__no_error,
VORBIS_need_more_data=1, // not a real error
VORBIS_invalid_api_mixing, // can't mix API modes
VORBIS_outofmem, // not enough memory
VORBIS_feature_not_supported, // uses floor 0
VORBIS_too_many_channels, // STB_VORBIS_MAX_CHANNELS is too small
VORBIS_file_open_failure, // fopen() failed
VORBIS_seek_without_length, // can't seek in unknown-length file
VORBIS_unexpected_eof=10, // file is truncated?
VORBIS_seek_invalid, // seek past EOF
// decoding errors (corrupt/invalid stream) -- you probably
// don't care about the exact details of these
// vorbis errors:
VORBIS_invalid_setup=20,
VORBIS_invalid_stream,
// ogg errors:
VORBIS_missing_capture_pattern=30,
VORBIS_invalid_stream_structure_version,
VORBIS_continued_packet_flag_invalid,
VORBIS_incorrect_stream_serial_number,
VORBIS_invalid_first_page,
VORBIS_bad_packet_type,
VORBIS_cant_find_last_page,
VORBIS_seek_failed,
VORBIS_ogg_skeleton_not_supported
};
#ifdef __cplusplus
}
#endif
#endif // STB_VORBIS_INCLUDE_STB_VORBIS_H
//
// HEADER ENDS HERE
//
//////////////////////////////////////////////////////////////////////////////

File diff suppressed because it is too large Load Diff

@ -1,16 +1,25 @@
CONFIG_BOOTLOADER_LOG_LEVEL_WARN=y CONFIG_BOOTLOADER_LOG_LEVEL_NONE=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y CONFIG_ESPTOOLPY_HEADER_FLASHSIZE_UPDATE=y
CONFIG_PARTITION_TABLE_CUSTOM=y CONFIG_PARTITION_TABLE_CUSTOM=y
CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE=y
CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y CONFIG_COMPILER_STACK_CHECK_MODE_NORM=y
CONFIG_BT_ENABLED=y
CONFIG_BT_CLASSIC_ENABLED=y
CONFIG_BT_A2DP_ENABLE=y
# CONFIG_BT_BLE_ENABLED is not set
# CONFIG_BT_MULTI_CONNECTION_ENBALE is not set
CONFIG_BT_ALLOCATION_FROM_SPIRAM_FIRST=y
CONFIG_BTDM_CTRL_MODE_BR_EDR_ONLY=y
# CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC is not set # CONFIG_TWAI_ERRATA_FIX_BUS_OFF_REC is not set
# CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST is not set # CONFIG_TWAI_ERRATA_FIX_TX_INTR_LOST is not set
# CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID is not set # CONFIG_TWAI_ERRATA_FIX_RX_FRAME_INVALID is not set
# CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT is not set # CONFIG_TWAI_ERRATA_FIX_RX_FIFO_CORRUPT is not set
CONFIG_I2S_ISR_IRAM_SAFE=y CONFIG_I2S_ISR_IRAM_SAFE=y
# CONFIG_ETH_USE_ESP32_EMAC is not set
# CONFIG_ETH_USE_SPI_ETHERNET is not set
CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y CONFIG_ESP_HTTP_CLIENT_ENABLE_DIGEST_AUTH=y
CONFIG_ESP32_REV_MIN_3=y CONFIG_ESP32_REV_MIN_3=y
CONFIG_ESP_PHY_REDUCE_TX_POWER=y
CONFIG_SPIRAM=y CONFIG_SPIRAM=y
CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y
CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y CONFIG_SPIRAM_ALLOW_BSS_SEG_EXTERNAL_MEMORY=y
@ -22,14 +31,20 @@ CONFIG_ESP_MAIN_TASK_STACK_SIZE=12000
CONFIG_ESP_INT_WDT_TIMEOUT_MS=1000 CONFIG_ESP_INT_WDT_TIMEOUT_MS=1000
CONFIG_ESP_TASK_WDT_TIMEOUT_S=10 CONFIG_ESP_TASK_WDT_TIMEOUT_S=10
CONFIG_ESP_IPC_TASK_STACK_SIZE=1536 CONFIG_ESP_IPC_TASK_STACK_SIZE=1536
CONFIG_ESP32_WIFI_STATIC_RX_BUFFER_NUM=10 CONFIG_ESP_WIFI_STATIC_RX_BUFFER_NUM=10
CONFIG_ESP32_WIFI_RX_BA_WIN=6 CONFIG_ESP_WIFI_RX_BA_WIN=6
CONFIG_ESP_WIFI_IRAM_OPT=y
CONFIG_ESP_WIFI_RX_IRAM_OPT=y
CONFIG_FATFS_VOLUME_COUNT=1
CONFIG_FATFS_LFN_HEAP=y CONFIG_FATFS_LFN_HEAP=y
CONFIG_FATFS_API_ENCODING_UTF_8=y CONFIG_FATFS_API_ENCODING_UTF_8=y
CONFIG_FATFS_USE_FASTSEEK=y CONFIG_FATFS_USE_FASTSEEK=y
CONFIG_FREERTOS_VTASKLIST_INCLUDE_COREID=y
CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y
CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y CONFIG_FREERTOS_WATCHPOINT_END_OF_STACK=y
CONFIG_HEAP_POISONING_COMPREHENSIVE=y CONFIG_HEAP_POISONING_COMPREHENSIVE=y
CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS=y CONFIG_HEAP_ABORT_WHEN_ALLOCATION_FAILS=y
CONFIG_WIFI_PROV_BLE_FORCE_ENCRYPTION=y
CONFIG_LV_COLOR_16_SWAP=y CONFIG_LV_COLOR_16_SWAP=y
CONFIG_LV_COLOR_MIX_ROUND_OFS=0 CONFIG_LV_COLOR_MIX_ROUND_OFS=0
CONFIG_LV_MEM_CUSTOM=y CONFIG_LV_MEM_CUSTOM=y

@ -7,16 +7,19 @@
#include "app_console.hpp" #include "app_console.hpp"
#include <dirent.h> #include <dirent.h>
#include <stdint.h>
#include <algorithm> #include <algorithm>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <iomanip>
#include <iostream> #include <iostream>
#include <ostream> #include <ostream>
#include <sstream> #include <sstream>
#include <string> #include <string>
#include "FreeRTOSConfig.h"
#include "audio_events.hpp" #include "audio_events.hpp"
#include "audio_fsm.hpp" #include "audio_fsm.hpp"
#include "database.hpp" #include "database.hpp"
@ -25,6 +28,8 @@
#include "esp_log.h" #include "esp_log.h"
#include "event_queue.hpp" #include "event_queue.hpp"
#include "ff.h" #include "ff.h"
#include "freertos/FreeRTOSConfig_arch.h"
#include "freertos/projdefs.h"
#include "index.hpp" #include "index.hpp"
#include "track.hpp" #include "track.hpp"
@ -324,6 +329,116 @@ void RegisterDbDump() {
esp_console_cmd_register(&cmd); esp_console_cmd_register(&cmd);
} }
int CmdTaskStats(int argc, char** argv) {
if (!configUSE_TRACE_FACILITY) {
std::cout << "configUSE_TRACE_FACILITY must be enabled" << std::endl;
std::cout << "also consider configTASKLIST_USE_COREID" << std::endl;
return 1;
}
static const std::string usage = "usage: task_stats";
if (argc != 1) {
std::cout << usage << std::endl;
return 1;
}
// Pad the number of tasks so that uxTaskGetSystemState still returns info if
// new tasks are started during measurement.
size_t num_tasks = uxTaskGetNumberOfTasks() + 4;
TaskStatus_t* start_status = new TaskStatus_t[num_tasks];
TaskStatus_t* end_status = new TaskStatus_t[num_tasks];
uint32_t start_elapsed_ticks = 0;
uint32_t end_elapsed_ticks = 0;
size_t start_num_tasks =
uxTaskGetSystemState(start_status, num_tasks, &start_elapsed_ticks);
vTaskDelay(pdMS_TO_TICKS(2500));
size_t end_num_tasks =
uxTaskGetSystemState(end_status, num_tasks, &end_elapsed_ticks);
std::vector<std::pair<uint32_t, std::string>> info_strings;
for (int i = 0; i < start_num_tasks; i++) {
int k = -1;
for (int j = 0; j < end_num_tasks; j++) {
if (start_status[i].xHandle == end_status[j].xHandle) {
k = j;
break;
}
}
if (k >= 0) {
uint32_t run_time =
end_status[k].ulRunTimeCounter - start_status[i].ulRunTimeCounter;
float time_percent =
static_cast<float>(run_time) /
static_cast<float>(end_elapsed_ticks - start_elapsed_ticks);
auto depth = uxTaskGetStackHighWaterMark2(start_status[i].xHandle);
float depth_kib = static_cast<float>(depth) / 1024.0f;
std::ostringstream str;
str << start_status[i].pcTaskName;
if (str.str().size() < 8) {
str << "\t\t";
} else {
str << "\t";
}
if (configTASKLIST_INCLUDE_COREID) {
if (start_status[i].xCoreID == tskNO_AFFINITY) {
str << "any\t";
} else {
str << start_status[i].xCoreID << "\t";
}
}
str << std::fixed << std::setprecision(1) << depth_kib;
str << " KiB";
if (depth_kib >= 10) {
str << "\t";
} else {
str << "\t\t";
}
str << std::fixed << std::setprecision(1) << time_percent * 100;
str << "%";
info_strings.push_back({run_time, str.str()});
}
}
std::sort(info_strings.begin(), info_strings.end(),
[](const auto& first, const auto& second) {
return first.first >= second.first;
});
std::cout << "name\t\t";
if (configTASKLIST_INCLUDE_COREID) {
std::cout << "core\t";
}
std::cout << "free stack\trun time" << std::endl;
for (const auto& i : info_strings) {
std::cout << i.second << std::endl;
}
delete[] start_status;
delete[] end_status;
return 0;
}
void RegisterTaskStates() {
esp_console_cmd_t cmd{.command = "task_stats",
.help = "prints performance info for all tasks",
.hint = NULL,
.func = &CmdTaskStats,
.argtable = NULL};
esp_console_cmd_register(&cmd);
}
auto AppConsole::RegisterExtraComponents() -> void { auto AppConsole::RegisterExtraComponents() -> void {
RegisterListDir(); RegisterListDir();
RegisterPlayFile(); RegisterPlayFile();
@ -336,6 +451,7 @@ auto AppConsole::RegisterExtraComponents() -> void {
RegisterDbTracks(); RegisterDbTracks();
RegisterDbIndex(); RegisterDbIndex();
RegisterDbDump(); RegisterDbDump();
RegisterTaskStates();
} }
} // namespace console } // namespace console

@ -5,7 +5,7 @@
idf_component_register( idf_component_register(
SRCS "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp" SRCS "audio_task.cpp" "chunk.cpp" "fatfs_audio_input.cpp"
"stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp" "stream_message.cpp" "i2s_audio_output.cpp" "stream_buffer.cpp" "track_queue.cpp"
"stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" "stream_event.cpp" "stream_info.cpp" "audio_fsm.cpp" "sink_mixer.cpp" "resample.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist") REQUIRES "codecs" "drivers" "cbor" "result" "tasks" "span" "memory" "tinyfsm" "database" "system_fsm" "playlist")

@ -34,6 +34,8 @@
#include "freertos/queue.h" #include "freertos/queue.h"
#include "freertos/ringbuf.h" #include "freertos/ringbuf.h"
#include "pipeline.hpp" #include "pipeline.hpp"
#include "sample.hpp"
#include "sink_mixer.hpp"
#include "span.hpp" #include "span.hpp"
#include "arena.hpp" #include "arena.hpp"
@ -107,7 +109,10 @@ auto Timer::bytes_to_samples(uint32_t bytes) -> uint32_t {
auto AudioTask::Start(IAudioSource* source, IAudioSink* sink) -> AudioTask* { auto AudioTask::Start(IAudioSource* source, IAudioSink* sink) -> AudioTask* {
AudioTask* task = new AudioTask(source, sink); AudioTask* task = new AudioTask(source, sink);
tasks::StartPersistent<tasks::Type::kAudio>([=]() { task->Main(); }); // Pin to CORE1 because codecs should be fixed point anyway, and being on
// the opposite core to the mixer maximises throughput in the worst case
// (some heavy codec like opus + resampling for bluetooth).
tasks::StartPersistent<tasks::Type::kAudio>(1, [=]() { task->Main(); });
return task; return task;
} }
@ -115,14 +120,12 @@ AudioTask::AudioTask(IAudioSource* source, IAudioSink* sink)
: source_(source), : source_(source),
sink_(sink), sink_(sink),
codec_(), codec_(),
mixer_(new SinkMixer(sink->stream())),
timer_(), timer_(),
has_begun_decoding_(false), has_begun_decoding_(false),
current_input_format_(), current_input_format_(),
current_output_format_(), current_output_format_(),
sample_buffer_(reinterpret_cast<std::byte*>( codec_buffer_(new RawStream(kSampleBufferSize)) {}
heap_caps_malloc(kSampleBufferSize,
MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT))),
sample_buffer_len_(kSampleBufferSize) {}
void AudioTask::Main() { void AudioTask::Main() {
for (;;) { for (;;) {
@ -226,7 +229,7 @@ auto AudioTask::BeginDecoding(InputStream& stream) -> bool {
codecs::ICodec::OutputFormat format = res.second.value(); codecs::ICodec::OutputFormat format = res.second.value();
StreamInfo::Pcm new_format{ StreamInfo::Pcm new_format{
.channels = format.num_channels, .channels = format.num_channels,
.bits_per_sample = format.bits_per_sample, .bits_per_sample = 32,
.sample_rate = format.sample_rate_hz, .sample_rate = format.sample_rate_hz,
}; };
@ -246,13 +249,18 @@ auto AudioTask::BeginDecoding(InputStream& stream) -> bool {
return false; return false;
} }
OutputStream writer{codec_buffer_.get()};
writer.prepare(new_format, {});
return true; return true;
} }
auto AudioTask::ContinueDecoding(InputStream& stream) -> bool { auto AudioTask::ContinueDecoding(InputStream& stream) -> bool {
while (!stream.data().empty()) { while (!stream.data().empty()) {
auto res = codec_->ContinueStream(stream.data(), OutputStream writer{codec_buffer_.get()};
{sample_buffer_, sample_buffer_len_});
auto res =
codec_->ContinueStream(stream.data(), writer.data_as<sample::Sample>());
stream.consume(res.first); stream.consume(res.first);
@ -263,9 +271,10 @@ auto AudioTask::ContinueDecoding(InputStream& stream) -> bool {
return false; return false;
} }
} else { } else {
xStreamBufferSend(sink_->stream(), sample_buffer_, writer.add(res.second->samples_written * sizeof(sample::Sample));
res.second->bytes_written, portMAX_DELAY);
timer_->AddBytes(res.second->bytes_written); InputStream reader{codec_buffer_.get()};
SendToSink(reader);
} }
} }
return true; return true;
@ -284,21 +293,23 @@ auto AudioTask::FinishDecoding(InputStream& stream) -> void {
std::unique_ptr<RawStream> mad_buffer; std::unique_ptr<RawStream> mad_buffer;
mad_buffer.reset(new RawStream(stream.data().size_bytes() + 8)); mad_buffer.reset(new RawStream(stream.data().size_bytes() + 8));
OutputStream writer{mad_buffer.get()}; OutputStream mad_writer{mad_buffer.get()};
std::copy(stream.data().begin(), stream.data().end(), std::copy(stream.data().begin(), stream.data().end(),
writer.data().begin()); mad_writer.data().begin());
std::fill(writer.data().begin(), writer.data().end(), std::byte{0}); std::fill(mad_writer.data().begin(), mad_writer.data().end(), std::byte{0});
InputStream padded_stream{mad_buffer.get()}; InputStream padded_stream{mad_buffer.get()};
auto res = codec_->ContinueStream(stream.data(), OutputStream writer{codec_buffer_.get()};
{sample_buffer_, sample_buffer_len_}); auto res =
codec_->ContinueStream(stream.data(), writer.data_as<sample::Sample>());
if (res.second.has_error()) { if (res.second.has_error()) {
return; return;
} }
xStreamBufferSend(sink_->stream(), sample_buffer_, writer.add(res.second->samples_written * sizeof(sample::Sample));
res.second->bytes_written, portMAX_DELAY);
timer_->AddBytes(res.second->bytes_written); InputStream reader{codec_buffer_.get()};
SendToSink(reader);
} }
} }
@ -319,24 +330,31 @@ auto AudioTask::ForwardPcmStream(StreamInfo::Pcm& format,
xStreamBufferSend(sink_->stream(), samples.data(), samples.size_bytes(), xStreamBufferSend(sink_->stream(), samples.data(), samples.size_bytes(),
portMAX_DELAY); portMAX_DELAY);
timer_->AddBytes(samples.size_bytes()); timer_->AddBytes(samples.size_bytes());
InputStream reader{codec_buffer_.get()};
SendToSink(reader);
return true; return true;
} }
auto AudioTask::ConfigureSink(const StreamInfo::Pcm& format, auto AudioTask::ConfigureSink(const StreamInfo::Pcm& format,
const Duration& duration) -> bool { const Duration& duration) -> bool {
if (format != current_output_format_) { if (format != current_output_format_) {
// The new format is different to the old one. Wait for the sink to drain current_output_format_ = format;
// before continuing. StreamInfo::Pcm new_sink_format = sink_->PrepareFormat(format);
while (!xStreamBufferIsEmpty(sink_->stream())) { if (new_sink_format != current_sink_format_) {
ESP_LOGI(kTag, "waiting for sink stream to drain..."); current_sink_format_ = new_sink_format;
// TODO(jacqueline): Get the sink drain ISR to notify us of this
// via semaphore instead of busy-ish waiting. // The new format is different to the old one. Wait for the sink to drain
vTaskDelay(pdMS_TO_TICKS(100)); // before continuing.
} while (!xStreamBufferIsEmpty(sink_->stream())) {
ESP_LOGI(kTag, "waiting for sink stream to drain...");
// TODO(jacqueline): Get the sink drain ISR to notify us of this
// via semaphore instead of busy-ish waiting.
vTaskDelay(pdMS_TO_TICKS(10));
}
ESP_LOGI(kTag, "configuring sink"); ESP_LOGI(kTag, "configuring sink");
if (!sink_->Configure(format)) { sink_->Configure(new_sink_format);
return false;
} }
} }
@ -345,4 +363,17 @@ auto AudioTask::ConfigureSink(const StreamInfo::Pcm& format,
return true; return true;
} }
auto AudioTask::SendToSink(InputStream& stream) -> void {
std::size_t bytes_to_send = stream.data().size_bytes();
std::size_t bytes_sent;
if (stream.info().format_as<StreamInfo::Pcm>() == current_sink_format_) {
bytes_sent = xStreamBufferSend(sink_->stream(), stream.data().data(),
bytes_to_send, portMAX_DELAY);
stream.consume(bytes_sent);
} else {
bytes_sent = mixer_->MixAndSend(stream, current_sink_format_.value());
}
timer_->AddBytes(bytes_sent);
}
} // namespace audio } // namespace audio

@ -30,6 +30,7 @@
#include "freertos/portmacro.h" #include "freertos/portmacro.h"
#include "freertos/projdefs.h" #include "freertos/projdefs.h"
#include "future_fetcher.hpp" #include "future_fetcher.hpp"
#include "idf_additions.h"
#include "span.hpp" #include "span.hpp"
#include "stream_info.hpp" #include "stream_info.hpp"
#include "tag_parser.hpp" #include "tag_parser.hpp"
@ -40,8 +41,8 @@ static const char* kTag = "SRC";
namespace audio { namespace audio {
static constexpr UINT kFileBufferSize = 4096 * 2; static constexpr UINT kFileBufferSize = 8 * 1024;
static constexpr UINT kStreamerBufferSize = 4096; static constexpr UINT kStreamerBufferSize = 64 * 1024;
static StreamBufferHandle_t sForwardDest = nullptr; static StreamBufferHandle_t sForwardDest = nullptr;
@ -143,7 +144,9 @@ FatfsAudioInput::FatfsAudioInput(
: IAudioSource(), : IAudioSource(),
tag_parser_(tag_parser), tag_parser_(tag_parser),
has_data_(xSemaphoreCreateBinary()), has_data_(xSemaphoreCreateBinary()),
streamer_buffer_(xStreamBufferCreate(kStreamerBufferSize, 1)), streamer_buffer_(xStreamBufferCreateWithCaps(kStreamerBufferSize,
1,
MALLOC_CAP_SPIRAM)),
streamer_(new FileStreamer(streamer_buffer_, has_data_)), streamer_(new FileStreamer(streamer_buffer_, has_data_)),
input_buffer_(new RawStream(kFileBufferSize)), input_buffer_(new RawStream(kFileBufferSize)),
source_mutex_(), source_mutex_(),

@ -115,10 +115,20 @@ auto I2SAudioOutput::AdjustVolumeDown() -> bool {
return true; return true;
} }
auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> bool { auto I2SAudioOutput::PrepareFormat(const StreamInfo::Pcm& orig)
-> StreamInfo::Pcm {
return StreamInfo::Pcm{
.channels = std::min<uint8_t>(orig.channels, 2),
.bits_per_sample = std::clamp<uint8_t>(orig.bits_per_sample, 16, 32),
.sample_rate = 44100,
//.sample_rate = std::clamp<uint32_t>(orig.sample_rate, 8000, 96000),
};
}
auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> void {
if (current_config_ && pcm == *current_config_) { if (current_config_ && pcm == *current_config_) {
ESP_LOGI(kTag, "ignoring unchanged format"); ESP_LOGI(kTag, "ignoring unchanged format");
return true; return;
} }
ESP_LOGI(kTag, "incoming audio stream: %u ch %u bpp @ %lu Hz", pcm.channels, ESP_LOGI(kTag, "incoming audio stream: %u ch %u bpp @ %lu Hz", pcm.channels,
@ -134,7 +144,7 @@ auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> bool {
break; break;
default: default:
ESP_LOGE(kTag, "dropping stream with out of bounds channels"); ESP_LOGE(kTag, "dropping stream with out of bounds channels");
return false; return;
} }
drivers::I2SDac::BitsPerSample bps; drivers::I2SDac::BitsPerSample bps;
@ -150,30 +160,36 @@ auto I2SAudioOutput::Configure(const StreamInfo::Pcm& pcm) -> bool {
break; break;
default: default:
ESP_LOGE(kTag, "dropping stream with unknown bps"); ESP_LOGE(kTag, "dropping stream with unknown bps");
return false; return;
} }
drivers::I2SDac::SampleRate sample_rate; drivers::I2SDac::SampleRate sample_rate;
switch (pcm.sample_rate) { switch (pcm.sample_rate) {
case 8000:
sample_rate = drivers::I2SDac::SAMPLE_RATE_8;
break;
case 32000:
sample_rate = drivers::I2SDac::SAMPLE_RATE_32;
break;
case 44100: case 44100:
sample_rate = drivers::I2SDac::SAMPLE_RATE_44_1; sample_rate = drivers::I2SDac::SAMPLE_RATE_44_1;
break; break;
case 48000: case 48000:
sample_rate = drivers::I2SDac::SAMPLE_RATE_48; sample_rate = drivers::I2SDac::SAMPLE_RATE_48;
break; break;
case 88200:
sample_rate = drivers::I2SDac::SAMPLE_RATE_88_2;
break;
case 96000:
sample_rate = drivers::I2SDac::SAMPLE_RATE_96;
break;
default: default:
ESP_LOGE(kTag, "dropping stream with unknown rate"); ESP_LOGE(kTag, "dropping stream with unknown rate");
return false; return;
} }
dac_->Reconfigure(ch, bps, sample_rate); dac_->Reconfigure(ch, bps, sample_rate);
current_config_ = pcm; current_config_ = pcm;
return true;
}
auto I2SAudioOutput::Send(const cpp::span<std::byte>& data) -> void {
dac_->WriteData(data);
} }
} // namespace audio } // namespace audio

@ -17,7 +17,6 @@ namespace audio {
class IAudioSink { class IAudioSink {
private: private:
// TODO: tune. at least about 12KiB seems right for mp3
static const std::size_t kDrainBufferSize = 24 * 1024; static const std::size_t kDrainBufferSize = 24 * 1024;
StreamBufferHandle_t stream_; StreamBufferHandle_t stream_;
@ -38,8 +37,8 @@ class IAudioSink {
virtual auto AdjustVolumeUp() -> bool = 0; virtual auto AdjustVolumeUp() -> bool = 0;
virtual auto AdjustVolumeDown() -> bool = 0; virtual auto AdjustVolumeDown() -> bool = 0;
virtual auto Configure(const StreamInfo::Pcm& format) -> bool = 0; virtual auto PrepareFormat(const StreamInfo::Pcm&) -> StreamInfo::Pcm = 0;
virtual auto Send(const cpp::span<std::byte>& data) -> void = 0; virtual auto Configure(const StreamInfo::Pcm& format) -> void = 0;
auto stream() -> StreamBufferHandle_t { return stream_; } auto stream() -> StreamBufferHandle_t { return stream_; }
}; };

@ -14,6 +14,7 @@
#include "audio_source.hpp" #include "audio_source.hpp"
#include "codec.hpp" #include "codec.hpp"
#include "pipeline.hpp" #include "pipeline.hpp"
#include "sink_mixer.hpp"
#include "stream_info.hpp" #include "stream_info.hpp"
namespace audio { namespace audio {
@ -63,18 +64,20 @@ class AudioTask {
auto ForwardPcmStream(StreamInfo::Pcm&, cpp::span<const std::byte>) -> bool; auto ForwardPcmStream(StreamInfo::Pcm&, cpp::span<const std::byte>) -> bool;
auto ConfigureSink(const StreamInfo::Pcm&, const Duration&) -> bool; auto ConfigureSink(const StreamInfo::Pcm&, const Duration&) -> bool;
auto SendToSink(InputStream&) -> void;
IAudioSource* source_; IAudioSource* source_;
IAudioSink* sink_; IAudioSink* sink_;
std::unique_ptr<codecs::ICodec> codec_; std::unique_ptr<codecs::ICodec> codec_;
std::unique_ptr<SinkMixer> mixer_;
std::unique_ptr<Timer> timer_; std::unique_ptr<Timer> timer_;
bool has_begun_decoding_; bool has_begun_decoding_;
std::optional<StreamInfo::Format> current_input_format_; std::optional<StreamInfo::Format> current_input_format_;
std::optional<StreamInfo::Pcm> current_output_format_; std::optional<StreamInfo::Pcm> current_output_format_;
std::optional<StreamInfo::Pcm> current_sink_format_;
std::byte* sample_buffer_; std::unique_ptr<RawStream> codec_buffer_;
std::size_t sample_buffer_len_;
}; };
} // namespace audio } // namespace audio

@ -0,0 +1,131 @@
/*
* FIR filter coefficients from resample-1.x smallfilter.h
* see Digital Audio Resampling Home Page located at
* http://ccrma.stanford.edu/~jos/resample/
*/
32767, 32766, 32764, 32760, 32755, 32749, 32741, 32731, 32721, 32708,
32695, 32679, 32663, 32645, 32625, 32604, 32582, 32558, 32533, 32506,
32478, 32448, 32417, 32385, 32351, 32316, 32279, 32241, 32202, 32161,
32119, 32075, 32030, 31984, 31936, 31887, 31836, 31784, 31731, 31676,
31620, 31563, 31504, 31444, 31383, 31320, 31256, 31191, 31124, 31056,
30987, 30916, 30845, 30771, 30697, 30621, 30544, 30466, 30387, 30306,
30224, 30141, 30057, 29971, 29884, 29796, 29707, 29617, 29525, 29433,
29339, 29244, 29148, 29050, 28952, 28852, 28752, 28650, 28547, 28443,
28338, 28232, 28125, 28017, 27908, 27797, 27686, 27574, 27461, 27346,
27231, 27115, 26998, 26879, 26760, 26640, 26519, 26398, 26275, 26151,
26027, 25901, 25775, 25648, 25520, 25391, 25262, 25131, 25000, 24868,
24735, 24602, 24467, 24332, 24197, 24060, 23923, 23785, 23647, 23507,
23368, 23227, 23086, 22944, 22802, 22659, 22515, 22371, 22226, 22081,
21935, 21789, 21642, 21494, 21346, 21198, 21049, 20900, 20750, 20600,
20449, 20298, 20146, 19995, 19842, 19690, 19537, 19383, 19230, 19076,
18922, 18767, 18612, 18457, 18302, 18146, 17990, 17834, 17678, 17521,
17365, 17208, 17051, 16894, 16737, 16579, 16422, 16264, 16106, 15949,
15791, 15633, 15475, 15317, 15159, 15001, 14843, 14685, 14527, 14369,
14212, 14054, 13896, 13739, 13581, 13424, 13266, 13109, 12952, 12795,
12639, 12482, 12326, 12170, 12014, 11858, 11703, 11548, 11393, 11238,
11084, 10929, 10776, 10622, 10469, 10316, 10164, 10011, 9860, 9708,
9557, 9407, 9256, 9106, 8957, 8808, 8659, 8511, 8364, 8216, 8070,
7924, 7778, 7633, 7488, 7344, 7200, 7057, 6914, 6773, 6631, 6490,
6350, 6210, 6071, 5933, 5795, 5658, 5521, 5385, 5250, 5115, 4981,
4848, 4716, 4584, 4452, 4322, 4192, 4063, 3935, 3807, 3680, 3554,
3429, 3304, 3180, 3057, 2935, 2813, 2692, 2572, 2453, 2335, 2217,
2101, 1985, 1870, 1755, 1642, 1529, 1418, 1307, 1197, 1088, 979, 872,
765, 660, 555, 451, 348, 246, 145, 44, -54, -153, -250, -347, -443,
-537, -631, -724, -816, -908, -998, -1087, -1175, -1263, -1349, -1435,
-1519, -1603, -1685, -1767, -1848, -1928, -2006, -2084, -2161, -2237,
-2312, -2386, -2459, -2531, -2603, -2673, -2742, -2810, -2878, -2944,
-3009, -3074, -3137, -3200, -3261, -3322, -3381, -3440, -3498, -3554,
-3610, -3665, -3719, -3772, -3824, -3875, -3925, -3974, -4022, -4069,
-4116, -4161, -4205, -4249, -4291, -4333, -4374, -4413, -4452, -4490,
-4527, -4563, -4599, -4633, -4666, -4699, -4730, -4761, -4791, -4820,
-4848, -4875, -4901, -4926, -4951, -4974, -4997, -5019, -5040, -5060,
-5080, -5098, -5116, -5133, -5149, -5164, -5178, -5192, -5205, -5217,
-5228, -5238, -5248, -5257, -5265, -5272, -5278, -5284, -5289, -5293,
-5297, -5299, -5301, -5303, -5303, -5303, -5302, -5300, -5298, -5295,
-5291, -5287, -5282, -5276, -5270, -5263, -5255, -5246, -5237, -5228,
-5217, -5206, -5195, -5183, -5170, -5157, -5143, -5128, -5113, -5097,
-5081, -5064, -5047, -5029, -5010, -4991, -4972, -4952, -4931, -4910,
-4889, -4867, -4844, -4821, -4797, -4774, -4749, -4724, -4699, -4673,
-4647, -4620, -4593, -4566, -4538, -4510, -4481, -4452, -4422, -4393,
-4363, -4332, -4301, -4270, -4238, -4206, -4174, -4142, -4109, -4076,
-4042, -4009, -3975, -3940, -3906, -3871, -3836, -3801, -3765, -3729,
-3693, -3657, -3620, -3584, -3547, -3510, -3472, -3435, -3397, -3360,
-3322, -3283, -3245, -3207, -3168, -3129, -3091, -3052, -3013, -2973,
-2934, -2895, -2855, -2816, -2776, -2736, -2697, -2657, -2617, -2577,
-2537, -2497, -2457, -2417, -2377, -2337, -2297, -2256, -2216, -2176,
-2136, -2096, -2056, -2016, -1976, -1936, -1896, -1856, -1817, -1777,
-1737, -1698, -1658, -1619, -1579, -1540, -1501, -1462, -1423, -1384,
-1345, -1306, -1268, -1230, -1191, -1153, -1115, -1077, -1040, -1002,
-965, -927, -890, -854, -817, -780, -744, -708, -672, -636, -600,
-565, -530, -494, -460, -425, -391, -356, -322, -289, -255, -222,
-189, -156, -123, -91, -59, -27, 4, 35, 66, 97, 127, 158, 188, 218,
247, 277, 306, 334, 363, 391, 419, 447, 474, 501, 528, 554, 581, 606,
632, 657, 683, 707, 732, 756, 780, 803, 827, 850, 872, 895, 917, 939,
960, 981, 1002, 1023, 1043, 1063, 1082, 1102, 1121, 1139, 1158, 1176,
1194, 1211, 1228, 1245, 1262, 1278, 1294, 1309, 1325, 1340, 1354,
1369, 1383, 1397, 1410, 1423, 1436, 1448, 1461, 1473, 1484, 1496,
1507, 1517, 1528, 1538, 1548, 1557, 1566, 1575, 1584, 1592, 1600,
1608, 1616, 1623, 1630, 1636, 1643, 1649, 1654, 1660, 1665, 1670,
1675, 1679, 1683, 1687, 1690, 1694, 1697, 1700, 1702, 1704, 1706,
1708, 1709, 1711, 1712, 1712, 1713, 1713, 1713, 1713, 1712, 1711,
1710, 1709, 1708, 1706, 1704, 1702, 1700, 1697, 1694, 1691, 1688,
1685, 1681, 1677, 1673, 1669, 1664, 1660, 1655, 1650, 1644, 1639,
1633, 1627, 1621, 1615, 1609, 1602, 1596, 1589, 1582, 1575, 1567,
1560, 1552, 1544, 1536, 1528, 1520, 1511, 1503, 1494, 1485, 1476,
1467, 1458, 1448, 1439, 1429, 1419, 1409, 1399, 1389, 1379, 1368,
1358, 1347, 1337, 1326, 1315, 1304, 1293, 1282, 1271, 1260, 1248,
1237, 1225, 1213, 1202, 1190, 1178, 1166, 1154, 1142, 1130, 1118,
1106, 1094, 1081, 1069, 1057, 1044, 1032, 1019, 1007, 994, 981, 969,
956, 943, 931, 918, 905, 892, 879, 867, 854, 841, 828, 815, 802, 790,
777, 764, 751, 738, 725, 713, 700, 687, 674, 662, 649, 636, 623, 611,
598, 585, 573, 560, 548, 535, 523, 510, 498, 486, 473, 461, 449, 437,
425, 413, 401, 389, 377, 365, 353, 341, 330, 318, 307, 295, 284, 272,
261, 250, 239, 228, 217, 206, 195, 184, 173, 163, 152, 141, 131, 121,
110, 100, 90, 80, 70, 60, 51, 41, 31, 22, 12, 3, -5, -14, -23, -32,
-41, -50, -59, -67, -76, -84, -93, -101, -109, -117, -125, -133, -140,
-148, -156, -163, -170, -178, -185, -192, -199, -206, -212, -219,
-226, -232, -239, -245, -251, -257, -263, -269, -275, -280, -286,
-291, -297, -302, -307, -312, -317, -322, -327, -332, -336, -341,
-345, -349, -354, -358, -362, -366, -369, -373, -377, -380, -384,
-387, -390, -394, -397, -400, -402, -405, -408, -411, -413, -416,
-418, -420, -422, -424, -426, -428, -430, -432, -433, -435, -436,
-438, -439, -440, -442, -443, -444, -445, -445, -446, -447, -447,
-448, -448, -449, -449, -449, -449, -449, -449, -449, -449, -449,
-449, -449, -448, -448, -447, -447, -446, -445, -444, -443, -443,
-442, -441, -440, -438, -437, -436, -435, -433, -432, -430, -429,
-427, -426, -424, -422, -420, -419, -417, -415, -413, -411, -409,
-407, -405, -403, -400, -398, -396, -393, -391, -389, -386, -384,
-381, -379, -376, -374, -371, -368, -366, -363, -360, -357, -355,
-352, -349, -346, -343, -340, -337, -334, -331, -328, -325, -322,
-319, -316, -313, -310, -307, -304, -301, -298, -294, -291, -288,
-285, -282, -278, -275, -272, -269, -265, -262, -259, -256, -252,
-249, -246, -243, -239, -236, -233, -230, -226, -223, -220, -217,
-213, -210, -207, -204, -200, -197, -194, -191, -187, -184, -181,
-178, -175, -172, -168, -165, -162, -159, -156, -153, -150, -147,
-143, -140, -137, -134, -131, -128, -125, -122, -120, -117, -114,
-111, -108, -105, -102, -99, -97, -94, -91, -88, -86, -83, -80, -78,
-75, -72, -70, -67, -65, -62, -59, -57, -55, -52, -50, -47, -45, -43,
-40, -38, -36, -33, -31, -29, -27, -25, -22, -20, -18, -16, -14, -12,
-10, -8, -6, -4, -2, 0, 0, 2, 4, 6, 8, 9, 11, 13, 14, 16, 17, 19, 21,
22, 24, 25, 27, 28, 29, 31, 32, 33, 35, 36, 37, 38, 40, 41, 42, 43,
44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58, 59,
59, 60, 61, 62, 62, 63, 63, 64, 64, 65, 66, 66, 66, 67, 67, 68, 68,
69, 69, 69, 70, 70, 70, 70, 71, 71, 71, 71, 71, 72, 72, 72, 72, 72,
72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72, 72,
72, 71, 71, 71, 71, 71, 70, 70, 70, 70, 69, 69, 69, 69, 68, 68, 68,
67, 67, 67, 66, 66, 66, 65, 65, 64, 64, 64, 63, 63, 62, 62, 62, 61,
61, 60, 60, 59, 59, 58, 58, 58, 57, 57, 56, 56, 55, 55, 54, 54, 53,
53, 52, 52, 51, 51, 50, 50, 49, 48, 48, 47, 47, 46, 46, 45, 45, 44,
44, 43, 43, 42, 42, 41, 41, 40, 39, 39, 38, 38, 37, 37, 36, 36, 35,
35, 34, 34, 33, 33, 32, 32, 31, 31, 30, 30, 29, 29, 28, 28, 27, 27,
26, 26, 25, 25, 24, 24, 23, 23, 23, 22, 22, 21, 21, 20, 20, 20, 19,
19, 18, 18, 17, 17, 17, 16, 16, 15, 15, 15, 14, 14, 14, 13, 13, 12,
12, 12, 11, 11, 11, 10, 10, 10, 9, 9, 9, 9, 8, 8, 8, 7, 7, 7, 7, 6, 6,
6, 6, 5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3, 3, 2, 2, 2, 2, 2, 1, 1, 1,
1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2, -2,
-2, -2, -2, -2, -2, -2, -2, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1,
-1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1,

@ -35,8 +35,8 @@ class I2SAudioOutput : public IAudioSink {
auto AdjustVolumeUp() -> bool override; auto AdjustVolumeUp() -> bool override;
auto AdjustVolumeDown() -> bool override; auto AdjustVolumeDown() -> bool override;
auto Configure(const StreamInfo::Pcm& format) -> bool override; auto PrepareFormat(const StreamInfo::Pcm&) -> StreamInfo::Pcm override;
auto Send(const cpp::span<std::byte>& data) -> void override; auto Configure(const StreamInfo::Pcm& format) -> void override;
I2SAudioOutput(const I2SAudioOutput&) = delete; I2SAudioOutput(const I2SAudioOutput&) = delete;
I2SAudioOutput& operator=(const I2SAudioOutput&) = delete; I2SAudioOutput& operator=(const I2SAudioOutput&) = delete;

@ -0,0 +1,44 @@
#pragma once
#include <sys/_stdint.h>
#include <vector>
#include "span.hpp"
#include "sample.hpp"
namespace audio {
class Resampler {
public:
Resampler(uint32_t source_sample_rate,
uint32_t target_sample_rate,
uint8_t num_channels);
~Resampler();
auto source_sample_rate() -> uint32_t { return source_sample_rate_; }
auto target_sample_rate() -> uint32_t { return target_sample_rate_; }
auto channels() -> uint_fast8_t { return num_channels_; }
auto Process(cpp::span<const sample::Sample> input,
cpp::span<sample::Sample> output,
bool end_of_data) -> std::pair<size_t, size_t>;
private:
auto Subsample(int channel) -> float;
auto ApplyFilter(cpp::span<float> filter, cpp::span<float> input) -> float;
uint32_t source_sample_rate_;
uint32_t target_sample_rate_;
float factor_;
uint8_t num_channels_;
std::vector<float*> channel_buffers_;
size_t channel_buffer_size_;
float output_offset_;
int32_t input_index_;
};
} // namespace audio

@ -0,0 +1,71 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <sys/_stdint.h>
#include <cstdint>
#include <memory>
#include "resample.hpp"
#include "sample.hpp"
#include "audio_decoder.hpp"
#include "audio_sink.hpp"
#include "audio_source.hpp"
#include "codec.hpp"
#include "pipeline.hpp"
#include "stream_info.hpp"
namespace audio {
/*
* Handles the final downmix + resample + quantisation stage of audio,
* generation sending the result directly to an IAudioSink.
*/
class SinkMixer {
public:
SinkMixer(StreamBufferHandle_t dest);
~SinkMixer();
auto MixAndSend(InputStream&, const StreamInfo::Pcm&) -> std::size_t;
private:
auto Main() -> void;
auto SetTargetFormat(const StreamInfo::Pcm& format) -> void;
auto HandleBytes() -> void;
auto Resample(InputStream&, OutputStream&) -> bool;
auto ApplyDither(cpp::span<sample::Sample> samples, uint_fast8_t bits)
-> void;
auto Downscale(cpp::span<sample::Sample>, cpp::span<int16_t>) -> void;
enum class Command {
kReadBytes,
kSetSourceFormat,
kSetTargetFormat,
};
struct Args {
Command cmd;
StreamInfo::Pcm format;
};
QueueHandle_t commands_;
SemaphoreHandle_t is_idle_;
std::unique_ptr<Resampler> resampler_;
std::unique_ptr<RawStream> input_stream_;
std::unique_ptr<RawStream> resampled_stream_;
StreamInfo::Pcm target_format_;
StreamBufferHandle_t source_;
StreamBufferHandle_t sink_;
};
} // namespace audio

@ -16,6 +16,7 @@
#include <utility> #include <utility>
#include <variant> #include <variant>
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/ringbuf.h" #include "freertos/ringbuf.h"
#include "freertos/stream_buffer.h" #include "freertos/stream_buffer.h"
@ -56,6 +57,18 @@ class StreamInfo {
bool operator==(const Encoded&) const = default; bool operator==(const Encoded&) const = default;
}; };
/*
* Two-channel, interleaved, 32-bit floating point pcm samples.
*/
struct FloatingPointPcm {
// Number of channels in this stream.
uint8_t channels;
// The sample rate.
uint32_t sample_rate;
bool operator==(const FloatingPointPcm&) const = default;
};
struct Pcm { struct Pcm {
// Number of channels in this stream. // Number of channels in this stream.
uint8_t channels; uint8_t channels;
@ -64,10 +77,14 @@ class StreamInfo {
// The sample rate. // The sample rate.
uint32_t sample_rate; uint32_t sample_rate;
auto bytes_per_sample() const -> uint8_t {
return bits_per_sample == 16 ? 2 : 4;
}
bool operator==(const Pcm&) const = default; bool operator==(const Pcm&) const = default;
}; };
typedef std::variant<std::monostate, Encoded, Pcm> Format; typedef std::variant<std::monostate, Encoded, FloatingPointPcm, Pcm> Format;
auto format() const -> const Format& { return format_; } auto format() const -> const Format& { return format_; }
auto set_format(Format f) -> void { format_ = f; } auto set_format(Format f) -> void { format_ = f; }
@ -94,10 +111,17 @@ class OutputStream;
class RawStream { class RawStream {
public: public:
explicit RawStream(std::size_t size); explicit RawStream(std::size_t size);
RawStream(std::size_t size, uint32_t);
~RawStream(); ~RawStream();
auto info() -> StreamInfo& { return info_; } auto info() -> StreamInfo& { return info_; }
auto data() -> cpp::span<std::byte>; auto data() -> cpp::span<std::byte>;
template <typename T>
auto data_as() -> cpp::span<T> {
auto orig = data();
return {reinterpret_cast<T*>(orig.data()), orig.size_bytes() / sizeof(T)};
}
auto empty() const -> bool { return info_.bytes_in_stream() == 0; }
private: private:
StreamInfo info_; StreamInfo info_;
@ -114,6 +138,12 @@ class InputStream {
const StreamInfo& info() const; const StreamInfo& info() const;
cpp::span<const std::byte> data() const; cpp::span<const std::byte> data() const;
template <typename T>
auto data_as() const -> cpp::span<const T> {
auto orig = data();
return {reinterpret_cast<const T*>(orig.data()),
orig.size_bytes() / sizeof(T)};
}
private: private:
RawStream* raw_; RawStream* raw_;
@ -131,6 +161,11 @@ class OutputStream {
const StreamInfo& info() const; const StreamInfo& info() const;
cpp::span<std::byte> data() const; cpp::span<std::byte> data() const;
template <typename T>
auto data_as() const -> cpp::span<T> {
auto orig = data();
return {reinterpret_cast<T*>(orig.data()), orig.size_bytes() / sizeof(T)};
}
private: private:
RawStream* raw_; RawStream* raw_;

@ -0,0 +1,205 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "resample.hpp"
/*
* This file contains the implementation for a 32-bit floating point resampler.
* It is largely based on David Bryant's ART resampler, which is BSD-licensed,
* and available at https://github.com/dbry/audio-resampler/.
*
* This resampler uses windowed sinc interpolation filters, with an additional
* lowpass filter to reduce aliasing.
*/
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <algorithm>
#include <cmath>
#include <numeric>
#include "esp_log.h"
#include "sample.hpp"
#include "stream_info.hpp"
namespace audio {
static constexpr double kLowPassRatio = 0.5;
static constexpr size_t kNumFilters = 64;
static constexpr size_t kFilterSize = 16;
typedef std::array<float, kFilterSize> Filter;
static std::array<Filter, kNumFilters + 1> sFilters{};
static bool sFiltersInitialised = false;
auto InitFilter(int index) -> void;
Resampler::Resampler(uint32_t source_sample_rate,
uint32_t target_sample_rate,
uint8_t num_channels)
: source_sample_rate_(source_sample_rate),
target_sample_rate_(target_sample_rate),
factor_(static_cast<double>(target_sample_rate) /
static_cast<double>(source_sample_rate)),
num_channels_(num_channels) {
channel_buffers_.resize(num_channels);
channel_buffer_size_ = kFilterSize * 16;
for (int i = 0; i < num_channels; i++) {
channel_buffers_[i] =
static_cast<float*>(calloc(sizeof(float), channel_buffer_size_));
}
output_offset_ = kFilterSize / 2.0f;
input_index_ = kFilterSize;
if (!sFiltersInitialised) {
sFiltersInitialised = true;
for (int i = 0; i < kNumFilters + 1; i++) {
InitFilter(i);
}
}
}
Resampler::~Resampler() {}
auto Resampler::Process(cpp::span<const sample::Sample> input,
cpp::span<sample::Sample> output,
bool end_of_data) -> std::pair<size_t, size_t> {
size_t samples_used = 0;
size_t samples_produced = 0;
size_t input_frames = input.size() / num_channels_;
size_t output_frames = output.size() / num_channels_;
int half_taps = kFilterSize / 2;
while (output_frames > 0) {
if (output_offset_ >= input_index_ - half_taps) {
if (input_frames > 0) {
// Check whether the channel buffers will overflow with the addition of
// this sample. If so, we need to move the remaining contents back to
// the beginning of the buffer.
if (input_index_ == channel_buffer_size_) {
for (int i = 0; i < num_channels_; ++i) {
memmove(channel_buffers_[i],
channel_buffers_[i] + channel_buffer_size_ - kFilterSize,
kFilterSize * sizeof(float));
}
output_offset_ -= channel_buffer_size_ - kFilterSize;
input_index_ -= channel_buffer_size_ - kFilterSize;
}
for (int i = 0; i < num_channels_; ++i) {
channel_buffers_[i][input_index_] =
sample::ToFloat(input[samples_used++]);
}
input_index_++;
input_frames--;
} else {
break;
}
} else {
for (int i = 0; i < num_channels_; i++) {
output[samples_produced++] = sample::FromFloat(Subsample(i));
}
// NOTE: floating point division here is potentially slow due to FPU
// limitations. Consider explicitly bunding the xtensa libgcc divsion via
// reciprocal implementation if we care about portability between
// compilers.
output_offset_ += 1.0f / factor_;
output_frames--;
}
}
return {samples_used, samples_produced};
}
/*
* Constructs the filter in-place for the given index of sFilters. This only
* needs to be done once, per-filter. 64-bit math is okay here, because filters
* will not be initialised within a performance critical path.
*/
auto InitFilter(int index) -> void {
Filter& filter = sFilters[index];
std::array<double, kFilterSize> working_buffer{};
double fraction = index / static_cast<double>(kNumFilters);
double filter_sum = 0.0;
for (int i = 0; i < kFilterSize; ++i) {
// "dist" is the absolute distance from the sinc maximum to the filter tap
// to be calculated, in radians.
double dist = fabs((kFilterSize / 2.0 - 1.0) + fraction - i) * M_PI;
// "ratio" is that distance divided by half the tap count such that it
// reaches π at the window extremes
double ratio = dist / (kFilterSize / 2.0);
double value;
if (dist != 0.0) {
value = sin(dist * kLowPassRatio) / (dist * kLowPassRatio);
// Hann window. We could alternatively use a Blackman Harris window,
// however our unusually small filter size makes the Hann window's
// steeper cutoff more important.
value *= 0.5 * (1.0 + cos(ratio));
} else {
value = 1.0;
}
working_buffer[i] = value;
filter_sum += value;
}
// Filter should have unity DC gain
double scaler = 1.0 / filter_sum;
double error = 0.0;
for (int i = kFilterSize / 2; i < kFilterSize;
i = kFilterSize - i - (i >= kFilterSize / 2)) {
working_buffer[i] *= scaler;
filter[i] = working_buffer[i] - error;
error += static_cast<double>(filter[i]) - working_buffer[i];
}
}
/*
* Performs sub-sampling with interpolation for the given channel. Assumes that
* the channel buffer has already been filled with samples.
*/
auto Resampler::Subsample(int channel) -> float {
cpp::span<float> source{channel_buffers_[channel], channel_buffer_size_};
int offset_integral = std::floor(output_offset_);
source = source.subspan(offset_integral);
float offset_fractional = output_offset_ - offset_integral;
offset_fractional *= kNumFilters;
int filter_index = std::floor(offset_fractional);
float sum1 = ApplyFilter(sFilters[filter_index],
{source.data() - kFilterSize / 2 + 1, kFilterSize});
offset_fractional -= filter_index;
float sum2 = ApplyFilter(sFilters[filter_index + 1],
{source.data() - kFilterSize / 2 + 1, kFilterSize});
return (sum2 * offset_fractional) + (sum1 * (1.0f - offset_fractional));
}
auto Resampler::ApplyFilter(cpp::span<float> filter, cpp::span<float> input)
-> float {
float sum = 0.0;
for (int i = 0; i < kFilterSize; i++) {
sum += filter[i] * input[i];
}
return sum;
}
} // namespace audio

@ -0,0 +1,224 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "sink_mixer.hpp"
#include <stdint.h>
#include <cmath>
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "freertos/portmacro.h"
#include "freertos/projdefs.h"
#include "idf_additions.h"
#include "resample.hpp"
#include "sample.hpp"
#include "stream_info.hpp"
#include "tasks.hpp"
static constexpr char kTag[] = "mixer";
static constexpr std::size_t kSourceBufferLength = 8 * 1024;
static constexpr std::size_t kSampleBufferLength = 240 * 2 * sizeof(int32_t);
namespace audio {
SinkMixer::SinkMixer(StreamBufferHandle_t dest)
: commands_(xQueueCreate(1, sizeof(Args))),
is_idle_(xSemaphoreCreateBinary()),
resampler_(nullptr),
source_(xStreamBufferCreateWithCaps(kSourceBufferLength,
1,
MALLOC_CAP_SPIRAM)),
sink_(dest) {
input_stream_.reset(new RawStream(kSampleBufferLength));
resampled_stream_.reset(new RawStream(kSampleBufferLength));
// Pin to CORE0 because we need the FPU.
// FIXME: A fixed point implementation could run freely on either core,
// which should lead to a big performance increase.
tasks::StartPersistent<tasks::Type::kMixer>(0, [&]() { Main(); });
}
SinkMixer::~SinkMixer() {
vQueueDelete(commands_);
vSemaphoreDelete(is_idle_);
vStreamBufferDelete(source_);
}
auto SinkMixer::MixAndSend(InputStream& input, const StreamInfo::Pcm& target)
-> std::size_t {
if (input.info().format_as<StreamInfo::Pcm>() !=
input_stream_->info().format_as<StreamInfo::Pcm>()) {
xSemaphoreTake(is_idle_, portMAX_DELAY);
Args args{
.cmd = Command::kSetSourceFormat,
.format = input.info().format_as<StreamInfo::Pcm>().value(),
};
xQueueSend(commands_, &args, portMAX_DELAY);
xSemaphoreGive(is_idle_);
}
if (target_format_ != target) {
xSemaphoreTake(is_idle_, portMAX_DELAY);
Args args{
.cmd = Command::kSetTargetFormat,
.format = target,
};
xQueueSend(commands_, &args, portMAX_DELAY);
xSemaphoreGive(is_idle_);
}
Args args{
.cmd = Command::kReadBytes,
.format = {},
};
xQueueSend(commands_, &args, portMAX_DELAY);
auto buf = input.data();
std::size_t bytes_sent =
xStreamBufferSend(source_, buf.data(), buf.size_bytes(), portMAX_DELAY);
input.consume(bytes_sent);
return bytes_sent;
}
auto SinkMixer::Main() -> void {
OutputStream input_receiver{input_stream_.get()};
xSemaphoreGive(is_idle_);
for (;;) {
Args args;
while (!xQueueReceive(commands_, &args, portMAX_DELAY)) {
}
switch (args.cmd) {
case Command::kSetSourceFormat:
ESP_LOGI(kTag, "setting source format");
input_receiver.prepare(args.format, {});
resampler_.reset();
break;
case Command::kSetTargetFormat:
ESP_LOGI(kTag, "setting target format");
target_format_ = args.format;
resampler_.reset();
break;
case Command::kReadBytes:
xSemaphoreTake(is_idle_, 0);
while (!xStreamBufferIsEmpty(source_)) {
auto buf = input_receiver.data();
std::size_t bytes_received = xStreamBufferReceive(
source_, buf.data(), buf.size_bytes(), portMAX_DELAY);
input_receiver.add(bytes_received);
HandleBytes();
}
xSemaphoreGive(is_idle_);
break;
}
}
}
auto SinkMixer::HandleBytes() -> void {
InputStream input{input_stream_.get()};
auto pcm = input.info().format_as<StreamInfo::Pcm>();
if (!pcm) {
ESP_LOGE(kTag, "mixer got unsupported data");
return;
}
if (*pcm == target_format_) {
// The happiest possible case: the input format matches the output
// format already. Streams like this should probably have bypassed the
// mixer.
// TODO(jacqueline): Make this an error; it's slow to use the mixer in this
// case, compared to just writing directly to the sink.
auto buf = input.data();
std::size_t bytes_sent =
xStreamBufferSend(sink_, buf.data(), buf.size_bytes(), portMAX_DELAY);
input.consume(bytes_sent);
return;
}
while (input_stream_->info().bytes_in_stream() >= sizeof(sample::Sample)) {
RawStream* output_source;
if (pcm->sample_rate != target_format_.sample_rate) {
OutputStream resampled_writer{resampled_stream_.get()};
if (Resample(input, resampled_writer)) {
// Zero samples used or written. We need more input.
break;
}
output_source = resampled_stream_.get();
} else {
output_source = input_stream_.get();
}
size_t bytes_consumed = output_source->info().bytes_in_stream();
size_t bytes_to_send = output_source->info().bytes_in_stream();
if (target_format_.bits_per_sample == 16) {
// This is slightly scary; we're basically reaching into the internals of
// the stream buffer to do in-place conversion of samples. Saving an
// extra buffer + copy into that buffer is certainly worth it however.
cpp::span<sample::Sample> src =
output_source->data_as<sample::Sample>().first(
output_source->info().bytes_in_stream() / sizeof(sample::Sample));
cpp::span<int16_t> dest{reinterpret_cast<int16_t*>(src.data()),
src.size()};
ApplyDither(src, 16);
Downscale(src, dest);
bytes_consumed = src.size_bytes();
bytes_to_send = src.size_bytes() / 2;
}
InputStream output{output_source};
cpp::span<const std::byte> buf = output.data();
size_t bytes_sent = 0;
while (bytes_sent < bytes_to_send) {
auto cropped = buf.subspan(bytes_sent, bytes_to_send - bytes_sent);
bytes_sent += xStreamBufferSend(sink_, cropped.data(),
cropped.size_bytes(), portMAX_DELAY);
}
output.consume(bytes_consumed);
}
}
auto SinkMixer::Resample(InputStream& in, OutputStream& out) -> bool {
if (resampler_ == nullptr) {
ESP_LOGI(kTag, "creating new resampler");
auto format = in.info().format_as<StreamInfo::Pcm>();
resampler_.reset(new Resampler(
format->sample_rate, target_format_.sample_rate, format->channels));
}
auto res = resampler_->Process(in.data_as<sample::Sample>(),
out.data_as<sample::Sample>(), false);
in.consume(res.first * sizeof(sample::Sample));
out.add(res.second * sizeof(sample::Sample));
return res.first == 0 && res.second == 0;
}
auto SinkMixer::Downscale(cpp::span<sample::Sample> samples,
cpp::span<int16_t> output) -> void {
for (size_t i = 0; i < samples.size(); i++) {
output[i] = sample::ToSigned16Bit(samples[i]);
}
}
auto SinkMixer::ApplyDither(cpp::span<sample::Sample> samples,
uint_fast8_t bits) -> void {
static uint32_t prnd;
for (auto& s : samples) {
prnd = (prnd * 0x19660dL + 0x3c6ef35fL) & 0xffffffffL;
s = sample::Clip(
static_cast<int64_t>(s) +
(static_cast<int>(prnd) >> (sizeof(sample::Sample) - bits)));
}
}
} // namespace audio

@ -30,6 +30,13 @@ RawStream::RawStream(std::size_t size)
assert(buffer_ != NULL); assert(buffer_ != NULL);
} }
RawStream::RawStream(std::size_t size, uint32_t caps)
: info_(),
buffer_size_(size),
buffer_(reinterpret_cast<std::byte*>(heap_caps_malloc(size, caps))) {
assert(buffer_ != NULL);
}
RawStream::~RawStream() { RawStream::~RawStream() {
heap_caps_free(buffer_); heap_caps_free(buffer_);
} }

@ -3,9 +3,9 @@
# SPDX-License-Identifier: GPL-3.0-only # SPDX-License-Identifier: GPL-3.0-only
idf_component_register( idf_component_register(
SRCS "codec.cpp" "mad.cpp" "foxenflac.cpp" "stbvorbis.cpp" SRCS "codec.cpp" "mad.cpp" "foxenflac.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "result" "span" "libmad" "libfoxenflac" "stb_vorbis") REQUIRES "result" "span" "libmad" "libfoxenflac")
target_compile_options("${COMPONENT_LIB}" PRIVATE ${EXTRA_WARNINGS}) target_compile_options("${COMPONENT_LIB}" PRIVATE ${EXTRA_WARNINGS})

@ -11,7 +11,6 @@
#include "foxenflac.hpp" #include "foxenflac.hpp"
#include "mad.hpp" #include "mad.hpp"
#include "stbvorbis.hpp"
#include "types.hpp" #include "types.hpp"
namespace codecs { namespace codecs {
@ -22,8 +21,6 @@ auto CreateCodecForType(StreamType type) -> std::optional<ICodec*> {
return new MadMp3Decoder(); return new MadMp3Decoder();
case StreamType::kFlac: case StreamType::kFlac:
return new FoxenFlacDecoder(); return new FoxenFlacDecoder();
case StreamType::kVorbis:
return new StbVorbisDecoder();
default: default:
return {}; return {};
} }

@ -12,6 +12,7 @@
#include "esp_log.h" #include "esp_log.h"
#include "foxen/flac.h" #include "foxen/flac.h"
#include "sample.hpp"
namespace codecs { namespace codecs {
@ -47,7 +48,6 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span<const std::byte> input)
OutputFormat format{ OutputFormat format{
.num_channels = static_cast<uint8_t>(channels), .num_channels = static_cast<uint8_t>(channels),
.bits_per_sample = 32, // libfoxenflac output is fixed-size.
.sample_rate_hz = static_cast<uint32_t>(fs), .sample_rate_hz = static_cast<uint32_t>(fs),
.duration_seconds = {}, .duration_seconds = {},
.bits_per_second = {}, .bits_per_second = {},
@ -62,7 +62,7 @@ auto FoxenFlacDecoder::BeginStream(const cpp::span<const std::byte> input)
} }
auto FoxenFlacDecoder::ContinueStream(cpp::span<const std::byte> input, auto FoxenFlacDecoder::ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output) cpp::span<sample::Sample> output)
-> Result<OutputInfo> { -> Result<OutputInfo> {
cpp::span<int32_t> output_as_samples{ cpp::span<int32_t> output_as_samples{
reinterpret_cast<int32_t*>(output.data()), output.size_bytes() / 4}; reinterpret_cast<int32_t*>(output.data()), output.size_bytes() / 4};
@ -78,7 +78,7 @@ auto FoxenFlacDecoder::ContinueStream(cpp::span<const std::byte> input,
if (samples_written > 0) { if (samples_written > 0) {
return {bytes_read, return {bytes_read,
OutputInfo{.bytes_written = samples_written * 4, OutputInfo{.samples_written = samples_written,
.is_finished_writing = state == FLAC_END_OF_FRAME}}; .is_finished_writing = state == FLAC_END_OF_FRAME}};
} }

@ -17,6 +17,7 @@
#include <utility> #include <utility>
#include "result.hpp" #include "result.hpp"
#include "sample.hpp"
#include "span.hpp" #include "span.hpp"
#include "types.hpp" #include "types.hpp"
@ -61,7 +62,6 @@ class ICodec {
struct OutputFormat { struct OutputFormat {
uint8_t num_channels; uint8_t num_channels;
uint8_t bits_per_sample;
uint32_t sample_rate_hz; uint32_t sample_rate_hz;
std::optional<uint32_t> duration_seconds; std::optional<uint32_t> duration_seconds;
@ -76,7 +76,7 @@ class ICodec {
-> Result<OutputFormat> = 0; -> Result<OutputFormat> = 0;
struct OutputInfo { struct OutputInfo {
std::size_t bytes_written; std::size_t samples_written;
bool is_finished_writing; bool is_finished_writing;
}; };
@ -84,7 +84,7 @@ class ICodec {
* Writes PCM samples to the given output buffer. * Writes PCM samples to the given output buffer.
*/ */
virtual auto ContinueStream(cpp::span<const std::byte> input, virtual auto ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output) cpp::span<sample::Sample> output)
-> Result<OutputInfo> = 0; -> Result<OutputInfo> = 0;
virtual auto SeekStream(cpp::span<const std::byte> input, virtual auto SeekStream(cpp::span<const std::byte> input,

@ -14,6 +14,7 @@
#include <utility> #include <utility>
#include "foxen/flac.h" #include "foxen/flac.h"
#include "sample.hpp"
#include "span.hpp" #include "span.hpp"
#include "codec.hpp" #include "codec.hpp"
@ -26,7 +27,7 @@ class FoxenFlacDecoder : public ICodec {
~FoxenFlacDecoder(); ~FoxenFlacDecoder();
auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override; auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
auto ContinueStream(cpp::span<const std::byte>, cpp::span<std::byte>) auto ContinueStream(cpp::span<const std::byte>, cpp::span<sample::Sample>)
-> Result<OutputInfo> override; -> Result<OutputInfo> override;
auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample) auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
-> Result<void> override; -> Result<void> override;

@ -13,6 +13,7 @@
#include <utility> #include <utility>
#include "mad.h" #include "mad.h"
#include "sample.hpp"
#include "span.hpp" #include "span.hpp"
#include "codec.hpp" #include "codec.hpp"
@ -35,7 +36,7 @@ class MadMp3Decoder : public ICodec {
* Writes samples for the current frame. * Writes samples for the current frame.
*/ */
auto ContinueStream(cpp::span<const std::byte> input, auto ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output) cpp::span<sample::Sample> output)
-> Result<OutputInfo> override; -> Result<OutputInfo> override;
auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample) auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)

@ -0,0 +1,65 @@
#pragma once
#include <stdint.h>
#include <algorithm>
#include <mad.h>
namespace sample {
// A signed, 32-bit PCM sample.
typedef int32_t Sample;
constexpr auto Clip(int64_t v) -> Sample {
if (v > INT32_MAX)
return INT32_MAX;
if (v < INT32_MIN)
return INT32_MIN;
return v;
}
constexpr auto FromSigned(int32_t src, uint_fast8_t bits) -> Sample {
// Left-align samples, effectively scaling them up to 32 bits.
return src << (sizeof(Sample) * 8 - bits);
}
constexpr auto FromUnsigned(uint32_t src, uint_fast8_t bits) -> Sample {
// Left-align, then substract the max value / 2 to make the sample centred
// around zero.
return (src << (sizeof(uint32_t) * 8 - bits)) - (~0UL >> 1);
}
constexpr auto FromFloat(float src) -> Sample {
return std::clamp<float>(src, -1.0f, 1.0f) * static_cast<float>(INT32_MAX);
}
constexpr auto FromDouble(double src) -> Sample {
return std::clamp<double>(src, -1.0, 1.0) * static_cast<double>(INT32_MAX);
}
constexpr auto FromMad(mad_fixed_t src) -> Sample {
// Round the bottom bits.
src += (1L << (MAD_F_FRACBITS - 24));
// Clip the leftover bits to within range.
if (src >= MAD_F_ONE)
src = MAD_F_ONE - 1;
else if (src < -MAD_F_ONE)
src = -MAD_F_ONE;
// Quantize.
return FromSigned(src >> (MAD_F_FRACBITS + 1 - 24), 24);
}
constexpr auto ToSigned16Bit(Sample src) -> int16_t {
return src >> 16;
}
static constexpr float kFactor = 1.0f / static_cast<float>(INT32_MAX);
constexpr auto ToFloat(Sample src) -> float {
return src * kFactor;
}
} // namespace sample

@ -17,24 +17,11 @@
#include "codec.hpp" #include "codec.hpp"
#include "esp_log.h" #include "esp_log.h"
#include "result.hpp" #include "result.hpp"
#include "sample.hpp"
#include "types.hpp" #include "types.hpp"
namespace codecs { namespace codecs {
static uint32_t mad_fixed_to_pcm(mad_fixed_t sample, uint8_t bits) {
// Round the bottom bits.
sample += (1L << (MAD_F_FRACBITS - bits));
// Clip the leftover bits to within range.
if (sample >= MAD_F_ONE)
sample = MAD_F_ONE - 1;
else if (sample < -MAD_F_ONE)
sample = -MAD_F_ONE;
// Quantize.
return sample >> (MAD_F_FRACBITS + 1 - bits);
}
MadMp3Decoder::MadMp3Decoder() { MadMp3Decoder::MadMp3Decoder() {
mad_stream_init(&stream_); mad_stream_init(&stream_);
mad_frame_init(&frame_); mad_frame_init(&frame_);
@ -83,7 +70,6 @@ auto MadMp3Decoder::BeginStream(const cpp::span<const std::byte> input)
uint8_t channels = MAD_NCHANNELS(&header); uint8_t channels = MAD_NCHANNELS(&header);
OutputFormat output{ OutputFormat output{
.num_channels = channels, .num_channels = channels,
.bits_per_sample = 24, // We always scale to 24 bits
.sample_rate_hz = header.samplerate, .sample_rate_hz = header.samplerate,
.duration_seconds = {}, .duration_seconds = {},
.bits_per_second = {}, .bits_per_second = {},
@ -100,7 +86,7 @@ auto MadMp3Decoder::BeginStream(const cpp::span<const std::byte> input)
} }
auto MadMp3Decoder::ContinueStream(cpp::span<const std::byte> input, auto MadMp3Decoder::ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output) cpp::span<sample::Sample> output)
-> Result<OutputInfo> { -> Result<OutputInfo> {
std::size_t bytes_read = 0; std::size_t bytes_read = 0;
if (current_sample_ < 0) { if (current_sample_ < 0) {
@ -133,32 +119,24 @@ auto MadMp3Decoder::ContinueStream(cpp::span<const std::byte> input,
bytes_read = GetBytesUsed(input.size_bytes()); bytes_read = GetBytesUsed(input.size_bytes());
} }
size_t output_byte = 0; size_t output_sample = 0;
while (current_sample_ < synth_.pcm.length) { while (current_sample_ < synth_.pcm.length) {
if (output_byte + (4 * synth_.pcm.channels) >= output.size()) { if (output_sample + synth_.pcm.channels >= output.size()) {
// We can't fit the next sample into the buffer. Stop now, and also avoid // We can't fit the next full frame into the buffer.
// writing the sample for only half the channels. return {bytes_read, OutputInfo{.samples_written = output_sample,
return {bytes_read, OutputInfo{.bytes_written = output_byte,
.is_finished_writing = false}}; .is_finished_writing = false}};
} }
for (int channel = 0; channel < synth_.pcm.channels; channel++) { for (int channel = 0; channel < synth_.pcm.channels; channel++) {
uint32_t sample_24 = output[output_sample++] =
mad_fixed_to_pcm(synth_.pcm.samples[channel][current_sample_], 24); sample::FromMad(synth_.pcm.samples[channel][current_sample_]);
// 24 bit samples must still be aligned to 32 bits. The LSB is ignored.
output[output_byte++] = static_cast<std::byte>(0);
output[output_byte++] = static_cast<std::byte>((sample_24)&0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 8) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 16) & 0xFF);
} }
current_sample_++; current_sample_++;
} }
// We wrote everything! Reset, ready for the next frame. // We wrote everything! Reset, ready for the next frame.
current_sample_ = -1; current_sample_ = -1;
return {bytes_read, OutputInfo{.bytes_written = output_byte, return {bytes_read, OutputInfo{.samples_written = output_sample,
.is_finished_writing = true}}; .is_finished_writing = true}};
} }

@ -5,6 +5,7 @@
idf_component_register( idf_component_register(
SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "battery.cpp" "storage.cpp" "i2c.cpp" SRCS "touchwheel.cpp" "i2s_dac.cpp" "gpios.cpp" "battery.cpp" "storage.cpp" "i2c.cpp"
"spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp" "spi.cpp" "display.cpp" "display_init.cpp" "samd.cpp" "relative_wheel.cpp" "wm8523.cpp"
"nvs.cpp" "bluetooth.cpp"
INCLUDE_DIRS "include" INCLUDE_DIRS "include"
REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks") REQUIRES "esp_adc" "fatfs" "result" "lvgl" "span" "tasks" "nvs_flash" "bt" "tinyfsm")
target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS}) target_compile_options(${COMPONENT_LIB} PRIVATE ${EXTRA_WARNINGS})

@ -0,0 +1,253 @@
#include "bluetooth.hpp"
#include <stdint.h>
#include <atomic>
#include <ostream>
#include <sstream>
#include "esp_a2dp_api.h"
#include "esp_avrc_api.h"
#include "esp_bt.h"
#include "esp_bt_device.h"
#include "esp_bt_main.h"
#include "esp_gap_bt_api.h"
#include "esp_log.h"
#include "esp_mac.h"
#include "tinyfsm/include/tinyfsm.hpp"
namespace drivers {
static constexpr char kTag[] = "bluetooth";
static std::atomic<StreamBufferHandle_t> sStream;
auto gap_cb(esp_bt_gap_cb_event_t event, esp_bt_gap_cb_param_t* param) -> void {
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::internal::Gap{.type = event, .param = param});
}
auto avrcp_cb(esp_avrc_ct_cb_event_t event, esp_avrc_ct_cb_param_t* param)
-> void {
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::internal::Avrc{.type = event, .param = param});
}
auto a2dp_cb(esp_a2d_cb_event_t event, esp_a2d_cb_param_t* param) -> void {
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::internal::A2dp{.type = event, .param = param});
}
auto a2dp_data_cb(uint8_t* buf, int32_t buf_size) -> int32_t {
if (buf == nullptr || buf_size <= 0) {
return 0;
}
StreamBufferHandle_t stream = sStream.load();
if (stream == nullptr) {
return 0;
}
return xStreamBufferReceive(stream, buf, buf_size, 0);
}
Bluetooth::Bluetooth() {
tinyfsm::FsmList<bluetooth::BluetoothState>::start();
}
auto Bluetooth::Enable() -> bool {
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::Enable{});
return !bluetooth::BluetoothState::is_in_state<bluetooth::Disabled>();
}
auto Bluetooth::Disable() -> void {
tinyfsm::FsmList<bluetooth::BluetoothState>::dispatch(
bluetooth::events::Disable{});
}
auto DeviceName() -> std::string {
uint8_t mac[8]{0};
esp_efuse_mac_get_default(mac);
std::ostringstream name;
name << "TANGARA " << std::hex << mac[0] << mac[1];
return name.str();
}
namespace bluetooth {
static bool sIsFirstEntry = true;
void Disabled::entry() {
if (sIsFirstEntry) {
// We only use BT Classic, to claw back ~60KiB from the BLE firmware.
esp_bt_controller_mem_release(ESP_BT_MODE_BLE);
sIsFirstEntry = false;
return;
}
esp_bluedroid_disable();
esp_bluedroid_deinit();
esp_bt_controller_disable();
}
void Disabled::react(const events::Enable&) {
esp_bt_controller_config_t config = BT_CONTROLLER_INIT_CONFIG_DEFAULT();
if (esp_bt_controller_init(&config) != ESP_OK) {
ESP_LOGE(kTag, "initialize controller failed");
return;
}
if (esp_bt_controller_enable(ESP_BT_MODE_CLASSIC_BT) != ESP_OK) {
ESP_LOGE(kTag, "enable controller failed");
return;
}
if (esp_bluedroid_init() != ESP_OK) {
ESP_LOGE(kTag, "initialize bluedroid failed");
return;
}
if (esp_bluedroid_enable() != ESP_OK) {
ESP_LOGE(kTag, "enable bluedroid failed");
return;
}
// Enable Secure Simple Pairing
esp_bt_sp_param_t param_type = ESP_BT_SP_IOCAP_MODE;
esp_bt_io_cap_t iocap = ESP_BT_IO_CAP_IO;
esp_bt_gap_set_security_param(param_type, &iocap, sizeof(uint8_t));
// Set a reasonable name for the device.
std::string name = DeviceName();
esp_bt_dev_set_device_name(name.c_str());
// Initialise GAP. This controls advertising our device, and scanning for
// other devices.
esp_bt_gap_register_callback(gap_cb);
// Initialise AVRCP. This handles playback controls; play/pause/volume/etc.
// esp_avrc_ct_init();
// esp_avrc_ct_register_callback(avrcp_cb);
// Initialise A2DP. This handles streaming audio. Currently ESP-IDF's SBC
// encoder only supports 2 channels of interleaved 16 bit samples, at
// 44.1kHz, so there is no additional configuration to be done for the
// stream itself.
esp_a2d_source_init();
esp_a2d_register_callback(a2dp_cb);
esp_a2d_source_register_data_callback(a2dp_data_cb);
// Don't let anyone interact with us before we're ready.
esp_bt_gap_set_scan_mode(ESP_BT_NON_CONNECTABLE, ESP_BT_NON_DISCOVERABLE);
transit<Scanning>();
}
static constexpr uint8_t kDiscoveryTimeSeconds = 10;
static constexpr uint8_t kDiscoveryMaxResults = 0;
void Scanning::entry() {
ESP_LOGI(kTag, "scanning for devices");
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY,
kDiscoveryTimeSeconds, kDiscoveryMaxResults);
}
void Scanning::exit() {
esp_bt_gap_cancel_discovery();
}
auto OnDeviceDiscovered(esp_bt_gap_cb_param_t* param) -> void {
ESP_LOGI(kTag, "device discovered");
}
void Scanning::react(const events::internal::Gap& ev) {
switch (ev.type) {
case ESP_BT_GAP_DISC_RES_EVT:
OnDeviceDiscovered(ev.param);
break;
case ESP_BT_GAP_DISC_STATE_CHANGED_EVT:
if (ev.param->disc_st_chg.state == ESP_BT_GAP_DISCOVERY_STOPPED) {
ESP_LOGI(kTag, "still scanning");
esp_bt_gap_start_discovery(ESP_BT_INQ_MODE_GENERAL_INQUIRY,
kDiscoveryTimeSeconds, kDiscoveryMaxResults);
}
break;
case ESP_BT_GAP_MODE_CHG_EVT:
// todo: mode change. is this important?
ESP_LOGI(kTag, "GAP mode changed");
break;
default:
ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type);
}
}
void Connecting::entry() {
ESP_LOGI(kTag, "connecting to device");
esp_a2d_source_connect(nullptr);
}
void Connecting::exit() {}
void Connecting::react(const events::internal::Gap& ev) {
switch (ev.type) {
case ESP_BT_GAP_AUTH_CMPL_EVT:
// todo: auth completed. check if we succeeded.
break;
case ESP_BT_GAP_PIN_REQ_EVT:
// todo: device needs a pin to connect.
break;
case ESP_BT_GAP_CFM_REQ_EVT:
// todo: device needs user to click okay.
break;
case ESP_BT_GAP_KEY_NOTIF_EVT:
// todo: device is telling us a password?
break;
case ESP_BT_GAP_KEY_REQ_EVT:
// todo: device needs a password
break;
case ESP_BT_GAP_MODE_CHG_EVT:
// todo: mode change. is this important?
break;
default:
ESP_LOGW(kTag, "unhandled GAP event: %u", ev.type);
}
}
void Connecting::react(const events::internal::A2dp& ev) {
switch (ev.type) {
case ESP_A2D_CONNECTION_STATE_EVT:
// todo: connection state changed. we might be connected!
break;
default:
ESP_LOGW(kTag, "unhandled A2DP event: %u", ev.type);
}
}
void Connected::react(const events::internal::A2dp& ev) {
switch (ev.type) {
case ESP_A2D_CONNECTION_STATE_EVT:
// todo: connection state changed. we might have dropped
break;
case ESP_A2D_AUDIO_STATE_EVT:
// todo: audio state changed. who knows, dude.
break;
default:
ESP_LOGW(kTag, "unhandled A2DP event: %u", ev.type);
}
}
void Connected::react(const events::internal::Avrc& ev) {
switch (ev.type) {
case ESP_AVRC_CT_CONNECTION_STATE_EVT:
// todo: avrc connected. send our capabilities.
default:
ESP_LOGW(kTag, "unhandled AVRC event: %u", ev.type);
}
}
} // namespace bluetooth
} // namespace drivers
FSM_INITIAL_STATE(drivers::bluetooth::BluetoothState,
drivers::bluetooth::Disabled)

@ -161,7 +161,6 @@ auto I2SDac::Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate)
word_length = 0b10; word_length = 0b10;
break; break;
case BPS_32: case BPS_32:
// TODO(jacqueline): Error on this? It's not supported anymore.
slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT; slot_config_.data_bit_width = I2S_DATA_BIT_WIDTH_32BIT;
slot_config_.ws_width = 32; slot_config_.ws_width = 32;
word_length = 0b11; word_length = 0b11;

@ -0,0 +1,108 @@
#pragma once
#include <string>
#include <vector>
#include <freertos/FreeRTOS.h>
#include <freertos/stream_buffer.h>
#include "esp_a2dp_api.h"
#include "esp_avrc_api.h"
#include "esp_gap_bt_api.h"
#include "tinyfsm.hpp"
#include "tinyfsm/include/tinyfsm.hpp"
namespace drivers {
/*
* A handle used to interact with the bluetooth state machine.
*/
class Bluetooth {
public:
Bluetooth();
auto Enable() -> bool;
auto Disable() -> void;
auto SetSource(StreamBufferHandle_t) -> void;
};
namespace bluetooth {
namespace events {
struct Enable : public tinyfsm::Event {};
struct Disable : public tinyfsm::Event {};
namespace internal {
struct Gap : public tinyfsm::Event {
esp_bt_gap_cb_event_t type;
esp_bt_gap_cb_param_t* param;
};
struct A2dp : public tinyfsm::Event {
esp_a2d_cb_event_t type;
esp_a2d_cb_param_t* param;
};
struct Avrc : public tinyfsm::Event {
esp_avrc_ct_cb_event_t type;
esp_avrc_ct_cb_param_t* param;
};
} // namespace internal
} // namespace events
class BluetoothState : public tinyfsm::Fsm<BluetoothState> {
public:
virtual ~BluetoothState(){};
virtual void entry() {}
virtual void exit() {}
virtual void react(const events::Enable& ev){};
virtual void react(const events::Disable& ev) = 0;
virtual void react(const events::internal::Gap& ev) = 0;
virtual void react(const events::internal::A2dp& ev) = 0;
virtual void react(const events::internal::Avrc& ev){};
};
class Disabled : public BluetoothState {
void entry() override;
void react(const events::Enable& ev) override;
void react(const events::Disable& ev) override{};
void react(const events::internal::Gap& ev) override {}
void react(const events::internal::A2dp& ev) override {}
};
class Scanning : public BluetoothState {
void entry() override;
void exit() override;
void react(const events::Disable& ev) override;
void react(const events::internal::Gap& ev) override;
void react(const events::internal::A2dp& ev) override;
};
class Connecting : public BluetoothState {
void entry() override;
void exit() override;
void react(const events::Disable& ev) override;
void react(const events::internal::Gap& ev) override;
void react(const events::internal::A2dp& ev) override;
};
class Connected : public BluetoothState {
void entry() override;
void exit() override;
void react(const events::Disable& ev) override;
void react(const events::internal::Gap& ev) override;
void react(const events::internal::A2dp& ev) override;
void react(const events::internal::Avrc& ev) override;
};
} // namespace bluetooth
} // namespace drivers

@ -51,14 +51,12 @@ class I2SDac {
BPS_32 = I2S_DATA_BIT_WIDTH_32BIT, BPS_32 = I2S_DATA_BIT_WIDTH_32BIT,
}; };
enum SampleRate { enum SampleRate {
SAMPLE_RATE_11_025 = 11025, SAMPLE_RATE_8 = 8000,
SAMPLE_RATE_16 = 16000,
SAMPLE_RATE_22_05 = 22050,
SAMPLE_RATE_32 = 32000, SAMPLE_RATE_32 = 32000,
SAMPLE_RATE_44_1 = 44100, SAMPLE_RATE_44_1 = 44100,
SAMPLE_RATE_48 = 48000, SAMPLE_RATE_48 = 48000,
SAMPLE_RATE_88_2 = 88200,
SAMPLE_RATE_96 = 96000, SAMPLE_RATE_96 = 96000,
SAMPLE_RATE_192 = 192000,
}; };
auto Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate) -> void; auto Reconfigure(Channels ch, BitsPerSample bps, SampleRate rate) -> void;

@ -0,0 +1,27 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include "esp_err.h"
#include "nvs.h"
namespace drivers {
class NvsStorage {
public:
static auto Open() -> NvsStorage*;
auto SchemaVersion() -> uint8_t;
explicit NvsStorage(nvs_handle_t);
~NvsStorage();
private:
nvs_handle_t handle_;
};
} // namespace drivers

@ -0,0 +1,73 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "nvs.hpp"
#include <stdint.h>
#include <cstdint>
#include <memory>
#include "esp_log.h"
#include "nvs.h"
#include "nvs_flash.h"
namespace drivers {
static constexpr char kTag[] = "nvm";
static constexpr uint8_t kSchemaVersion = 1;
static constexpr char kKeyVersion[] = "ver";
auto NvsStorage::Open() -> NvsStorage* {
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES) {
ESP_LOGW(kTag, "partition needs initialisation");
nvs_flash_erase();
err = nvs_flash_init();
}
if (err != ESP_OK) {
ESP_LOGE(kTag, "failed to init nvm");
return nullptr;
}
nvs_handle_t handle;
if ((err = nvs_open("tangara", NVS_READWRITE, &handle)) != ESP_OK) {
ESP_LOGE(kTag, "failed to open nvs namespace");
return nullptr;
}
std::unique_ptr<NvsStorage> instance = std::make_unique<NvsStorage>(handle);
if (instance->SchemaVersion() < kSchemaVersion) {
ESP_LOGW(kTag, "namespace needs downgrading");
nvs_erase_all(handle);
nvs_set_u8(handle, kKeyVersion, kSchemaVersion);
err = nvs_commit(handle);
if (err != ESP_OK) {
ESP_LOGW(kTag, "failed to init namespace");
return nullptr;
}
}
ESP_LOGI(kTag, "nvm storage initialised okay");
return instance.release();
}
NvsStorage::NvsStorage(nvs_handle_t handle) : handle_(handle) {}
NvsStorage::~NvsStorage() {
nvs_close(handle_);
nvs_flash_deinit();
}
auto NvsStorage::SchemaVersion() -> uint8_t {
uint8_t ret;
if (nvs_get_u8(handle_, kKeyVersion, &ret) != ESP_OK) {
return UINT8_MAX;
}
return ret;
}
} // namespace drivers

@ -63,7 +63,7 @@ auto SdStorage::Create(IGpios* gpio) -> cpp::result<SdStorage*, Error> {
// Will return ESP_ERR_INVALID_RESPONSE if there is no card // Will return ESP_ERR_INVALID_RESPONSE if there is no card
esp_err_t err = sdmmc_card_init(host.get(), card.get()); esp_err_t err = sdmmc_card_init(host.get(), card.get());
if (err != ESP_OK) { if (err != ESP_OK) {
ESP_LOGW(kTag, "Failed to read, err: %d", err); ESP_LOGW(kTag, "Failed to read, err: %s", esp_err_to_name(err));
return cpp::fail(Error::FAILED_TO_READ); return cpp::fail(Error::FAILED_TO_READ);
} }
@ -74,7 +74,21 @@ auto SdStorage::Create(IGpios* gpio) -> cpp::result<SdStorage*, Error> {
// Mount right now, not on first operation. // Mount right now, not on first operation.
FRESULT ferr = f_mount(fs, "", 1); FRESULT ferr = f_mount(fs, "", 1);
if (ferr != FR_OK) { if (ferr != FR_OK) {
ESP_LOGW(kTag, "Failed to mount, err: %d", ferr); std::string err_str;
switch (ferr) {
case FR_DISK_ERR:
err_str = "FR_DISK_ERR";
break;
case FR_NOT_READY:
err_str = "FR_NOT_READY";
break;
case FR_NO_FILESYSTEM:
err_str = "FR_NO_FILESYSTEM";
break;
default:
err_str = std::to_string(ferr);
}
ESP_LOGW(kTag, "Failed to mount, err: %s", err_str.c_str());
return cpp::fail(Error::FAILED_TO_MOUNT); return cpp::fail(Error::FAILED_TO_MOUNT);
} }

@ -13,6 +13,7 @@
#include "event_queue.hpp" #include "event_queue.hpp"
#include "gpios.hpp" #include "gpios.hpp"
#include "lvgl/lvgl.h" #include "lvgl/lvgl.h"
#include "nvs.hpp"
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "spi.hpp" #include "spi.hpp"
#include "system_events.hpp" #include "system_events.hpp"
@ -50,9 +51,10 @@ auto Booting::entry() -> void {
ESP_LOGI(kTag, "installing remaining drivers"); ESP_LOGI(kTag, "installing remaining drivers");
sSamd.reset(drivers::Samd::Create()); sSamd.reset(drivers::Samd::Create());
sBattery.reset(drivers::Battery::Create()); sBattery.reset(drivers::Battery::Create());
sNvs.reset(drivers::NvsStorage::Open());
sTagParser.reset(new database::TagParserImpl()); sTagParser.reset(new database::TagParserImpl());
if (!sSamd || !sBattery) { if (!sSamd || !sBattery || !sNvs) {
events::System().Dispatch(FatalError{}); events::System().Dispatch(FatalError{});
events::Ui().Dispatch(FatalError{}); events::Ui().Dispatch(FatalError{});
return; return;

@ -13,6 +13,7 @@
#include "database.hpp" #include "database.hpp"
#include "display.hpp" #include "display.hpp"
#include "gpios.hpp" #include "gpios.hpp"
#include "nvs.hpp"
#include "relative_wheel.hpp" #include "relative_wheel.hpp"
#include "samd.hpp" #include "samd.hpp"
#include "storage.hpp" #include "storage.hpp"
@ -54,6 +55,7 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
protected: protected:
static std::shared_ptr<drivers::Gpios> sGpios; static std::shared_ptr<drivers::Gpios> sGpios;
static std::shared_ptr<drivers::Samd> sSamd; static std::shared_ptr<drivers::Samd> sSamd;
static std::shared_ptr<drivers::NvsStorage> sNvs;
static std::shared_ptr<drivers::TouchWheel> sTouch; static std::shared_ptr<drivers::TouchWheel> sTouch;
static std::shared_ptr<drivers::RelativeWheel> sRelativeTouch; static std::shared_ptr<drivers::RelativeWheel> sRelativeTouch;

@ -17,6 +17,7 @@ namespace system_fsm {
std::shared_ptr<drivers::Gpios> SystemState::sGpios; std::shared_ptr<drivers::Gpios> SystemState::sGpios;
std::shared_ptr<drivers::Samd> SystemState::sSamd; std::shared_ptr<drivers::Samd> SystemState::sSamd;
std::shared_ptr<drivers::NvsStorage> SystemState::sNvs;
std::shared_ptr<drivers::TouchWheel> SystemState::sTouch; std::shared_ptr<drivers::TouchWheel> SystemState::sTouch;
std::shared_ptr<drivers::RelativeWheel> SystemState::sRelativeTouch; std::shared_ptr<drivers::RelativeWheel> SystemState::sRelativeTouch;

@ -34,6 +34,10 @@ auto Name<Type::kAudio>() -> std::string {
return "AUDIO"; return "AUDIO";
} }
template <> template <>
auto Name<Type::kMixer>() -> std::string {
return "MIXER";
}
template <>
auto Name<Type::kDatabase>() -> std::string { auto Name<Type::kDatabase>() -> std::string {
return "DB"; return "DB";
} }
@ -77,6 +81,14 @@ auto AllocateStack<Type::kFileStreamer>() -> cpp::span<StackType_t> {
size}; size};
} }
template <>
auto AllocateStack<Type::kMixer>() -> cpp::span<StackType_t> {
std::size_t size = 4 * 1024;
return {static_cast<StackType_t*>(
heap_caps_malloc(size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT)),
size};
}
// Leveldb is designed for non-embedded use cases, where stack space isn't so // Leveldb is designed for non-embedded use cases, where stack space isn't so
// much of a concern. It therefore uses an eye-wateringly large amount of stack. // much of a concern. It therefore uses an eye-wateringly large amount of stack.
template <> template <>
@ -105,6 +117,10 @@ auto Priority() -> UBaseType_t;
// Realtime audio is the entire point of this device, so give this task the // Realtime audio is the entire point of this device, so give this task the
// highest priority. // highest priority.
template <> template <>
auto Priority<Type::kMixer>() -> UBaseType_t {
return 12;
}
template <>
auto Priority<Type::kAudio>() -> UBaseType_t { auto Priority<Type::kAudio>() -> UBaseType_t {
return 11; return 11;
} }

@ -36,6 +36,8 @@ enum class Type {
kFileStreamer, kFileStreamer,
// The main audio pipeline task. // The main audio pipeline task.
kAudio, kAudio,
// TODO
kMixer,
// Task for running database queries. // Task for running database queries.
kDatabase, kDatabase,
// Task for internal database operations // Task for internal database operations
@ -57,9 +59,19 @@ template <Type t>
auto StartPersistent(const std::function<void(void)>& fn) -> void { auto StartPersistent(const std::function<void(void)>& fn) -> void {
StaticTask_t* task_buffer = new StaticTask_t; StaticTask_t* task_buffer = new StaticTask_t;
cpp::span<StackType_t> stack = AllocateStack<t>(); cpp::span<StackType_t> stack = AllocateStack<t>();
xTaskCreateStatic(&PersistentMain, Name<t>().c_str(), stack.size(),
new std::function<void(void)>(fn), Priority<t>(),
stack.data(), task_buffer);
}
template <Type t>
auto StartPersistent(BaseType_t core, const std::function<void(void)>& fn)
-> void {
StaticTask_t* task_buffer = new StaticTask_t;
cpp::span<StackType_t> stack = AllocateStack<t>();
xTaskCreateStaticPinnedToCore(&PersistentMain, Name<t>().c_str(), xTaskCreateStaticPinnedToCore(&PersistentMain, Name<t>().c_str(),
stack.size(), new std::function<void(void)>(fn), stack.size(), new std::function<void(void)>(fn),
Priority<t>(), stack.data(), task_buffer, 0); Priority<t>(), stack.data(), task_buffer, core);
} }
class Worker { class Worker {

@ -48,12 +48,25 @@
namespace ui { namespace ui {
static const char* kTag = "lv_task"; static const char* kTag = "lv_task";
static const TickType_t kMaxFrameRate = pdMS_TO_TICKS(33);
static int sTimerId;
static SemaphoreHandle_t sFrameSemaphore;
auto next_frame(TimerHandle_t) {
xSemaphoreGive(sFrameSemaphore);
}
void LvglMain(std::weak_ptr<drivers::RelativeWheel> weak_touch_wheel, void LvglMain(std::weak_ptr<drivers::RelativeWheel> weak_touch_wheel,
std::weak_ptr<drivers::Display> weak_display) { std::weak_ptr<drivers::Display> weak_display) {
ESP_LOGI(kTag, "init lvgl"); ESP_LOGI(kTag, "init lvgl");
lv_init(); lv_init();
sFrameSemaphore = xSemaphoreCreateBinary();
auto timer =
xTimerCreate("lvgl_frame", kMaxFrameRate, pdTRUE, &sTimerId, next_frame);
xTimerStart(timer, portMAX_DELAY);
lv_theme_t* base_theme = lv_theme_basic_init(NULL); lv_theme_t* base_theme = lv_theme_basic_init(NULL);
lv_disp_set_theme(NULL, base_theme); lv_disp_set_theme(NULL, base_theme);
static themes::Theme sTheme{}; static themes::Theme sTheme{};
@ -80,9 +93,9 @@ void LvglMain(std::weak_ptr<drivers::RelativeWheel> weak_touch_wheel,
} }
lv_task_handler(); lv_task_handler();
// 30 FPS
// TODO(jacqueline): make this dynamic // Wait for the signal to loop again.
vTaskDelay(pdMS_TO_TICKS(33)); xSemaphoreTake(sFrameSemaphore, portMAX_DELAY);
} }
} }

@ -20,7 +20,6 @@ list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/lvgl")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/result") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/result")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/shared_string") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/shared_string")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/span") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/span")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/stb_vorbis")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm") list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm")
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)

Loading…
Cancel
Save