Fast SD Playback for Multichannel Audio File

grinch

Well-known member
Hi, I am working on a project using the Teensy 4.1 to play back spatial audio via the CS42448 codec.

I'd like the project to be able to play back recorded spatial files from the SD reader, and I wrote a modified version of the SD file player object for this purpose.

However, I'm hitting a roadblock where it seems like the SD file read speed is insufficient for 8-channels. Is there any way to get around this? I'm already ignoring the header information when looping, so there's not much more I can optimize code wise. Any suggestions?

Here is my multichannel file reader object for reference:

Code:
//modified from play_sd_wav.cpp by Emmett Palaima

#include <Arduino.h>
#include "play_sd_wav_xch.h"
#include "spi_interrupt.h"


#define STATE_DIRECT_16BIT_XCH		1 //16bit wave file, 44100kHz SR, any number of channels
#define STATE_UNSUPPORTED_FORMAT	2 //if the file is not 16bit, or sample rate is not 44100kHz
#define STATE_PARSE1			8  // looking for 20 byte ID header
#define STATE_PARSE2			9  // looking for 16 byte format header
#define STATE_PARSE3			10 // looking for 8 byte data header
#define STATE_PARSE4			11 // ignoring unknown chunk after "fmt "
#define STATE_PARSE5			12 // ignoring unknown chunk before "fmt "
#define STATE_STOP			13
#define IS_REPEATING 1

void AudioPlaySdWavXCH::begin(void)
{
	state = STATE_STOP;
	state_play = STATE_STOP;
	data_length = 0;
  isRepeating = false;
	for(int chan = 0; chan < WAV_MAX_CHANNELS; ++chan){
    if(block[chan]){
  		release(block[chan]);
  		block[chan] = NULL;
    }
	}
}


bool AudioPlaySdWavXCH::play(const char *filename)
{
	stop();
#if defined(HAS_KINETIS_SDHC)	
	if (!(SIM_SCGC3 & SIM_SCGC3_SDHC)) AudioStartUsingSPI();
#else 	
	AudioStartUsingSPI();
#endif
	__disable_irq();
	wavfile = SD.open(filename);
  Serial.println(wavfile.available());
  if(!wavfile) Serial.println("File not found!");
	__enable_irq();
	if (!wavfile) {
	#if defined(HAS_KINETIS_SDHC)	
		if (!(SIM_SCGC3 & SIM_SCGC3_SDHC)) AudioStopUsingSPI();
	#else 	
		AudioStopUsingSPI();
	#endif			
		return false;
	}
	buffer_length = 0;
	buffer_offset = 0;
	state_play = STATE_STOP;
	data_length = 20;
	header_offset = 0;
	state = STATE_PARSE1;
	return true;
}

void AudioPlaySdWavXCH::stop(void)
{
	__disable_irq();
	if (state != STATE_STOP) {
    //weird memory release method:
    audio_block_t* b[WAV_MAX_CHANNELS];
		for(int chan = 0; chan < WAV_MAX_CHANNELS; ++chan){
			b[chan] = block[chan];
			block[chan] = NULL;
		}
		state = STATE_STOP;
		__enable_irq();
   //weird memory release method:
    for(int chan = 0; chan < WAV_MAX_CHANNELS; ++chan){
      if(b[chan]){ release(b[chan]); }
    }
		wavfile.close();
	#if defined(HAS_KINETIS_SDHC)	
		if (!(SIM_SCGC3 & SIM_SCGC3_SDHC)) AudioStopUsingSPI();
	#else 	
		AudioStopUsingSPI();
	#endif	
	} else {
		__enable_irq();
	}
}


void AudioPlaySdWavXCH::update(void)
{
	int32_t n;

	// only update if we're playing
	if (state == STATE_STOP) return;

  //allocate more than enough channels, since we don't know how many we'll need yet
	for(int chan = 0; chan < WAV_MAX_CHANNELS; ++chan){
		block[chan] = allocate();
		if(block[chan] == NULL){
			--chan;
			while(chan >= 0){
				release(block[chan]);
				--chan;
			}
			return;
		}
	}
	block_offset = 0;

	// is there buffered data?
	n = buffer_length - buffer_offset;
	if (n > 0) {
		// we have buffered data
		if (consume(n)) return; // it was enough to transmit audio
	}

	// we only get to this point when buffer[512] is empty
	if (state != STATE_STOP && wavfile.available()) {
		// we can read more data from the file...
		readagain:
		buffer_length = wavfile.read(buffer, MULTI_BUFFERSIZE);
//    Serial.println(buffer_length);
		if (buffer_length == 0) goto end;
		buffer_offset = 0;
		bool parsing = (state >= 8);
		bool txok = consume(buffer_length);
//    Serial.println(txok);
//    Serial.println(state);
		if (txok) {
			if (state != STATE_STOP) return;
		} else {
			if (state != STATE_STOP) {
				if (parsing && state < 8) goto readagain;
				else goto cleanup;
			}
		}
	}
end:	// end of file reached or other reason to stop
  if(state != STATE_STOP && isRepeating){
//    Serial.println("Trying Repeat:");
//    Serial.println(wavfile.available());
//    Serial.println(wavfile.seek(44));
//    Serial.println(wavfile.available());
    wavfile.seek(44);
    goto readagain;
  }
	wavfile.close();
#if defined(HAS_KINETIS_SDHC)	
	if (!(SIM_SCGC3 & SIM_SCGC3_SDHC)) AudioStopUsingSPI();
#else 	
	AudioStopUsingSPI();
#endif	
	state_play = STATE_STOP;
	state = STATE_STOP;
cleanup:
	for(int chan = 0; chan < WAV_MAX_CHANNELS; ++chan){
		if (block[chan]) {
			if (block_offset > 0) {
				for (uint32_t i=block_offset; i < AUDIO_BLOCK_SAMPLES; i++) {
					block[chan]->data[i] = 0;
				}
				transmit(block[chan], chan);
				if(numChannels == 1){
					transmit(block[chan], 1);
				}
			}
			release(block[chan]);
			block[chan] = NULL;
		}
	}
}


// https://ccrma.stanford.edu/courses/422/projects/WaveFormat/

// Consume already buffered data.  Returns true if audio transmitted.
bool AudioPlaySdWavXCH::consume(uint32_t size)
{
	uint32_t len;
	uint8_t lsb, msb;
	const uint8_t *p;

	p = buffer + buffer_offset;
start:
	if (size == 0) return false;
#if 0
	Serial.print("AudioPlaySdWav consume, ");
	Serial.print("size = ");
	Serial.print(size);
	Serial.print(", buffer_offset = ");
	Serial.print(buffer_offset);
	Serial.print(", data_length = ");
	Serial.print(data_length);
	Serial.print(", space = ");
	Serial.print((AUDIO_BLOCK_SAMPLES - block_offset) * 2);
	Serial.print(", state = ");
	Serial.println(state);
#endif
	switch (state) {
	  // parse wav file header, is this really a .wav file?
	  case STATE_PARSE1:
		len = data_length;
		if (size < len) len = size;
		memcpy((uint8_t *)header + header_offset, p, len);
		header_offset += len;
		buffer_offset += len;
		data_length -= len;
		if (data_length > 0) return false;
		// parse the header...
		if (header[0] == 0x46464952 && header[2] == 0x45564157) {
			//Serial.println("is wav file");
			if (header[3] == 0x20746D66) {
				// "fmt " header
				if (header[4] < 16) {
					// WAV "fmt " info must be at least 16 bytes
					break;
				}
				if (header[4] > sizeof(header)) {
					// if such .wav files exist, increasing the
					// size of header[] should accomodate them...
					//Serial.println("WAVEFORMATEXTENSIBLE too long");
					break;
				}
//				Serial.println("header ok");
				header_offset = 0;
				state = STATE_PARSE2;
			} else {
				// first chuck is something other than "fmt "
				//Serial.print("skipping \"");
				//Serial.printf("\" (%08X), ", __builtin_bswap32(header[3]));
				//Serial.print(header[4]);
				//Serial.println(" bytes");
				header_offset = 12;
				state = STATE_PARSE5;
			}
			p += len;
			size -= len;
			data_length = header[4];
			goto start;
		}
		//Serial.println("unknown WAV header");
		break;

	  // check & extract key audio parameters
	  case STATE_PARSE2:
		len = data_length;
		if (size < len) len = size;
		memcpy((uint8_t *)header + header_offset, p, len);
		header_offset += len;
		buffer_offset += len;
		data_length -= len;
		if (data_length > 0) return false;
		if (parse_format()) {
//			Serial.println("audio format ok");
			p += len;
			size -= len;
			data_length = 8;
			header_offset = 0;
			state = STATE_PARSE3;
			goto start;
		}
		Serial.println("unknown audio format");
		break;

	  // find the data chunk
	  case STATE_PARSE3: // 10
		len = data_length;
		if (size < len) len = size;
		memcpy((uint8_t *)header + header_offset, p, len);
		header_offset += len;
		buffer_offset += len;
		data_length -= len;
		if (data_length > 0) return false;
		//Serial.print("chunk id = ");
		//Serial.print(header[0], HEX);
		//Serial.print(", length = ");
		//Serial.println(header[1]);
		p += len;
		size -= len;
		data_length = header[1];
		if (header[0] == 0x61746164) {
			leftover_bytes = 0;
			state = state_play;
			total_length = data_length;
		} else {
			state = STATE_PARSE4;
		}
		goto start;

	  // ignore any extra unknown chunks (title & artist info)
	  case STATE_PARSE4: // 11
		if (size < data_length) {
			data_length -= size;
			buffer_offset += size;
			return false;
		}
		p += data_length;
		size -= data_length;
		buffer_offset += data_length;
		data_length = 8;
		header_offset = 0;
		state = STATE_PARSE3;
//		Serial.println("consumed unknown chunk");
		goto start;

	  // skip past "junk" data before "fmt " header
	  case STATE_PARSE5:
		len = data_length;
		if (size < len) len = size;
		buffer_offset += len;
		data_length -= len;
		if (data_length > 0) return false;
		p += len;
		size -= len;
		data_length = 8;
		state = STATE_PARSE1;
		goto start;

	  // playing stereo at native sample rate
	  case STATE_DIRECT_16BIT_XCH:
//    Serial.println("Attempting to play audio");
		if (size > data_length) size = data_length;
		data_length -= size;
		if (leftover_bytes && numChannels > 1) {
      Serial.println("Leftover bytes!");
			block[0]->data[block_offset] = header[0];
//PAH fix problem with left+right channels being swapped
			leftover_bytes = 0;
			goto right16;
		}
		while (1) {
			lsb = *p++;
			msb = *p++;
			size -= 2;
			if (size == 0) {
				if (data_length == 0) break;
				header[0] = (msb << 8) | lsb;
				leftover_bytes = 2;
				return false;
			}
      //assign data being read to output channels:
			block[0]->data[block_offset] = (msb << 8) | lsb;
			right16:
			for(int chan = 1; chan < numChannels; ++chan){
				lsb = *p++;
				msb = *p++;
				size -= 2;
				block[chan]->data[block_offset] = (msb << 8) | lsb;
			}
      block_offset++;
			if (block_offset >= AUDIO_BLOCK_SAMPLES) {
				for(int chan = 0; chan < numChannels; ++chan){
					transmit(block[chan], chan);
					release(block[chan]);
					block[chan] = NULL;
				}
        for(int chan = numChannels; chan < WAV_MAX_CHANNELS; ++chan){
          release(block[chan]);
          block[chan] = NULL;
        }
				data_length += size;
				buffer_offset = p - buffer;
				if (data_length == 0 && !isRepeating) state = STATE_STOP; //check on this, added repeating code
				return true;
			}
			if (size == 0) {
				if (data_length == 0) break;
				leftover_bytes = 0;
				return false;
			}
		}
		// end of file reached
		if (block_offset > 0) {
			// TODO: fill remainder of last block with zero and transmit
		}
		if(!isRepeating){ state = STATE_STOP; } //check on this, added code for repeat
		return false;

	  // ignore any extra data after playing
	  // or anything following any error
	  case STATE_STOP:
		return false;

	  // this is not supposed to happen!
	  default:
		Serial.print("AudioPlaySdWav, unknown state: ");
    Serial.println(state);
	}
	state_play = STATE_STOP;
	state = STATE_STOP;
	return false;
}

/*
00000000  52494646 66EA6903 57415645 666D7420  RIFFf.i.WAVEfmt 
00000010  10000000 01000200 44AC0000 10B10200  ........D.......
00000020  04001000 4C495354 3A000000 494E464F  ....LIST:...INFO
00000030  494E414D 14000000 49205761 6E742054  INAM....I Want T
00000040  6F20436F 6D65204F 76657200 49415254  o Come Over.IART
00000050  12000000 4D656C69 73736120 45746865  ....Melissa Ethe
00000060  72696467 65006461 746100EA 69030100  ridge.data..i...
00000070  FEFF0300 FCFF0400 FDFF0200 0000FEFF  ................
00000080  0300FDFF 0200FFFF 00000100 FEFF0300  ................
00000090  FDFF0300 FDFF0200 FFFF0100 0000FFFF  ................
*/

// SD library on Teensy3 at 96 MHz
//  256 byte chunks, speed is 443272 bytes/sec
//  512 byte chunks, speed is 468023 bytes/sec

#define B2M_44100 (uint32_t)((double)4294967296000.0 / AUDIO_SAMPLE_RATE_EXACT) // 97352592
#define B2M_22050 (uint32_t)((double)4294967296000.0 / AUDIO_SAMPLE_RATE_EXACT * 2.0)
#define B2M_11025 (uint32_t)((double)4294967296000.0 / AUDIO_SAMPLE_RATE_EXACT * 4.0)

bool AudioPlaySdWavXCH::parse_format(void)
{
	uint8_t num = 0;
	uint16_t format;
	uint16_t channels;
	uint32_t rate, b2m;
	uint16_t bits;

  //assume the file is correctly formatted, change it if it isn't:
  state_play = STATE_DIRECT_16BIT_XCH;

	format = header[0];

	if (format != 1){
    Serial.print("Unsupported Format: ");
    Serial.println(format);
	  state_play = STATE_UNSUPPORTED_FORMAT;
	}

	rate = header[1];
	if (rate == 44100) {
		b2m = B2M_44100;
	} else {
    Serial.print("Unsupported Sample Rate: ");
    Serial.println(rate);
    state_play = STATE_UNSUPPORTED_FORMAT;
	}

	channels = header[0] >> 16;
	numChannels = channels;
	b2m >>= (numChannels - 1);

	bits = header[3] >> 16;
	if (bits == 16) {
    b2m >>= 1;
    num |= 2;
	}else{
    Serial.print("Unsupported Bit Depth: ");
    Serial.println(bits);
    state_play = STATE_UNSUPPORTED_FORMAT;
	}

	bytes2millis = b2m;

#if 0
  Serial.print("  channels = ");
  Serial.println(numChannels);
  Serial.print("  bits = ");
  Serial.println(bits);
  Serial.println(
	Serial.print("  bytes2millis = ");
	Serial.println(b2m);
#endif

  if(state_play == STATE_UNSUPPORTED_FORMAT){
    return false;
  }

	// we're not checking the byte rate and block align fields
	// if they're not the expected values, all we could do is
	// return false.  Do any real wav files have unexpected
	// values in these other fields?
//	state_play = num;
	return true;
}


bool AudioPlaySdWavXCH::isPlaying(void)
{
	uint8_t s = *(volatile uint8_t *)&state;
	return (s < 8);
}

uint32_t AudioPlaySdWavXCH::positionMillis(void)
{
	uint8_t s = *(volatile uint8_t *)&state;
	if (s >= 8) return 0;
	uint32_t tlength = *(volatile uint32_t *)&total_length;
	uint32_t dlength = *(volatile uint32_t *)&data_length;
	uint32_t offset = tlength - dlength;
	uint32_t b2m = *(volatile uint32_t *)&bytes2millis;
	return ((uint64_t)offset * b2m) >> 32;
}


uint32_t AudioPlaySdWavXCH::lengthMillis(void)
{
	uint8_t s = *(volatile uint8_t *)&state;
	if (s >= 8) return 0;
	uint32_t tlength = *(volatile uint32_t *)&total_length;
	uint32_t b2m = *(volatile uint32_t *)&bytes2millis;
	return ((uint64_t)tlength * b2m) >> 32;
}

Code:
//modified from play_sd_wav.h by Emmett Palaima

#ifndef play_sd_wav_xch_h_
#define play_sd_wav_xch_h_

#define MULTI_BUFFERSIZE 1024
#define WAV_MAX_CHANNELS 10

#include "Arduino.h"
#include "AudioStream.h"
#include "SD.h"

class AudioPlaySdWavXCH : public AudioStream
{
public:
	AudioPlaySdWavXCH(void) : AudioStream(0, NULL) { begin(); }
	void begin(void);
	bool play(const char *filename);
	void stop(void);
	bool isPlaying(void);
	uint32_t positionMillis(void);
	uint32_t lengthMillis(void);
	virtual void update(void);
  bool setRepeating(bool onOff){ isRepeating = onOff; }
private:
	File wavfile;
	bool consume(uint32_t size);
	bool parse_format(void);
	uint8_t numChannels;
	uint32_t header[10];		// temporary storage of wav header data
	uint32_t data_length;		// number of bytes remaining in current section
	uint32_t total_length;		// number of audio data bytes in file
	uint32_t bytes2millis;
	audio_block_t* block[WAV_MAX_CHANNELS];
	uint16_t block_offset;		// how much data is in block_left & block_right
	uint8_t buffer[MULTI_BUFFERSIZE];		// buffer one block of data
	uint16_t buffer_offset;		// where we're at consuming "buffer"
	uint16_t buffer_length;		// how much data is in "buffer" (512 until last read)
	uint8_t header_offset;		// number of bytes in header[]
	uint8_t state;
	uint8_t state_play;
	uint8_t leftover_bytes;
  bool isRepeating;
};

#endif
 
You might want to take a look at this thread, assuming you haven't already. It's certainly capable of playing back an 8-channel audio file, and I believe you should have no problem playing two. You'd need to do that, because I haven't implemented looping and don't plan to, so to loop without glitches you would need to start playback of the same file using a second AudioPlayWAVoct object. Precise starting is catered for by pre-buffering part of your audio file, if you wish. Support freely available, ideally on that thread so all information is kept in one place and findable by future generations!

I think any attempt to stream from SD while reading it during the audio update is pretty much doomed to failure: SD cards are just too unpredictable.

I don't know what you mean when you say "fast" playback.
 
You might want to take a look at this thread, assuming you haven't already. It's certainly capable of playing back an 8-channel audio file, and I believe you should have no problem playing two. You'd need to do that, because I haven't implemented looping and don't plan to, so to loop without glitches you would need to start playback of the same file using a second AudioPlayWAVoct object. Precise starting is catered for by pre-buffering part of your audio file, if you wish. Support freely available, ideally on that thread so all information is kept in one place and findable by future generations!

I think any attempt to stream from SD while reading it during the audio update is pretty much doomed to failure: SD cards are just too unpredictable.

I don't know what you mean when you say "fast" playback.

Thanks! This thread looks very helpful I will check it out. Is the basic idea here just to read the entire file into internal memory before playing it back? Or to do so in large sections at a time such that the read operation is more efficient?

When I say fast I just mean fast enough to work without glitching.
 
Just to read in large enough chunks that SD latency shouldn’t be an issue.

You control the chunk size by creating a buffer before starting playback, which gets reloaded when it’s half empty. The reload happens during yield(), e.g. when loop() is allowed to exit or delay() or yield() are called. For an 8-channel file you’d probably want to use a 32k buffer, maybe even 64k. If you have PSRAM you can use that, the performance impact isn’t too bad.
 
Awesome, is there an example you recommend from this branch for getting started with just reading an 8 channel wav file from the SD?
 
The closest example would be https://github.com/h4yn0nnym0u5e/Au...AudioTestPlayMultiSD/AudioTestPlayMultiSD.ino, which will need some modifications:
  • more AudioMemory - I'd suggest maybe 30
  • bigger values for createBuffer(): as noted above, I'd start with 32k instead of the 2k in the example
  • change AudioPlayWAVstereo to AudioPlayWAVoct
  • add in some mixers so both playRaw1 and playRaw2 can be routed to your AudioOutoutTDM
  • patchcords to suit
  • no need to use two different SD cards, of course
 
The closest example would be https://github.com/h4yn0nnym0u5e/Au...AudioTestPlayMultiSD/AudioTestPlayMultiSD.ino, which will need some modifications:
  • more AudioMemory - I'd suggest maybe 30
  • bigger values for createBuffer(): as noted above, I'd start with 32k instead of the 2k in the example
  • change AudioPlayWAVstereo to AudioPlayWAVoct
  • add in some mixers so both playRaw1 and playRaw2 can be routed to your AudioOutoutTDM
  • patchcords to suit
  • no need to use two different SD cards, of course

Okay, thanks, started checking it out today. It seems like this is part of a much larger overhaul of the Teensy codebase, and there are a lot of overlapping dependencies that this relies on which do not live in the same place.

1. Is there a way to try this without having to set up / install the whole new overhauled codebase? Trying to keep this strictly to the dependencies that are absolutely necessary for the enhanced wavefile reading since this all has to coexisting with code I've already written for other projects.

2. If I don't have to install the overhauled codebase, what dependencies are strictly necessary for the wavefile reader and where are they located in the repo?

3. If I do have to set up the entire overhauled codebase what is the best way to do that? Do I just clone the repo and replace the library folders inside my Teensyduino install, or is there a better way to do that?
 
It should just be a change to the Audio library, though obviously if you have other changed or new objects then there's likely to be a conflict to sort out. So the single simplest way of trying it out would be to check out the relevant branch into your local libraries folder, as with any other library. Arduino should then use it in preference to the stock library, and the build process will indicate it did so. To stop using it but keep it in reserve, as it were, I just move the whole folder from libraries/ to libraries-unused/, so Arduino doesn't spot it.

2023-10-20 09_51_27-D__swd_git_!dynamic_libraries_Audio.png

It's far better not to tinker with the libraries inside the Teensyduino install, not least because they'll get overwritten when you update. If you wanted to try that, I'd recommend Googling how to do a "portable" install of the Arduino IDE, and messing with that - doesn't matter if you break it then!

In terms of dependencies, yes, sorry, it's become a bit messy over the last year or so. I've tidied things a bit by merging Paul's latest master branch, as he recently went through pretty much all the header files and tweaked the nested #includes. The absolutely vital files are (I think):
Code:
Audio.h
AudioBuffer.cpp
AudioBuffer.h
play_wav_buffered.cpp
play_wav_buffered.h
record_wav_buffered.cpp
record_wav_buffered.h
oscope.h
The last shouldn't be needed, it's just there for debug assistance during development, but as yet I've not found a way to omit it cleanly for the "production" library. Shouldn't impact your code, though.

In addition to those, it's useful to have:
Code:
\gui\index.html
\gui\red\ui\view.js
keywords.txt
effect_envelope.cpp
This gives you a version of the Design Tool which lets you place the multi-track playback (and record) objects. The keywords.txt file makes the new functions highlight in the Arduino IDE. And there's a bug-fixed version of the envelope effect, without which the SDpiano demo doesn't work.

Irrelevant things that shouldn't really be there are:
Code:
effect_fade.cpp
effect_fade.h
input_spdif3.cpp
output_spdif3.cpp
output_spdif3.h
These fix a bug in the fade effect, and allow the SPDIF input to act as the master audio clock, respectively.
 
Thanks for the very thorough reply! Seems to be compiling after an IDE update to Arduino 2.2.1 just by including the following files:

AudioBuffer.cpp
AudioBuffer.h
play_wav_buffered.cpp
play_wav_buffered.h

Away from the bench atm so just doing this headlesss, but hopefully will be able to test this this weekend.
 
It's certainly capable of playing back an 8-channel audio file, and I believe you should have no problem playing two. You'd need to do that, because I haven't implemented looping and don't plan to, so to loop without glitches you would need to start playback of the same file using a second AudioPlayWAVoct object.

Was able to test this class finally and it's working for basic 8 channel playback using the new Arduino IDE and including the files I mentioned in the last post manually in the sketch folder:

Code:
AudioBuffer.cpp
AudioBuffer.h
play_wav_buffered.cpp
play_wav_buffered.h

In regards to looping, I have implemented this before for the audio library SD card class, it was pretty simple in that I just told the class to start reading from the beginning of the file (skipping the header) once it reached the end. This didn't take anything crazy. I might take a crack at implementing that here as well since it's obviously a very useful feature and removes a bunch of complexity in terms of having to manually set up some hacky crossfade thing every time you want this basic functionality. I will certainly be using it all the time for my application.

It looks like the place to implement it would be the loadBuffer function here:

Code:
void AudioPlayWAVbuffered::loadBuffer(uint8_t* pb, size_t sz, bool firstLoad /* = false */)
{
  size_t got;
 
// SCOPE_HIGH(); 
// SCOPESER_TX(objnum);
  if (sz > 0 && !eof) // read triggered, but there's no room or already stopped - ignore the request
  {
    
{//----------------------------------------------------
  size_t av = getAvailable();
  if (av != 0 && av < lowWater && !eof)
    lowWater = av;
// SCOPESER_TX((av >> 8) & 0xFF);
// SCOPESER_TX(av & 0xFF);
}//----------------------------------------------------
    uint32_t now = micros();
    got = wavfile.read(pb,sz);  // try for that
    readMicros.newValue(micros() - now);
    
    if (got < sz) // there wasn't enough data
    {
      if (got < 0)
        got = 0;
      memset(pb+got,0,sz-got); // zero the rest of the buffer
      eof = true;
    }
    if (!firstLoad) // empty on first load, don't record result
      bufferAvail.newValue(getAvailable()); // worse than lowWater
    readExecuted(got);
  }
  readPending = false;
// SCOPE_LOW();
}

In the case of (got < sz) we know the end of the file has been reached, and if a looping flag is set to true, we could just trigger a new read starting at the beginning of the file.

Is there anything that wouldn't work about this or that I should consider in terms of how the rest of the class is working? The code is fairly dense in terms of all the scheduling of SD reads going on (I think this uses it's own interrupt thread?), so just wanted to check before making changes based on this analysis.
 
This worked for me, just had to make two additional changes to the update function to maintain position tracking and allowing the file to read additional data past the end of the filesize when looping (this data now comes from the beginning of the file):
Code:
// also, don't play past end of data
      // could leave some data unread in buffer, but
      // it's not audio!
      if (toRead > data_length && !looping) //LOOP MOD!!!
        toRead = data_length;

Code:
// deal with position tracking //LOOP MOD!!!
    if (got <= data_length){
      data_length -= got;
    }else{
      if(looping){
        data_length = total_length + data_length - got;
      }else{
        data_length = 0;
      }
    }

Put the full test sketch on github if you want to check it out. Could be a neat thing to incorporate into this wave file player: https://github.com/hhaudio/Teensy_LoopingBufferedWav
 
Great, glad you got it working!

I've taken a quick look at your mods - I think ... as you didn't branch I can't diff it to see what you did in detail. One easy win is that you don't need to assume a WAV header is 44 bytes long; the true start of audio data should be in firstAudio (member of AudioWAVdata which is one of the base classes for AudioPlayWAVbuffered. If you find the value is wrong for any given file, please let me know, as that constitutes a bug...

I think that once looping starts you may suffer a loss of read efficiency, as you'll no longer be loading data on SD sector boundaries. Dealing with the general case for this was one of the things that put me off implementing looping internally. But if it's good enough for your use case, excellent news. The speed loss is probably minor, but everyone bangs on about reading on sector boundaries so I did it that way.

There's no extra interrupt thread. All I've done is to defer all SD accesses to foreground code by using the EventResponder library, which actions any triggered events during yield() calls (end of loop(), during long-enough delay() calls, and if called explicitly). Actually it actions one of the queued triggered events per yield(), which IMHO is the Wrong Thing, but I have yet to find anyone who concurs.
 
Back
Top