diff --git a/.vscode/launch.json b/.vscode/launch.json index bc4953826..408f33698 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,57 +1,93 @@ { - "version": "0.2.0", - "configurations": [ - { - "name": "Launch Tests Locally", - "type": "cppdbg", - "request": "launch", - "program": "${workspaceFolder}/tests/build/bin/libDaisy_gtest", - "stopAtEntry": false, - "cwd": "${workspaceFolder}/tests/build/bin", - "environment": [], - "externalConsole": false, - "logging": { - "engineLogging": false - }, - "preLaunchTask": "build-libDaisy-tests", - "osx": { - "MIMode": "lldb", - }, - "windows": { - "MIMode": "gdb", - } - }, - { - "name": "Debug", - "configFiles": [ - "interface/stlink.cfg", - "target/stm32h7x.cfg" - ], - "cwd": "${workspaceFolder}", - "debuggerArgs": [ - "-d", - "${workspaceRoot}" - ], - // Here's where you can put the path to the program you want to debug: - //"executable": "${workspaceRoot}/examples/SDMMC_HelloWorld/build/SDMMC_HelloWorld.elf", - "executable": "${workspaceRoot}/examples/uart/Dma_Receive/build/Dma_Receive.elf", - "interface": "swd", - "openOCDLaunchCommands": [ - "init", - "reset init", - "gdb_breakpoint_override hard" - ], - "preRestartCommands": [ - "load", - "enable breakpoint", - "monitor reset" - ], - "request": "launch", - "runToEntryPoint": "true", - "servertype": "openocd", - "showDevDebugOutput": "none", - "svdFile": "${workspaceRoot}/.vscode/STM32H750x.svd", - "type": "cortex-debug" - } - ] -} + "version": "0.2.0", + "configurations": [ + { + "name": "Launch Tests Locally", + "type": "cppdbg", + "request": "launch", + "program": "${workspaceFolder}/tests/build/bin/libDaisy_gtest", + "stopAtEntry": false, + "cwd": "${workspaceFolder}/tests/build/bin", + "environment": [], + "externalConsole": false, + "logging": { + "engineLogging": false + }, + "preLaunchTask": "build-libDaisy-tests", + "osx": { + "MIMode": "lldb", + }, + "windows": { + "MIMode": "gdb", + } + }, + { + "name": "Debug", + "configFiles": [ + "interface/stlink.cfg", + "target/stm32h7x.cfg" + ], + "cwd": "${workspaceFolder}", + "debuggerArgs": [ + "-d", + "${workspaceRoot}" + ], + // Here's where you can put the path to the program you want to debug: + //"executable": "${workspaceRoot}/examples/SDMMC_HelloWorld/build/SDMMC_HelloWorld.elf", + "executable": "${workspaceRoot}/examples/uart/Dma_Receive/build/Dma_Receive.elf", + "interface": "swd", + "openOCDLaunchCommands": [ + "init", + "reset init", + "gdb_breakpoint_override hard" + ], + "preRestartCommands": [ + "load", + "enable breakpoint", + "monitor reset" + ], + "request": "launch", + "runToEntryPoint": "true", + "servertype": "openocd", + "showDevDebugOutput": "none", + "svdFile": "${workspaceRoot}/.vscode/STM32H750x.svd", + "type": "cortex-debug" + }, + { + "name": "Debug (J-Link)", + "cwd": "${workspaceRoot}", + "device": "STM32H750IB", + "executable": "${workspaceRoot}/examples/WavPlayer/build/WavPlayer.elf", + "interface": "swd", + "osx": { + "armToolchainPath": "/Applications/ArmGNUToolchain/12.3.rel1/arm-none-eabi/bin", + "serverpath": "/Applications/SEGGER/JLink/JLinkGDBServerCLExe" + }, + // "preLaunchTask": "Build Project", + "request": "launch", + "rttConfig": { + "address": "auto", + "decoders": [ + { + "port": 0.000000, + "type": "console" + } + ], + "enabled": true + }, + "runToEntryPoint": "main", + "serialNumber": "", + "servertype": "jlink", + "svdFile": "${workspaceFolder}/.vscode/STM32H750x.svd", + "type": "cortex-debug", + "liveWatch": { + "enabled": true, + "samplesPerSecond": 4 + }, + "windows": { + "armToolchainPath": "${workspaceFolder}/tools/cortex_m_gcc13_win/bin", + "serverpath": "C:/Program Files/SEGGER/JLink/JLinkGDBServerCL.exe" + } + }, + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 91dffeaa8..29a551f0b 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,95 +1,108 @@ { - "cortex-debug.enableTelemetry": false, - "files.associations": { - "*.tcc": "cpp", - "unordered_map": "cpp", - "fstream": "cpp", - "istream": "cpp", - "numeric": "cpp", - "ostream": "cpp", - "sstream": "cpp", - "array": "cpp", - "atomic": "cpp", - "bit": "cpp", - "cctype": "cpp", - "clocale": "cpp", - "cmath": "cpp", - "cstdarg": "cpp", - "cstddef": "cpp", - "cstdint": "cpp", - "cstdio": "cpp", - "cstdlib": "cpp", - "cstring": "cpp", - "cwchar": "cpp", - "cwctype": "cpp", - "deque": "cpp", - "vector": "cpp", - "exception": "cpp", - "algorithm": "cpp", - "functional": "cpp", - "iterator": "cpp", - "memory": "cpp", - "memory_resource": "cpp", - "optional": "cpp", - "random": "cpp", - "string": "cpp", - "string_view": "cpp", - "system_error": "cpp", - "tuple": "cpp", - "type_traits": "cpp", - "utility": "cpp", - "initializer_list": "cpp", - "iosfwd": "cpp", - "limits": "cpp", - "new": "cpp", - "stdexcept": "cpp", - "streambuf": "cpp", - "typeinfo": "cpp", - "complex": "cpp", - "bitset": "cpp", - "cinttypes": "cpp", - "ctime": "cpp", - "iomanip": "cpp", - "iostream": "cpp", - "any": "cpp", - "chrono": "cpp", - "condition_variable": "cpp", - "forward_list": "cpp", - "list": "cpp", - "map": "cpp", - "set": "cpp", - "unordered_set": "cpp", - "ratio": "cpp", - "regex": "cpp", - "shared_mutex": "cpp", - "variant": "cpp" - }, - "editor.tokenColorCustomizations": { - "textMateRules": [ - { - "scope": "googletest.failed", - "settings": { - "foreground": "#f00" - } - }, - { - "scope": "googletest.passed", - "settings": { - "foreground": "#0f0" - } - }, - { - "scope": "googletest.run", - "settings": { - "foreground": "#0f0" - } - } - ] - }, - "testMate.cpp.test.executables": "tests/build/bin/*", - "cmake.sourceDirectory": [ - "${workspaceFolder}", - "${workspaceFolder}/tests", - ], - "cmake.buildDirectory": "${sourceDirectory}/build", -} + "cortex-debug.enableTelemetry": false, + "files.associations": { + "*.tcc": "cpp", + "unordered_map": "cpp", + "fstream": "cpp", + "istream": "cpp", + "numeric": "cpp", + "ostream": "cpp", + "sstream": "cpp", + "array": "cpp", + "atomic": "cpp", + "bit": "cpp", + "cctype": "cpp", + "clocale": "cpp", + "cmath": "cpp", + "cstdarg": "cpp", + "cstddef": "cpp", + "cstdint": "cpp", + "cstdio": "cpp", + "cstdlib": "cpp", + "cstring": "cpp", + "cwchar": "cpp", + "cwctype": "cpp", + "deque": "cpp", + "vector": "cpp", + "exception": "cpp", + "algorithm": "cpp", + "functional": "cpp", + "iterator": "cpp", + "memory": "cpp", + "memory_resource": "cpp", + "optional": "cpp", + "random": "cpp", + "string": "cpp", + "string_view": "cpp", + "system_error": "cpp", + "tuple": "cpp", + "type_traits": "cpp", + "utility": "cpp", + "initializer_list": "cpp", + "iosfwd": "cpp", + "limits": "cpp", + "new": "cpp", + "stdexcept": "cpp", + "streambuf": "cpp", + "typeinfo": "cpp", + "complex": "cpp", + "bitset": "cpp", + "cinttypes": "cpp", + "ctime": "cpp", + "iomanip": "cpp", + "iostream": "cpp", + "any": "cpp", + "chrono": "cpp", + "condition_variable": "cpp", + "forward_list": "cpp", + "list": "cpp", + "map": "cpp", + "set": "cpp", + "unordered_set": "cpp", + "ratio": "cpp", + "regex": "cpp", + "shared_mutex": "cpp", + "variant": "cpp", + "__bit_reference": "cpp", + "__config": "cpp", + "__hash_table": "cpp", + "__locale": "cpp", + "__node_handle": "cpp", + "__tree": "cpp", + "compare": "cpp", + "__memory": "cpp", + "filesystem": "cpp", + "ios": "cpp", + "locale": "cpp", + "mutex": "cpp", + "__split_buffer": "cpp" + }, + "editor.tokenColorCustomizations": { + "textMateRules": [ + { + "scope": "googletest.failed", + "settings": { + "foreground": "#f00" + } + }, + { + "scope": "googletest.passed", + "settings": { + "foreground": "#0f0" + } + }, + { + "scope": "googletest.run", + "settings": { + "foreground": "#0f0" + } + } + ] + }, + "testMate.cpp.test.executables": "tests/build/bin/*", + "cmake.sourceDirectory": [ + "${workspaceFolder}", + "${workspaceFolder}/tests", + ], + "cmake.buildDirectory": "${sourceDirectory}/build", +} \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index f282769e5..3fc33eb9c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -68,7 +68,6 @@ add_library(daisy STATIC ${MODULE_DIR}/hid/usb_host.cpp ${MODULE_DIR}/hid/usb_midi.cpp ${MODULE_DIR}/hid/usb.cpp - ${MODULE_DIR}/hid/wavplayer.cpp ${MODULE_DIR}/per/adc.cpp ${MODULE_DIR}/per/dac.cpp ${MODULE_DIR}/per/gpio.cpp @@ -115,6 +114,11 @@ target_link_libraries(daisy PUBLIC FatFs ) +# There is a abstract `IReader` interface that allows that can be swappped out +# for other i/o, but libDaisy assumes FatFS will always be present. +# It may make sense for this to move to better facillitate testing, etc. +add_compile_definitions(FILEIO_ENABLE_FATFS_READER) + target_compile_options(daisy PRIVATE -Wall @@ -126,6 +130,7 @@ target_compile_options(daisy PRIVATE $<$:-Wno-missing-attributes> ) + set_target_properties(daisy PROPERTIES CXX_STANDARD 14 CXX_STANDARD_REQUIRED @@ -133,6 +138,7 @@ set_target_properties(daisy PROPERTIES C_STANDARD_REQUIRED ) + # Don't add examples if we're being included in another project if(PROJECT_IS_TOP_LEVEL) add_subdirectory(examples EXCLUDE_FROM_ALL) diff --git a/Makefile b/Makefile index bdfc4f11b..03874ca1c 100644 --- a/Makefile +++ b/Makefile @@ -47,7 +47,6 @@ hid/rgb_led \ hid/switch \ hid/usb \ hid/usb_midi \ -hid/wavplayer \ hid/logger \ hid/usb_host \ per/adc \ @@ -298,6 +297,9 @@ C_DEFS = \ -DDATA_IN_D2_SRAM # ^ added for easy startup access +# File I/O Abstract Interface for FatFS: +C_DEFS += -DFILEIO_ENABLE_FATFS_READER + C_INCLUDES = \ -I$(MODULE_DIR) \ -I$(MODULE_DIR)/sys \ diff --git a/core/Makefile b/core/Makefile index f8266e3e4..2e2864d49 100644 --- a/core/Makefile +++ b/core/Makefile @@ -116,6 +116,7 @@ C_DEFS += \ -DARM_MATH_CM7 \ -DUSE_FULL_LL_DRIVER + # for debugging bootloaded applications ifdef DEBUG C_DEFS += -DDEBUG @@ -171,6 +172,9 @@ C_SOURCES += $(FATFS_SOURCES) endif C_INCLUDES += -I$(FATFS_DIR) +# File I/O Abstract Interface for FatFS: +C_DEFS += -DFILEIO_ENABLE_FATFS_READER + # compile gcc flags ASFLAGS = $(MCU) $(AS_DEFS) $(AS_INCLUDES) $(OPT) -Wall -fdata-sections -ffunction-sections diff --git a/examples/FileTable/CMakeLists.txt b/examples/FileTable/CMakeLists.txt new file mode 100644 index 000000000..68057baa5 --- /dev/null +++ b/examples/FileTable/CMakeLists.txt @@ -0,0 +1,3 @@ +set(FIRMWARE_NAME FileTable) +set(FIRMWARE_SOURCES main.cpp) +include(DaisyProject) diff --git a/examples/FileTable/Makefile b/examples/FileTable/Makefile new file mode 100644 index 000000000..4ce224eb1 --- /dev/null +++ b/examples/FileTable/Makefile @@ -0,0 +1,17 @@ +# Project Name +TARGET = FileTable + +# Sources +CPP_SOURCES = main.cpp + +DEBUG=0 +OPT=-O3 + +USE_FATFS=1 + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/FileTable/main.cpp b/examples/FileTable/main.cpp new file mode 100644 index 000000000..83041f005 --- /dev/null +++ b/examples/FileTable/main.cpp @@ -0,0 +1,82 @@ +/** Simple example of FileTable class + * + * This demonstrates quickly indexing a subset of files on a disk. + */ +#include "daisy_seed.h" + +using namespace daisy; + +static constexpr const size_t kMaxFiles = 32; + +static DaisySeed hw; +static SdmmcHandler sdmmc; +static FatFSInterface fsi; +static FileTable file_table; + +int main(void) +{ + /** Initialize our hardware */ + hw.Init(true); + + /** SD Card / FatFS Interface Init */ + SdmmcHandler::Config sdcfg; + sdcfg.Defaults(); + sdcfg.speed = SdmmcHandler::Speed::STANDARD; + sdcfg.width = SdmmcHandler::BusWidth::BITS_1; + sdmmc.Init(sdcfg); + fsi.Init(FatFSInterface::Config::Media::MEDIA_SD); + f_mount(&fsi.GetSDFileSystem(), "/", 1); + + /** Fill the table with any files found in the root directory + * This will only fill up to the number of files specified + * in the template parameter for the class. + * + * Without a second argument, this will include any files. + */ + file_table.Fill("/"); + + /** Write a log file containing tab separated information about + * each file including the file name, position, size + */ + file_table.WriteLog("file_table-all.txt"); + + /** Empty the table of its contents */ + file_table.Clear(); + + /** Instead, if we supply a second argument, we can limit + * the files to those with a specific sub-string at the + * end (like file extensions). + */ + file_table.Fill("/", ".txt"); + file_table.WriteLog("file_table-text.txt"); + + /** And we'll do the same again for WAV files. */ + file_table.Clear(); + file_table.Fill("/", ".wav"); + file_table.WriteLog("file_table-wav.txt"); + + /** We can loop through the files, checking their sizes */ + size_t largest_size = 0; + int slot = -1; + for (size_t i = 0; i < file_table.GetNumFiles(); i++) { + auto fsize = file_table.GetFileSize(i); + if (fsize > largest_size) { + largest_size = fsize; + slot = i; + } + } + + /** Print this once the device is connected to serial. */ + hw.StartLog(true); + System::Delay(100); + hw.PrintLine("File Info:"); + if (slot >= 0) { + hw.PrintLine("The largest file at %d bytes is: %s", + file_table.GetFileSize(slot), + file_table.GetFileName(slot)); + } else { + hw.PrintLine("No files found.."); + } + + while(1){} +} diff --git a/examples/WavParser/CMakeLists.txt b/examples/WavParser/CMakeLists.txt new file mode 100644 index 000000000..32135dc38 --- /dev/null +++ b/examples/WavParser/CMakeLists.txt @@ -0,0 +1,3 @@ +set(FIRMWARE_NAME WavParser) +set(FIRMWARE_SOURCES main.cpp) +include(DaisyProject) diff --git a/examples/WavParser/Makefile b/examples/WavParser/Makefile new file mode 100644 index 000000000..9b2791676 --- /dev/null +++ b/examples/WavParser/Makefile @@ -0,0 +1,17 @@ +# Project Name +TARGET = WavParser + +# Sources +CPP_SOURCES = main.cpp + +DEBUG=0 +OPT=-O3 + +USE_FATFS=1 + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/WavParser/main.cpp b/examples/WavParser/main.cpp new file mode 100644 index 000000000..aebb64373 --- /dev/null +++ b/examples/WavParser/main.cpp @@ -0,0 +1,90 @@ +/** Simple example of WavParser class + * + * Prints out some information about the WAV files on + * an attached SD card. + * + * To run this: + * 1. Put some WAV files on an SD Card + * (This program will look at the first four it finds). + * 2. Program the Daisy with this example, and the SD Card connected. + * 3. Connect to the Daisy via USB Serial + * 4. A list of the files found with some audio info will be output + */ +#include "daisy_seed.h" + +using namespace daisy; + +static constexpr const size_t kMaxFiles = 4; + +static DaisySeed hw; +static SdmmcHandler sdmmc; +static FatFSInterface fsi; +static FileTable file_table; +static FIL file; + +int main(void) +{ + /** Initialize our hardware */ + hw.Init(true); + hw.StartLog(true); + + /** SD Card / FatFS Interface Init */ + SdmmcHandler::Config sdcfg; + sdcfg.Defaults(); + sdcfg.speed = SdmmcHandler::Speed::STANDARD; + sdcfg.width = SdmmcHandler::BusWidth::BITS_1; + sdmmc.Init(sdcfg); + fsi.Init(FatFSInterface::Config::Media::MEDIA_SD); + f_mount(&fsi.GetSDFileSystem(), "/", 1); + + /** We'll fill up the table with WAV files and + * then parse those, and present some info. + */ + file_table.Fill("/", ".wav"); + + + if(file_table.GetNumFiles() > 0) + { + for(size_t i = 0; i < file_table.GetNumFiles(); i++) + { + auto sta = f_open( + &file, file_table.GetFileName(i), (FA_OPEN_EXISTING | FA_READ)); + if(sta != FR_OK) + { + hw.PrintLine("Could not open: %s", file_table.GetFileName(i)); + continue; + } + FileReader reader(&file); + WavParser parser; + if(!parser.parse(reader)) + { + hw.PrintLine("Error parsing file: %s", + file_table.GetFileName(i)); + continue; + } + + const auto& info = parser.info(); + hw.PrintLine("File Information: %s", file_table.GetFileName(i)); + hw.PrintLine("\tSample Rate:\t%d", info.sampleRate); + hw.PrintLine("\tChannels:\t%d", info.numChannels); + hw.PrintLine("\tBit Depth:\t%d", info.bitsPerSample); + + /** File Duration in seconds */ + size_t dur_samples + = parser.dataSize() + / ((info.bitsPerSample / 8) * info.numChannels); + float dur_seconds = static_cast(dur_samples) + / static_cast(info.sampleRate); + float frac = dur_seconds - static_cast(dur_seconds); + hw.PrintLine("\tDuration (seconds):\t%d.%02d", + static_cast(dur_seconds), + static_cast(frac * 100.f)); + + /** Number of metadata chunks */ + hw.PrintLine("\tMetaData Chunks:\t%d", parser.metadataCount()); + } + } + + + while(1) {} +} diff --git a/examples/WavPlayer/CMakeLists.txt b/examples/WavPlayer/CMakeLists.txt new file mode 100644 index 000000000..2de406d9f --- /dev/null +++ b/examples/WavPlayer/CMakeLists.txt @@ -0,0 +1,3 @@ +set(FIRMWARE_NAME WavPlayer) +set(FIRMWARE_SOURCES main.cpp) +include(DaisyProject) diff --git a/examples/WavPlayer/Makefile b/examples/WavPlayer/Makefile new file mode 100644 index 000000000..759f9e388 --- /dev/null +++ b/examples/WavPlayer/Makefile @@ -0,0 +1,17 @@ +# Project Name +TARGET = WavPlayer + +# Sources +CPP_SOURCES = main.cpp + +DEBUG=0 +OPT=-O3 + +USE_FATFS=1 + +# Library Locations +LIBDAISY_DIR = ../.. + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core +include $(SYSTEM_FILES_DIR)/Makefile diff --git a/examples/WavPlayer/loop.wav b/examples/WavPlayer/loop.wav new file mode 100644 index 000000000..abc5610e9 Binary files /dev/null and b/examples/WavPlayer/loop.wav differ diff --git a/examples/WavPlayer/main.cpp b/examples/WavPlayer/main.cpp new file mode 100644 index 000000000..e3e9070d7 --- /dev/null +++ b/examples/WavPlayer/main.cpp @@ -0,0 +1,87 @@ +/** Simple demonstration of WAV file playback + * + * When the program starts, it will attempt to load, and start looping + * the file, "loop.wav". + * + * The "loop.wav" file used here is included in the repo for convenience. + * The file is a 48kHz stereo, 16-bit sine wave at 440Hz -6dB + * + * Any 16-bit WAV file can be used with this class, but sample-rate + * is not automatically adjusted for. + * + * The included file was created with sox, using the following command: + * sox -n -r 48000 -b 16 -c 2 loop.wav synth 1 sine 440 gain -6 + */ +#include "daisy_seed.h" + +using namespace daisy; + +static constexpr const size_t kTransferSize = 16384; + +static DaisySeed hw; +static SdmmcHandler sdmmc; +static FatFSInterface fsi; +static WavPlayer player; + +void AudioCallback(AudioHandle::InputBuffer in, + AudioHandle::OutputBuffer out, + size_t size) +{ + for(size_t i = 0; i < size; i++) + { + // Fill two channels of data per sample + float samps[2]; + player.Stream(samps, 2); + out[0][i] = samps[0]; + out[1][i] = samps[1]; + } +} + +int main(void) +{ + /** Initialize our hardware */ + hw.Init(true); + + /** The SD Card/FatFS Initialization remains unchanged + * For multiple WavPlayer objects, or playback at + * faster playback speeds or sample rates it is recommended + * to use 4-bit I/O, and as fast a speed as the PCB layout permits. + * + * These settings are minimal for demonstration purposes. + */ + SdmmcHandler::Config sdcfg; + sdcfg.Defaults(); + sdcfg.speed = SdmmcHandler::Speed::STANDARD; + sdcfg.width = SdmmcHandler::BusWidth::BITS_1; + sdmmc.Init(sdcfg); + fsi.Init(FatFSInterface::Config::Media::MEDIA_SD); + f_mount(&fsi.GetSDFileSystem(), "/", 1); + + /** Open Loop.WAV + * And blink very fast if there's a problem + */ + if (player.Init("loop.wav") != WavPlayer::Result::Ok) { + // Error.. + while(true) { + // Blink really fast if there was a problem + hw.SetLed((System::GetNow() & 127) < 63); + } + } + + /** Enable Looping playback of the audio file */ + player.SetLooping(true); + player.SetPlaying(true); + player.Restart(); + + /** Start the Audio */ + hw.StartAudio(AudioCallback); + + while(1) + { + /** Blink Slower in normal operation */ + hw.SetLed((System::GetNow() & 511) < 255); + + /** This does the actual Disk I/O whenever the Audio FIFOs are low */ + player.Prepare(); + } +} diff --git a/src/daisy.h b/src/daisy.h index 0d07e8163..26c23b426 100644 --- a/src/daisy.h +++ b/src/daisy.h @@ -41,7 +41,6 @@ #include "hid/disp/color_display.h" #include "hid/disp/oled_color_display.h" #include "hid/disp/graphics_common.h" -#include "hid/wavplayer.h" #include "hid/led.h" #include "hid/rgb_led.h" #include "dev/sr_595.h" @@ -66,6 +65,8 @@ #include "ui/FullScreenItemMenu.h" #include "util/scopedirqblocker.h" #include "util/CpuLoadMeter.h" +#include "util/FileReader.h" +#include "util/FileTable.h" #include "util/FIFO.h" #include "util/FixedCapStr.h" #include "util/MappedValue.h" @@ -73,6 +74,8 @@ #include "util/Stack.h" #include "util/VoctCalibration.h" #include "util/WaveTableLoader.h" +#include "util/WavParser.h" +#include "util/WavPlayer.h" #include "util/WavWriter.h" #endif #endif diff --git a/src/hid/wavplayer.cpp b/src/hid/wavplayer.cpp deleted file mode 100644 index 7082d10b2..000000000 --- a/src/hid/wavplayer.cpp +++ /dev/null @@ -1,166 +0,0 @@ -#include -#include "hid/wavplayer.h" - -using namespace daisy; - -void WavPlayer::Init(const char *search_path) -{ - // First check for all .wav files, and add them to the list until its full or there are no more. - // Only checks '/' - FRESULT result = FR_OK; - FILINFO fno; - DIR dir; - char * fn; - file_sel_ = 0; - file_cnt_ = 0; - playing_ = true; - looping_ = false; - // Open Dir and scan for files. - if(f_opendir(&dir, search_path) != FR_OK) - { - return; - } - do - { - result = f_readdir(&dir, &fno); - // Exit if bad read or NULL fname - if(result != FR_OK || fno.fname[0] == 0) - break; - // Skip if its a directory or a hidden file. - if(fno.fattrib & (AM_HID | AM_DIR)) - continue; - // Now we'll check if its .wav and add to the list. - fn = fno.fname; - if(file_cnt_ < kMaxFiles - 1) - { - if(strstr(fn, ".wav") || strstr(fn, ".WAV")) - { - strcpy(file_info_[file_cnt_].name, search_path); - strcat(file_info_[file_cnt_].name, fn); - file_cnt_++; - // For now lets break anyway to test. - // break; - } - } - else - { - break; - } - } while(result == FR_OK); - f_closedir(&dir); - // Now we'll go through each file and load the WavInfo. - for(size_t i = 0; i < file_cnt_; i++) - { - size_t bytesread; - if(f_open(&fil_, file_info_[i].name, (FA_OPEN_EXISTING | FA_READ)) - == FR_OK) - { - // Populate the WAV Info - if(f_read(&fil_, - (void *)&file_info_[i].raw_data, - sizeof(WAV_FormatTypeDef), - &bytesread) - != FR_OK) - { - // Maybe add return type - return; - } - f_close(&fil_); - } - } - // fill buffer with first file preemptively. - buff_state_ = BUFFER_STATE_PREPARE_0; - Open(0); - Prepare(); - read_ptr_ = 0; -} - - -int WavPlayer::Open(size_t sel) -{ - if(sel != file_sel_) - { - f_close(&fil_); - file_sel_ = sel < file_cnt_ ? sel : file_cnt_ - 1; - } - // Set Buffer Position - return f_open( - &fil_, file_info_[file_sel_].name, (FA_OPEN_EXISTING | FA_READ)); -} - -int WavPlayer::Close() -{ - return f_close(&fil_); -} - -int16_t WavPlayer::Stream() -{ - int16_t samp; - if(playing_) - { - samp = buff_[read_ptr_]; - // Increment rpo - read_ptr_ = (read_ptr_ + 1) % kBufferSize; - if(read_ptr_ == 0) - buff_state_ = BUFFER_STATE_PREPARE_1; - else if(read_ptr_ == kBufferSize / 2) - buff_state_ = BUFFER_STATE_PREPARE_0; - } - else - { - samp = 0; - if(looping_) - playing_ = true; - } - return samp; -} - -void WavPlayer::Prepare() -{ - if(buff_state_ != BUFFER_STATE_IDLE) - { - size_t offset, bytesread, rxsize; - bytesread = 0; - rxsize = (kBufferSize / 2) * sizeof(buff_[0]); - offset = buff_state_ == BUFFER_STATE_PREPARE_1 ? kBufferSize / 2 : 0; - f_read(&fil_, &buff_[offset], rxsize, &bytesread); - if(bytesread < rxsize || f_eof(&fil_)) - { - if(looping_) - { - Restart(); - f_read(&fil_, - &buff_[offset + (bytesread / 2)], - rxsize - bytesread, - &bytesread); - } - else - { - playing_ = false; - } - } - buff_state_ = BUFFER_STATE_IDLE; - } -} - -void WavPlayer::Restart() -{ - playing_ = true; - f_lseek(&fil_, - sizeof(WAV_FormatTypeDef) - + file_info_[file_sel_].raw_data.SubChunk1Size); -} - -WavPlayer::BufferState WavPlayer::GetNextBuffState() -{ - size_t next_samp; - next_samp = (read_ptr_ + 1) % kBufferSize; - if(next_samp < kBufferSize / 2) - { - return BUFFER_STATE_PREPARE_1; - } - else - { - return BUFFER_STATE_PREPARE_0; - } -} diff --git a/src/hid/wavplayer.h b/src/hid/wavplayer.h deleted file mode 100644 index 481949d72..000000000 --- a/src/hid/wavplayer.h +++ /dev/null @@ -1,102 +0,0 @@ -/* Current Limitations: -- 1x Playback speed only -- 16-bit, mono files only (otherwise fun weirdness can happen). -- Only 1 file playing back at a time. -- Not sure how this would interfere with trying to use the SDCard/FatFs outside of -this module. However, by using the extern'd SDFile, etc. I think that would break things. -*/ -#pragma once -#ifndef DSY_WAVPLAYER_H -#define DSY_WAVPLAYER_H /**< Macro */ -#include "daisy_core.h" -#include "util/wav_format.h" -#include "ff.h" - -#define WAV_FILENAME_MAX \ - 256 /**< Maximum LFN (set to same in FatFs (ffconf.h) */ - -namespace daisy -{ -// TODO: add bitrate, samplerate, length, etc. -/** Struct containing details of Wav File. */ -struct WavFileInfo -{ - WAV_FormatTypeDef raw_data; /**< Raw wav data */ - char name[WAV_FILENAME_MAX]; /**< Wav filename */ -}; - -/* -TODO: -- Make template-y to reduce memory usage. -*/ - - -/** Wav Player that will load .wav files from an SD Card, -and then provide a method of accessing the samples with -double-buffering. */ -class WavPlayer -{ - public: - WavPlayer() {} - ~WavPlayer() {} - - /** Initializes the WavPlayer, loading up to max_files of wav files from an SD Card. */ - void Init(const char* search_path); - - /** Opens the file at index sel for reading. - \param sel File to open - */ - int Open(size_t sel); - - /** Closes whatever file is currently open. - \return & - */ - int Close(); - - /** \return The next sample if playing, otherwise returns 0 */ - int16_t Stream(); - - /** Collects buffer for playback when needed. */ - void Prepare(); - - /** Resets the playback position to the beginning of the file immediately */ - void Restart(); - - /** Sets whether or not the current file will repeat after completing playback. - \param loop To loop or not to loop. - */ - inline void SetLooping(bool loop) { looping_ = loop; } - - /** \return Whether the WavPlayer is looping or not. */ - inline bool GetLooping() const { return looping_; } - - /** \return The number of files loaded by the WavPlayer */ - inline size_t GetNumberFiles() const { return file_cnt_; } - - /** \return currently selected file.*/ - inline size_t GetCurrentFile() const { return file_sel_; } - - private: - enum BufferState - { - BUFFER_STATE_IDLE, - BUFFER_STATE_PREPARE_0, - BUFFER_STATE_PREPARE_1, - }; - - BufferState GetNextBuffState(); - - static constexpr size_t kMaxFiles = 8; - static constexpr size_t kBufferSize = 4096; - WavFileInfo file_info_[kMaxFiles]; - size_t file_cnt_, file_sel_; - BufferState buff_state_; - int16_t buff_[kBufferSize]; - size_t read_ptr_; - bool looping_, playing_; - FIL fil_; -}; - -} // namespace daisy - -#endif diff --git a/src/util/FIFO.h b/src/util/FIFO.h index e44208014..21544a313 100644 --- a/src/util/FIFO.h +++ b/src/util/FIFO.h @@ -198,7 +198,7 @@ class FIFOBase return true; } - /** removes the element "idx" positions behind the first element + /** removes the element "idx" positions behind the first element * and returns true if successful */ bool Remove(size_t idx) { diff --git a/src/util/FileReader.h b/src/util/FileReader.h new file mode 100644 index 000000000..a3ee896bc --- /dev/null +++ b/src/util/FileReader.h @@ -0,0 +1,105 @@ +#pragma once + +#ifdef FILEIO_ENABLE_FATFS_READER +#include "ff.h" +#endif +#ifdef FILEIO_ENABLE_CSTDIO_READER +#include +#endif + +namespace daisy +{ +// Abstract reader interface (minimal). Provide concrete implementation for +// platform. +class IReader +{ + public: + virtual ~IReader() = default; + virtual size_t read(void* dst, + size_t bytes) + = 0; // returns bytes actually read + virtual bool seek(uint32_t pos) = 0; // absolute seek from start + virtual uint32_t position() const = 0; // current absolute position + virtual uint32_t size() const = 0; // total size if known (0 if unknown) +}; + +#ifdef FILEIO_ENABLE_FATFS_READER +class FileReader : public IReader +{ + public: + explicit FileReader(FIL* f) : f_(f) + { + if(f_) + { + size_ = static_cast(f_size(f_)); + f_rewind(f_); + } + } + size_t read(void* dst, size_t bytes) override + { + /** This doesn't report any errors that may occur w/ the + * Filesystem.. that maybe something we want to add.. + */ + UINT br = 0; + f_read(f_, dst, bytes, &br); + return br; + } + bool seek(uint32_t pos) override + { + return f_lseek(f_, static_cast(pos)) == FR_OK; + } + uint32_t position() const override + { + long p = f_tell(f_); + return p < 0 ? 0u : static_cast(p); + } + uint32_t size() const override { return size_; } + + private: + FIL* f_ = nullptr; + uint32_t size_ = 0; +}; +#endif + +#ifdef FILEIO_ENABLE_CSTDIO_READER +class FileReader : public IReader +{ + public: + explicit FileReader(FILE* f) : f_(f) + { + if(f_) + { + long pos = ftell(f_); + if(pos < 0) + pos = 0; + if(fseek(f_, 0, SEEK_END) == 0) + { + long end = ftell(f_); + if(end > 0) + size_ = static_cast(end); + } + fseek(f_, pos, SEEK_SET); + } + } + size_t read(void* dst, size_t bytes) override + { + return fread(dst, 1, bytes, f_); + } + bool seek(uint32_t pos) override + { + return fseek(f_, static_cast(pos), SEEK_SET) == 0; + } + uint32_t position() const override + { + long p = ftell(f_); + return p < 0 ? 0u : static_cast(p); + } + uint32_t size() const override { return size_; } + + private: + FILE* f_ = nullptr; + uint32_t size_ = 0; +}; +#endif + +} // namespace daisy \ No newline at end of file diff --git a/src/util/FileTable.h b/src/util/FileTable.h new file mode 100644 index 000000000..67a64db7d --- /dev/null +++ b/src/util/FileTable.h @@ -0,0 +1,244 @@ +#pragma once +#pragma once +#include +#include + +#include "ff.h" + +namespace daisy +{ +/** Utility class for creating an index of file names + * and lengths from a given directory. + * + * Useful for grouping .wav files for playback, etc. + */ +template +class FileTable +{ + public: + static constexpr const size_t kMaxCustomFileNameLen = _MAX_LFN; + static constexpr const size_t kMaxFileSlots = max_slots; + + /** Reset the table to its initial, empty state. */ + void Clear() + { + for(auto &item : table) + { + std::fill(item.name, item.name + kMaxCustomFileNameLen, 0x00); + item.size = 0; + } + num_files_found = 0; + } + + /** Search the path provided, and fill the table with files that match the pattern provided. + * The loaded files are sorted alphabetically by filename + * + * @param path path to the directory to search. + * + * @param endswith string suffix to compare to, often a file extension (i.e. ".wav") + * if this is null, then all files will be loaded. + * + * @return true if files are loaded, otherwise false + */ + bool Fill(const char *path, const char *endswith = nullptr) + { + FRESULT res = FR_OK; + if(path == nullptr) + { + return false; + } + FILINFO fno; + res = f_opendir(&dir, path); + size_t cnt = 0; + if(res == FR_OK) + { + for(;;) + { + res = f_readdir(&dir, &fno); + if(fno.fname[0] == 0) + break; //< escape w/ no file + bool valid_file_attrs = !(fno.fattrib & AM_HID) + && !(fno.fattrib & AM_DIR) + && !(fno.fattrib & AM_SYS); + + bool valid_size = fno.fsize > 0; + + bool valid_name = false; + if(endswith != nullptr) + { + uint32_t suffix_len = strlen(endswith); + valid_name = strstr(fno.fname, endswith) != nullptr + && strlen(fno.fname) > suffix_len + && strlen(fno.fname) < kMaxCustomFileNameLen; + } + else + { + valid_name = strlen(fno.fname) < kMaxCustomFileNameLen; + } + + if(valid_file_attrs && valid_size && valid_name) + { + // Copy this file into the slot of the table, and increment + strcpy(table[cnt].name, fno.fname); + table[cnt].size = fno.fsize; + cnt++; + } + if(cnt > kMaxFileSlots - 1) + break; + } + f_closedir(&dir); + } + num_files_found = cnt; + SortTable(); + return res == FR_OK; + } + + /** Generates a simple log file, and writes it to the destination + * + * The file will contain a list of all of the files loaded, with their + * slot position, and file size. + */ + bool WriteLog(const char *log_file_name) + { + FRESULT res + = f_open(&file, log_file_name, (FA_CREATE_ALWAYS | FA_WRITE)); + if(res == FR_OK) + { + if(num_files_found > 0) + { + for(size_t i = 0; i < num_files_found; i++) + { + char line_buff[kMaxCustomFileNameLen + 32]; + std::fill(line_buff, line_buff + sizeof(line_buff), 0x00); + sprintf(line_buff, + "%d:\t%s\t%d bytes\n", + i + 1, + table[i].name, + table[i].size); + UINT bw = 0; + res = f_write(&file, line_buff, strlen(line_buff), &bw); + if(res != FR_OK) + { + return f_close(&file) == FR_OK; + } + } + } + else + { + const char *text = "No matching files found..."; + UINT bw = 0; + res = f_write(&file, text, strlen(text), &bw); + } + f_close(&file); + } + return res == FR_OK; + } + + /** Returns whether there is a file present at the index */ + inline bool IsFileInSlot(size_t idx) const { return table[idx].size > 0; } + + /** Returns the size of the file in the specified slot. */ + inline size_t GetFileSize(size_t idx) const { return table[idx].size; } + + /** Returns the name of the file in the specified slot. */ + inline const char *GetFileName(size_t idx) const { return table[idx].name; } + + /** Returns the number of files found in the table. */ + inline size_t GetNumFiles() const { return num_files_found; } + + /** Flag-y bits for Loading and Saving + * This class can act like an interface for the actual I/O. + * + * This has to be manually managed, but can be used to coordinate + * loading/storing data. + */ + + inline bool IsLoadPending() const { return load_pending_; } + + inline void ClearLoadPending() + { + load_pending_ = false; + slot_for_load_save_ = -1; + } + inline void SetLoadPending(int slot) + { + load_pending_ = true; + slot_for_load_save_ = slot; + } + + inline bool IsSavePending() const { return save_pending_; } + + inline void ClearSavePending() + { + save_pending_ = false; + slot_for_load_save_ = -1; + } + inline void SetSavePending(int slot) + { + save_pending_ = true; + slot_for_load_save_ = slot; + } + + inline int GetSlotForSaveLoad() const { return slot_for_load_save_; } + + private: + struct FileInfo + { + char name[kMaxCustomFileNameLen]; + size_t size; + }; + + /** Sorts the table by each FileInfo's name member. */ + void SortTable() + { + if(num_files_found < 2) + return; + + for(size_t i = 1; i < num_files_found; ++i) + { + FileInfo key = table[i]; + size_t j = i; + while(j > 0 && CaseInsensitiveCmp(table[j - 1].name, key.name) > 0) + { + table[j] = table[j - 1]; + --j; + } + table[j] = key; + } + } + + FIL file; + DIR dir; + FileInfo table[kMaxFileSlots]; + size_t num_files_found; + + // Flags for saving/loading of files (not the best place for these) + // but this saves adding more back-and-forth between the new UiPage + // and the actual diskio + bool load_pending_; + bool save_pending_; + int slot_for_load_save_; + + // Internal helper for sorting files on to a known order. + static inline int CaseInsensitiveCmp(const char *a, const char *b) + { + // ASCII-only case fold adequate for FAT volume typical usage + unsigned char ca, cb; + while(*a && *b) + { + ca = static_cast(*a); + cb = static_cast(*b); + ca = static_cast(std::tolower(ca)); + cb = static_cast(std::tolower(cb)); + if(ca != cb) + return (ca < cb) ? -1 : 1; + ++a; + ++b; + } + if(*a == *b) + return 0; + return (*a == '\0') ? -1 : 1; + } +}; + +} // namespace daisy \ No newline at end of file diff --git a/src/util/WavParser.h b/src/util/WavParser.h new file mode 100644 index 000000000..037910d9f --- /dev/null +++ b/src/util/WavParser.h @@ -0,0 +1,303 @@ +#pragma once + +// Minimal, allocation-free WAV (RIFF) parser suitable for embedded (e.g., +// STM32). Supports canonical PCM / IEEE float WAV, handles JUNK and unknown +// chunks by skipping. +// Does not load sample data; records data offset & length +// so caller can stream. +// +// Limitations / Assumptions: +// - Little-endian host or platform where manual LE decoding is used (safe on +// STM32/Cortex-M). +// - No dynamic allocation; fixed maximum number of metadata entries. +// - Ignores extensible format extra fields beyond what is necessary for basic +// parsing. +// - Caller provides an abstract Reader (seek + read) so this can work with +// FatFS, cstdio, raw data, etc. +// +// Typical usage: +// FileReader fr(fopen("file.wav", "rb")); +// WavParser parser; +// if(parser.parse(fr)) { +// // Use parser.info() to get format, sampleRate, etc. +// // Use parser.dataOffset(), parser.dataSize() to stream audio. +// } +// + +#include +#include +#include "FileReader.h" + +namespace daisy +{ +struct WavFormatInfo +{ + uint16_t audioFormat = 0; // 1 = PCM, 3 = IEEE float, 0xFFFE = extensible + uint16_t numChannels = 0; + uint32_t sampleRate = 0; + uint32_t byteRate = 0; + uint16_t blockAlign = 0; + uint16_t bitsPerSample = 0; + // For extensible (0xFFFE) + uint16_t validBitsPerSample = 0; // if provided + uint32_t channelMask = 0; // if provided + uint16_t subFormat = 0; // wFormatTag of the sub-format GUID (first 2 bytes) +}; + +struct MetadataEntry +{ + uint32_t fourcc = 0; // chunk id + uint32_t size = 0; // payload size (before padding) + uint32_t offset = 0; // file offset of chunk data +}; + +// Utility to form a FourCC constant at compile time: FCC("RIFF") not constexpr +// in pre-C++20 easily. +constexpr uint32_t make_fourcc(char a, char b, char c, char d) +{ + return (uint32_t(uint8_t(a))) | (uint32_t(uint8_t(b)) << 8) + | (uint32_t(uint8_t(c)) << 16) | (uint32_t(uint8_t(d)) << 24); +} + + +class WavParser +{ + public: + static constexpr uint32_t FOURCC_RIFF = make_fourcc('R', 'I', 'F', 'F'); + static constexpr uint32_t FOURCC_WAVE = make_fourcc('W', 'A', 'V', 'E'); + static constexpr uint32_t FOURCC_FMT = make_fourcc('f', 'm', 't', ' '); + static constexpr uint32_t FOURCC_DATA = make_fourcc('d', 'a', 't', 'a'); + static constexpr uint32_t FOURCC_JUNK = make_fourcc('J', 'U', 'N', 'K'); + static constexpr uint32_t FOURCC_FACT = make_fourcc('f', 'a', 'c', 't'); + static constexpr uint32_t FOURCC_LIST = make_fourcc('L', 'I', 'S', 'T'); + static constexpr uint32_t FOURCC_INFO = make_fourcc('I', 'N', 'F', 'O'); + + static constexpr int MAX_METADATA_CHUNKS = 16; // tunable + + WavParser() = default; + + bool parse(IReader& r) + { + reset(); + if(!read_riff_header(r)) + return false; + while(r.position() + 8 <= fileSize_) + { + ChunkHeader ch; + if(!read_chunk_header(r, ch)) + return false; + if(ch.id == FOURCC_FMT) + { + if(!parse_fmt_chunk(r, ch)) + return false; + } + else if(ch.id == FOURCC_DATA) + { + dataOffset_ = r.position(); + dataSize_ = ch.size; + // Skip data (we only record offset). Allow early break if we've got + // fmt. + if(!skip_chunk_payload(r, ch.size)) + return false; + haveData_ = true; + } + else + { + // Store metadata if room + if(metadataCount_ < MAX_METADATA_CHUNKS) + { + metadata_[metadataCount_].fourcc = ch.id; + metadata_[metadataCount_].size = ch.size; + metadata_[metadataCount_].offset = r.position(); + metadataCount_++; + } + if(!skip_chunk_payload(r, ch.size)) + return false; + } + + // Chunks are padded to even size + if(ch.size & 1) + { + uint8_t pad; + if(r.read(&pad, 1) != 1) + break; + } + + if(haveFmt_ && haveData_) + break; // parsed what we need + } + return haveFmt_ && haveData_; + } + + const WavFormatInfo& info() const { return fmt_; } + uint32_t dataOffset() const { return dataOffset_; } + uint32_t dataSize() const { return dataSize_; } + const MetadataEntry* metadata() const { return metadata_; } + int metadataCount() const { return metadataCount_; } + + private: + struct ChunkHeader + { + uint32_t id; + uint32_t size; + }; + + void reset() + { + fmt_ = WavFormatInfo{}; + haveFmt_ = false; + haveData_ = false; + dataOffset_ = 0; + dataSize_ = 0; + metadataCount_ = 0; + fileSize_ = 0; + } + + static uint16_t rd_u16(const uint8_t* b) + { + return uint16_t(b[0]) | (uint16_t(b[1]) << 8); + } + static uint32_t rd_u32(const uint8_t* b) + { + return uint32_t(b[0]) | (uint32_t(b[1]) << 8) | (uint32_t(b[2]) << 16) + | (uint32_t(b[3]) << 24); + } + + bool read_exact(IReader& r, void* dst, size_t n) + { + return r.read(dst, n) == n; + } + + bool read_riff_header(IReader& r) + { + uint8_t hdr[12]; + if(!read_exact(r, hdr, 12)) + return false; + uint32_t riff = rd_u32(hdr + 0); + uint32_t fileSizeMinus8 = rd_u32(hdr + 4); // size of file - 8 + uint32_t wave = rd_u32(hdr + 8); + if(riff != FOURCC_RIFF || wave != FOURCC_WAVE) + return false; + fileSize_ = fileSizeMinus8 + 8; // nominal + if(r.size() != 0) + fileSize_ = r.size(); // trust reader if known + return true; + } + + bool read_chunk_header(IReader& r, ChunkHeader& ch) + { + uint8_t buf[8]; + if(!read_exact(r, buf, 8)) + return false; + ch.id = rd_u32(buf); + ch.size = rd_u32(buf + 4); + return true; + } + + bool skip_chunk_payload(IReader& r, uint32_t sz) + { + // Seek ahead instead of reading to avoid buffer. + uint32_t target = r.position() + sz; + return r.seek(target); + } + + bool parse_fmt_chunk(IReader& r, const ChunkHeader& ch) + { + if(ch.size < 16) + return false; + uint8_t core[16]; + if(!read_exact(r, core, 16)) + return false; + fmt_.audioFormat = rd_u16(core + 0); + fmt_.numChannels = rd_u16(core + 2); + fmt_.sampleRate = rd_u32(core + 4); + fmt_.byteRate = rd_u32(core + 8); + fmt_.blockAlign = rd_u16(core + 12); + fmt_.bitsPerSample = rd_u16(core + 14); + uint32_t consumed = 16; + + if(fmt_.audioFormat != 1 && fmt_.audioFormat != 3 + && fmt_.audioFormat != 0xFFFE) + { + // unsupported basic format + skip_rest_of_chunk(r, ch, consumed); + return false; + } + + if(ch.size > consumed) + { + // Read the remaining bytes (small), up to a cap we care about + uint32_t remain = ch.size - consumed; + // We'll process extension for extensible + if(fmt_.audioFormat == 0xFFFE && remain >= 2) + { + uint8_t extSizeBuf[2]; + if(!read_exact(r, extSizeBuf, 2)) + return false; + consumed += 2; + uint16_t extSize = rd_u16(extSizeBuf); + if(extSize >= 22 && remain >= 2 + 22) + { // extensible has at least 22 bytes after cbSize + uint8_t ext[22]; + if(!read_exact(r, ext, 22)) + return false; + consumed += 22; + fmt_.validBitsPerSample = rd_u16(ext + 0); + fmt_.channelMask = rd_u32(ext + 2); + fmt_.subFormat = rd_u16( + ext + + 6); // first two bytes of GUID contain the actual format tag + // skip any rest of ext + if(extSize > 22) + { + uint32_t skip = extSize - 22; + if(!skip_bytes(r, skip)) + return false; + consumed += skip; + } + } + else + { + // skip remainder if not long enough + if(!skip_bytes(r, remain - 2)) + return false; // we already read extSizeBuf + consumed = ch.size; // consumed all + } + } + else + { + // skip any unneeded extended bytes for PCM / float + if(!skip_bytes(r, remain)) + return false; + consumed = ch.size; + } + } + haveFmt_ = true; + return true; + } + + bool skip_bytes(IReader& r, uint32_t count) + { + uint32_t target = r.position() + count; + return r.seek(target); + } + + bool + skip_rest_of_chunk(IReader& r, const ChunkHeader& ch, uint32_t consumed) + { + if(consumed < ch.size) + return skip_bytes(r, ch.size - consumed); + return true; + } + + WavFormatInfo fmt_{}; + bool haveFmt_ = false; + bool haveData_ = false; + uint32_t dataOffset_ = 0; + uint32_t dataSize_ = 0; + MetadataEntry metadata_[MAX_METADATA_CHUNKS]; + int metadataCount_ = 0; + uint32_t fileSize_ = 0; +}; + +} // namespace daisy diff --git a/src/util/WavPlayer.h b/src/util/WavPlayer.h new file mode 100644 index 000000000..da2598d31 --- /dev/null +++ b/src/util/WavPlayer.h @@ -0,0 +1,558 @@ +#pragma once + +#include "daisy.h" +#include "ff.h" +#include "WavParser.h" +#include "FileReader.h" + +namespace daisy +{ +/** + * WAV file Streaming Playback + * + * At this time, this class only supports streaming of 16-bit WAV Files + * The output of this class will be in float converted from 16-bit integers + * and linearly interpolated for non-integer playback speeds. + * + * Due to the implementation, reverse playback is not possible with this class. + * + * The workspace_bytes template parameter is used to set the size in bytes of + * audio samples within the FIFO. + * + * The bulk of amount of memory used by this class is approximately: + * (2 * workspace_bytes); + * This could hypothetically be reduced by half by directly accessing the + * FIFO's inner array (requires modifications to FIFO class), or making + * a new type of queue data structure + * + * Whenever the Stream function results in a the samples FIFO being less than 75% + * full, it will generate a request for new data. So the average disk i/o + * transaction will be the `workspace_bytes` / 4. However, + * There are times, like when restarting playback, or opening a different file, + * that will trigger the entire buffer to be filled. + */ +template +class WavPlayer +{ + public: + WavPlayer() {} + ~WavPlayer() {} + + /** Return values for status, and errors. */ + enum class Result + { + Ok, + FileNotFoundError, + PlaybackUnderrun, + PrepareOverrun, + NewSamplesRequested, + DiskError, + }; + + /** Relevent audio file data for playback details */ + struct FileInfo + { + size_t channels, length, samplerate, data_start; + size_t data_size_bytes; + }; + + /** Initialize, and open a single file by name for playback */ + Result Init(const char* name) + { + /** Open the file */ + auto res = Open(name); + if(res != Result::Ok) + return res; + + for(size_t i = 0; i < kMaxAudioChannels; i++) + { + current_sample_[i] = 0.f; + previous_sample_[i] = 0.f; + } + pos_acc_ = 0.f; + playback_speed_ = 1.f; + looping_ = false; + playing_ = false; + + return Result::Ok; + } + + /** Open a file, and prepare audio for streaming */ + Result Open(const char* name) + { + if(is_open_) + f_close(&file_); + auto sta = f_open(&file_, name, (FA_OPEN_EXISTING | FA_READ)); + switch(sta) + { + case FR_OK: break; + case FR_NO_FILE: + case FR_NO_PATH: return Result::FileNotFoundError; + default: return Result::DiskError; + } + is_open_ = true; + + daisy::FileReader reader(&file_); + daisy::WavParser parser; + if(!parser.parse(reader)) + return Result::DiskError; + const auto& info = parser.info(); + file_info_.channels = info.numChannels; + file_info_.samplerate = info.sampleRate; + auto bd = info.bitsPerSample; + file_info_.data_size_bytes = parser.dataSize(); + file_info_.length + = file_info_.data_size_bytes / ((bd / 8) * file_info_.channels); + file_info_.data_start = parser.dataOffset(); + + // Compute frame size (bytes per sample frame) + frame_bytes_ = (file_info_.channels) * (bd / 8); + if(frame_bytes_ == 0) + return Result::DiskError; + + // Seek to start of data + if(f_lseek(&file_, file_info_.data_start) != FR_OK) + return Result::DiskError; + + // Prime FIFO with frame-aligned read + std::fill(buff_raw_, buff_raw_ + kRxSizeSamples, 0); + UINT bytes_read = 0; + size_t bytes_to_read + = std::min((size_t)workspace_bytes, file_info_.data_size_bytes); + // Align down to full frames + bytes_to_read -= (bytes_to_read % frame_bytes_); + if(bytes_to_read > 0) + { + if(f_read(&file_, (void*)buff_raw_, bytes_to_read, &bytes_read) + != FR_OK) + { + f_close(&file_); + return Result::DiskError; + } + if(bytes_read != bytes_to_read) + { + f_close(&file_); + return Result::DiskError; + } + } + + buff_fifo_.Clear(); + size_t samps_to_write = bytes_read / sizeof(int16_t); + // Align push count to whole frames (multiples of channels) + if(file_info_.channels > 0) + samps_to_write -= (samps_to_write % file_info_.channels); + for(size_t i = 0; i < samps_to_write; i++) + { + if(!buff_fifo_.PushBack(buff_raw_[i])) + break; + } + + position_ = 0; + pending_read_req_ = false; + pending_seek_req_ = false; + bytes_left_in_chunk_ = (bytes_read <= file_info_.data_size_bytes) + ? (file_info_.data_size_bytes - bytes_read) + : 0; + + return Result::Ok; + } + + + /** Close a file, and clear the data */ + Result Close() + { + f_close(&file_); + file_info_.channels = 0; + file_info_.data_start = 0; + file_info_.length = 0; + file_info_.samplerate = 0; + is_open_ = false; + playing_ = false; + return Result::Ok; + } + + /** To be executed in the main while loop, or other interruptable areas of code. + * This will perform the actual Disk I/O for streaming audio into the buffers + * used for playback. + */ + Result Prepare() + { + while(!request_fifo_.IsEmpty()) + { + auto req = request_fifo_.PopFront(); + switch(req.type) + { + case IoRequest::Type::Read: + { + size_t bytes_requested = req.data * sizeof(int16_t); + // Align to full frames + bytes_requested -= (bytes_requested % frame_bytes_); + if(bytes_requested == 0) + { + pending_read_req_ = false; + break; + } + + UINT total_bytes_read = 0; + UINT bytes_read = 0; + + std::fill(buff_raw_, buff_raw_ + kRxSizeSamples, 0); + + // Read up to end of data chunk + size_t first_span + = std::min(bytes_requested, bytes_left_in_chunk_); + // Align first_span to frame boundary as well + first_span -= (first_span % frame_bytes_); + if(first_span > 0) + { + if(f_read(&file_, + (void*)buff_raw_, + first_span, + &bytes_read) + != FR_OK) + return Result::DiskError; + if(bytes_read != first_span) + return Result::DiskError; + total_bytes_read += bytes_read; + bytes_left_in_chunk_ -= bytes_read; + } + + // If need more and looping, wrap and read more (frame-aligned) + if(total_bytes_read < bytes_requested && looping_) + { + if(f_lseek(&file_, file_info_.data_start) != FR_OK) + return Result::DiskError; + + size_t remaining_bytes + = bytes_requested - total_bytes_read; + // Align remaining as well (it already is, but keep consistent) + remaining_bytes -= (remaining_bytes % frame_bytes_); + if(remaining_bytes > 0) + { + UINT bytes_read2 = 0; + char* tbuff + = ((char*)(buff_raw_) + total_bytes_read); + // Do not exceed the chunk size on wrap + size_t span2 = std::min(remaining_bytes, + file_info_.data_size_bytes); + // Align span2 + span2 -= (span2 % frame_bytes_); + if(span2 > 0) + { + if(f_read(&file_, + (void*)tbuff, + span2, + &bytes_read2) + != FR_OK) + return Result::DiskError; + if(bytes_read2 != span2) + return Result::DiskError; + total_bytes_read += bytes_read2; + bytes_left_in_chunk_ + = (bytes_read2 + <= file_info_.data_size_bytes) + ? (file_info_.data_size_bytes + - bytes_read2) + : 0; + } + } + } + + // Push into FIFO; align to full frames + size_t samps_to_write = total_bytes_read / sizeof(int16_t); + if(file_info_.channels > 0) + samps_to_write + -= (samps_to_write % file_info_.channels); + + for(size_t i = 0; i < samps_to_write; i++) + { + if(!buff_fifo_.PushBack(buff_raw_[i])) + { + pending_read_req_ = false; + return Result::PrepareOverrun; + } + } + pending_read_req_ = false; + } + break; + case IoRequest::Type::Seek: + { + size_t dest_bytes = req.data * sizeof(int16_t); + // Clamp and align to frame boundary + if(dest_bytes > file_info_.data_size_bytes) + dest_bytes = file_info_.data_size_bytes; + dest_bytes -= (dest_bytes % frame_bytes_); + + if(f_lseek(&file_, file_info_.data_start + dest_bytes) + != FR_OK) + return Result::DiskError; + + bytes_left_in_chunk_ + = file_info_.data_size_bytes - dest_bytes; + pending_seek_req_ = false; + } + break; + default: break; + } + } + return Result::Ok; + } + + /** Stream Audio from disk at the current playback speed. + * + * Each call to this will increment the playback position's + * internal accumulator by the playback speed. + * Anytime this accumulator exceeds 1.0, it will update it's + * position tracker, and pop the next sample from the FIFO of + * audio samples. + * Whenever the contents of the audio sample FIFO fall below + * 75% of it's capacity, a request is generated to refill it. + * The maximum playback speed possible is limited to the following + * factors: + * - SD Card Bus-width + * - SD Card Clock Speed + * - workspace_bytes setting (consequently, transfer sizes) + * + * It is possible to allow higher playback speeds, and improve + * bandwidth by using higher workspace sizes, with the trade-offs + * being memory, and latency with certain transactions. + * + * @param samples buffer of floats to fill with audio samples from disk + * + * @param num_channels number of channels provided to fill. This can be + * different from the number of channels in the file. + */ + Result Stream(float* samples, size_t num_channels) + { + auto channels = file_info_.channels; + + for(size_t i = 0; i < num_channels; i++) + samples[i] = 0.f; + + if(!buff_fifo_.IsEmpty() && playing_) + { + size_t ch_out = std::min(channels, num_channels); + for(size_t i = 0; i < ch_out; i++) + { + samples[i] + = previous_sample_[i] + + pos_acc_ * (current_sample_[i] - previous_sample_[i]); + } + + pos_acc_ += playback_speed_; + while(pos_acc_ >= 1.f) + { + position_ += 1; + pos_acc_ -= 1.f; + for(size_t i = 0; i < channels; i++) + { + previous_sample_[i] = current_sample_[i]; + current_sample_[i] = s162f(buff_fifo_.PopFront()); + } + } + } + + if(position_ >= (file_info_.length > 0 ? file_info_.length : 1)) + { + position_ = 0; + if(!looping_) + { + playing_ = false; + } + else + { + pos_acc_ = 0.f; + for(size_t i = 0; i < kMaxAudioChannels; i++) + previous_sample_[i] = current_sample_[i]; + } + } + + // Request new samples in whole frames + bool requested_new_samps = false; + if(buff_fifo_.GetNumElements() < kRxFifoThreshold && !pending_read_req_) + { + size_t free_slots = (kRxSizeSamples - buff_fifo_.GetNumElements()); + size_t rx_qty = (free_slots > 1) ? (free_slots - 1) : 0; + // Align to multiples of channels + if(file_info_.channels > 0) + rx_qty -= (rx_qty % file_info_.channels); + + if(rx_qty > 0) + { + request_fifo_.PushBack( + IoRequest(IoRequest::Type::Read, rx_qty)); + pending_read_req_ = true; + requested_new_samps = true; + } + } + if(requested_new_samps) + return Result::NewSamplesRequested; + else if(buff_fifo_.IsEmpty() && playing_) + return Result::PlaybackUnderrun; + else + return Result::Ok; + } + + /** Clear all playback samples, and return to the beginning of the audio file immediately */ + void Restart() + { + buff_fifo_.Clear(); + request_fifo_.Clear(); + + pos_acc_ = 0.f; + for(size_t i = 0; i < kMaxAudioChannels; i++) + current_sample_[i] = previous_sample_[i] = 0.f; + + bytes_left_in_chunk_ = file_info_.data_size_bytes; + + request_fifo_.PushBack(IoRequest(IoRequest::Type::Seek, 0)); + + // Request a frame-aligned quantity + size_t req_samps = kRxSizeSamples; + if(file_info_.channels > 0) + req_samps -= (req_samps % file_info_.channels); + if(req_samps > 0) + request_fifo_.PushBack(IoRequest(IoRequest::Type::Read, req_samps)); + + pending_read_req_ = true; + pending_seek_req_ = true; + position_ = 0; + playing_ = true; + } + + /** Return the number of samples in the open audio file */ + inline size_t GetDurationInSamples() const + { + return file_info_.length > 0 ? file_info_.length : 1; + } + + /** Return the number of audio channels in the open audio file */ + inline size_t GetChannels() const { return file_info_.channels; } + + /** Returns the position of the playhead in samples from the start of the file */ + inline uint32_t GetPosition() const { return position_; } + + /** Returns a 0-1 representation of the playhead position within the file. */ + inline float GetNormalizedPosition() const + { + size_t duration = GetDurationInSamples(); + return static_cast(position_) / static_cast(duration); + } + + /** Set whether the audio file will automatically continue playing + * from the beginning after reaching the end of file. + */ + inline void SetLooping(bool state) { looping_ = state; } + + /** Return whether the player is looping or not. */ + inline bool GetLooping() const { return looping_; } + + inline void SetPlaying(bool state) { playing_ = state; } + inline bool GetPlaying() const { return playing_; } + + /** Direct setter of playback speed as a ratio + * compared to original speed. + * For example, 1.0 equals original speed, 0.5 is half-speed, etc. + */ + inline void SetPlaybackSpeedRatio(const float speed) + { + if(speed >= 0.f) + { + playback_speed_ = speed; + } + } + + /** Sets playback speed as a number of semitones offset from original pitch + * For example, +7 a ratio of 1.5, +12 a ratio of 2, -12 a ratio of 0.5, etc. + */ + inline void SetPlaybackSpeedSemitones(const float semitones) + { + playback_speed_ = std::pow(2.f, semitones / 12.f); + } + + private: + /** Request containing necessary data to seek, or refill the + * streaming FIFO of samples + */ + struct IoRequest + { + enum class Type + { + Read, + Seek, + Unknown, + }; + /** type of request to be made */ + Type type; + + /** multi-purpose value for size/position depending on type of request. + * For read requests this will be the qty of samples to request. + * For seek requests this will be the position in samples to jump to. + */ + size_t data; + + /** Constructor for IoRequest: */ + IoRequest(Type t, size_t val) + { + type = t; + data = val; + } + + IoRequest() : type(Type::Unknown), data(0) {} + + ~IoRequest() {} + }; + + /** Number of samples that fit into the workspace */ + static const constexpr size_t kRxSizeSamples + = (workspace_bytes / sizeof(int16_t)); + + /** Threshold at which new samples are requested to fill up the FIFO */ + static const constexpr size_t kRxFifoThreshold = ((kRxSizeSamples / 4) * 3); + + /** Currently maximum support is for stereo files.. */ + static const constexpr size_t kMaxAudioChannels = 8; + + /** Queues and Buffers */ + + /** Primary Request Queue for Disk I/O */ + daisy::FIFO request_fifo_; + + /** Buffer containing the samples requested from disk. */ + daisy::FIFO buff_fifo_; + + /** Intermediate buffer between the Disk I/O and the FIFO + * With modifications to the existing FIFO class, or a specialized + * SampleQueue of some sort, we could remove the requirement + * for this intermediate buffer. + */ + int16_t buff_raw_[kRxSizeSamples]; + + /** File Specific Information */ + + size_t position_; //< position within audio data (in samples) + FileInfo file_info_; //< Info for the currently open file + + /** Playback Parameters */ + bool looping_, playing_; + float playback_speed_; + + /** Converted float sample used for interpolated varispeed playback */ + float current_sample_[kMaxAudioChannels]; + + /** Previous float sample used for interpolated varispeed playback */ + float previous_sample_[kMaxAudioChannels]; + + /** Internal resources */ + + FIL file_; + bool is_open_; + bool pending_read_req_; + bool pending_seek_req_; + float pos_acc_; + size_t bytes_left_in_chunk_; // remaining bytes in WAV data chunk + size_t frame_bytes_; // bytes per sample frame (channels * bytes-per-sample) +}; + + +} // namespace daisy