Yet Another File Player (and recorder)

Outstanding! And yes, the Teensy 4.1 should be way easier to work with, from a software point of view. They can be a bit delicate electronically, so take care. Did you order a Rev D audio adaptor as well? The pinout of the 4.x isn't the same as for 3.x, though you can cobble a 4.x and a Rev C adaptor together if you need to.

Ah, indeed, the SGTL5000 enable click. Very useful for debugging, but a bit annoying in Real Life.

I'm curious as to how apparent the system latency is going to be as you "pile on many more layers" during overdubbing etc. What you hear is a few milliseconds delayed from when it came off the SD card, and there's a delay from the recording input to the SD card write buffer. Probably only about 6-12ms, I think, but it might accumulate. You may need to use the optional startFrom parameter of the playback object to keep things aligned truly tightly. And of course punch-in is a whole 'nother game!
 
Yeah, I realize I've only so far tackled the *easy* problems.

I was thinking along the lines of having a data file for each recording "project" that would list included files and their relative start times to provide playback offsets to be in sync, all played together. How I would determine how different the start times are for each file I don't yet know... A timer...timing...something...haha! I'll (try to) figure it out.

And yes, I got the Rev D audio adaptor too. And 16MB PSRAM. Should kick.."arse" as they say where you are... ha!
 
You will indeed be able to prod some serious buttock with a Teensy 4.1 and 16Mb of PSRAM!

It may be helpful to synchronise your code with the audio updates. At the moment the only way I know of doing that "within the rules" is to have an AudioRecordQueue object connected to some reliable source of input, like the I2S input itself. Things like a mixer are not reliable - if you set their gain to 0.0 then they stop delivering audio data and rely on the downstream objects interpreting this as silence ... which the AudioRecordQueue does by not sending blocks to the application! Anyway, if your code triggers its activity whenever queue.available() goes from 0 to 1, it knows an audio update has just occurred (you must readBuffer() and freeBuffer() the resulting queued data, no need to do anything with it). If you keep your relative start times in terms of audio updates you should be able to keep everything in sync as you pause, rewind, restart and so on.
 
Hmmm. That idea won’t work if an SD card read or write takes a long time (i.e. over 2.9ms), and you had wanted to start another playback or recording in the interval. We might need a “scheduled start” option… I’ll give it some thought.
 
Thanks very much for helping me with this project! I wouldn't have even attempted if if not for your new buffered read/write objects and library improvements.

So there are a couple of synchronization issues: 1) Determining the start time for playback of files recorded at different times and 2) once started, preventing the files from drifting out of sync due to varying SD read times. (I don't know if this would be a problem.) From my earlier tests, I was able to *play* 4 or 5 pre-recorded audio files, just starting them one after the other, and they seemed to start simultaneously and keep in sync while they were playing. I don't know if that was just luck though...
 
No worries. I'm enjoying myself tinkering with the code, and glad it's getting some use so I can be more confident it works as intended.

On the sync issues, #2 should not be a problem - I can't see a way in which files will drift out of sync, if they're in sync to begin with. If it does happen, either my code will detect the buffer over/underrun and stop, or at least there will be a 2.9ms defect which I'd expect to be audible. For #1, I'm going to give it some thought and maybe throw the question open to a wider audience - what's the best / simplest / most consistent way of "doing audio stuff" on a strict timebase? It'll still be up to you to design your recording project file, but some fairly low-level help might be a good plan to support this sort of thing. It should probably be a separate thread - if I start one, I'll post here so you know where to find it.
 
It'll still be up to you to design your recording project file

I was afraid my previous post implied that I was relying on your help throughout the project. Of course not. I'm just very thankful that you've been responsive to my posts and have been willing to think about ways to modify the library to make my project possible. And in so doing making Teensy a more capable audio microcontroller. I appreciate it!

I just received the 4.1 so once I get that set up I'll see if I can increase the track count a bit.
 
So...In trying to squeeze the most I can out of the Teensy 3.6, I had the idea to build up a layer of audio tracks by:
1. Record the first track to say, "track1.wav".
2. Record an overdub track to "track2.wav" while playing back track1.wav. Track1 would be mixed together with track2 during the record process.
3. The next overdub track would record to track 3.wav while playing back track2.wav. (track2 is a mix of track1 and the overdub recording.)
4. ... and so on ...

This works, the files seem to be in sync, and only requires a single AudioPlayWAVstereo object and a single AudioRecordWAVstereo object.

*However* this requires playing back the entirety of the longest track on every overdub.

So my question is, is there any way to perform a non-realtime mix of 2 or more files?

This feature would be useful in general as I think most digital recorders don't require multiple tracks to be played back in realtime in order to produce a final mix.

Thanks as always for your input!
 
There's all sorts of ways, but it's definitely not trivial... the trouble is, as soon as you do a "simple 50-50 mix" to a file, you then want to change the mix ... dynamically through the file ... while adding effects! But for a simple mix it should be OK, the WAV format is fairly straightforward (look at the AudioWAVdata class in AudioBuffer.cpp and .h - there's probably stuff in there you can use).

I was toying with the idea of having a way to disconnect the audio updates from the master clock (e.g. the I²S module) and just run updates as fast as possible. Not simple, but you could then use any of the audio objects, apart from the input and output ones. Most objects just do calculations and have only a rudimentary concept of real time, so if you tell them it's time for an update they just get on with it, whether or not 2.9ms has elapsed since the last one!
 
There's all sorts of ways, but it's definitely not trivial... the trouble is, as soon as you do a "simple 50-50 mix" to a file, you then want to change the mix ... dynamically through the file ... while adding effects! But for a simple mix it should be OK, the WAV format is fairly straightforward (look at the AudioWAVdata class in AudioBuffer.cpp and .h - there's probably stuff in there you can use).

The scenario I described would be for a simple, no effects mix. And realizing the mix is then "frozen". I was just toying with the idea of doing it in the simplest way using the least amount of resources. My ultimate goal is to implement a full featured multitrack recorder that would save each recorded file separately and provide for a way to later open some amount of those files and allow them to be mixed together, having individual volume control and maybe pan and effects as well. Even in this scenario, it would be nice, once happy with a mix, to be able to press a button and have the mix saved to a file, waiting only for processing.

I'll check in those source files and see if I can figure out how to combine (mix) 2 files together with a single function call.

I was toying with the idea of having a way to disconnect the audio updates from the master clock (e.g. the I²S module) and just run updates as fast as possible. Not simple, but you could then use any of the audio objects, apart from the input and output ones. Most objects just do calculations and have only a rudimentary concept of real time, so if you tell them it's time for an update they just get on with it, whether or not 2.9ms has elapsed since the last one!

If I understand that correctly, that would add some nice flexibility and speed things up. I'll check out the source code.

Thanks again!
 
Hello, I've been playing with this library for a looper project I'm working on. Is it possible to preload audio from a file while it is still being recorded? I've had success preloading files from the SD card and they all start in sync as expected. I'd love to be able to preload a tiny bit of audio though while recording, so when it stops recording it will play back instantly.
 
I suspect not at the moment, because the WAV header isn’t written until recording stops. But it’d probably be possible to add a function to write an interim header to the file so preload would work. I’d also need to check that the file open mode doesn’t prevent opening for read while it’s also open for writing. Not sure what the filesystem allows.

I can’t get to this for a couple of days, I’m afraid, but I’ll take a look when I can.
 
I suspect not at the moment, because the WAV header isn’t written until recording stops. But it’d probably be possible to add a function to write an interim header to the file so preload would work. I’d also need to check that the file open mode doesn’t prevent opening for read while it’s also open for writing. Not sure what the filesystem allows.

I can’t get to this for a couple of days, I’m afraid, but I’ll take a look when I can.

Yeah it seems that currently when trying to preload while recording the file path of the preload object is returning null. No worries on time, I really appreciate the work you have put into this!
 
OK, I've pushed a new commit to GitHub, which should allow you to start playback (or pre-load an AudioPreload object) while the file is still being recorded. This includes an example in Examples/Audio/Buffered/PreloadWhileRecording - doesn't need any special hardware, it generates its own audio, though of course you do need an SD card and audio adaptor!

Two new functions are provided to be called from your user application:
  • AudioRecordWAVbuffered::writeCurrentHeader() works on your currently-recording object, to write an interim header
  • AudioPlayWAVbuffered::adjustHeaderInfo() works on a playback object that was started before recording finished; use after recording finishes to prevent playback stopping early because only the interim header was available when it was started
The example file uses both of these. It plays a C major scale while recording it, then seamlessly starts playback, stops recording a bit later, does the adjustHeaderInfo() magic, and keeps looping the playback. I've used two voice objects and two playback, so note starts overlap the release tails during recording, and playback restarts overlap the recorded release tails. Seems to work OK, but obviously this is "hot off the press" so quite likely to have some shortcomings.

Here's the example's design:
2023-08-07 12_50_22-Audio System Design Tool for Teensy Audio Library.png
 
Wow this is great, thank you so much!
I'm testing with a teensy 4.0 and had to change the buffer from inExt to inHeap. My code basically starts recording then after 1 sec I run the new writeCurrentHeader then preload the file into my preload object.
As far as I can tell it is working but I'm curious, what should be printing out when running Serial.printf("Preload returned %d\n",preLoad1.preLoad(scaleFile)); ? I always get "Preload returned 0".
 
You're right, inHeap would be a better choice for the example - I'll change that.

The preload should always return 0 (no error), another value means an error occurred such as an invalid header. So if you get no error, your preload is good to go...
 
Excellent, thanks again for your work! I'll keep playing around with it and report back if I run into any issues. A little background on my project, it's an eight track eurorack looper module that has a synced MIDI clock. Thanks to the buffered playback I'm able to play all eight tracks at once from the SD card.
looper.jpg
 
You’re very welcome, I’m having a blast making this better as time goes on!

Looks like a very cool project, hope we can get a post with video and audio on the blog project section, when you’re ready.
 
Well there is still much work to be done on my project before I'm ready to share anything. It's a bit of a mess currently.

On the preload update, I have found some potential issues for my use case. I'm not sure if I'm just using it incorrectly but here is what I have found.

In the example, if a file doesn't already exist on the SD card called scale.wav when trying to preload, it returns a 4 and doesn't play anything. I'm guessing that is because the file doesn't yet exist? I assumed once you started a recording, even if that file wasn't previously on the SD, it would have been created when the recording started. If I re-upload the example or just power cycle the teensy after that first time it works correctly.

Also, if I do a short recording, then later attempt to do a longer recording using the same file, the first time it plays, it will stop short even though I'm calling adjustHeaderInfo after I stop the recording. I haven't timed it, but it feels like where it stops is about the length of the first shorter recording. The second time it is played, it plays all the way through. It feels like both these issues might be related.

Let me know if that makes sense. I can try to setup a small bit of code to illustrate this if that helps. Thanks again!
 
Well there is still much work to be done on my project before I'm ready to share anything. It's a bit of a mess currently.
Been there, done that!

On the preload update, I have found some potential issues for my use case. I'm not sure if I'm just using it incorrectly but here is what I have found.

In the example, if a file doesn't already exist on the SD card called scale.wav when trying to preload, it returns a 4 and doesn't play anything. I'm guessing that is because the file doesn't yet exist? I assumed once you started a recording, even if that file wasn't previously on the SD, it would have been created when the recording started. If I re-upload the example or just power cycle the teensy after that first time it works correctly.
Correct, you can't preload a file that doesn't exist. When you start a recording it exists, but is empty (for a new file) or contains the previous file contents (for a file that already existed): only when the buffer is half-full does the (incomplete) header get written to the file, along with some audio data. You must wait for enough audio data to fill your preload buffer to be written to the file before doing the .writeCurrentHeader() and .preLoad() calls.

Also, if I do a short recording, then later attempt to do a longer recording using the same file, the first time it plays, it will stop short even though I'm calling adjustHeaderInfo after I stop the recording. I haven't timed it, but it feels like where it stops is about the length of the first shorter recording. The second time it is played, it plays all the way through. It feels like both these issues might be related.
That shouldn't happen. After you .stop() the recording, the WAV header should be correct, but the playback object doesn't know about it until you .adjustHeaderInfo(), after which it ought to play to the end of the recording. You must .adjustHeaderInfo() before playback reaches the point where you (last) called .writeCurrentHeader(). In the example sketch, the preload buffer is 16k or about 186ms, which I .writeCurrentHeader() / .preLoad() when recording has been active for 500ms. It then starts playback after recording for 2000ms, stops recording at 2250ms (by which time the preload has played and file playback has been going for 64ms, and would stop after another 250ms, but...) and immediately does the .adjustHeaderInfo() to tell the playback that there's 2250ms of audio in the file, not just 500ms.

The first recording is overwritten by the new one, but as noted above, only a half-buffer time after recording is started, so you could be trying to do the preload before that happens, if you have a small preload buffer and a large record buffer. The AudioRecordWAVbuffered::lengthMillis() function tells you how much audio is actually in the recording while it's in progress - that might be helpful.

Let me know if that makes sense. I can try to setup a small bit of code to illustrate this if that helps. Thanks again!
Yes, if the above hasn't suggested a solution, some code would be very useful. If you can base it on the example, so much the better, I'll have a running start that way.
 
Okay then. I tried the non-existent file case by changing my example to delete it in setup(), and it didn't preload or play. Which was odd. So I've put a call to flush() every time a file is written while recording, and that seems to fix it. The change is pushed to the repository, so please give that a go and see if it's an improvement. It'll probably fix the second issue, too ... I'm amazed that an 8k write isn't saved immediately, but possibly it's a weird SD card thing. I did try putting the flush() in writeCurrentHeader(), but that didn't seem to work properly.
 
Hmm...Ive read through that many times and I still can't see what I'm doing wrong. I've setup a quick bit of code that will reproduce my issue. You will need a push button to start/stop recording/playback.

It just plays a melody while recording and sets a timer to preload after 500ms. Then when you hit the button again it will start playback and set a timer to stop recording/adjust header after 250ms. If you make a short recording, then a longer one, the long one will cut out when playing back.

This is very similar to how I have things setup in my code. Let me know if I'm just totally misunderstanding the way this works. Thanks again for your time!

Code:
//Example of preload issue I'm running into.
//First press button and let playout for a short period, maybe 2 secs, press again to stop
//Next press button and let playout for longer, maybe 4 secs, press again to stop
//Second playback will cut short


#include <Audio.h>
#include <Bounce.h>
#define BUTTON 4


// GUItool: begin automatically generated code
AudioSynthWaveformSine sine1;       //xy=148.8888931274414,311.11112117767334
AudioPlayWAVstereo playWAVstereo1;  //xy=252.00000762939453,460.55554008483887
AudioEffectEnvelope envelope1;      //xy=329.99999237060547,333.3333160877228
AudioRecordWAVmono recordWAVmono1;  //xy=340.1111755371094,282.77779960632324
AudioMixer4 mixer1;                 //xy=468.88887786865234,451.1111145019531
AudioOutputI2S i2s1;                //xy=684.2777481079102,448.4999752044678

AudioConnection patchCord1(sine1, envelope1);
AudioConnection patchCord2(sine1, recordWAVmono1);
AudioConnection patchCord3(playWAVstereo1, 0, mixer1, 1);
AudioConnection patchCord4(envelope1, 0, mixer1, 0);
AudioConnection patchCord5(mixer1, 0, i2s1, 0);
AudioConnection patchCord6(mixer1, 0, i2s1, 1);

AudioControlSGTL5000 sgtl5000_1;  //xy=1064.750015258789,629.2500114440918
// GUItool: end automatically generated code


Bounce bouncer = Bounce(BUTTON, 5);
bool playing;
elapsedMillis playTm;

AudioPreload preLoad1;

const char* bleepFile = "bleepBloop.wav";

elapsedMillis preLoadTm;
bool doPreLoad = false;

elapsedMillis latStopTm;
bool doLateStop = false;


void setup() {
  pinMode(BUTTON, INPUT_PULLUP);

  AudioMemory(10);
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.1f);

  playWAVstereo1.createBuffer(16384, AudioBuffer::inHeap);
  recordWAVmono1.createBuffer(16384, AudioBuffer::inHeap);
  preLoad1.createBuffer(16384, AudioBuffer::inHeap);

  while (!(SD.begin(10))) {
    // loop here if no SD card, printing a message
    Serial.println("Insert SD card");
    delay(1000);
  }
}

void loop() {
  if (bouncer.update()) {
    if (bouncer.read() == LOW) {
      playing = !playing;

      if (playing) { //Start playing bleeps, start recording, start a timer to preload after 500ms
        recordWAVmono1.record(bleepFile);
        doPreLoad = true;
        preLoadTm = 0;
      } else { //Start playing back recording, start timer to stop recording after 250ms
        envelope1.noteOff();
        playWAVstereo1.play(preLoad1);
        latStopTm = 0;
        doLateStop = true;
      }

    }
  }

  //just random bleeps from sine
  if (playing && playTm > 250) {
    int randFreq = random(220, 440);
    sine1.frequency(randFreq);
    envelope1.noteOn();
    playTm = 0;
  }

  //Do the preload after 500ms of recording
  if (playing && preLoadTm > 500 && doPreLoad) {
    Serial.printf("Header at %d bytes\n", recordWAVmono1.writeCurrentHeader());
    Serial.printf("Preload returned %d\n", preLoad1.preLoad(bleepFile));
    doPreLoad = false;
  }

  //Stop recording and adjust header after 250ms of playback
  if (doLateStop && latStopTm > 250) {
    recordWAVmono1.stop();
    Serial.printf("Header adjusted by %lu bytes\n", playWAVstereo1.adjustHeaderInfo());
    doLateStop = false;
  }
}
 
Back
Top