commit
						e1181fbe59
					
				| @ -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
										
									
								
							
						| @ -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, | ||||||
| @ -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
 | ||||||
| @ -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
 | ||||||
| @ -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
 | ||||||
| @ -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) | ||||||
| @ -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
 | ||||||
| @ -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
 | ||||||
					Loading…
					
					
				
		Reference in new issue