delay(1) after non-blocking I2S::write() returned 0 causes audio distortion (No underflow) #1491
-
Main issue (for now): If there is a tiny delay() after a non-blocking I2S::write() returns 0, audio is heavily distorted. But NO underflow occurs. I expect a delay(1) if write returned 0 to cause no problems. Because I assume that write() returned 0 due to full buffers. StarWars30.h from here: https://github.com/pschatzmann/arduino-audio-tools/tree/main/examples/examples-stream/streams-memory_raw-i2s #include <Arduino.h>
#include <I2S.h>
#include <StarWars30.h>
I2S i2s = I2S(OUTPUT);
const unsigned char *ptr = StarWars30_raw;
size_t toPlay = StarWars30_raw_len;
long sampleRate = 22050;
void setup() {
pinMode(LED_BUILTIN,OUTPUT);
digitalWrite(LED_BUILTIN,LOW);
i2s.setBCLK(26); //WS = 27
i2s.setDATA(28);
i2s.setBitsPerSample(16);
i2s.setBuffers(32,16);
if(i2s.begin(sampleRate)){
digitalWrite(LED_BUILTIN,HIGH);
}
}
void loop() {
while(toPlay){
//===Make the track stereo and quieter
int16_t frame[2];
frame[0] = (*(int16_t*)ptr) /8;
frame[1] = (*(int16_t*)ptr) /8;
//===Write one L+R sample to I2S
while(!i2s.write(*(int32_t*)frame,false)){
//while(!i2s.write((uint8_t*)frame,4)){
delay(1);//No underflows get detected but audio is weirdly distorted.
//delay(100);//Causes underflow (that gets detected) and "silence stutter silence stutter..." (as expected)
//remove both delays: audio is played nicely. There are tiny pops though
}
//i2s.write(*(int32_t*)frame,true);//use blocking write instead of non-blocking: audio is played nicely. There are tiny pops though
toPlay -= 2;
ptr += 2;
//===Detect underflows. Turn LED off if underflow was detected
if(toPlay < StarWars30_raw_len - 512){//give it some time to fill the buffers. There is always an underflow at the beginning
if(i2s.getOverUnderflow()){
digitalWrite(LED_BUILTIN,LOW);
}
} else {
i2s.getOverUnderflow();
}
}
//===END
i2s.end();
} Added to libraries/I2S/src/I2S.h: bool getOverUnderflow(){
return _arb->getOverUnderflow();
} How it sounds with the delay(1): https://www.youtube.com/watch?v=p6itk2GX288 My guesses:
Context what I'm even trying to do +2 bonus problems I'm having: TL;DR of the issue I'm trying to fix: Additional info in case you don't know the arduino-audio-tools library and how it works: There might be two additional issues/weird behaviours.
|
Beta Was this translation helpful? Give feedback.
Replies: 5 comments 6 replies
-
Anyway, looking at the math: At 22050hz, you need ~22 samples in 1 millisecond. Let's bump that up to 2x assuming some wiggle in the delay() call and some overhead elsewhere. So ~44 samples per ms. Your However, with each buffer having 16 LR samples, that means @ 22khz you need to service an IRQ every If at the higher buffer size playback is still spotty, we'll need to get a logic analyzer on the I2S bus to see what's going on. (In that case, send in a SilenceValue of
The PIO has its own clocking and is unaffected by the core sleeping. If the PIO FIFO underflows (filled by DMA) then the I2S clock/data will stop until more data is pushed into the FIFO. |
Beta Was this translation helpful? Give feedback.
-
Thank you for your ideas and information. As for power: I'm using an MAX98357 I2S amp which is powered by Vbus of the pico. And both are powered by my PC's USB. Some more testing: Increasing the amount of buffers (16, 32, 64, 128, 512) but keeping the size at a constant 64: (Size of 4096 caused the track to be interrupted by complete noise. Amount of 1024 caused loud beeps about every 2 seconds. I expected weird behaviour like this with extreme sizes.) setBuffers(16,64) and:
unsigned long endOfWait = millis();
endOfWait += 1;
while(millis() < endOfWait); => no noticeable change vs delay(1) either. To check if the other core has any effect on this, I tried using setBuffers(16,64) and:
So, the second core has, as far as I can tell, zero effect on this issue. Using blocking write but adding this above the blocking write: while(!i2s.availableForWrite()){
delay(1);
}
i2s.write(*(int32_t*)frame,true); => Same effect as the delay() after non-blocking write returned 0. I think it really has something to do with how often interrupts are serviced. Workaround for now: Have absolutely no delay on the core that handles I2S. Don't busy wait with millis() either. Not even when the buffers are full. I think I can work with that. I'm just still curious why, though it's probably a bit over my head. :D |
Beta Was this translation helpful? Give feedback.
-
Update to bonus issue 2:
This "type of pop" occurs even when there is zero delay. And even when using blocking write. But:
|
Beta Was this translation helpful? Give feedback.
-
Good debug, thanks a lot! It does narrows things down.
This may indicate that the USB interrupt is the root of the trouble. It runs on core0 1000 times a second, at least, and it may block the DMA done IRQ from processing while it's ongoing. For one more test, would you be able to build using It may also point to a mutex issue on the list of full/empty buffers, but there's nothing to test to pinpoint that. I'll just need to eyeball things in the code. As for the I'll also need to double check I maximize the PIO FIFOs (combine TX/RX) which also helps to take pressure off the system. |
Beta Was this translation helpful? Give feedback.
-
I'm using Platformio. Adding With USB disabled, the pops are still there. |
Beta Was this translation helpful? Give feedback.
Just as suspected from (4) above, the DMA swapover had an issue. Please try the very simple change in #1500 (with `setBuffers(4, 64) just for sanity since OTW your IRQs are pretty hot and heavy). Your MCVE seems to play fine now with that change, with either delay or busy_wait_ms.
--edit-- setBuffers(32, 16) w/delay(1) also seems to work too. Higher IRQ load, but enough buffers to handle 1ms sleeps every now and then.