Require assistance porting WaveHC to Zero. Specifically timers + SD lib

I've posted a github repository with my work so far here:

I saw AlloyseTech has some some work with the timers here:

After looking through which timers are used where I agree with his decision to use TC5 for audio, as it is normally used by Tone, and you don't really need Tone at the same time as a Wav file. And TC4 is used for servos, which I need at the same time as Wav playback. TC3 is also available, but it would probably be best to leave that for something else. And I don't know what TCC0, 1, and 2 might be used for by the standad Arduino libs or even if they can be used for this sort of thing. As for TC6 and 7, those don't exist except on the SAMD21J.

I decided it would also be easier at the same time to just port the library over to the new SD library instead of trying to use the older SDfat stuff. They appear to be really similar and based on the older SDfat lib, so I think maybe it will work with a few class name changes and a few other tweaks. I don't know if there will be any performance hit though. But I removed all the extra files for SD and fat access, as well as the DAC files, as we'll be using the on board DAC for this.

Finally, this version of the library contains a bunch of tweaks I made. It has fine volume adjustment so you can control the volume with a pot digitally, and it limits the slew rate of the volume to avoid pops. This also works to prevent pops when initially powering up the board, as it slews the speaker to center instead of jumping there. In addition it has the ability to automatically loop a wav, and it supports peak detection which I use for sound reactive lighting.

So anyway... the first order of business is that I don't understand how the TC's on the Zero work precisely. I think they can be configured like the Atmega... The WaveHC lib needs two counters on one timer, each triggering a different ISR at different times, and I don't know how to accomplish that. So far I've only been able to find simple examples that set up a single function, and maybe you can only set up one function and you have to work things differently. I'm not sure.

Code like this:

void TC5_Handler (void) __attribute__ ((weak, alias("Audio_Handler")));

I don't really understand. It seems to be setting up one handler... but I don't see how you would set up more than one for each counter. Nor do I know what "weak" does. And it seems strange to me to specify the name of the function we want to call in quotes like that. I don't understand that at all. I would have expected like, a function pointer or something.

On the other hand in the servo lib you have this:

void Servo_Handler(timer16_Sequence_t timer, Tc *pTc, uint8_t channel, uint8_t intFlag);
#if defined (_useTimer1)
void HANDLER_FOR_TIMER1(void) {
    Servo_Handler(_timer1, TC_FOR_TIMER1, CHANNEL_FOR_TIMER1, INTFLAG_BIT_FOR_TIMER_1);
}
#endif
#if defined (_useTimer2)
void HANDLER_FOR_TIMER2(void) {
    Servo_Handler(_timer2, TC_FOR_TIMER2, CHANNEL_FOR_TIMER2, INTFLAG_BIT_FOR_TIMER_2);
}
#endif

void Servo_Handler(timer16_Sequence_t timer, Tc *tc, uint8_t channel, uint8_t intFlag)

And that seems to set up two different handlers for the two counters... or channels, I guess they're now called? On TC4... except I don't know from where the name HANDLER_FOR_TIMER1 comes from or how you specify that is called when that channel? reaches the specified value.

And of course there's other code in the library I still have to fix up. It doesn't write to the DAC yet, it is still set up for the MCP external dac, and for the old SD library, and the multiplication code in assembly needs to be ripped out since that was for the atmega. But for now I'm just trying to figure out how to replace the two ISR() function calls and set up those timers.

void TC5_Handler (void) __attribute__ ((weak, alias("Audio_Handler")));

The TC5_Handler() is an interrupt handler, anytimes there is interrupt triggered on TC5 (overflow, match, etc...), this function is called. You have to check in the function what is the cause of the interrupt. The weak attribute is something a bit special and looks strange at first. In a few words, it lets you declare another function which will replace it later. Here it's Audio_Handler(), which is called at each TC5 interrupt.

Doesn't my Zerodio lib fits your needs in term of audio? I've been working on it a lot a few months ago but didn't push it on github : it now play 4 wav file simultaneously, with volume adjustment, pop reduction etc. It needs a lot of work now to clean everything and solve some glitches.

I haven't tried your library because I thought you said somewhere it was buggy, and it looked a bit too simple on first glance. I assumed it didn't have the ability to loop files, and I wasn't sure if it could play WAV files with any sample rate or bit depth, or if there were going to be performance issues with the SD reading because I thought WaveHC might be streaming the reads in the background so it wouldn't cause hiccups in my application.

Just a matter of what I was more familiar with and I didn't see the ability to loop files or adjust volume pop out at me immediately. I should probably go back and have a closer look at it. After I cleaned out all the extra stuff in WaveHC it got a whole lot simpler looking.

As for my needs, I have a new board I'm releasing on Kickstarter soon... next week, I hope, and it's got an amplifier built in, so I need audio for a demo, and I want to be able to loop sound effects and tell people that there is an audio library available for them to use. Don't worry, it doesn't compete with that project I know you're working on. :slight_smile:

Playing four files simultaneously is pretty sweet. I'm curious how you were able to manage that because I thought with WaveHC the bottleneck, aside from the single 512 byte buffer, was the speed at which you can read the SD card over SPI, and the Zero isn't much faster there, unless perhaps you're using DMA transfers? WaveHC wasn't able to do more than one 44KHz file at a time and only just barely.

Well, the version I have on GitHub is a bit buggy in fact. You'll be able to read a wave file from your SD card correctly, but no fancy feature. The one I'm working on is much more powerfull : I use TC5 as an Audio handler, and TC3 as an SD Handler. It won't read 4 channels 16bit 44.1kHz stereo audio at the same time : My test are for 22050Hz 8bit mono wave file.

I would love to be able to use DMA transfer for the SD card, but that's a bit out of my capabilities... I try to bring the subject sometimes on the Storage section of the forum. Who knows, maybe someone will help us some day :slight_smile:

First things you should go for is the latest SdFat lib, which is better than the standard SD lib.
Second things which is mandatory : double buffering.

The hardest part is not generating the interrupt (for this, you can use my Zerodio lib as a good basis), it's to make your function as efficient as possible, play with interrupt's priority (reading audio file is less important than pushing a sample to the DAC for example), avoid SD card communication as much as possible...

More hardware related : I've been having a lot of trouble to get a clean signal from the DAC of SAMD21, and I'm still not 100% satisfied about the quality. You should really take care when choosing your pinmux : I've made the error once to put some high speed SPI on pins being supplied by VDDANA...

You mention double buffering… I wonder if using a circular buffer would be worthwhile?

Good discussion on them here:

My thoughts on this are that if you used a circular buffer you wouldn’t need to disable interrupts when reading from the SD card. The audio playback could continue as usual, and there would be no potential for a race condition when swapping the pointers.

Of course you would still need the same size buffer, because you need to read 512 bytes at a time from the SD card.

I use TC5 as an Audio handler, and TC3 as an SD Handler.

Why? That’s precisely what I was asking about how to accomplish with a single TC, like WaveHC does. Given how few TC there are it would seem prudent to use as few as possible.

It won’t read 4 channels 16bit 44.1kHz stereo audio at the same time : My test are for 22050Hz 8bit mono wave file.

I see. I was planning on using 16bit 44KHz mono for this demo just to see if I could, but with WaveHC on an Atmega I’d been using 22KHz 16bit mono.

22KHz 8bit mono certainly isn’t too bad a compromise in exchange for being able to mix audio files, but it would be nice if one could make a trade off and mix say two channels of 44KHz 8bit or two channels of 22KHz 16bit audio if you wanted to.

Hm… 48,000hz x 16 bit = 768,000 bits per second, theoretical max SPI bus speed 20MHz… I wonder if with DMA 4 channels of 48KHz 16bit mono audio would become possible? You’d be using 4K of ram to do it, but with 16K available that’s not too bad. Even with 8K on my Tau, 4K left is still not too shabby.

More hardware related : I’ve been having a lot of trouble to get a clean signal from the DAC of SAMD21, and I’m still not 100% satisfied about the quality. You should really take care when choosing your pinmux : I’ve made the error once to put some high speed SPI on pins being supplied by VDDANA.

Don’t really have the option with this board of changing that, but I’ll look into it. I assume you used the Zero’s standard SPI pins when you ran into your problem which may play in my favor as I am using the processor I used on the Tau which moves the SPI to pins 10-13 out of necessity.

First things you should go for is the latest SdFat lib, which is better than the standard SD lib.

In what way is it better?

I don’t doubt that is is, but I would prefer using the standard libraries where possible, because what if someone wants to access files on the SD card when they’re not playing audio? I do that one one of my boards to read config files. Obviously they could use SDFat to do that, but if all the tutorials are about how to do that with the SD library, that could be confusing, and who knows if using both libraries will conflict with one another.

Of course if there is a major performance boost from doing so…

More hardware related : I've been having a lot of trouble to get a clean signal from the DAC of SAMD21, and I'm still not 100% satisfied about the quality. You should really take care when choosing your pinmux : I've made the error once to put some high speed SPI on pins being supplied by VDDANA...

Hm, I checked the datasheet to see if I was okay with the pins I chose for accessing my SD card and they all use VDDIO, so I should be all set there.

I use double buffering because it lets some room on when I read the audio file : first, I fill the two buffer. when the first one has been fully pushed to the DAC, I switch buffer inside the DAC ISR. The TC3 ISR is always triggered at around 50Hz, and checks if the unused buffer has already been filled. If not, it start a transfer to read data from file in the ISR. TC5 ISR has higher priority : it means that if I need to push data to the DAC while reading the file, it will just pause the transfer, write data to the DAC, and go back to the TC3 ISR for file transfer.I don’t use the same ISR to write to the DAC and read from file because reading from the SD card doesn’t take always the same times, and if I have multiple audio file to read, it takes a lot of random times, which sometimes is hearable between 2 DAC update.

The main SdFat advantage is that you can have multiple file open at the same time. And transfer speed is better, depends on the card, but overall it’s better. SdFat is also much more supported now than the standard SD lib : Greiman is doing constant update, improving performances and compatible boards.

I used one dedicated SPI for the SD card and another one for other peripherals. I had not to much choices.

16bit is not really relevant since the DAC has only 10bit resolution… Using DMA is great to lighten CPU load, it won’t make transfer a lot faster. Of course it will be faster, but I’m not sure 48kHz 4channel would be possible.

Attached is a scope view of the SD’s SCK pin with 4 channel 22050kHz 8bit audio…

A solution to avoid using 2 TC, would be clocking the DAC at the wanted samplerate in free running mode and set up the DMA transfer to the DAC on Data Buffer Empty events (see table 35.6.3).

The main SdFat advantage is that you can have multiple file open at the same time. And transfer speed is better, depends on the card, but overall it's better.

I see.

I used one dedicated SPI for the SD card and another one for other peripherals. I had not to much choices.

Yeah, a long time ago I designed a board that used a single SPI line for my DAC and LED drivers, and I spent a month trying to get the transfer fast enough to make it work. In the end I had to create a new data line with some jumper wires. The SD being on the same SPI bus would be 10x as bad.

16bit is not really relevant since the DAC has only 10bit resolution...

Well, that's not entirely true. You can definitely hear a difference between 8 bit and 10 bit audio. There's less hiss in quieter sections with 10 bit. But I suppose that may not matter if there's already noise in the audio system. And you can at least take advantage of the 10 bit when mixing and doing digital volume adjustment.

Using DMA is great to lighten CPU load, it won't make transfer a lot faster. Of course it will be faster, but I'm not sure 48kHz 4channel would be possible.

Well I don't recall who it was but when I released the Pixel someone was doing some tests with DMA transfers to those sorts of displays and getting far higher frame rates than I could get no matter how much I optimized the SPI library. I only barely got better speed than on the Atmega, while the DMA blew it away.

Attached is a scope view of the SD's SCK pin with 4 channel 22050kHz 8bit audio...

Kinda hard to tell much from that aside from that it's busy most of the time?

Well, it's an hard choice : 8bit audio or 10 bit audio for simple or double the time needed to read the data... It's up to you, the overall library will work the same :slight_smile:
If you have 4 channel 8bit audio, the mixing result will give you a 10bit value :wink:

I was the guy testing SPI DMA. It went from around 8fps to 10fps to blit a binary image from the SD card. I attached the picture so that you see how much CPU time is left to the user application with 4channels 22kHz 8bit audio playing... That's why DMA could really help : all this CPU time would be available :slight_smile:

AloyseTech:
If you have 4 channel 8bit audio, the mixing result will give you a 10bit value :wink:

Well, yes... but if you do that then the maximum volume of any single sound effect is only 1/4 what it would be normally, which isn't great if you're playing sounds through a tiny speaker where volume is already at a premium. Actually, it's not great for one of my boards where I've got a 40W amp either because you need all that volume to be able to hear the thing in a noisy comic convention hall. :slight_smile:

But it's easy enough to modify the mixing to suit one's needs. You could add all the channels, or maybe split them into pairs, average them, and then add the results, which would be equivalent to dividing every sample by 2 before adding. Or you could just add them in a 32 bit number and then divide that by 2.

I did a little digging on mixing algorithms, and I don't see any which are recommended to be used, but I did find someone mention that if you add a bunch of sources then they may not clip as much as you expect:

The short version is that 10 sine waves added would have a maximum value 10x higher than normal, but 10 samples of random noise might be only 3.2x higher. And as he got these numbers from decibels, it was easy enough to calculate that with 4 random samples, the maximum value would average around 1.6x. So dividing by 2 would seem to be sufficient to mix 4 channels while avoiding clipping too often.

But given that I'm going for maximum volume in my case even a 50% reduction would be too much. So it might be prudent to provide some options for how the channels get mixed. This could be as simple as adding all the channels and then allowing the user to specify how much the value is shifted, so they can boost it with a shift left by 2 if they want to play one 8 bit sample at max 10-bit volume, or they can "boost" it by 1 if they want to play 4 samples but only divide the volume by 2 so it will be louder but may have some clipping, or they can boost it by 0 to have the same result you have where each sample is divided by 4. You could just call this the gain, and it would be separate from the regular fine volume control.

I was the guy testing SPI DMA. It went from around 8fps to 10fps to blit a binary image from the SD card. I attached the picture so that you see how much CPU time is left to the user application with 4channels 22kHz 8bit audio playing... That's why DMA could really help : all this CPU time would be available :slight_smile:

I remembered it being more like 16fps at my best jumping up to 49fps with DMA. But I haven't looked at that thread in a while so I could be mis-remembering.