Ailurux 2 years ago
commit 039272455a
  1. 8
      .reuse/dep5
  2. 73
      LICENSES/Apache-2.0.txt
  3. 2
      lib/leveldb/util/arena.cc
  4. 8
      lib/libfoxenflac/CMakeLists.txt
  5. 2022
      lib/libfoxenflac/flac.c
  6. 297
      lib/libfoxenflac/include/foxen/flac.h
  7. 8
      lib/stb_vorbis/CMakeLists.txt
  8. 418
      lib/stb_vorbis/include/stb_vorbis.h
  9. 5584
      lib/stb_vorbis/stb_vorbis.c
  10. 78
      src/app_console/app_console.cpp
  11. 5
      src/app_console/include/app_console.hpp
  12. 122
      src/audio/audio_decoder.cpp
  13. 54
      src/audio/audio_fsm.cpp
  14. 6
      src/audio/audio_task.cpp
  15. 42
      src/audio/fatfs_audio_input.cpp
  16. 1
      src/audio/include/audio_decoder.hpp
  17. 10
      src/audio/include/audio_events.hpp
  18. 19
      src/audio/include/audio_fsm.hpp
  19. 5
      src/audio/include/fatfs_audio_input.hpp
  20. 4
      src/audio/include/stream_info.hpp
  21. 4
      src/codecs/CMakeLists.txt
  22. 7
      src/codecs/codec.cpp
  23. 80
      src/codecs/foxenflac.cpp
  24. 60
      src/codecs/include/codec.hpp
  25. 38
      src/codecs/include/foxenflac.hpp
  26. 24
      src/codecs/include/mad.hpp
  27. 42
      src/codecs/include/stbvorbis.hpp
  28. 2
      src/codecs/include/types.hpp
  29. 179
      src/codecs/mad.cpp
  30. 128
      src/codecs/stbvorbis.cpp
  31. 2
      src/database/CMakeLists.txt
  32. 150
      src/database/database.cpp
  33. 5
      src/database/env_esp.cpp
  34. 26
      src/database/include/database.hpp
  35. 2
      src/database/include/env_esp.hpp
  36. 36
      src/database/include/records.hpp
  37. 166
      src/database/include/song.hpp
  38. 6
      src/database/include/tag_parser.hpp
  39. 169
      src/database/include/track.hpp
  40. 38
      src/database/records.cpp
  41. 20
      src/database/tag_parser.cpp
  42. 80
      src/database/test/test_database.cpp
  43. 22
      src/database/test/test_records.cpp
  44. 22
      src/database/track.cpp
  45. 6
      src/drivers/samd.cpp
  46. 4
      src/drivers/touchwheel.cpp
  47. 4
      src/system_fsm/booting.cpp
  48. 7
      src/system_fsm/include/system_events.hpp
  49. 5
      src/system_fsm/include/system_fsm.hpp
  50. 6
      src/system_fsm/running.cpp
  51. 2
      src/system_fsm/system_fsm.cpp
  52. 22
      src/tasks/tasks.cpp
  53. 5
      src/tasks/tasks.hpp
  54. 2
      tools/cmake/common.cmake

@ -19,6 +19,10 @@ Files: lib/leveldb/*
Copyright: 2011 The LevelDB Authors
License: LicenseRef-LevelDB
Files: lib/libfoxenflac/*
Copyright: 2018-2022 Andreas Stöckel
License: GPL-2.0-or-later
Files: lib/libmad/*
Copyright: 2000-2004 Underbit Technologies, Inc.
License: GPL-2.0-or-later
@ -39,6 +43,10 @@ Files: lib/span/include/*
Copyright: 2018 Tristan Brindle
License: BSL-1.0
Files: lib/stb_vorbis/*
Copyright: 2017 Sean Barrett
License: Unlicense
Files: lib/tinyfsm/*
Copyright: 2012-2022 Axel Burri
License: MIT

@ -0,0 +1,73 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
(a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

@ -36,7 +36,7 @@ char* Arena::AllocateFallback(size_t bytes) {
}
char* Arena::AllocateAligned(size_t bytes) {
const int align = (sizeof(void*) > 8) ? sizeof(void*) : 8;
const int align = 4;
static_assert((align & (align - 1)) == 0,
"Pointer size should be a power of 2");
size_t current_mod = reinterpret_cast<uintptr_t>(alloc_ptr_) & (align - 1);

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

File diff suppressed because it is too large Load Diff

@ -0,0 +1,297 @@
/*
* libfoxenflac -- Tiny FLAC Decoder Library
* Copyright (C) 2018-2022 Andreas Stöckel
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* @file flac.h
*
* Provides a decoder for FLAC (Free Lossless Audio Codec).
*
* @author Andreas Stöckel
*/
#ifndef FOXEN_FLAC_H
#define FOXEN_FLAC_H
#include <stdint.h>
#ifndef FX_EXPORT
#if __EMSCRIPTEN__
#import <emscripten.h>
#define FX_EXPORT EMSCRIPTEN_KEEPALIVE
#else
#define FX_EXPORT
#endif /* __EMSCRIPTEN__ */
#endif /* FX_EXPORT */
#ifdef __cplusplus
extern "C" {
#endif
/**
* Value returned by the fx_flac_get_streaminfo() method if the given streaminfo
* key is invalid.
*/
#define FLAC_INVALID_METADATA_KEY 0x7FFFFFFFFFFFFFFFULL
/**
* Maximum number of channels that can be encoded in a FLAC stream.
*/
#define FLAC_MAX_CHANNEL_COUNT 8U
/**
* Maximum block size that can be used if the stream is encoded in the FLAC
* Subset format and the sample rate is smaller than 48000 kHz.
*/
#define FLAC_SUBSET_MAX_BLOCK_SIZE_48KHZ 4608U
/**
* Maximum block size than can always be safely used if the stream is encoded
* in the FLAC Subset format.
*/
#define FLAC_SUBSET_MAX_BLOCK_SIZE 16384U
/**
* Maximum block size in samples that can be used in a FLAC stream.
*/
#define FLAC_MAX_BLOCK_SIZE 65535U
/**
* Opaque struct representing a FLAC decoder.
*/
struct fx_flac;
/**
* Typedef for the fx_flac struct.
*/
typedef struct fx_flac fx_flac_t;
/**
* Enum representing the state of a FLAC decoder instance.
*/
typedef enum {
/**
* The decoder is in an error state; the decoder cannot recover from this
* error. This error may for example occur if the data in the stream is
* invalid, or the stream has a format that is outside the maximum specs
* that are supported by the decoder. Call fx_flac_reset() and start anew!
*/
FLAC_ERR = -1,
/**
* The decoder is currently in its initial state, fx_flac_process() has not
* been called.
*/
FLAC_INIT = 0,
/**
* The decoder found the beginning of the metadata packet!
*/
FLAC_IN_METADATA = 1,
/**
* The decoder is done reading the current metadata block, this may be
* followed by more metadata blocks, in which case the state is reset to
* FLAC_IN_METADATA.
*/
FLAC_END_OF_METADATA = 2,
/**
* The decoder is currently searching for an audio frame.
*/
FLAC_SEARCH_FRAME = 3,
/**
* The decoder is currently inside the stream of audio frames.
*/
FLAC_IN_FRAME = 4,
/**
* The decoder successfully decoded an entire frame. Write the data to the
* client.
*/
FLAC_DECODED_FRAME = 5,
/**
* The decoder reached the end of a block.
*/
FLAC_END_OF_FRAME = 6
} fx_flac_state_t;
/**
* Enum used in fx_flac_get_streaminfo() to query metadata about the stream.
*/
typedef enum {
FLAC_KEY_MIN_BLOCK_SIZE = 0,
FLAC_KEY_MAX_BLOCK_SIZE = 1,
FLAC_KEY_MIN_FRAME_SIZE = 2,
FLAC_KEY_MAX_FRAME_SIZE = 3,
FLAC_KEY_SAMPLE_RATE = 4,
FLAC_KEY_N_CHANNELS = 5,
FLAC_KEY_SAMPLE_SIZE = 6,
FLAC_KEY_N_SAMPLES = 7,
FLAC_KEY_MD5_SUM_0 = 128,
FLAC_KEY_MD5_SUM_1 = 129,
FLAC_KEY_MD5_SUM_2 = 130,
FLAC_KEY_MD5_SUM_3 = 131,
FLAC_KEY_MD5_SUM_4 = 132,
FLAC_KEY_MD5_SUM_5 = 133,
FLAC_KEY_MD5_SUM_6 = 134,
FLAC_KEY_MD5_SUM_7 = 135,
FLAC_KEY_MD5_SUM_8 = 136,
FLAC_KEY_MD5_SUM_9 = 137,
FLAC_KEY_MD5_SUM_A = 138,
FLAC_KEY_MD5_SUM_B = 139,
FLAC_KEY_MD5_SUM_C = 140,
FLAC_KEY_MD5_SUM_D = 141,
FLAC_KEY_MD5_SUM_E = 142,
FLAC_KEY_MD5_SUM_F = 143,
} fx_flac_streaminfo_key_t;
/**
* Returns the size of the FLAC decoder instance in bytes. This assumes that the
* FLAC audio that is being decoded uses the maximum settings, i.e. the largest
* bit depth and block size. See fx_flac_init() regarding parameters.
*
* @return zero if the given parameters are out of range, the number of bytes
* required to hold the FLAC decoder structure otherwise.
*/
FX_EXPORT uint32_t fx_flac_size(uint32_t max_block_size, uint8_t max_channels);
/**
* Initializes the FLAC decoder at the given memory location. Each decoder can
* decode exactly one stream at a time.
*
* @param mem is a pointer at the memory region at which the FLAC decoder should
* store its private data. The memory region must be at last as large as
* indicated by fx_flac_size(). May be NULL, in which case NULL is returned.
* @param max_block_size is the maximum block size for which the FLAC instance
* will provide a buffer. For streams in the Subset format (which is used per
* default in most FLAC encoders), max_block_size should can be set to 4608 if
* the sample rate is <= 48000kHz, otherwise, for larger sample rates,
* max_block_size must be set to 16384.
* @param max_channels is the maximum number of channels that will be decoded.
* @return a pointer at the FLAC decoder instance; note that this pointer may be
* different from what was passed to mem. However, you may still pass the
* original `mem` as `inst` parameter to other functions. Returns NULL if the
* input pointer is NULL or the given parameters are invalid.
*/
FX_EXPORT fx_flac_t *fx_flac_init(void *mem, uint16_t max_block_size,
uint8_t max_channels);
/**
* Macro which calls malloc to allocate memory for a new fx_flac instance. The
* returned pointer must be freed using free. Returns NULL if the allocation
* fails or the given parameters are invalid.
*
* Note that this code is implemented as a macro to prevent explicitly having
* a dependency on malloc while still providing a convenient allocation routine.
*/
#define FX_FLAC_ALLOC(max_block_size, max_channels) \
(fx_flac_size((max_block_size), (max_channels)) == 0U) \
? NULL \
: fx_flac_init(malloc(fx_flac_size((max_block_size), (max_channels))), \
(max_block_size), (max_channels))
/**
* Returns a new fx_flac instance that is sufficient to decode FLAC streams in
* the FLAC Subset format with DAT parameters, i.e. up to 48 kHz, and two
* channels. This will allocate about 40 kiB of memory.
*/
#define FX_FLAC_ALLOC_SUBSET_FORMAT_DAT() \
FX_FLAC_ALLOC(FLAC_SUBSET_MAX_BLOCK_SIZE_48KHZ, 2U)
/**
* Returns a new fx_flac instance that is sufficient to decode FLAC streams in
* the FLAC Subset format. This will allocate about 1.5 MiB of memory.
*/
#define FX_FLAC_ALLOC_SUBSET_FORMAT_ANY() \
FX_FLAC_ALLOC(FLAC_SUBSET_MAX_BLOCK_SIZE, FLAC_MAX_CHANNEL_COUNT)
/**
* Returns a new fx_flac instance that is sufficient to decode any valid FLAC
* stream. Note that this will allocate between 2-3 MiB of memory.
*/
#define FX_FLAC_ALLOC_DEFAULT() \
FX_FLAC_ALLOC(FLAC_MAX_BLOCK_SIZE, FLAC_MAX_CHANNEL_COUNT)
/**
* Resets the FLAC decoder.
*
* @param inst is the FLAC decoder that should be reset.
*/
FX_EXPORT void fx_flac_reset(fx_flac_t *inst);
/**
* Returns the current decoder state.
*
* @param inst is the FLAC decoder instance for which the state should be
* returned.
* @return the current state of the decoder.
*/
FX_EXPORT fx_flac_state_t fx_flac_get_state(const fx_flac_t *inst);
/**
* Returns metadata about the FLAC stream that is currently being parsed. This
* function may only be called if the decoder is in the state
* FLAC_END_OF_METADATA or greater, otherwise the result may be undefined
* (it will likely return zero for most of the metadata keys).
*
* @param inst is a pointer at the FLAC decoder instance for which the metadata
* should be retrieved.
* @param key is the metadata that should be retrieved.
* @return the requested metadata value or FLAC_INVALID_METADATA_KEY if the
* given key is unknown.
*/
FX_EXPORT int64_t fx_flac_get_streaminfo(const fx_flac_t *inst,
fx_flac_streaminfo_key_t key);
/**
* Decodes the given raw FLAC data; the given data must be RAW FLAC data as
* specified in the FLAC format specification https://xiph.org/flac/format.html
* This function will always return right after the decoder transitions to a new
* relevant state.
*
* @param inst is the decoder instance.
* @param in is a pointer at the encoded bytestream.
* @param in_len is a pointer at a integer containing the number of valid bytes
* in "in". After the function returns, in will contain the number of bytes that
* were actually read. This number may be zero if the decoder is in the FLAC_ERR
* or FLAC_STREAM_DONE state, or the internal buffers are full and need to be
* flushed to the provided output first.
* @param out is a pointer at a memory region that will accept the decoded
* interleaved audio data. Samples are decoded as 32-bit signed integer; the
* minimum and maximum value will depend on the original bit depth of the audio
* stored in the bitstream. If this is NULL, the decoder will silently discard
* the output.
* @param out_len is a pointer at an integer containing the number of available
* signed 32-bit integers at the memory address pointed at by out. After the
* function returns, this value will contain the number of samples that were
* written. If this is NULL, the deocder will silently discard the output.
* @return the current state of the decoder. If the state transitions to
* FLAC_END_OF_METADATA, FLAC_END_OF_FRAME or FLAC_END_OF_STREAM this function
* will return immediately; only the data up to the point causing the transition
* has been read.
*/
FX_EXPORT fx_flac_state_t fx_flac_process(fx_flac_t *inst, const uint8_t *in,
uint32_t *in_len, int32_t *out,
uint32_t *out_len);
#ifdef __cplusplus
}
#endif
#endif /* FOXEN_FLAC_H */

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

@ -0,0 +1,418 @@
// 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

@ -20,14 +20,11 @@
#include "esp_console.h"
#include "esp_log.h"
#include "event_queue.hpp"
#include "ff.h"
namespace console {
static AppConsole* sInstance = nullptr;
std::string toSdPath(const std::string& filepath) {
return std::string("/") + filepath;
}
std::weak_ptr<database::Database> AppConsole::sDatabase;
int CmdListDir(int argc, char** argv) {
static const std::string usage = "usage: ls [directory]";
@ -36,7 +33,7 @@ int CmdListDir(int argc, char** argv) {
return 1;
}
auto lock = sInstance->database_.lock();
auto lock = AppConsole::sDatabase.lock();
if (lock == nullptr) {
std::cout << "storage is not available" << std::endl;
return 1;
@ -44,18 +41,38 @@ int CmdListDir(int argc, char** argv) {
std::string path;
if (argc == 2) {
path = toSdPath(argv[1]);
path = argv[1];
} else {
path = toSdPath("");
path = "";
}
FF_DIR dir;
FRESULT res = f_opendir(&dir, path.c_str());
if (res != FR_OK) {
std::cout << "failed to open directory. does it exist?" << std::endl;
return 1;
}
DIR* dir;
struct dirent* ent;
dir = opendir(path.c_str());
while ((ent = readdir(dir))) {
std::cout << ent->d_name << std::endl;
for (;;) {
FILINFO info;
res = f_readdir(&dir, &info);
if (res != FR_OK || info.fname[0] == 0) {
// No more files in the directory.
break;
} else {
std::cout << path;
if (!path.ends_with('/') && !path.empty()) {
std::cout << '/';
}
std::cout << info.fname;
if (info.fattrib & AM_DIR) {
std::cout << '/';
}
std::cout << std::endl;
}
}
closedir(dir);
f_closedir(&dir);
return 0;
}
@ -101,7 +118,7 @@ int CmdDbInit(int argc, char** argv) {
return 1;
}
auto db = sInstance->database_.lock();
auto db = AppConsole::sDatabase.lock();
if (!db) {
std::cout << "no database open" << std::endl;
return 1;
@ -121,21 +138,22 @@ void RegisterDbInit() {
esp_console_cmd_register(&cmd);
}
int CmdDbSongs(int argc, char** argv) {
static const std::string usage = "usage: db_songs";
int CmdDbTracks(int argc, char** argv) {
static const std::string usage = "usage: db_tracks";
if (argc != 1) {
std::cout << usage << std::endl;
return 1;
}
auto db = sInstance->database_.lock();
auto db = AppConsole::sDatabase.lock();
if (!db) {
std::cout << "no database open" << std::endl;
return 1;
}
std::unique_ptr<database::Result<database::Song>> res(db->GetSongs(5).get());
std::unique_ptr<database::Result<database::Track>> res(
db->GetTracks(20).get());
while (true) {
for (database::Song s : res->values()) {
for (database::Track s : res->values()) {
std::cout << s.tags().title.value_or("[BLANK]") << std::endl;
}
if (res->next_page()) {
@ -149,11 +167,11 @@ int CmdDbSongs(int argc, char** argv) {
return 0;
}
void RegisterDbSongs() {
esp_console_cmd_t cmd{.command = "db_songs",
.help = "lists titles of ALL songs in the database",
void RegisterDbTracks() {
esp_console_cmd_t cmd{.command = "db_tracks",
.help = "lists titles of ALL tracks in the database",
.hint = NULL,
.func = &CmdDbSongs,
.func = &CmdDbTracks,
.argtable = NULL};
esp_console_cmd_register(&cmd);
}
@ -165,7 +183,7 @@ int CmdDbDump(int argc, char** argv) {
return 1;
}
auto db = sInstance->database_.lock();
auto db = AppConsole::sDatabase.lock();
if (!db) {
std::cout << "no database open" << std::endl;
return 1;
@ -200,14 +218,6 @@ void RegisterDbDump() {
esp_console_cmd_register(&cmd);
}
AppConsole::AppConsole(const std::weak_ptr<database::Database>& database)
: database_(database) {
sInstance = this;
}
AppConsole::~AppConsole() {
sInstance = nullptr;
}
auto AppConsole::RegisterExtraComponents() -> void {
RegisterListDir();
RegisterPlayFile();
@ -217,7 +227,7 @@ auto AppConsole::RegisterExtraComponents() -> void {
RegisterAudioStatus();
*/
RegisterDbInit();
RegisterDbSongs();
RegisterDbTracks();
RegisterDbDump();
}

@ -15,10 +15,7 @@ namespace console {
class AppConsole : public Console {
public:
explicit AppConsole(const std::weak_ptr<database::Database>& database);
virtual ~AppConsole();
const std::weak_ptr<database::Database>& database_;
static std::weak_ptr<database::Database> sDatabase;
protected:
virtual auto RegisterExtraComponents() -> void;

@ -14,6 +14,7 @@
#include <memory>
#include <variant>
#include "codec.hpp"
#include "freertos/FreeRTOS.h"
#include "esp_heap_caps.h"
@ -50,6 +51,9 @@ auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
// Reuse the existing codec if we can. This will help with gapless playback,
// since we can potentially just continue to decode as we were before,
// without any setup overhead.
// TODO(jacqueline): Reconsider this. It makes a lot of things harder to smash
// streams together at this layer.
/*
if (current_codec_ != nullptr && current_input_format_) {
auto cur_encoding = std::get<StreamInfo::Encoded>(*current_input_format_);
if (cur_encoding.type == encoded.type) {
@ -58,6 +62,7 @@ auto AudioDecoder::ProcessStreamInfo(const StreamInfo& info) -> bool {
return true;
}
}
*/
current_input_format_ = info.format;
ESP_LOGI(kTag, "creating new decoder");
@ -80,69 +85,90 @@ auto AudioDecoder::Process(const std::vector<InputStream>& inputs,
OutputStream* output) -> void {
auto input = inputs.begin();
const StreamInfo& info = input->info();
if (std::holds_alternative<std::monostate>(info.format) ||
info.bytes_in_stream == 0) {
// TODO(jacqueline): should we clear the stream format?
// output->prepare({});
return;
}
// Check the input stream's format has changed (or, by extension, if this is
// the first stream).
if (!current_input_format_ || *current_input_format_ != info.format) {
// The input stream has changed! Immediately throw everything away and
// start from scratch.
has_samples_to_send_ = false;
ProcessStreamInfo(info);
if (!ProcessStreamInfo(info)) {
return;
}
ESP_LOGI(kTag, "beginning new stream");
auto res = current_codec_->BeginStream(input->data());
input->consume(res.first);
if (res.second.has_error()) {
// TODO(jacqueline): Handle errors.
return;
}
// The stream started successfully. Record what format the samples are in.
codecs::ICodec::OutputFormat format = res.second.value();
current_output_format_ = StreamInfo::Pcm{
.channels = format.num_channels,
.bits_per_sample = format.bits_per_sample,
.sample_rate = format.sample_rate_hz,
};
if (info.seek_to_seconds) {
seek_to_sample_ = *info.seek_to_seconds * format.sample_rate_hz;
} else {
seek_to_sample_.reset();
}
}
current_codec_->SetInput(input->data());
while (seek_to_sample_) {
ESP_LOGI(kTag, "seeking forwards...");
auto res = current_codec_->SeekStream(input->data(), *seek_to_sample_);
input->consume(res.first);
if (res.second.has_error()) {
auto err = res.second.error();
if (err == codecs::ICodec::Error::kOutOfInput) {
return;
} else {
// TODO(jacqueline): Handle errors.
seek_to_sample_.reset();
}
} else {
seek_to_sample_.reset();
}
}
has_input_remaining_ = true;
while (true) {
if (has_samples_to_send_) {
auto format = current_codec_->GetOutputFormat();
if (format.has_value()) {
current_output_format_ = StreamInfo::Pcm{
.channels = format->num_channels,
.bits_per_sample = format->bits_per_sample,
.sample_rate = format->sample_rate_hz,
};
if (!output->prepare(*current_output_format_)) {
break;
}
auto write_res = current_codec_->WriteOutputSamples(output->data());
output->add(write_res.first);
has_samples_to_send_ = !write_res.second;
if (has_samples_to_send_) {
// We weren't able to fit all the generated samples into the output
// buffer. Stop trying; we'll finish up during the next pass.
break;
}
}
// TODO(jacqueline): Pass through seek info here?
if (!output->prepare(*current_output_format_)) {
ESP_LOGI(kTag, "waiting for buffer to become free");
break;
}
auto res = current_codec_->ProcessNextFrame();
if (res.has_error()) {
// TODO(jacqueline): Handle errors.
auto res = current_codec_->ContinueStream(input->data(), output->data());
input->consume(res.first);
if (res.second.has_error()) {
if (res.second.error() == codecs::ICodec::Error::kOutOfInput) {
ESP_LOGW(kTag, "out of input");
ESP_LOGW(kTag, "(%u bytes left)", input->data().size_bytes());
has_input_remaining_ = false;
// We can't be halfway through sending samples if the codec is asking
// for more input.
has_samples_to_send_ = false;
input->mark_incomplete();
} else {
// TODO(jacqueline): Handle errors.
ESP_LOGE(kTag, "codec return fatal error");
}
return;
}
has_input_remaining_ = !res.value();
if (!has_input_remaining_) {
// We're out of useable data in this buffer. Finish immediately; there's
// nothing to send.
input->mark_incomplete();
codecs::ICodec::OutputInfo out_info = res.second.value();
output->add(out_info.bytes_written);
has_samples_to_send_ = !out_info.is_finished_writing;
if (has_samples_to_send_) {
// We weren't able to fit all the generated samples into the output
// buffer. Stop trying; we'll finish up during the next pass.
break;
} else {
has_samples_to_send_ = true;
}
}
std::size_t pos = current_codec_->GetInputPosition();
if (pos > 0) {
input->consume(pos - 1);
}
}
} // namespace audio

@ -5,6 +5,7 @@
*/
#include "audio_fsm.hpp"
#include <future>
#include <memory>
#include <variant>
#include "audio_decoder.hpp"
@ -14,6 +15,7 @@
#include "i2s_audio_output.hpp"
#include "i2s_dac.hpp"
#include "pipeline.hpp"
#include "track.hpp"
namespace audio {
@ -28,7 +30,7 @@ std::unique_ptr<FatfsAudioInput> AudioState::sFileSource;
std::unique_ptr<I2SAudioOutput> AudioState::sI2SOutput;
std::vector<std::unique_ptr<IAudioElement>> AudioState::sPipeline;
std::deque<AudioState::EnqueuedItem> AudioState::sSongQueue;
std::deque<AudioState::EnqueuedItem> AudioState::sTrackQueue;
auto AudioState::Init(drivers::GpioExpander* gpio_expander,
std::weak_ptr<database::Database> database) -> bool {
@ -59,18 +61,38 @@ auto AudioState::Init(drivers::GpioExpander* gpio_expander,
return true;
}
void AudioState::react(const system_fsm::StorageMounted& ev) {
sDatabase = ev.db;
}
namespace states {
void Uninitialised::react(const system_fsm::BootComplete&) {
transit<Standby>();
}
void Standby::react(const PlayFile& ev) {
if (sFileSource->OpenFile(ev.filename)) {
transit<Playback>();
void Standby::react(const InputFileOpened& ev) {
transit<Playback>();
}
void Standby::react(const PlayTrack& ev) {
auto db = sDatabase.lock();
if (!db) {
ESP_LOGW(kTag, "database not open; ignoring play request");
return;
}
if (ev.data) {
sFileSource->OpenFile(ev.data->filepath());
} else {
sFileSource->OpenFile(db->GetTrackPath(ev.id));
}
}
void Standby::react(const PlayFile& ev) {
sFileSource->OpenFile(ev.filename);
}
void Playback::entry() {
ESP_LOGI(kTag, "beginning playback");
sI2SOutput->SetInUse(true);
@ -81,16 +103,34 @@ void Playback::exit() {
sI2SOutput->SetInUse(false);
}
void Playback::react(const PlayTrack& ev) {
sTrackQueue.push_back(EnqueuedItem(ev.id));
}
void Playback::react(const PlayFile& ev) {
sTrackQueue.push_back(EnqueuedItem(ev.filename));
}
void Playback::react(const InputFileOpened& ev) {}
void Playback::react(const InputFileFinished& ev) {
ESP_LOGI(kTag, "finished file");
if (sSongQueue.empty()) {
if (sTrackQueue.empty()) {
return;
}
EnqueuedItem next_item = sSongQueue.front();
sSongQueue.pop_front();
EnqueuedItem next_item = sTrackQueue.front();
sTrackQueue.pop_front();
if (std::holds_alternative<std::string>(next_item)) {
sFileSource->OpenFile(std::get<std::string>(next_item));
} else if (std::holds_alternative<database::TrackId>(next_item)) {
auto db = sDatabase.lock();
if (!db) {
ESP_LOGW(kTag, "database not open; ignoring play request");
return;
}
sFileSource->OpenFile(
db->GetTrackPath(std::get<database::TrackId>(next_item)));
}
}

@ -45,7 +45,7 @@ namespace task {
static const char* kTag = "task";
// The default amount of time to wait between pipeline iterations for a single
// song.
// track.
static constexpr uint_fast16_t kDefaultDelayTicks = pdMS_TO_TICKS(5);
static constexpr uint_fast16_t kMaxDelayTicks = pdMS_TO_TICKS(10);
static constexpr uint_fast16_t kMinDelayTicks = pdMS_TO_TICKS(1);
@ -54,7 +54,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
// The stream format for bytes currently in the sink buffer.
std::optional<StreamInfo::Format> output_format;
// How long to wait between pipeline iterations. This is reset for each song,
// How long to wait between pipeline iterations. This is reset for each track,
// and readjusted on the fly to maintain a reasonable amount playback buffer.
// Buffering too much will mean we process samples inefficiently, wasting CPU
// time, whilst buffering too little will affect the quality of the output.
@ -126,7 +126,7 @@ void AudioTaskMain(std::unique_ptr<Pipeline> pipeline, IAudioSink* sink) {
if (sink_stream.info().bytes_in_stream == 0) {
// No new bytes to sink, so skip sinking completely.
ESP_LOGI(kTag, "no bytes to sink");
ESP_LOGW(kTag, "no bytes to sink");
continue;
}

@ -8,7 +8,9 @@
#include <stdint.h>
#include <algorithm>
#include <chrono>
#include <cstdint>
#include <future>
#include <memory>
#include <string>
#include <variant>
@ -24,12 +26,12 @@
#include "audio_element.hpp"
#include "chunk.hpp"
#include "song.hpp"
#include "stream_buffer.hpp"
#include "stream_event.hpp"
#include "stream_info.hpp"
#include "stream_message.hpp"
#include "tag_parser.hpp"
#include "track.hpp"
#include "types.hpp"
static const char* kTag = "SRC";
@ -38,6 +40,7 @@ namespace audio {
FatfsAudioInput::FatfsAudioInput()
: IAudioElement(),
pending_path_(),
current_file_(),
is_file_open_(false),
current_container_(),
@ -45,22 +48,32 @@ FatfsAudioInput::FatfsAudioInput()
FatfsAudioInput::~FatfsAudioInput() {}
auto FatfsAudioInput::OpenFile(std::future<std::optional<std::string>>&& path)
-> void {
pending_path_ = std::move(path);
}
auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
if (is_file_open_) {
f_close(&current_file_);
is_file_open_ = false;
}
if (pending_path_) {
pending_path_ = {};
}
ESP_LOGI(kTag, "opening file %s", path.c_str());
database::TagParserImpl tag_parser;
database::SongTags tags;
database::TrackTags tags;
if (!tag_parser.ReadAndParseTags(path, &tags)) {
ESP_LOGE(kTag, "failed to read tags");
return false;
tags.encoding = database::Encoding::kFlac;
// return false;
}
auto stream_type = ContainerToStreamType(tags.encoding);
if (!stream_type.has_value()) {
ESP_LOGE(kTag, "couldn't match container to stream");
return false;
}
@ -87,16 +100,33 @@ auto FatfsAudioInput::OpenFile(const std::string& path) -> bool {
return false;
}
events::Dispatch<InputFileOpened, AudioState>({});
is_file_open_ = true;
return true;
}
auto FatfsAudioInput::NeedsToProcess() const -> bool {
return is_file_open_;
return is_file_open_ || pending_path_;
}
auto FatfsAudioInput::Process(const std::vector<InputStream>& inputs,
OutputStream* output) -> void {
if (pending_path_) {
ESP_LOGI(kTag, "waiting for path");
if (!pending_path_->valid()) {
pending_path_ = {};
} else {
if (pending_path_->wait_for(std::chrono::seconds(0)) ==
std::future_status::ready) {
ESP_LOGI(kTag, "path ready!");
auto result = pending_path_->get();
if (result) {
OpenFile(*result);
}
}
}
}
if (!is_file_open_) {
return;
}
@ -144,8 +174,8 @@ auto FatfsAudioInput::ContainerToStreamType(database::Encoding enc)
return codecs::StreamType::kPcm;
case database::Encoding::kFlac:
return codecs::StreamType::kFlac;
case database::Encoding::kOgg:
return codecs::StreamType::kOgg;
case database::Encoding::kOgg: // Misnamed; this is Ogg Vorbis.
return codecs::StreamType::kVorbis;
case database::Encoding::kUnsupported:
default:
return {};

@ -42,6 +42,7 @@ class AudioDecoder : public IAudioElement {
std::unique_ptr<codecs::ICodec> current_codec_;
std::optional<StreamInfo::Format> current_input_format_;
std::optional<StreamInfo::Format> current_output_format_;
std::optional<std::size_t> seek_to_sample_;
bool has_samples_to_send_;
bool has_input_remaining_;

@ -10,7 +10,7 @@
#include "tinyfsm.hpp"
#include "song.hpp"
#include "track.hpp"
namespace audio {
@ -18,12 +18,12 @@ struct PlayFile : tinyfsm::Event {
std::string filename;
};
struct PlaySong : tinyfsm::Event {
database::SongId id;
std::optional<database::SongData> data;
std::optional<database::SongTags> tags;
struct PlayTrack : tinyfsm::Event {
database::TrackId id;
std::optional<database::TrackData> data;
};
struct InputFileOpened : tinyfsm::Event {};
struct InputFileFinished : tinyfsm::Event {};
struct AudioPipelineIdle : tinyfsm::Event {};

@ -17,9 +17,9 @@
#include "gpio_expander.hpp"
#include "i2s_audio_output.hpp"
#include "i2s_dac.hpp"
#include "song.hpp"
#include "storage.hpp"
#include "tinyfsm.hpp"
#include "track.hpp"
#include "system_events.hpp"
@ -38,10 +38,13 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
/* Fallback event handler. Does nothing. */
void react(const tinyfsm::Event& ev) {}
void react(const system_fsm::StorageMounted&);
virtual void react(const system_fsm::BootComplete&) {}
virtual void react(const PlaySong&) {}
virtual void react(const PlayTrack&) {}
virtual void react(const PlayFile&) {}
virtual void react(const InputFileOpened&) {}
virtual void react(const InputFileFinished&) {}
virtual void react(const AudioPipelineIdle&) {}
@ -55,8 +58,8 @@ class AudioState : public tinyfsm::Fsm<AudioState> {
static std::unique_ptr<I2SAudioOutput> sI2SOutput;
static std::vector<std::unique_ptr<IAudioElement>> sPipeline;
typedef std::variant<database::SongId, std::string> EnqueuedItem;
static std::deque<EnqueuedItem> sSongQueue;
typedef std::variant<database::TrackId, std::string> EnqueuedItem;
static std::deque<EnqueuedItem> sTrackQueue;
};
namespace states {
@ -69,8 +72,10 @@ class Uninitialised : public AudioState {
class Standby : public AudioState {
public:
void react(const PlaySong&) override {}
void react(const InputFileOpened&) override;
void react(const PlayTrack&) override;
void react(const PlayFile&) override;
using AudioState::react;
};
@ -79,6 +84,10 @@ class Playback : public AudioState {
void entry() override;
void exit() override;
void react(const PlayTrack&) override;
void react(const PlayFile&) override;
void react(const InputFileOpened&) override;
void react(const InputFileFinished&) override;
void react(const AudioPipelineIdle&) override;

@ -7,6 +7,7 @@
#pragma once
#include <cstdint>
#include <future>
#include <memory>
#include <string>
#include <vector>
@ -18,8 +19,8 @@
#include "ff.h"
#include "freertos/message_buffer.h"
#include "freertos/queue.h"
#include "song.hpp"
#include "span.hpp"
#include "track.hpp"
#include "audio_element.hpp"
#include "stream_buffer.hpp"
@ -33,6 +34,7 @@ class FatfsAudioInput : public IAudioElement {
FatfsAudioInput();
~FatfsAudioInput();
auto OpenFile(std::future<std::optional<std::string>>&& path) -> void;
auto OpenFile(const std::string& path) -> bool;
auto NeedsToProcess() const -> bool override;
@ -47,6 +49,7 @@ class FatfsAudioInput : public IAudioElement {
auto ContainerToStreamType(database::Encoding)
-> std::optional<codecs::StreamType>;
std::optional<std::future<std::optional<std::string>>> pending_path_;
FIL current_file_;
bool is_file_open_;

@ -6,6 +6,7 @@
#pragma once
#include <stdint.h>
#include <cstdint>
#include <optional>
#include <string>
@ -30,6 +31,9 @@ struct StreamInfo {
// generated audio, etc.)
std::optional<std::size_t> length_bytes{};
//
std::optional<uint32_t> seek_to_seconds{};
struct Encoded {
// The codec that this stream is associated with.
codecs::StreamType type;

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

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

@ -0,0 +1,80 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "foxenflac.hpp"
#include <stdint.h>
#include <cstdlib>
#include "esp_log.h"
#include "foxen/flac.h"
namespace codecs {
FoxenFlacDecoder::FoxenFlacDecoder()
: flac_(FX_FLAC_ALLOC(FLAC_MAX_BLOCK_SIZE, 2)) {}
FoxenFlacDecoder::~FoxenFlacDecoder() {
free(flac_);
}
auto FoxenFlacDecoder::BeginStream(const cpp::span<const std::byte> input)
-> Result<OutputFormat> {
uint32_t bytes_used = input.size_bytes();
fx_flac_state_t state =
fx_flac_process(flac_, reinterpret_cast<const uint8_t*>(input.data()),
&bytes_used, NULL, NULL);
if (state != FLAC_END_OF_METADATA) {
return {bytes_used, cpp::fail(Error::kMalformedData)};
}
int64_t channels = fx_flac_get_streaminfo(flac_, FLAC_KEY_N_CHANNELS);
int64_t fs = fx_flac_get_streaminfo(flac_, FLAC_KEY_SAMPLE_RATE);
if (channels == FLAC_INVALID_METADATA_KEY ||
fs == FLAC_INVALID_METADATA_KEY) {
return {bytes_used, cpp::fail(Error::kMalformedData)};
}
return {bytes_used,
OutputFormat{
.num_channels = static_cast<uint8_t>(channels),
.bits_per_sample = 32, // libfoxenflac output is fixed-size.
.sample_rate_hz = static_cast<uint32_t>(fs),
}};
}
auto FoxenFlacDecoder::ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output)
-> Result<OutputInfo> {
cpp::span<int32_t> output_as_samples{
reinterpret_cast<int32_t*>(output.data()), output.size_bytes() / 4};
uint32_t bytes_read = input.size_bytes();
uint32_t samples_written = output_as_samples.size();
fx_flac_state_t state =
fx_flac_process(flac_, reinterpret_cast<const uint8_t*>(input.data()),
&bytes_read, output_as_samples.data(), &samples_written);
if (state == FLAC_ERR) {
return {bytes_read, cpp::fail(Error::kMalformedData)};
}
if (samples_written > 0) {
return {bytes_read,
OutputInfo{.bytes_written = samples_written * 4,
.is_finished_writing = state == FLAC_END_OF_FRAME}};
}
// No error, but no samples written. We must be out of data.
return {bytes_read, cpp::fail(Error::kOutOfInput)};
}
auto FoxenFlacDecoder::SeekStream(cpp::span<const std::byte> input,
std::size_t target_sample) -> Result<void> {
// TODO(jacqueline): Implement me.
return {0, {}};
}
} // namespace codecs

@ -21,48 +21,58 @@
namespace codecs {
/*
* Common interface to be implemented by all audio decoders.
*/
class ICodec {
public:
virtual ~ICodec() {}
/* Errors that may be returned by codecs. */
enum class Error {
// Indicates that more data is required before this codec can finish its
// operation. E.g. the input buffer ends with a truncated frame.
kOutOfInput,
// Indicates that the data within the input buffer is fatally malformed.
kMalformedData,
kInternalError,
};
/*
* Alias for more readable return types. All codec methods, success or
* failure, should also return the number of bytes they consumed.
*/
template <typename T>
using Result = std::pair<std::size_t, cpp::result<T, Error>>;
struct OutputFormat {
uint8_t num_channels;
uint8_t bits_per_sample;
uint32_t sample_rate_hz;
};
virtual auto GetOutputFormat() -> std::optional<OutputFormat> = 0;
enum ProcessingError { MALFORMED_DATA };
virtual auto SetInput(cpp::span<const std::byte> input) -> void = 0;
/*
* Returns the codec's next read position within the input buffer. If the
* codec is out of usable data, but there is still some data left in the
* stream, that data should be prepended to the next input buffer.
* Decodes metadata or headers from the given input stream, and returns the
* format for the samples that will be decoded from it.
*/
virtual auto GetInputPosition() -> std::size_t = 0;
virtual auto BeginStream(cpp::span<const std::byte> input)
-> Result<OutputFormat> = 0;
/*
* Read one frame (or equivalent discrete chunk) from the input, and
* synthesize output samples for it.
*
* Returns true if we are out of usable data from the input stream, or false
* otherwise.
*/
virtual auto ProcessNextFrame() -> cpp::result<bool, ProcessingError> = 0;
struct OutputInfo {
std::size_t bytes_written;
bool is_finished_writing;
};
/*
* Writes PCM samples to the given output buffer.
*
* Returns the number of bytes that were written, and true if all of the
* samples synthesized from the last call to `ProcessNextFrame` have been
* written. If this returns false, then this method should be called again
* after flushing the output buffer.
*/
virtual auto WriteOutputSamples(cpp::span<std::byte> output)
-> std::pair<std::size_t, bool> = 0;
virtual auto ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output)
-> Result<OutputInfo> = 0;
virtual auto SeekStream(cpp::span<const std::byte> input,
std::size_t target_sample) -> Result<void> = 0;
};
auto CreateCodecForType(StreamType type) -> std::optional<ICodec*>;

@ -0,0 +1,38 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "foxen/flac.h"
#include "span.hpp"
#include "codec.hpp"
namespace codecs {
class FoxenFlacDecoder : public ICodec {
public:
FoxenFlacDecoder();
~FoxenFlacDecoder();
auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
auto ContinueStream(cpp::span<const std::byte>, cpp::span<std::byte>)
-> Result<OutputInfo> override;
auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
-> Result<void> override;
private:
fx_flac_t* flac_;
};
} // namespace codecs

@ -24,12 +24,22 @@ class MadMp3Decoder : public ICodec {
MadMp3Decoder();
~MadMp3Decoder();
auto GetOutputFormat() -> std::optional<OutputFormat> override;
auto SetInput(cpp::span<const std::byte> input) -> void override;
auto GetInputPosition() -> std::size_t override;
auto ProcessNextFrame() -> cpp::result<bool, ProcessingError> override;
auto WriteOutputSamples(cpp::span<std::byte> output)
-> std::pair<std::size_t, bool> override;
/*
* Returns the output format for the next frame in the stream. MP3 streams
* may represent multiple distinct tracks, with different bitrates, and so we
* handle the stream only on a frame-by-frame basis.
*/
auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
/*
* Writes samples for the current frame.
*/
auto ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output)
-> Result<OutputInfo> override;
auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
-> Result<void> override;
private:
mad_stream stream_;
@ -37,6 +47,8 @@ class MadMp3Decoder : public ICodec {
mad_synth synth_;
int current_sample_;
auto GetInputPosition() -> std::size_t;
};
} // namespace codecs

@ -0,0 +1,42 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <cstddef>
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <utility>
#include "stb_vorbis.h"
#include "codec.hpp"
namespace codecs {
class StbVorbisDecoder : public ICodec {
public:
StbVorbisDecoder();
~StbVorbisDecoder();
auto BeginStream(cpp::span<const std::byte>) -> Result<OutputFormat> override;
auto ContinueStream(cpp::span<const std::byte>, cpp::span<std::byte>)
-> Result<OutputInfo> override;
auto SeekStream(cpp::span<const std::byte> input, std::size_t target_sample)
-> Result<void> override;
private:
stb_vorbis* vorbis_;
int current_sample_;
int num_channels_;
int num_samples_;
float** samples_array_;
};
} // namespace codecs

@ -13,7 +13,7 @@ namespace codecs {
enum class StreamType {
kMp3,
kPcm,
kOgg,
kVorbis,
kFlac,
};

@ -13,11 +13,12 @@
#include "mad.h"
#include "codec.hpp"
#include "result.hpp"
#include "types.hpp"
namespace codecs {
static uint32_t scaleToBits(mad_fixed_t sample, uint8_t bits) {
static uint32_t mad_fixed_to_pcm(mad_fixed_t sample, uint8_t bits) {
// Round the bottom bits.
sample += (1L << (MAD_F_FRACBITS - bits));
@ -42,93 +43,167 @@ MadMp3Decoder::~MadMp3Decoder() {
mad_synth_finish(&synth_);
}
auto MadMp3Decoder::GetOutputFormat() -> std::optional<OutputFormat> {
if (synth_.pcm.channels == 0 || synth_.pcm.samplerate == 0) {
return {};
}
return std::optional<OutputFormat>({
.num_channels = static_cast<uint8_t>(synth_.pcm.channels),
.bits_per_sample = 24,
.sample_rate_hz = synth_.pcm.samplerate,
});
auto MadMp3Decoder::GetInputPosition() -> std::size_t {
return stream_.next_frame - stream_.buffer;
}
auto MadMp3Decoder::SetInput(cpp::span<const std::byte> input) -> void {
auto MadMp3Decoder::BeginStream(const cpp::span<const std::byte> input)
-> Result<OutputFormat> {
mad_stream_buffer(&stream_,
reinterpret_cast<const unsigned char*>(input.data()),
input.size());
}
auto MadMp3Decoder::GetInputPosition() -> std::size_t {
return stream_.next_frame - stream_.buffer;
}
auto MadMp3Decoder::ProcessNextFrame() -> cpp::result<bool, ProcessingError> {
// Whatever was last synthesized is now invalid, so ensure we don't try to
// send it.
current_sample_ = -1;
// Decode the next frame. To signal errors, this returns -1 and
// stashes an error code in the stream structure.
if (mad_frame_decode(&frame_, &stream_) < 0) {
// To get the output format for MP3 streams, we simply need to decode the
// first frame header.
mad_header header;
mad_header_init(&header);
while (mad_header_decode(&header, &stream_) < 0) {
if (MAD_RECOVERABLE(stream_.error)) {
// Recoverable errors are usually malformed parts of the stream.
// We can recover from them by just retrying the decode.
return false;
continue;
} else {
// Don't bother checking for other errors; if the first part of the stream
// doesn't even contain a header then something's gone wrong.
return {GetInputPosition(), cpp::fail(Error::kMalformedData)};
}
if (stream_.error == MAD_ERROR_BUFLEN) {
// The decoder ran out of bytes before it completed a frame. We
// need to return back to the caller to give us more data.
return true;
}
// The error is unrecoverable. Give up.
return cpp::fail(MALFORMED_DATA);
}
// We've successfully decoded a frame!
// Now we need to synthesize PCM samples based on the frame, and send
// them downstream.
mad_synth_frame(&synth_, &frame_);
current_sample_ = 0;
return false;
uint8_t channels = MAD_NCHANNELS(&header);
return {GetInputPosition(),
OutputFormat{
.num_channels = channels,
.bits_per_sample = 24, // We always scale to 24 bits
.sample_rate_hz = header.samplerate,
}};
}
auto MadMp3Decoder::WriteOutputSamples(cpp::span<std::byte> output)
-> std::pair<std::size_t, bool> {
size_t output_byte = 0;
// First ensure that we actually have some samples to send off.
auto MadMp3Decoder::ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output)
-> Result<OutputInfo> {
if (current_sample_ < 0) {
return std::make_pair(output_byte, true);
mad_stream_buffer(&stream_,
reinterpret_cast<const unsigned char*>(input.data()),
input.size());
// Decode the next frame. To signal errors, this returns -1 and
// stashes an error code in the stream structure.
while (mad_frame_decode(&frame_, &stream_) < 0) {
if (MAD_RECOVERABLE(stream_.error)) {
// Recoverable errors are usually malformed parts of the stream.
// We can recover from them by just retrying the decode.
continue;
}
if (stream_.error == MAD_ERROR_BUFLEN) {
// The decoder ran out of bytes before it completed a frame. We
// need to return back to the caller to give us more data.
return {GetInputPosition(), cpp::fail(Error::kOutOfInput)};
}
// The error is unrecoverable. Give up.
return {GetInputPosition(), cpp::fail(Error::kMalformedData)};
}
// We've successfully decoded a frame! Now synthesize samples to write out.
mad_synth_frame(&synth_, &frame_);
current_sample_ = 0;
}
size_t output_byte = 0;
while (current_sample_ < synth_.pcm.length) {
if (output_byte + (2 * synth_.pcm.channels) >= output.size()) {
return std::make_pair(output_byte, false);
if (output_byte + (4 * synth_.pcm.channels) >= output.size()) {
// We can't fit the next sample into the buffer. Stop now, and also avoid
// writing the sample for only half the channels.
return {GetInputPosition(), OutputInfo{.bytes_written = output_byte,
.is_finished_writing = false}};
}
for (int channel = 0; channel < synth_.pcm.channels; channel++) {
uint32_t sample_24 =
scaleToBits(synth_.pcm.samples[channel][current_sample_], 24);
mad_fixed_to_pcm(synth_.pcm.samples[channel][current_sample_], 24);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 16) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 8) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24)&0xFF);
// 24 bit samples must still be aligned to 32 bits. The LSB is ignored.
output[output_byte++] = static_cast<std::byte>(0);
/*
uint16_t sample_16 =
scaleToBits(synth_.pcm.samples[channel][current_sample_], 16);
output[output_byte++] = static_cast<std::byte>((sample_16 >> 8) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_16)&0xFF);
*/
}
current_sample_++;
}
// We wrote everything! Reset, ready for the next frame.
current_sample_ = -1;
return std::make_pair(output_byte, true);
return {GetInputPosition(), OutputInfo{.bytes_written = output_byte,
.is_finished_writing = true}};
}
auto MadMp3Decoder::SeekStream(cpp::span<const std::byte> input,
std::size_t target_sample) -> Result<void> {
mad_stream_buffer(&stream_,
reinterpret_cast<const unsigned char*>(input.data()),
input.size());
std::size_t current_sample = 0;
std::size_t samples_per_frame = 0;
while (true) {
current_sample += samples_per_frame;
// First, decode the header for this frame.
mad_header header;
mad_header_init(&header);
while (mad_header_decode(&header, &stream_) < 0) {
if (MAD_RECOVERABLE(stream_.error)) {
// Recoverable errors are usually malformed parts of the stream.
// We can recover from them by just retrying the decode.
continue;
} else {
// Don't bother checking for other errors; if the first part of the
// stream doesn't even contain a header then something's gone wrong.
return {GetInputPosition(), cpp::fail(Error::kMalformedData)};
}
}
// Calculate samples per frame if we haven't already.
if (samples_per_frame == 0) {
samples_per_frame = 32 * MAD_NSBSAMPLES(&header);
}
// Work out how close we are to the target.
std::size_t samples_to_go = target_sample - current_sample;
std::size_t frames_to_go = samples_to_go / samples_per_frame;
if (frames_to_go > 3) {
// The target is far in the distance. Keep skipping through headers only.
continue;
}
// The target is within the next few frames. We should decode these, to give
// the decoder a chance to sync with the stream.
while (mad_frame_decode(&frame_, &stream_) < 0) {
if (MAD_RECOVERABLE(stream_.error)) {
continue;
}
if (stream_.error == MAD_ERROR_BUFLEN) {
return {GetInputPosition(), cpp::fail(Error::kOutOfInput)};
}
// The error is unrecoverable. Give up.
return {GetInputPosition(), cpp::fail(Error::kMalformedData)};
}
if (frames_to_go <= 1) {
// The target is within the next couple of frames. We should start
// synthesizing a frame early because this guy says so:
// https://lists.mars.org/hyperkitty/list/mad-dev@lists.mars.org/message/UZSHXZTIZEF7FZ4KFOR65DUCKAY2OCUT/
mad_synth_frame(&synth_, &frame_);
}
if (frames_to_go == 0) {
// The target is actually within this frame! Set up for the ContinueStream
// call.
current_sample_ =
(target_sample > current_sample) ? target_sample - current_sample : 0;
return {GetInputPosition(), {}};
}
}
}
} // namespace codecs

@ -0,0 +1,128 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "stbvorbis.hpp"
#include <stdint.h>
#include <cstdint>
#include <optional>
#include "stb_vorbis.h"
namespace codecs {
StbVorbisDecoder::StbVorbisDecoder()
: vorbis_(nullptr),
current_sample_(-1),
num_channels_(0),
num_samples_(0),
samples_array_(NULL) {}
StbVorbisDecoder::~StbVorbisDecoder() {
if (vorbis_ != nullptr) {
stb_vorbis_close(vorbis_);
}
}
static uint32_t scaleToBits(float sample, uint8_t bits) {
// Scale to range.
int32_t max_val = (1 << (bits - 1));
int32_t fixed_point = sample * max_val;
// Clamp within bounds.
fixed_point = std::clamp(fixed_point, -max_val, max_val);
// Remove sign.
return *reinterpret_cast<uint32_t*>(&fixed_point);
}
auto StbVorbisDecoder::BeginStream(const cpp::span<const std::byte> input)
-> Result<OutputFormat> {
if (vorbis_ != nullptr) {
stb_vorbis_close(vorbis_);
vorbis_ = nullptr;
}
current_sample_ = -1;
int bytes_read = 0;
int error = 0;
vorbis_ =
stb_vorbis_open_pushdata(reinterpret_cast<const uint8_t*>(input.data()),
input.size_bytes(), &bytes_read, &error, NULL);
if (error != 0) {
return {0, cpp::fail(Error::kMalformedData)};
}
stb_vorbis_info info = stb_vorbis_get_info(vorbis_);
return {bytes_read,
OutputFormat{.num_channels = static_cast<uint8_t>(info.channels),
.bits_per_sample = 24,
.sample_rate_hz = info.sample_rate}};
}
auto StbVorbisDecoder::ContinueStream(cpp::span<const std::byte> input,
cpp::span<std::byte> output)
-> Result<OutputInfo> {
std::size_t bytes_used = 0;
if (current_sample_ < 0) {
num_channels_ = 0;
num_samples_ = 0;
samples_array_ = NULL;
while (true) {
auto cropped = input.subspan(bytes_used);
std::size_t b = stb_vorbis_decode_frame_pushdata(
vorbis_, reinterpret_cast<const uint8_t*>(cropped.data()),
cropped.size_bytes(), &num_channels_, &samples_array_, &num_samples_);
if (b == 0) {
return {bytes_used, cpp::fail(Error::kOutOfInput)};
}
bytes_used += b;
if (num_samples_ == 0) {
// Decoder is synchronising. Decode more bytes.
continue;
}
if (num_channels_ == 0 || samples_array_ == NULL) {
// The decoder isn't satisfying its contract.
return {bytes_used, cpp::fail(Error::kInternalError)};
}
current_sample_ = 0;
break;
}
}
// We successfully decoded a frame. Time to write out the samples.
std::size_t output_byte = 0;
while (current_sample_ < num_samples_) {
if (output_byte + (2 * num_channels_) >= output.size()) {
return {0, OutputInfo{.bytes_written = output_byte,
.is_finished_writing = false}};
}
for (int channel = 0; channel < num_channels_; channel++) {
float raw_sample = samples_array_[channel][current_sample_];
uint16_t sample_24 = scaleToBits(raw_sample, 24);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 16) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24 >> 8) & 0xFF);
output[output_byte++] = static_cast<std::byte>((sample_24)&0xFF);
// Pad to 32 bits for alignment.
output[output_byte++] = static_cast<std::byte>(0);
}
current_sample_++;
}
current_sample_ = -1;
return {bytes_used, OutputInfo{.bytes_written = output_byte,
.is_finished_writing = true}};
}
auto StbVorbisDecoder::SeekStream(cpp::span<const std::byte> input,
std::size_t target_sample) -> Result<void> {
// TODO(jacqueline): Implement me.
return {0, {}};
}
} // namespace codecs

@ -3,7 +3,7 @@
# SPDX-License-Identifier: GPL-3.0-only
idf_component_register(
SRCS "env_esp.cpp" "database.cpp" "song.cpp" "records.cpp" "file_gatherer.cpp" "tag_parser.cpp"
SRCS "env_esp.cpp" "database.cpp" "track.cpp" "records.cpp" "file_gatherer.cpp" "tag_parser.cpp"
INCLUDE_DIRS "include"
REQUIRES "result" "span" "esp_psram" "fatfs" "libtags" "komihash" "cbor" "tasks")

@ -17,6 +17,7 @@
#include "esp_log.h"
#include "ff.h"
#include "freertos/projdefs.h"
#include "leveldb/cache.h"
#include "leveldb/db.h"
#include "leveldb/iterator.h"
@ -28,16 +29,16 @@
#include "file_gatherer.hpp"
#include "records.hpp"
#include "result.hpp"
#include "song.hpp"
#include "tag_parser.hpp"
#include "tasks.hpp"
#include "track.hpp"
namespace database {
static SingletonEnv<leveldb::EspEnv> sEnv;
static const char* kTag = "DB";
static const char kSongIdKey[] = "next_song_id";
static const char kTrackIdKey[] = "next_track_id";
static std::atomic<bool> sIsDbOpen(false);
@ -68,12 +69,13 @@ auto Database::Open(IFileGatherer* gatherer, ITagParser* parser)
return cpp::fail(DatabaseError::ALREADY_OPEN);
}
leveldb::sBackgroundThread.reset(
tasks::Worker::Start<tasks::Type::kDatabaseBackground>());
std::shared_ptr<tasks::Worker> worker(
tasks::Worker::Start<tasks::Type::kDatabase>());
leveldb::sBackgroundThread = std::weak_ptr<tasks::Worker>(worker);
return worker
->Dispatch<cpp::result<Database*, DatabaseError>>(
[&]() -> cpp::result<Database*, DatabaseError> {
[=]() -> cpp::result<Database*, DatabaseError> {
leveldb::DB* db;
leveldb::Cache* cache = leveldb::NewLRUCache(24 * 1024);
leveldb::Options options;
@ -121,15 +123,15 @@ Database::~Database() {
delete db_;
delete cache_;
leveldb::sBackgroundThread = std::weak_ptr<tasks::Worker>();
leveldb::sBackgroundThread.reset();
sIsDbOpen.store(false);
}
auto Database::Update() -> std::future<void> {
return worker_task_->Dispatch<void>([&]() -> void {
// Stage 1: verify all existing songs are still valid.
ESP_LOGI(kTag, "verifying existing songs");
// Stage 1: verify all existing tracks are still valid.
ESP_LOGI(kTag, "verifying existing tracks");
const leveldb::Snapshot* snapshot = db_->GetSnapshot();
leveldb::ReadOptions read_options;
read_options.fill_cache = false;
@ -138,8 +140,8 @@ auto Database::Update() -> std::future<void> {
OwningSlice prefix = CreateDataPrefix();
it->Seek(prefix.slice);
while (it->Valid() && it->key().starts_with(prefix.slice)) {
std::optional<SongData> song = ParseDataValue(it->value());
if (!song) {
std::optional<TrackData> track = ParseDataValue(it->value());
if (!track) {
// The value was malformed. Drop this record.
ESP_LOGW(kTag, "dropping malformed metadata");
db_->Delete(leveldb::WriteOptions(), it->key());
@ -147,33 +149,33 @@ auto Database::Update() -> std::future<void> {
continue;
}
if (song->is_tombstoned()) {
ESP_LOGW(kTag, "skipping tombstoned %lx", song->id());
if (track->is_tombstoned()) {
ESP_LOGW(kTag, "skipping tombstoned %lx", track->id());
it->Next();
continue;
}
SongTags tags;
if (!tag_parser_->ReadAndParseTags(song->filepath(), &tags) ||
TrackTags tags;
if (!tag_parser_->ReadAndParseTags(track->filepath(), &tags) ||
tags.encoding == Encoding::kUnsupported) {
// We couldn't read the tags for this song. Either they were
// We couldn't read the tags for this track. Either they were
// malformed, or perhaps the file is missing. Either way, tombstone
// this record.
ESP_LOGW(kTag, "entombing missing #%lx", song->id());
dbPutSongData(song->Entomb());
ESP_LOGW(kTag, "entombing missing #%lx", track->id());
dbPutTrackData(track->Entomb());
it->Next();
continue;
}
uint64_t new_hash = tags.Hash();
if (new_hash != song->tags_hash()) {
// This song's tags have changed. Since the filepath is exactly the
if (new_hash != track->tags_hash()) {
// This track's tags have changed. Since the filepath is exactly the
// same, we assume this is a legitimate correction. Update the
// database.
ESP_LOGI(kTag, "updating hash (%llx -> %llx)", song->tags_hash(),
ESP_LOGI(kTag, "updating hash (%llx -> %llx)", track->tags_hash(),
new_hash);
dbPutSongData(song->UpdateHash(new_hash));
dbPutHash(new_hash, song->id());
dbPutTrackData(track->UpdateHash(new_hash));
dbPutHash(new_hash, track->id());
}
it->Next();
@ -182,9 +184,9 @@ auto Database::Update() -> std::future<void> {
db_->ReleaseSnapshot(snapshot);
// Stage 2: search for newly added files.
ESP_LOGI(kTag, "scanning for new songs");
ESP_LOGI(kTag, "scanning for new tracks");
file_gatherer_->FindFiles("", [&](const std::string& path) {
SongTags tags;
TrackTags tags;
if (!tag_parser_->ReadAndParseTags(path, &tags) ||
tags.encoding == Encoding::kUnsupported) {
// No parseable tags; skip this fiile.
@ -194,32 +196,32 @@ auto Database::Update() -> std::future<void> {
// Check for any existing record with the same hash.
uint64_t hash = tags.Hash();
OwningSlice key = CreateHashKey(hash);
std::optional<SongId> existing_hash;
std::optional<TrackId> existing_hash;
std::string raw_entry;
if (db_->Get(leveldb::ReadOptions(), key.slice, &raw_entry).ok()) {
existing_hash = ParseHashValue(raw_entry);
}
if (!existing_hash) {
// We've never met this song before! Or we have, but the entry is
// malformed. Either way, record this as a new song.
SongId id = dbMintNewSongId();
// We've never met this track before! Or we have, but the entry is
// malformed. Either way, record this as a new track.
TrackId id = dbMintNewTrackId();
ESP_LOGI(kTag, "recording new 0x%lx", id);
dbPutSong(id, path, hash);
dbPutTrack(id, path, hash);
return;
}
std::optional<SongData> existing_data = dbGetSongData(*existing_hash);
std::optional<TrackData> existing_data = dbGetTrackData(*existing_hash);
if (!existing_data) {
// We found a hash that matches, but there's no data record? Weird.
SongData new_data(*existing_hash, path, hash);
dbPutSongData(new_data);
TrackData new_data(*existing_hash, path, hash);
dbPutTrackData(new_data);
return;
}
if (existing_data->is_tombstoned()) {
ESP_LOGI(kTag, "exhuming song %lu", existing_data->id());
dbPutSongData(existing_data->Exhume(path));
ESP_LOGI(kTag, "exhuming track %lu", existing_data->id());
dbPutTrackData(existing_data->Exhume(path));
} else if (existing_data->filepath() != path) {
ESP_LOGW(kTag, "tag hash collision");
}
@ -227,14 +229,26 @@ auto Database::Update() -> std::future<void> {
});
}
auto Database::GetSongs(std::size_t page_size) -> std::future<Result<Song>*> {
return worker_task_->Dispatch<Result<Song>*>([=, this]() -> Result<Song>* {
Continuation<Song> c{.iterator = nullptr,
.prefix = CreateDataPrefix().data,
.start_key = CreateDataPrefix().data,
.forward = true,
.was_prev_forward = true,
.page_size = page_size};
auto Database::GetTrackPath(TrackId id)
-> std::future<std::optional<std::string>> {
return worker_task_->Dispatch<std::optional<std::string>>(
[=, this]() -> std::optional<std::string> {
auto track_data = dbGetTrackData(id);
if (track_data) {
return track_data->filepath();
}
return {};
});
}
auto Database::GetTracks(std::size_t page_size) -> std::future<Result<Track>*> {
return worker_task_->Dispatch<Result<Track>*>([=, this]() -> Result<Track>* {
Continuation<Track> c{.iterator = nullptr,
.prefix = CreateDataPrefix().data,
.start_key = CreateDataPrefix().data,
.forward = true,
.was_prev_forward = true,
.page_size = page_size};
return dbGetPage(c);
});
}
@ -260,32 +274,32 @@ auto Database::GetPage(Continuation<T>* c) -> std::future<Result<T>*> {
[=, this]() -> Result<T>* { return dbGetPage(copy); });
}
template auto Database::GetPage<Song>(Continuation<Song>* c)
-> std::future<Result<Song>*>;
template auto Database::GetPage<Track>(Continuation<Track>* c)
-> std::future<Result<Track>*>;
template auto Database::GetPage<std::string>(Continuation<std::string>* c)
-> std::future<Result<std::string>*>;
auto Database::dbMintNewSongId() -> SongId {
SongId next_id = 1;
auto Database::dbMintNewTrackId() -> TrackId {
TrackId next_id = 1;
std::string val;
auto status = db_->Get(leveldb::ReadOptions(), kSongIdKey, &val);
auto status = db_->Get(leveldb::ReadOptions(), kTrackIdKey, &val);
if (status.ok()) {
next_id = BytesToSongId(val).value_or(next_id);
next_id = BytesToTrackId(val).value_or(next_id);
} else if (!status.IsNotFound()) {
// TODO(jacqueline): Handle this more.
ESP_LOGE(kTag, "failed to get next song id");
ESP_LOGE(kTag, "failed to get next track id");
}
if (!db_->Put(leveldb::WriteOptions(), kSongIdKey,
SongIdToBytes(next_id + 1).slice)
if (!db_->Put(leveldb::WriteOptions(), kTrackIdKey,
TrackIdToBytes(next_id + 1).slice)
.ok()) {
ESP_LOGE(kTag, "failed to write next song id");
ESP_LOGE(kTag, "failed to write next track id");
}
return next_id;
}
auto Database::dbEntomb(SongId id, uint64_t hash) -> void {
auto Database::dbEntomb(TrackId id, uint64_t hash) -> void {
OwningSlice key = CreateHashKey(hash);
OwningSlice val = CreateHashValue(id);
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
@ -293,7 +307,7 @@ auto Database::dbEntomb(SongId id, uint64_t hash) -> void {
}
}
auto Database::dbPutSongData(const SongData& s) -> void {
auto Database::dbPutTrackData(const TrackData& s) -> void {
OwningSlice key = CreateDataKey(s.id());
OwningSlice val = CreateDataValue(s);
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
@ -301,7 +315,7 @@ auto Database::dbPutSongData(const SongData& s) -> void {
}
}
auto Database::dbGetSongData(SongId id) -> std::optional<SongData> {
auto Database::dbGetTrackData(TrackId id) -> std::optional<TrackData> {
OwningSlice key = CreateDataKey(id);
std::string raw_val;
if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) {
@ -311,7 +325,7 @@ auto Database::dbGetSongData(SongId id) -> std::optional<SongData> {
return ParseDataValue(raw_val);
}
auto Database::dbPutHash(const uint64_t& hash, SongId i) -> void {
auto Database::dbPutHash(const uint64_t& hash, TrackId i) -> void {
OwningSlice key = CreateHashKey(hash);
OwningSlice val = CreateHashValue(i);
if (!db_->Put(leveldb::WriteOptions(), key.slice, val.slice).ok()) {
@ -319,7 +333,7 @@ auto Database::dbPutHash(const uint64_t& hash, SongId i) -> void {
}
}
auto Database::dbGetHash(const uint64_t& hash) -> std::optional<SongId> {
auto Database::dbGetHash(const uint64_t& hash) -> std::optional<TrackId> {
OwningSlice key = CreateHashKey(hash);
std::string raw_val;
if (!db_->Get(leveldb::ReadOptions(), key.slice, &raw_val).ok()) {
@ -329,10 +343,10 @@ auto Database::dbGetHash(const uint64_t& hash) -> std::optional<SongId> {
return ParseHashValue(raw_val);
}
auto Database::dbPutSong(SongId id,
const std::string& path,
const uint64_t& hash) -> void {
dbPutSongData(SongData(id, path, hash));
auto Database::dbPutTrack(TrackId id,
const std::string& path,
const uint64_t& hash) -> void {
dbPutTrackData(TrackData(id, path, hash));
dbPutHash(hash, id);
}
@ -455,24 +469,24 @@ auto Database::dbGetPage(const Continuation<T>& c) -> Result<T>* {
return new Result<T>(std::move(records), next_page, prev_page);
}
template auto Database::dbGetPage<Song>(const Continuation<Song>& c)
-> Result<Song>*;
template auto Database::dbGetPage<Track>(const Continuation<Track>& c)
-> Result<Track>*;
template auto Database::dbGetPage<std::string>(
const Continuation<std::string>& c) -> Result<std::string>*;
template <>
auto Database::ParseRecord<Song>(const leveldb::Slice& key,
const leveldb::Slice& val)
-> std::optional<Song> {
std::optional<SongData> data = ParseDataValue(val);
auto Database::ParseRecord<Track>(const leveldb::Slice& key,
const leveldb::Slice& val)
-> std::optional<Track> {
std::optional<TrackData> data = ParseDataValue(val);
if (!data || data->is_tombstoned()) {
return {};
}
SongTags tags;
TrackTags tags;
if (!tag_parser_->ReadAndParseTags(data->filepath(), &tags)) {
return {};
}
return Song(*data, tags);
return Track(*data, tags);
}
template <>

@ -15,6 +15,7 @@
#include <cstring>
#include <functional>
#include <limits>
#include <memory>
#include <mutex>
#include <queue>
#include <set>
@ -39,7 +40,7 @@
namespace leveldb {
std::weak_ptr<tasks::Worker> sBackgroundThread;
std::shared_ptr<tasks::Worker> sBackgroundThread;
std::string ErrToStr(FRESULT err) {
switch (err) {
@ -463,7 +464,7 @@ EspEnv::EspEnv() {}
void EspEnv::Schedule(
void (*background_work_function)(void* background_work_arg),
void* background_work_arg) {
auto worker = sBackgroundThread.lock();
auto worker = sBackgroundThread;
if (worker) {
worker->Dispatch<void>(
[=]() { std::invoke(background_work_function, background_work_arg); });

@ -23,9 +23,9 @@
#include "leveldb/slice.h"
#include "records.hpp"
#include "result.hpp"
#include "song.hpp"
#include "tag_parser.hpp"
#include "tasks.hpp"
#include "track.hpp"
namespace database {
@ -82,7 +82,9 @@ class Database {
auto Update() -> std::future<void>;
auto GetSongs(std::size_t page_size) -> std::future<Result<Song>*>;
auto GetTrackPath(TrackId id) -> std::future<std::optional<std::string>>;
auto GetTracks(std::size_t page_size) -> std::future<Result<Track>*>;
auto GetDump(std::size_t page_size) -> std::future<Result<std::string>*>;
template <typename T>
@ -109,14 +111,14 @@ class Database {
ITagParser* tag_parser,
std::shared_ptr<tasks::Worker> worker);
auto dbMintNewSongId() -> SongId;
auto dbEntomb(SongId song, uint64_t hash) -> void;
auto dbMintNewTrackId() -> TrackId;
auto dbEntomb(TrackId track, uint64_t hash) -> void;
auto dbPutSongData(const SongData& s) -> void;
auto dbGetSongData(SongId id) -> std::optional<SongData>;
auto dbPutHash(const uint64_t& hash, SongId i) -> void;
auto dbGetHash(const uint64_t& hash) -> std::optional<SongId>;
auto dbPutSong(SongId id, const std::string& path, const uint64_t& hash)
auto dbPutTrackData(const TrackData& s) -> void;
auto dbGetTrackData(TrackId id) -> std::optional<TrackData>;
auto dbPutHash(const uint64_t& hash, TrackId i) -> void;
auto dbGetHash(const uint64_t& hash) -> std::optional<TrackId>;
auto dbPutTrack(TrackId id, const std::string& path, const uint64_t& hash)
-> void;
template <typename T>
@ -128,9 +130,9 @@ class Database {
};
template <>
auto Database::ParseRecord<Song>(const leveldb::Slice& key,
const leveldb::Slice& val)
-> std::optional<Song>;
auto Database::ParseRecord<Track>(const leveldb::Slice& key,
const leveldb::Slice& val)
-> std::optional<Track>;
template <>
auto Database::ParseRecord<std::string>(const leveldb::Slice& key,
const leveldb::Slice& val)

@ -18,7 +18,7 @@
namespace leveldb {
extern std::weak_ptr<tasks::Worker> sBackgroundThread;
extern std::shared_ptr<tasks::Worker> sBackgroundThread;
// Tracks the files locked by EspEnv::LockFile().
//

@ -13,7 +13,7 @@
#include "leveldb/db.h"
#include "leveldb/slice.h"
#include "song.hpp"
#include "track.hpp"
namespace database {
@ -31,49 +31,49 @@ class OwningSlice {
};
/*
* Returns the prefix added to every SongData key. This can be used to iterate
* Returns the prefix added to every TrackData key. This can be used to iterate
* over every data record in the database.
*/
auto CreateDataPrefix() -> OwningSlice;
/* Creates a data key for a song with the specified id. */
auto CreateDataKey(const SongId& id) -> OwningSlice;
/* Creates a data key for a track with the specified id. */
auto CreateDataKey(const TrackId& id) -> OwningSlice;
/*
* Encodes a SongData instance into bytes, in preparation for storing it within
* Encodes a TrackData instance into bytes, in preparation for storing it within
* the database. This encoding is consistent, and will remain stable over time.
*/
auto CreateDataValue(const SongData& song) -> OwningSlice;
auto CreateDataValue(const TrackData& track) -> OwningSlice;
/*
* Parses bytes previously encoded via CreateDataValue back into a SongData. May
* return nullopt if parsing fails.
* Parses bytes previously encoded via CreateDataValue back into a TrackData.
* May return nullopt if parsing fails.
*/
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData>;
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<TrackData>;
/* Creates a hash key for the specified hash. */
auto CreateHashKey(const uint64_t& hash) -> OwningSlice;
/*
* Encodes a hash value (at this point just a song id) into bytes, in
* Encodes a hash value (at this point just a track id) into bytes, in
* preparation for storing within the database. This encoding is consistent, and
* will remain stable over time.
*/
auto CreateHashValue(SongId id) -> OwningSlice;
auto CreateHashValue(TrackId id) -> OwningSlice;
/*
* Parses bytes previously encoded via CreateHashValue back into a song id. May
* Parses bytes previously encoded via CreateHashValue back into a track id. May
* return nullopt if parsing fails.
*/
auto ParseHashValue(const leveldb::Slice&) -> std::optional<SongId>;
auto ParseHashValue(const leveldb::Slice&) -> std::optional<TrackId>;
/* Encodes a SongId as bytes. */
auto SongIdToBytes(SongId id) -> OwningSlice;
/* Encodes a TrackId as bytes. */
auto TrackIdToBytes(TrackId id) -> OwningSlice;
/*
* Converts a song id encoded via SongIdToBytes back into a SongId. May return
* nullopt if parsing fails.
* Converts a track id encoded via TrackIdToBytes back into a TrackId. May
* return nullopt if parsing fails.
*/
auto BytesToSongId(const std::string& bytes) -> std::optional<SongId>;
auto BytesToTrackId(const std::string& bytes) -> std::optional<TrackId>;
} // namespace database

@ -1,166 +0,0 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <stdint.h>
#include <optional>
#include <string>
#include <utility>
#include "leveldb/db.h"
#include "span.hpp"
namespace database {
/*
* Uniquely describes a single song within the database. This value will be
* consistent across database updates, and should ideally (but is not guaranteed
* to) endure even across a song being removed and re-added.
*
* Four billion songs should be enough for anybody.
*/
typedef uint32_t SongId;
/*
* Audio file encodings that we are aware of. Used to select an appropriate
* decoder at play time.
*
* Values of this enum are persisted in this database, so it is probably never a
* good idea to change the int representation of an existing value.
*/
enum class Encoding {
kUnsupported = 0,
kMp3 = 1,
kWav = 2,
kOgg = 3,
kFlac = 4,
};
/*
* Owning container for tag-related song metadata that was extracted from a
* file.
*/
struct SongTags {
Encoding encoding;
std::optional<std::string> title;
// TODO(jacqueline): It would be nice to use shared_ptr's for the artist and
// album, since there's likely a fair number of duplicates for each
// (especially the former).
std::optional<std::string> artist;
std::optional<std::string> album;
std::optional<int> channels;
std::optional<int> sample_rate;
std::optional<int> bits_per_sample;
/*
* Returns a hash of the 'identifying' tags of this song. That is, a hash that
* can be used to determine if one song is likely the same as another, across
* things like re-encoding, re-mastering, or moving the underlying file.
*/
auto Hash() const -> uint64_t;
bool operator==(const SongTags&) const = default;
};
/*
* Immutable owning container for all of the metadata we store for a particular
* song. This includes two main kinds of metadata:
* 1. static(ish) attributes, such as the id, path on disk, hash of the tags
* 2. dynamic attributes, such as the number of times this song has been
* played.
*
* Because a SongData is immutable, it is thread safe but will not reflect any
* changes to the dynamic attributes that may happen after it was obtained.
*
* Songs may be 'tombstoned'; this indicates that the song is no longer present
* at its previous location on disk, and we do not have any existing files with
* a matching tags_hash. When this is the case, we ignore this SongData for most
* purposes. We keep the entry in our database so that we can properly restore
* dynamic attributes (such as play count) if the song later re-appears on disk.
*/
class SongData {
private:
const SongId id_;
const std::string filepath_;
const uint64_t tags_hash_;
const uint32_t play_count_;
const bool is_tombstoned_;
public:
/* Constructor used when adding new songs to the database. */
SongData(SongId id, const std::string& path, uint64_t hash)
: id_(id),
filepath_(path),
tags_hash_(hash),
play_count_(0),
is_tombstoned_(false) {}
SongData(SongId id,
const std::string& path,
uint64_t hash,
uint32_t play_count,
bool is_tombstoned)
: id_(id),
filepath_(path),
tags_hash_(hash),
play_count_(play_count),
is_tombstoned_(is_tombstoned) {}
auto id() const -> SongId { return id_; }
auto filepath() const -> std::string { return filepath_; }
auto play_count() const -> uint32_t { return play_count_; }
auto tags_hash() const -> uint64_t { return tags_hash_; }
auto is_tombstoned() const -> bool { return is_tombstoned_; }
auto UpdateHash(uint64_t new_hash) const -> SongData;
/*
* Marks this song data as a 'tombstone'. Tombstoned songs are not playable,
* and should not generally be shown to users.
*/
auto Entomb() const -> SongData;
/*
* Clears the tombstone bit of this song, and updates the path to reflect its
* new location.
*/
auto Exhume(const std::string& new_path) const -> SongData;
bool operator==(const SongData&) const = default;
};
/*
* Immutable and owning combination of a song's tags and metadata.
*
* Note that instances of this class may have a fairly large memory impact, due
* to the large number of strings they own. Prefer to query the database again
* (which has its own caching layer), rather than retaining Song instances for a
* long time.
*/
class Song {
public:
Song(const SongData& data, const SongTags& tags) : data_(data), tags_(tags) {}
Song(const Song& other) = default;
auto data() const -> const SongData& { return data_; }
auto tags() const -> const SongTags& { return tags_; }
bool operator==(const Song&) const = default;
Song operator=(const Song& other) const { return Song(other); }
private:
const SongData data_;
const SongTags tags_;
};
void swap(Song& first, Song& second);
} // namespace database

@ -8,20 +8,20 @@
#include <string>
#include "song.hpp"
#include "track.hpp"
namespace database {
class ITagParser {
public:
virtual ~ITagParser() {}
virtual auto ReadAndParseTags(const std::string& path, SongTags* out)
virtual auto ReadAndParseTags(const std::string& path, TrackTags* out)
-> bool = 0;
};
class TagParserImpl : public ITagParser {
public:
virtual auto ReadAndParseTags(const std::string& path, SongTags* out)
virtual auto ReadAndParseTags(const std::string& path, TrackTags* out)
-> bool override;
};

@ -0,0 +1,169 @@
/*
* Copyright 2023 jacqueline <me@jacqueline.id.au>
*
* SPDX-License-Identifier: GPL-3.0-only
*/
#pragma once
#include <stdint.h>
#include <optional>
#include <string>
#include <utility>
#include "leveldb/db.h"
#include "span.hpp"
namespace database {
/*
* Uniquely describes a single track within the database. This value will be
* consistent across database updates, and should ideally (but is not guaranteed
* to) endure even across a track being removed and re-added.
*
* Four billion tracks should be enough for anybody.
*/
typedef uint32_t TrackId;
/*
* Audio file encodings that we are aware of. Used to select an appropriate
* decoder at play time.
*
* Values of this enum are persisted in this database, so it is probably never a
* good idea to change the int representation of an existing value.
*/
enum class Encoding {
kUnsupported = 0,
kMp3 = 1,
kWav = 2,
kOgg = 3,
kFlac = 4,
};
/*
* Owning container for tag-related track metadata that was extracted from a
* file.
*/
struct TrackTags {
Encoding encoding;
std::optional<std::string> title;
// TODO(jacqueline): It would be nice to use shared_ptr's for the artist and
// album, since there's likely a fair number of duplicates for each
// (especially the former).
std::optional<std::string> artist;
std::optional<std::string> album;
std::optional<int> channels;
std::optional<int> sample_rate;
std::optional<int> bits_per_sample;
/*
* Returns a hash of the 'identifying' tags of this track. That is, a hash
* that can be used to determine if one track is likely the same as another,
* across things like re-encoding, re-mastering, or moving the underlying
* file.
*/
auto Hash() const -> uint64_t;
bool operator==(const TrackTags&) const = default;
};
/*
* Immutable owning container for all of the metadata we store for a particular
* track. This includes two main kinds of metadata:
* 1. static(ish) attributes, such as the id, path on disk, hash of the tags
* 2. dynamic attributes, such as the number of times this track has been
* played.
*
* Because a TrackData is immutable, it is thread safe but will not reflect any
* changes to the dynamic attributes that may happen after it was obtained.
*
* Tracks may be 'tombstoned'; this indicates that the track is no longer
* present at its previous location on disk, and we do not have any existing
* files with a matching tags_hash. When this is the case, we ignore this
* TrackData for most purposes. We keep the entry in our database so that we can
* properly restore dynamic attributes (such as play count) if the track later
* re-appears on disk.
*/
class TrackData {
private:
const TrackId id_;
const std::string filepath_;
const uint64_t tags_hash_;
const uint32_t play_count_;
const bool is_tombstoned_;
public:
/* Constructor used when adding new tracks to the database. */
TrackData(TrackId id, const std::string& path, uint64_t hash)
: id_(id),
filepath_(path),
tags_hash_(hash),
play_count_(0),
is_tombstoned_(false) {}
TrackData(TrackId id,
const std::string& path,
uint64_t hash,
uint32_t play_count,
bool is_tombstoned)
: id_(id),
filepath_(path),
tags_hash_(hash),
play_count_(play_count),
is_tombstoned_(is_tombstoned) {}
auto id() const -> TrackId { return id_; }
auto filepath() const -> std::string { return filepath_; }
auto play_count() const -> uint32_t { return play_count_; }
auto tags_hash() const -> uint64_t { return tags_hash_; }
auto is_tombstoned() const -> bool { return is_tombstoned_; }
auto UpdateHash(uint64_t new_hash) const -> TrackData;
/*
* Marks this track data as a 'tombstone'. Tombstoned tracks are not playable,
* and should not generally be shown to users.
*/
auto Entomb() const -> TrackData;
/*
* Clears the tombstone bit of this track, and updates the path to reflect its
* new location.
*/
auto Exhume(const std::string& new_path) const -> TrackData;
bool operator==(const TrackData&) const = default;
};
/*
* Immutable and owning combination of a track's tags and metadata.
*
* Note that instances of this class may have a fairly large memory impact, due
* to the large number of strings they own. Prefer to query the database again
* (which has its own caching layer), rather than retaining Track instances for
* a long time.
*/
class Track {
public:
Track(const TrackData& data, const TrackTags& tags)
: data_(data), tags_(tags) {}
Track(const Track& other) = default;
auto data() const -> const TrackData& { return data_; }
auto tags() const -> const TrackTags& { return tags_; }
bool operator==(const Track&) const = default;
Track operator=(const Track& other) const { return Track(other); }
private:
const TrackData data_;
const TrackTags tags_;
};
void swap(Track& first, Track& second);
} // namespace database

@ -14,7 +14,7 @@
#include "cbor.h"
#include "esp_log.h"
#include "song.hpp"
#include "track.hpp"
namespace database {
@ -60,14 +60,14 @@ auto CreateDataPrefix() -> OwningSlice {
return OwningSlice({data, 2});
}
auto CreateDataKey(const SongId& id) -> OwningSlice {
auto CreateDataKey(const TrackId& id) -> OwningSlice {
std::ostringstream output;
output.put(kDataPrefix).put(kFieldSeparator);
output << SongIdToBytes(id).data;
output << TrackIdToBytes(id).data;
return OwningSlice(output.str());
}
auto CreateDataValue(const SongData& song) -> OwningSlice {
auto CreateDataValue(const TrackData& track) -> OwningSlice {
uint8_t* buf;
std::size_t buf_len = cbor_encode(&buf, [&](CborEncoder* enc) {
CborEncoder array_encoder;
@ -77,28 +77,28 @@ auto CreateDataValue(const SongData& song) -> OwningSlice {
ESP_LOGE(kTag, "encoding err %u", err);
return;
}
err = cbor_encode_int(&array_encoder, song.id());
err = cbor_encode_int(&array_encoder, track.id());
if (err != CborNoError && err != CborErrorOutOfMemory) {
ESP_LOGE(kTag, "encoding err %u", err);
return;
}
err = cbor_encode_text_string(&array_encoder, song.filepath().c_str(),
song.filepath().size());
err = cbor_encode_text_string(&array_encoder, track.filepath().c_str(),
track.filepath().size());
if (err != CborNoError && err != CborErrorOutOfMemory) {
ESP_LOGE(kTag, "encoding err %u", err);
return;
}
err = cbor_encode_uint(&array_encoder, song.tags_hash());
err = cbor_encode_uint(&array_encoder, track.tags_hash());
if (err != CborNoError && err != CborErrorOutOfMemory) {
ESP_LOGE(kTag, "encoding err %u", err);
return;
}
err = cbor_encode_int(&array_encoder, song.play_count());
err = cbor_encode_int(&array_encoder, track.play_count());
if (err != CborNoError && err != CborErrorOutOfMemory) {
ESP_LOGE(kTag, "encoding err %u", err);
return;
}
err = cbor_encode_boolean(&array_encoder, song.is_tombstoned());
err = cbor_encode_boolean(&array_encoder, track.is_tombstoned());
if (err != CborNoError && err != CborErrorOutOfMemory) {
ESP_LOGE(kTag, "encoding err %u", err);
return;
@ -114,7 +114,7 @@ auto CreateDataValue(const SongData& song) -> OwningSlice {
return OwningSlice(as_str);
}
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> {
auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<TrackData> {
CborParser parser;
CborValue container;
CborError err;
@ -135,7 +135,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> {
if (err != CborNoError) {
return {};
}
SongId id = raw_int;
TrackId id = raw_int;
err = cbor_value_advance(&val);
if (err != CborNoError || !cbor_value_is_text_string(&val)) {
return {};
@ -176,7 +176,7 @@ auto ParseDataValue(const leveldb::Slice& slice) -> std::optional<SongData> {
return {};
}
return SongData(id, path, hash, play_count, is_tombstoned);
return TrackData(id, path, hash, play_count, is_tombstoned);
}
auto CreateHashKey(const uint64_t& hash) -> OwningSlice {
@ -193,15 +193,15 @@ auto CreateHashKey(const uint64_t& hash) -> OwningSlice {
return OwningSlice(output.str());
}
auto ParseHashValue(const leveldb::Slice& slice) -> std::optional<SongId> {
return BytesToSongId(slice.ToString());
auto ParseHashValue(const leveldb::Slice& slice) -> std::optional<TrackId> {
return BytesToTrackId(slice.ToString());
}
auto CreateHashValue(SongId id) -> OwningSlice {
return SongIdToBytes(id);
auto CreateHashValue(TrackId id) -> OwningSlice {
return TrackIdToBytes(id);
}
auto SongIdToBytes(SongId id) -> OwningSlice {
auto TrackIdToBytes(TrackId id) -> OwningSlice {
uint8_t buf[8];
CborEncoder enc;
cbor_encoder_init(&enc, buf, sizeof(buf), 0);
@ -211,7 +211,7 @@ auto SongIdToBytes(SongId id) -> OwningSlice {
return OwningSlice(as_str);
}
auto BytesToSongId(const std::string& bytes) -> std::optional<SongId> {
auto BytesToTrackId(const std::string& bytes) -> std::optional<TrackId> {
CborParser parser;
CborValue val;
cbor_parser_init(reinterpret_cast<const uint8_t*>(bytes.data()), bytes.size(),

@ -17,7 +17,7 @@ namespace libtags {
struct Aux {
FIL file;
FILINFO info;
SongTags* tags;
TrackTags* tags;
};
static int read(Tagctx* ctx, void* buf, int cnt) {
@ -71,8 +71,14 @@ static void toc(Tagctx* ctx, int ms, int offset) {}
static const std::size_t kBufSize = 1024;
static const char* kTag = "TAGS";
auto TagParserImpl::ReadAndParseTags(const std::string& path, SongTags* out)
auto TagParserImpl::ReadAndParseTags(const std::string& path, TrackTags* out)
-> bool {
if (path.ends_with(".m4a")) {
// TODO(jacqueline): Re-enabled once libtags is fixed.
ESP_LOGW(kTag, "skipping m4a %s", path.c_str());
return false;
}
libtags::Aux aux;
aux.tags = out;
if (f_stat(path.c_str(), &aux.info) != FR_OK ||
@ -96,6 +102,7 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, SongTags* out)
if (res != 0) {
// Parsing failed.
ESP_LOGE(kTag, "tag parsing failed, reason %d", res);
return false;
}
@ -103,6 +110,15 @@ auto TagParserImpl::ReadAndParseTags(const std::string& path, SongTags* out)
case Fmp3:
out->encoding = Encoding::kMp3;
break;
case Fogg:
out->encoding = Encoding::kOgg;
break;
case Fflac:
out->encoding = Encoding::kFlac;
break;
case Fwav:
out->encoding = Encoding::kWav;
break;
default:
out->encoding = Encoding::kUnsupported;
}

@ -18,41 +18,41 @@
#include "file_gatherer.hpp"
#include "i2c_fixture.hpp"
#include "leveldb/db.h"
#include "song.hpp"
#include "spi_fixture.hpp"
#include "tag_parser.hpp"
#include "track.hpp"
namespace database {
class TestBackends : public IFileGatherer, public ITagParser {
public:
std::map<std::string, SongTags> songs;
std::map<std::string, TrackTags> tracks;
auto MakeSong(const std::string& path, const std::string& title) -> void {
SongTags tags;
auto MakeTrack(const std::string& path, const std::string& title) -> void {
TrackTags tags;
tags.encoding = Encoding::kMp3;
tags.title = title;
songs[path] = tags;
tracks[path] = tags;
}
auto FindFiles(const std::string& root,
std::function<void(const std::string&)> cb) -> void override {
for (auto keyval : songs) {
for (auto keyval : tracks) {
std::invoke(cb, keyval.first);
}
}
auto ReadAndParseTags(const std::string& path, SongTags* out)
auto ReadAndParseTags(const std::string& path, TrackTags* out)
-> bool override {
if (songs.contains(path)) {
*out = songs.at(path);
if (tracks.contains(path)) {
*out = tracks.at(path);
return true;
}
return false;
}
};
TEST_CASE("song database", "[integration]") {
TEST_CASE("track database", "[integration]") {
I2CFixture i2c;
SpiFixture spi;
drivers::DriverCache drivers;
@ -60,104 +60,104 @@ TEST_CASE("song database", "[integration]") {
Database::Destroy();
TestBackends songs;
auto open_res = Database::Open(&songs, &songs);
TestBackends tracks;
auto open_res = Database::Open(&tracks, &tracks);
REQUIRE(open_res.has_value());
std::unique_ptr<Database> db(open_res.value());
SECTION("empty database") {
std::unique_ptr<Result<Song>> res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> res(db->GetTracks(10).get());
REQUIRE(res->values().size() == 0);
}
SECTION("add new songs") {
songs.MakeSong("song1.mp3", "Song 1");
songs.MakeSong("song2.wav", "Song 2");
songs.MakeSong("song3.exe", "Song 3");
SECTION("add new tracks") {
tracks.MakeTrack("track1.mp3", "Track 1");
tracks.MakeTrack("track2.wav", "Track 2");
tracks.MakeTrack("track3.exe", "Track 3");
db->Update();
std::unique_ptr<Result<Song>> res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> res(db->GetTracks(10).get());
REQUIRE(res->values().size() == 3);
CHECK(*res->values().at(0).tags().title == "Song 1");
CHECK(*res->values().at(0).tags().title == "Track 1");
CHECK(res->values().at(0).data().id() == 1);
CHECK(*res->values().at(1).tags().title == "Song 2");
CHECK(*res->values().at(1).tags().title == "Track 2");
CHECK(res->values().at(1).data().id() == 2);
CHECK(*res->values().at(2).tags().title == "Song 3");
CHECK(*res->values().at(2).tags().title == "Track 3");
CHECK(res->values().at(2).data().id() == 3);
SECTION("update with no filesystem changes") {
db->Update();
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
REQUIRE(new_res->values().size() == 3);
CHECK(res->values().at(0) == new_res->values().at(0));
CHECK(res->values().at(1) == new_res->values().at(1));
CHECK(res->values().at(2) == new_res->values().at(2));
}
SECTION("update with all songs gone") {
songs.songs.clear();
SECTION("update with all tracks gone") {
tracks.tracks.clear();
db->Update();
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
CHECK(new_res->values().size() == 0);
SECTION("update with one song returned") {
songs.MakeSong("song2.wav", "Song 2");
SECTION("update with one track returned") {
tracks.MakeTrack("track2.wav", "Track 2");
db->Update();
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
REQUIRE(new_res->values().size() == 1);
CHECK(res->values().at(1) == new_res->values().at(0));
}
}
SECTION("update with one song gone") {
songs.songs.erase("song2.wav");
SECTION("update with one track gone") {
tracks.tracks.erase("track2.wav");
db->Update();
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
REQUIRE(new_res->values().size() == 2);
CHECK(res->values().at(0) == new_res->values().at(0));
CHECK(res->values().at(2) == new_res->values().at(1));
}
SECTION("update with tags changed") {
songs.MakeSong("song3.exe", "The Song 3");
tracks.MakeTrack("track3.exe", "The Track 3");
db->Update();
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
REQUIRE(new_res->values().size() == 3);
CHECK(res->values().at(0) == new_res->values().at(0));
CHECK(res->values().at(1) == new_res->values().at(1));
CHECK(*new_res->values().at(2).tags().title == "The Song 3");
CHECK(*new_res->values().at(2).tags().title == "The Track 3");
// The id should not have changed, since this was just a tag update.
CHECK(res->values().at(2).data().id() ==
new_res->values().at(2).data().id());
}
SECTION("update with one new song") {
songs.MakeSong("my song.midi", "Song 1 (nightcore remix)");
SECTION("update with one new track") {
tracks.MakeTrack("my track.midi", "Track 1 (nightcore remix)");
db->Update();
std::unique_ptr<Result<Song>> new_res(db->GetSongs(10).get());
std::unique_ptr<Result<Track>> new_res(db->GetTracks(10).get());
REQUIRE(new_res->values().size() == 4);
CHECK(res->values().at(0) == new_res->values().at(0));
CHECK(res->values().at(1) == new_res->values().at(1));
CHECK(res->values().at(2) == new_res->values().at(2));
CHECK(*new_res->values().at(3).tags().title ==
"Song 1 (nightcore remix)");
"Track 1 (nightcore remix)");
CHECK(new_res->values().at(3).data().id() == 4);
}
SECTION("get songs with pagination") {
std::unique_ptr<Result<Song>> res(db->GetSongs(1).get());
SECTION("get tracks with pagination") {
std::unique_ptr<Result<Track>> res(db->GetTracks(1).get());
REQUIRE(res->values().size() == 1);
CHECK(res->values().at(0).data().id() == 1);

@ -25,9 +25,9 @@ std::string ToHex(const std::string& s) {
namespace database {
TEST_CASE("database record encoding", "[unit]") {
SECTION("song id to bytes") {
SongId id = 1234678;
OwningSlice as_bytes = SongIdToBytes(id);
SECTION("track id to bytes") {
TrackId id = 1234678;
OwningSlice as_bytes = TrackIdToBytes(id);
SECTION("encodes correctly") {
// Purposefully a brittle test, since we need to be very careful about
@ -44,18 +44,18 @@ TEST_CASE("database record encoding", "[unit]") {
}
SECTION("round-trips") {
CHECK(*BytesToSongId(as_bytes.data) == id);
CHECK(*BytesToTrackId(as_bytes.data) == id);
}
SECTION("encodes compactly") {
OwningSlice small_id = SongIdToBytes(1);
OwningSlice large_id = SongIdToBytes(999999);
OwningSlice small_id = TrackIdToBytes(1);
OwningSlice large_id = TrackIdToBytes(999999);
CHECK(small_id.data.size() < large_id.data.size());
}
SECTION("decoding rejects garbage") {
std::optional<SongId> res = BytesToSongId("i'm gay");
std::optional<TrackId> res = BytesToTrackId("i'm gay");
CHECK(res.has_value() == false);
}
@ -73,7 +73,7 @@ TEST_CASE("database record encoding", "[unit]") {
}
SECTION("data values") {
SongData data(123, "/some/path.mp3", 0xACAB, 69, true);
TrackData data(123, "/some/path.mp3", 0xACAB, 69, true);
OwningSlice enc = CreateDataValue(data);
@ -109,7 +109,7 @@ TEST_CASE("database record encoding", "[unit]") {
}
SECTION("decoding rejects garbage") {
std::optional<SongData> res = ParseDataValue("hi!");
std::optional<TrackData> res = ParseDataValue("hi!");
CHECK(res.has_value() == false);
}
@ -129,14 +129,14 @@ TEST_CASE("database record encoding", "[unit]") {
SECTION("hash values") {
OwningSlice val = CreateHashValue(123456);
CHECK(val.data == SongIdToBytes(123456).data);
CHECK(val.data == TrackIdToBytes(123456).data);
SECTION("round-trips") {
CHECK(ParseHashValue(val.slice) == 123456);
}
SECTION("decoding rejects garbage") {
std::optional<SongId> res = ParseHashValue("the first song :)");
std::optional<TrackId> res = ParseHashValue("the first track :)");
CHECK(res.has_value() == false);
}

@ -4,7 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "song.hpp"
#include "track.hpp"
#include <komihash.h>
@ -19,8 +19,8 @@ auto HashString(komihash_stream_t* stream, std::string str) -> void {
* Uses a komihash stream to incrementally hash tags. This lowers the function's
* memory footprint a little so that it's safe to call from any stack.
*/
auto SongTags::Hash() const -> uint64_t {
// TODO(jacqueline): this function doesn't work very well for songs with no
auto TrackTags::Hash() const -> uint64_t {
// TODO(jacqueline): this function doesn't work very well for tracks with no
// tags at all.
komihash_stream_t stream;
komihash_stream_init(&stream, 0);
@ -30,20 +30,20 @@ auto SongTags::Hash() const -> uint64_t {
return komihash_stream_final(&stream);
}
auto SongData::UpdateHash(uint64_t new_hash) const -> SongData {
return SongData(id_, filepath_, new_hash, play_count_, is_tombstoned_);
auto TrackData::UpdateHash(uint64_t new_hash) const -> TrackData {
return TrackData(id_, filepath_, new_hash, play_count_, is_tombstoned_);
}
auto SongData::Entomb() const -> SongData {
return SongData(id_, filepath_, tags_hash_, play_count_, true);
auto TrackData::Entomb() const -> TrackData {
return TrackData(id_, filepath_, tags_hash_, play_count_, true);
}
auto SongData::Exhume(const std::string& new_path) const -> SongData {
return SongData(id_, new_path, tags_hash_, play_count_, false);
auto TrackData::Exhume(const std::string& new_path) const -> TrackData {
return TrackData(id_, new_path, tags_hash_, play_count_, false);
}
void swap(Song& first, Song& second) {
Song temp = first;
void swap(Track& first, Track& second) {
Track temp = first;
first = second;
second = temp;
}

@ -38,7 +38,7 @@ Samd::Samd() {
.read(&raw_res, I2C_MASTER_NACK)
.stop();
ESP_LOGI(kTag, "checking samd firmware rev");
ESP_ERROR_CHECK(transaction.Execute());
transaction.Execute();
ESP_LOGI(kTag, "samd firmware: %u", raw_res);
}
Samd::~Samd() {}
@ -53,7 +53,7 @@ auto Samd::ReadChargeStatus() -> std::optional<ChargeStatus> {
.read(&raw_res, I2C_MASTER_NACK)
.stop();
ESP_LOGI(kTag, "checking charge status");
ESP_ERROR_CHECK(transaction.Execute());
transaction.Execute();
ESP_LOGI(kTag, "raw charge status: %x", raw_res);
uint8_t usb_state = raw_res & 0b11;
@ -83,7 +83,7 @@ auto Samd::WriteAllowUsbMsc(bool is_allowed) -> void {
.write_addr(kAddress, I2C_MASTER_WRITE)
.write_ack(kRegisterUsbMsc, is_allowed)
.stop();
ESP_ERROR_CHECK(transaction.Execute());
transaction.Execute();
}
auto Samd::ReadUsbMscStatus() -> UsbMscStatus {

@ -65,7 +65,7 @@ void TouchWheel::WriteRegister(uint8_t reg, uint8_t val) {
.write_addr(kTouchWheelAddress, I2C_MASTER_WRITE)
.write_ack(reg, val)
.stop();
ESP_ERROR_CHECK(transaction.Execute());
transaction.Execute();
}
uint8_t TouchWheel::ReadRegister(uint8_t reg) {
@ -78,7 +78,7 @@ uint8_t TouchWheel::ReadRegister(uint8_t reg) {
.write_addr(kTouchWheelAddress, I2C_MASTER_READ)
.read(&res, I2C_MASTER_NACK)
.stop();
ESP_ERROR_CHECK(transaction.Execute());
transaction.Execute();
return res;
}

@ -27,8 +27,6 @@ namespace states {
static const char kTag[] = "BOOT";
console::AppConsole* Booting::sAppConsole;
auto Booting::entry() -> void {
ESP_LOGI(kTag, "beginning tangara boot");
ESP_LOGI(kTag, "installing early drivers");
@ -78,7 +76,7 @@ auto Booting::entry() -> void {
auto Booting::exit() -> void {
// TODO(jacqueline): Gate this on something. Debug flag? Flashing mode?
sAppConsole = new console::AppConsole(sDatabase);
sAppConsole = new console::AppConsole();
sAppConsole->Launch();
}

@ -6,6 +6,9 @@
#pragma once
#include <memory>
#include "database.hpp"
#include "tinyfsm.hpp"
namespace system_fsm {
@ -38,7 +41,9 @@ struct StorageUnmountRequested : tinyfsm::Event {};
/*
* Sent by SysState when the system storage has been successfully mounted.
*/
struct StorageMounted : tinyfsm::Event {};
struct StorageMounted : tinyfsm::Event {
std::weak_ptr<database::Database> db;
};
struct StorageError : tinyfsm::Event {};

@ -56,6 +56,8 @@ class SystemState : public tinyfsm::Fsm<SystemState> {
static std::shared_ptr<drivers::SdStorage> sStorage;
static std::shared_ptr<drivers::Display> sDisplay;
static std::shared_ptr<database::Database> sDatabase;
static console::AppConsole* sAppConsole;
};
namespace states {
@ -65,9 +67,6 @@ namespace states {
* looks good.
*/
class Booting : public SystemState {
private:
static console::AppConsole* sAppConsole;
public:
void entry() override;
void exit() override;

@ -4,6 +4,7 @@
* SPDX-License-Identifier: GPL-3.0-only
*/
#include "app_console.hpp"
#include "freertos/projdefs.h"
#include "result.hpp"
@ -28,6 +29,7 @@ void Running::entry() {
vTaskDelay(pdMS_TO_TICKS(250));
auto storage_res = drivers::SdStorage::Create(sGpioExpander.get());
if (storage_res.has_error()) {
ESP_LOGW(kTag, "failed to mount!");
events::Dispatch<StorageError, SystemState, audio::AudioState, ui::UiState>(
StorageError());
return;
@ -38,15 +40,17 @@ void Running::entry() {
ESP_LOGI(kTag, "opening database");
auto database_res = database::Database::Open();
if (database_res.has_error()) {
ESP_LOGW(kTag, "failed to open!");
events::Dispatch<StorageError, SystemState, audio::AudioState, ui::UiState>(
StorageError());
return;
}
sDatabase.reset(database_res.value());
console::AppConsole::sDatabase = sDatabase;
ESP_LOGI(kTag, "storage loaded okay");
events::Dispatch<StorageMounted, SystemState, audio::AudioState, ui::UiState>(
StorageMounted());
StorageMounted{.db = sDatabase});
}
void Running::exit() {

@ -20,6 +20,8 @@ std::shared_ptr<drivers::SdStorage> SystemState::sStorage;
std::shared_ptr<drivers::Display> SystemState::sDisplay;
std::shared_ptr<database::Database> SystemState::sDatabase;
console::AppConsole* SystemState::sAppConsole;
void SystemState::react(const FatalError& err) {
if (!is_in_state<states::Error>()) {
transit<states::Error>();

@ -5,7 +5,9 @@
*/
#include "tasks.hpp"
#include <functional>
#include "esp_heap_caps.h"
#include "freertos/FreeRTOS.h"
#include "freertos/portmacro.h"
@ -31,6 +33,10 @@ template <>
auto Name<Type::kDatabase>() -> std::string {
return "DB";
}
template <>
auto Name<Type::kDatabaseBackground>() -> std::string {
return "DB_BG";
}
template <Type t>
auto AllocateStack() -> cpp::span<StackType_t>;
@ -39,7 +45,7 @@ auto AllocateStack() -> cpp::span<StackType_t>;
// amount of stack space.
template <>
auto AllocateStack<Type::kAudio>() -> cpp::span<StackType_t> {
std::size_t size = 32 * 1024;
std::size_t size = 48 * 1024;
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_DEFAULT)),
size};
}
@ -67,6 +73,12 @@ auto AllocateStack<Type::kDatabase>() -> cpp::span<StackType_t> {
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)),
size};
}
template <>
auto AllocateStack<Type::kDatabaseBackground>() -> cpp::span<StackType_t> {
std::size_t size = 256 * 1024;
return {static_cast<StackType_t*>(heap_caps_malloc(size, MALLOC_CAP_SPIRAM)),
size};
}
// 2048 bytes in internal ram
// 302 KiB in external ram.
@ -106,6 +118,10 @@ template <>
auto Priority<Type::kDatabase>() -> UBaseType_t {
return 8;
}
template <>
auto Priority<Type::kDatabaseBackground>() -> UBaseType_t {
return 7;
}
template <Type t>
auto WorkerQueueSize() -> std::size_t;
@ -114,6 +130,10 @@ template <>
auto WorkerQueueSize<Type::kDatabase>() -> std::size_t {
return 8;
}
template <>
auto WorkerQueueSize<Type::kDatabaseBackground>() -> std::size_t {
return 8;
}
template <>
auto WorkerQueueSize<Type::kUiFlush>() -> std::size_t {

@ -36,6 +36,8 @@ enum class Type {
kAudio,
// Task for running database queries.
kDatabase,
// Task for internal database operations
kDatabaseBackground,
};
template <Type t>
@ -102,6 +104,9 @@ class Worker {
}
~Worker();
Worker(const Worker&) = delete;
Worker& operator=(const Worker&) = delete;
};
/* Specialisation of Evaluate for functions that return nothing. */

@ -12,11 +12,13 @@ set(COMPONENTS "")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/catch2")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/cbor")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/komihash")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libfoxenflac")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libmad")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/libtags")
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/span")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/stb_vorbis")
list(APPEND EXTRA_COMPONENT_DIRS "$ENV{PROJ_PATH}/lib/tinyfsm")
include($ENV{IDF_PATH}/tools/cmake/project.cmake)

Loading…
Cancel
Save