Skip to content

Conversation

@stephenhensley
Copy link
Collaborator

Summary

The goal of this PR is to provide functional, streaming WAV file playback within libDaisy.
The existing WavPlayer class was not robust enough for most use cases, and was highly susceptible to glitches, streaming artifacts, etc.

In the process of fixing the internal issues with the WavPlayer, I decided some of its built-in behavior was pretty opinionated, and that it would be better to abstract away some of the file selection mechanics (they were unsorted, and only accessible via index, etc.)

So this is a breaking change, but utilities have been added to accomplish the original behavior, and additional quality of life improvements have been added.
Specifically:

  • WavParser is a new, more involved WAV parser that parses other RIFF file chunks used for metadata, provides useful audio file information, and offset/size information for the PCM data, and other meta data sections.
    • The WavParser introduces a new portable, FileReader based on an IReader abstract interface. This allows this small wrapper around the actual I/O to be replaced to allow for desktop environments, locally stored binary data, etc. instead of being fixed to FatFS).
  • FileTable was added to allow the collection of file paths into an array, recreating the original multi-file support, but allowing more flexibility, and use beyond WAV files.

Previoulsy, you might have had:

// Init and Open
player.Init("/");
player.Open(0);

// In audio callback:
int16_t samp = player.Stream();

// In main while()
player.Prepare();

With this set of changes you can do:

// Init and Open
auto result = player.Init("myfile.wav");

// In audio callback:
float samps[2];
auto result = player.Stream(samps, 2);

// In main while()
auto result = player.Prepare();

If you want recreate the previous multi-file support it would look more like:

// Globals:
WavPlayer<4096> player;
FileTable<8> file_table;

// Init and Open
file_table.Fill("/", ".wav");
auto result = player.Init(file_table.GetFileName(0));

Changes

Internally, the entire class was reworked to rely on a FIFO of samples to pull from, with request generation being done when the number of elements remaining in the FIFO falls below a certain threshold.
This provides more flexibility with the timing between needing more samples, and them becoming available. It also opens up the opportunity to centralize these requests in a single manager to better allow for simultaneous playback of multiple files.

All functions return Result type status that can be helpful in error detection/recovery.

The previous implementation would not function properly when metadata, or additional RIFF file chunks were present within the WAV file. With the new WavParser, these factors no longer cause issues, and it is possible to access, and use metadata more easily.

The class now supports multiple audio channels more easily, and due to this Stream function no longer returns a single mono sample. Instead, it takes a pointer to an array of samples to fill, with the number of samples the user wants.
This channel number can be different than that of the file being played back.

The WavPlayer itself has been extended to automatically convert samples to float from int16_t, and provide a simple API for vari-speed playback.

With the combination of multi-channel support, and vari-speed playback, some tighter frame alignment was necessary for avoiding playback discrepancies, especially with files that contain metadata.

The class is now a template class with the buffer size for the internal FIFO as an argument.
The efficiency of the Disk I/O increases with this size, but the latency of Restart, and the memory usage increases with this size as well.

With maximum settings (SD Card set to VERY_FAST, and workspace_bytes = 32kB) streaming of a file at 8x speed, or 3-octaves up) is possible.

Examples

At the time of opening this PR, there is a single WavPlayer example within libDaisy's example directory.
It is designed to work with the Daisy Pod and USB MIDI.
It will load up to 8 files in alphabetical order, opening the first. MIDI Notes will playback the file at different playback speeds.
The encoder will select from the pool of files, SW 1 will pause/resume playback, SW 2 will enable disable looping, which is indicated by the Seed's Built-in LED.

Prior to merging, I will split this example into a few simpler examples that can run standalone without the Daisy Pod encoder/buttons, and add separate examples for the FileTable, and WavParser.

Future Work and Limitations

There are some limitations to the existing class, and opportunities around libDaisy that can be done in future PRs.

Limitations

At this time the class assumes 16-bit audio data, and will not function properly with files of different bit-depths.
Supporting other bit-depths is large enough in scope, that it warrants a separate update.

The class does not take sample rate into account, but this can be separately inspected with WavParser.
It may make sense to cache this internally, and add an optional flag for adjusting playback speed to match the system sample rate.

Playback in reverse is not implemented, and would be substantial more i/o heavy than forward playback.
For the moment, I have left that out. However, it is probably worth adding at some point.

It is possible to run multiple of these. However, there is no shared system for File I/O Requests.
The work of abstracting the IoRequest management into a static class that can manage requests, clear pending-flags, etc. is also a bit more involved than makes sense to tackle right now.

Library Improvements

The addition of the FileReader/IReader abstract interface for File I/O presents the opportunity to extend that, and write some improved testing for classes that rely on disk i/o to function.

The new WavParser is currently only in use within these few objects.
The WavWriter (streaming audio recording), and WaveTableLoader (loading audio data into internal buffers) still use the old fixed, WAV_FormatTypeDef header. The latter is still susceptible to issues with files containing additional metadata.
So it will be worth while to update these classes to use the new parser, and remove the issue-prone fixed-size enum that is currently in use.

@stephenhensley stephenhensley marked this pull request as draft September 11, 2025 22:20
@github-actions
Copy link

github-actions bot commented Sep 11, 2025

Test Results

150 tests  ±0   150 ✅ ±0   0s ⏱️ ±0s
  1 suites ±0     0 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 2c814cf. ± Comparison against base commit c6887c6.

♻️ This comment has been updated with latest results.

@stephenhensley
Copy link
Collaborator Author

stephenhensley commented Sep 11, 2025

There is likely some clean up to still do here. I also have not validated this on bootloader apps, or with the CMake build system.

In addition, the existing example needs to be simplified, and a few more simple examples should be added.

edit: looks like the conditional inclusion of the FatFS FileReader is breaking the build for everything that includes the WavPlayer(i.e. daisy.h) which makes sense. I can just make that always available via the core/Makefile, rather than being gated by USE_FATFS.

@stephenhensley stephenhensley marked this pull request as ready for review September 18, 2025 23:51
@stephenhensley
Copy link
Collaborator Author

Okay, I think this is ready to merge.
I've simplified the bulky WavPlayer example from when the PR was opened, and added a few separate examples for the FileTable and WavParser.

@stephenhensley stephenhensley merged commit ea7f924 into master Sep 19, 2025
15 checks passed
@stephenhensley stephenhensley deleted the wavplayer-fix branch September 19, 2025 14:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants