WaveplayerEx

@wmxz,
yes the name is subject to change..

@mjs: Thank you! I've added your code as example.

Morning Frank
Out of curiosity I decided to test as a SPI 1G NAND chip, so I put a bunch of files other than the SDTEST files:
Code:
Instances: 1
1
LittleFS Test
TotalSize (Bytes): 131596288
started
printDirectory
--------------
FILE	Nums_7dot1_16_44100.wav		6385552
FILE	SDTEST1.wav		16787550
FILE	SDTEST2.wav		16425698
FILE	SDTEST3.wav		13617358
FILE	SDTEST4.wav		17173152
FILE	calculations.wav		426300
FILE	completed.wav		276460
FILE	dangerous_to_remain.wav		372892
FILE	enough_info.wav		513388
FILE	functional.wav		237356
FILE	odd1.wav		553004
FILE	one_moment.wav		202236
FILE	operational.wav		772140
FILE	sorry_dave.wav		791164
FILE	stop.wav		200844

 0 dirs with 15 files of Size 74735094 Bytes

Playing file: Nums_7dot1_16_44100.wav
Format:65534 Bits:16
Playing file: SDTEST1.wav
Format:1 Bits:16
Playing file: calculations.wav
Format:1 Bits:16
Playing file: dangerous_to_remain.wav
Format:1 Bits:16
Playing file: odd1.wav
Format:1 Bits:16

One bit of warning though - when using LittleFS files names are case sensitive. So while this worked when using the SD Card:
Code:
playFile("SDTEST1.WAV");
it will not work for LittleFS since in actuality on the disk its:
Code:
playFile("SDTEST1.wav");
where wav is in lower case - so just be forewarned
 
Oh, then I must have renamed my files at some point.

Probably me working on it too early in the morning. Took awhile for the light bulb to come on. This is what is the example currently.

Code:
  playFile("Nums_7dot1_16_44100.wav");
  delay(500);
  playFile("SDTEST1.WAV");  // filenames are always uppercase 8.3 format
  delay(500);
  playFile("SDTEST2.WAV");
.....

It ran fine from the SD Card even though the file extension was all in caps. But when I ran it from Flash (SPI or QSPI) with LittleFS had to change the file extension to lower case since that was what was the acutally format for the filename SDTEST2.wav for instance - was driving me crazy because only the number file would play - then the light bulb came on
 
Ok, I think its almost ready to use now.
Needs some more testing... :)

Latest version

Example audio files are on github.

Features:


  • Sample rate agnostic (does not check if its correct - so easy to use with other samplerates, (edit audiostream.h)
  • up to 8 Channels
  • 8 or 16Bit
  • delay() after start not needed anymore
  • any audio block size
  • interleaved reads: only one file access on each audio-cycle
  • lastErr(void) returns the last error
  • addMemoryForRead(size_t bytes) adds memory
  • Several files can start synchronized.

It uses malloc() to manage its memory. For example files with 8 voices need way more memory than stereo. The interleaved reads increase the amount of needed memory, too.
The amount is calculated automatically, so addMemoryForRead() is *not* needed in most cases.

It's not that dead simple to calculate the optional additional memory - i'll descrivbe that later. Too less additonal memory makes no sense and gets ignored.
It needs to be a multiple of # of instances, samplesize, audioblock-size etc...

Looks like this in the code

I renamed it to AudioPlayWav.
I think, it can replace the old player completely..

@Paul? What do you think? Have you tried it?
@Wcalvert: Synced start is possible. Start all waves in paused mode: play(filename, true), then start them together with pause(false), surrounded by AudioNoInterrupts. They will start emitting Audio data with the next audio cycle.
 
Last edited:
Public functions:
Code:
    bool play(File file);
    bool play(File file, bool paused);

    bool play(const char *filename);
    bool play(const char *filename, bool paused); //start in paused state?

    bool addMemoryForRead(size_t bytes); //add memory

    void togglePlayPause(void);
    void pause(bool pause);
    void stop(void);

    bool isPlaying(void);
    bool isPaused(void);
    bool isStopped(void);

    uint32_t positionMillis(void);
    uint32_t lengthMillis(void);
    uint32_t numBits(void);
    uint32_t numChannels(void);
    uint32_t sampleRate(void);

    uint8_t lastErr(void);         // returns last error

Error values:
Code:
#define APW_ERR_OK              0 // no Error
#define APW_ERR_FORMAT          1 // not supported Format
#define APW_ERR_FILE            2 // File not readable (does ist exist? large enough?)
#define APW_ERR_OUT_OF_MEMORY   3 // Not enough dynamic memory available
#define APW_ERR_NO_AUDIOBLOCKS  4
 
Last edited:
Sweet!

This is probably old news since you appear to have it working, but...

With the Tympan_Library, I write 4-channel WAV files all the time. I always use 16-bit int, but I do a variety of sample rates...everything from 20 kHz up to 96 kHz. I think that I tried 192kHz once, but I don't remember. Once I recorded the files, I pulled the SD card and read them in via Audacity.

The only trick to making this whole thing happen was getting the WAV header correct (see here, skip down to the method "wavHeaderInt16"). But, if I figured it out, you probably figured it out even more easily.

As for reading audio from an SD, I've been stuck reading reading only stereo files (see here, skip down to the "update" method). I like that you thought to extend this to a greater number of channels!

Chip
 
Yep thats where I downloaded from. When I do right click and do a save it comes down as lower case - maybe because my default player is VLC? See screenshot below:
Capture.PNG
 
Looks like there is a big fat bug re: additional Mem and intervleave... i've set the repo to private until it is fixed..
 
Last edited:
Yes I had no Idea how to do it... could have been a show stopper.
*still testing*


Oh my,... now concurrent play does not work anymore.
I'll take a break.
 
Last edited:
I'll take a break.
..and it was helpful. issue fixed.

So, a description.
The way addMemoryForRead() works has changed.
I think it is much easier to use now. There are not many cases where it is necessary, nevertheless it may be useful in some cases.
Normally the waveplayer itself knows how much memory it needs. This is calculated for each file individually:

Memory:
AUDIO_BLOCK_SAMPLES * Channels * BytesPerSample * NumberOfInstances

This looks like a lot, but it is not. In the normal case (1 instance) it leads to exactly the same memory usage as the old player.
With 8 channels, 16Bit you get 128 * 8 * 2 * 1 = 2048 bytes. This is a not much for the T4, but also a T3.2 should be able to do it.
(With a blocksize of 128, even the Teensy LC can play a stereo file)

The player tries to maximize the SD throughput. It reads as much as possible, i.e. it fills its entire memory when it accesses the file.
This saves the expensive addressing when multiple players are running at the same time.

In the following we are talking about multiple concurrent instances - and only then addMemoryForRead() can be useful.
A simple concurrent start can look like this, for example:

(1)
Code:
  playWav1.play("Nums_7dot1_16_44100.wav");    
  playWav2.play("SDTEST3.WAV");

This is not quite optimal, because an audio interrupt can happen between both calls. I.e. the starts would be about 3 milliseconds apart in this case.
Often this may not matter.

This attempt to prevent this is not good:
Code:
  AudioNoInterrupts();
  playWav1.play("Nums_7dot1_16_44100.wav");    
  playWav2.play("SDTEST3.WAV");    
  AudioInterrupts();
This may lead to a short sound dropout of the library if it happens just before an interrupt.

Better is this:
(2)
Code:
  playWav1.play("Nums_7dot1_16_44100.wav", true); //start in paused mode
  playWav2.play("SDTEST3.WAV", true);
  AudioNoInterrupts();
  playWav1.pause(false); //un-pause, start playing
  playWav2.pause(false);
  AudioInterrupts();
This is a sychronized start. Both files start granted at the same time.

Now we are slowly approaching the point where additional storage becomes interesting. Probably.

(2) looks always like this - and in most cases (1) looks indentical:
pic_9_2.png
(yes, the probes are not calibrated - sorry for this, could not find the tool)

So, you see that it channel 2( blue line ) gets read from SD in middle between the reads of channel 1 (yellow)

In some cases of (1) - if the second files starts an interrupt later, it can look like this:
pic_9_1.png

And only for this - i.e. when not synchronized start - AND when it is somehow interesting for the main program (there are very few cases... I can't even think of a simple example)
additional memory becomes interesting:

Code:
playWav1.addMemoryForRead(2);
playWav2.addMemoryForRead(2);
(This must happen before play() )

A values of 2 doubles the used memory, 3 would be the tripple (use it for three files for example)
Now, it always looks like (1).
This is needed, because in the default case, the player just does not have memory to play 3 milliseconds without accessing the file (you remember - the 2nd file may start later (if no sync start))
Now, it looks good again:
pic_9_2.png

So... I hope this explanation was a little helpful.

Back to a single file:

The normal frequency of reads is 172Hz. If you want a lower frequency, you can use addMemoryForRead(), too.
i.e. "addMemoryForRead(2)" would half this frequency to 86Hz. If you need that.

And, last:
- addMemoryForRead(0) and addMemoryForRead(1) do nothing.
Edit "- if you use it, use it for every player. Not using it for all instances makes no sense." - changed in latest version. It's sufficient to change it for one instance - all other instances will us it, too.
 
Last edited:
You may see, that the first file ( it is a 8 channel 16 bit file) does not need that much more time for the reads.
If someone sends me a 10, 12, 14 and a 16 channel file, i can test it, and perhaps increase the max number in the code. I wonder if that will work.

Edit:
Just tried to play the Nums_7dot1_16_44100.wav twice. SO.. 16 channels, 16 bit parallel - 8 channels from each file. Seems to work :) How far can we go?

Edit again:
Increased the smple freqency to 88.2 khz: Now I hear 2x Mickey Mouse. I think there are still no artifacts. So... calc back.. 32 channels(?!)

Edit again:
There is still *much* room, even with the last experiment.
 
Last edited:
I have changed the behaviour of addMemoryForRead().
It is now a global setting - setting it for one instance sets it for all others, too.
I think that's better and prevents bugs.
 
Struggling a bit with this, though possibly because I'm trying something which is not the correct use case... I have 6 mono WAV files which I want to play simultaneously, taking advantage of the round-robin buffering. But I can't get even two to work nicely!
Code:
// Simple WAV file player example, adapted

#include <Audio.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <SerialFlash.h>

#define AudioPlaySdWav AudioPlayWav // divert normal player to Frank's one

// GUItool: begin automatically generated code
AudioPlaySdWav             track1;     //xy=323,171
AudioPlaySdWav             track2;     //xy=323,171
AudioPlaySdWav             track3;     //xy=323,171
AudioPlaySdWav             track4;     //xy=323,171
AudioPlaySdWav             track5;     //xy=323,171
AudioPlaySdWav             track6;     //xy=323,171
AudioRecordQueue         queue1;         //xy=401,90
AudioRecordQueue         queue2;         //xy=402,132
AudioMixer4              mixer1;         //xy=647,123
AudioMixer4              mixer3;         //xy=648,212
//AudioOutputPT8211        pt8211_1;       //xy=828,169
AudioOutputI2S           pt8211_1;           //xy=953,400

AudioConnection          patchCord1(track1, 0, mixer1, 0);
AudioConnection          patchCord2(track2, 0, mixer3, 0);
AudioConnection          patchCord3(track3, 0, mixer1, 1);
AudioConnection          patchCord4(track4, 0, mixer3, 1);
AudioConnection          patchCord5(track5, 0, mixer1, 2);
AudioConnection          patchCord6(track6, 0, mixer3, 2);
AudioConnection          patchCord7(track1, 0, queue1, 0);
AudioConnection          patchCord8(track2, 0, queue2, 0);
AudioConnection          patchCord9(mixer1, 0, pt8211_1, 0);
AudioConnection          patchCord10(mixer3, 0, pt8211_1, 1);
AudioControlSGTL5000     sgtl5000_1;     //xy=942,297
// GUItool: end automatically generated code

#define SDCARD_CS_PIN    BUILTIN_SDCARD
#define SDCARD_MOSI_PIN  11  // not actually used
#define SDCARD_SCK_PIN   13  // not actually used



/*********************************************************************************/

void setup() {
  Serial.begin(9600);
  if (CrashReport) {
    Serial.println(CrashReport);
    CrashReport.clear();
  }

  AudioMemory(50);

  SPI.setMOSI(SDCARD_MOSI_PIN);
  SPI.setSCK(SDCARD_SCK_PIN);
  while (!(SD.begin(SDCARD_CS_PIN))) {
    // stop here, but print a message repetitively
    //while (1) {
//      Serial.println("Unable to access the SD card");
      delay(500);
    //}
  }

  mixer1.gain(0,0.2);
  mixer1.gain(1,0.0);
  mixer1.gain(2,0.0);
  mixer1.gain(3,0.0);
  mixer3.gain(0,0.2);
  mixer3.gain(1,0.2);
  mixer3.gain(2,0.2);
  mixer3.gain(3,0.2);
  
  sgtl5000_1.enable();
  sgtl5000_1.volume(0.05);

  Serial.println(F("block filepos1 filepos2 wav1 wav2"));
}


void loop() {
  int count = 497;

  track2.play("sine110.wav",true);
  track1.play("sine440.wav",true);
  
  Serial.print(track1.filePos());
  Serial.print(' ');
  Serial.print(track2.filePos());  
  Serial.println();
#if 1  // doesn't make any difference to enable these
  track3.play("sine330.wav",true);
  track4.play("sine220.wav",true);
  track5.play("sine550.wav",true);
  track6.play("sine660.wav",true);
#endif
  queue1.begin();
  queue2.begin();
  delay(4);
  AudioNoInterrupts();
  track1.pause(false);
  AudioInterrupts();
  delay(6);
  AudioNoInterrupts();
  track2.pause(false);
  track3.pause(false);
  track4.pause(false);
  track5.pause(false);
  track6.pause(false);
  AudioInterrupts();

  while (count > 0)
  {
    if (queue1.available() > 0)
    {
      int16_t* q1 = queue1.readBuffer();
      int16_t* q2 = queue2.readBuffer();
      int pulse = 16000;
      
      for (int i=0;i<128 && count > 0;i+=4)
      {
        Serial.print(pulse);
        Serial.print(' ');
        pulse = 0;
              
        Serial.print(track1.filePos() * 1.0f); // N.B. filePos() added for debug
        Serial.print(' ');
  
        Serial.print(track2.filePos() * 1.0f);
        Serial.print(' ');
  
        if (NULL != q1)
          Serial.print(q1[i]);
        Serial.print(' ');
        if (NULL != q2)
          Serial.print(q2[i]);

        Serial.println();
        count--;
      }
  
      queue1.freeBuffer();
      queue2.freeBuffer();
      pulse = 16000;
      
    }
  }
  AudioNoInterrupts();
  while (1)
    ;
}

I've added filepos() to the play_wav object for debug purposes:

Code:
uint32_t AudioPlayWav::filePos(void)
{
	if (wavfile)
		return wavfile.position();
	else
		return 12345678UL;
}

The test WAV files "sineNN0.wav" are what you'd expect: 3s of 44100Hz sine waves at 110*N Hz - can send these if you need them. AudioRecordQueue is my modified one which returns a proper silent block as needed (this issue was how I found that problem!).

Using the serial plotter, you'd expect to see a couple of sine waves starting at an offset time from one another, due to the delay. Instead I get:

2021-08-24 08_38_05-NVIDIA GeForce Overlay.jpg
blue spikes: one per audio block; red / yellow traces: file position and audio for track1; green / magenta traces: same for track2

It appears not to be pre-loading the second WAV file on the call to play() - note the small filePos value (green trace: looks like the header read has occurred OK), compared to the red trace. Then somehow track2 is managing to output a block of the track1 data before loading and playing its own data. Very odd. I've puzzled over your code for a bit and simply can't see how this could happen, so I have to throw it over to you to figure out!

Best regards

Jonathan
 
Yes, playing some files with sync start is not the ideal case - if you don't really need it. SD accesses will be pretty random, and the adressing on SD needs much time. Better is to use one file with all channels needed.

But, nevertheless, there indeed seems to be a problem. I've not looked at the code for a longer time now, so I have to re-read it myself :) and it is of course possible that there is a problem, or something I did not think of.
But it has to wait a few days, till weekend.
 
Sure, no hurry. Multiple separate files is probably a more common use case than 8-track WAVs, judging by the number of posts about it! I've been trying to think of a sane way of doing the SD reads outside the audio interrupt, but so far nothing has occurred to me that's really clean to use...

best regards

Jonathan
 
"Good" is just a matter of opinion :eek: Harder to do, definitely, but few of the posts I've seen made me think "you're taking totally the wrong approach, think again". Well, not from the SD playback perspective, anyway...

I have a nice little multi-track recorder (Tascam DP-008) which uses up to 32Gb SDHC cards, and can play 8 tracks while recording 2 (hmmm ... actually, it's only 8 track ... so play 6 and record 2), plus some limited effects. It's pretty old, been around since at least 2013, probably not much RAM or CPU under the hood, so I'd think a 2021 Teensy should be able to do at least that much. And I'm sure it could, if only bright folk like us can just crack the SD access problem in a fairly usable way.
 
Back
Top