Streamed waveform keeps changing sampling rate

Posted on
  • I have wired a headphone amplifier to pin A4 and copied the last example from this page:
    http://www.espruino.com/Waveform
    to stream a waveform from the SD card.

    For some strange reason, the playback sampling rate does not remain constant. It seems that every time a new buffer is used, the sampling rate changes to a random value roughly in the range +/- 100%, so a recorded voice alternates between normal, Mickey Mouse and monster a few times per second.

    What could be causing this? I am using the example code (only changed filenames). It doesn't matter if I try to play with 8, 11, 16 or 22 kHz, I always get this strange behavior.

  • Can you post the exact code you run?

  • For convenience I added a watch to Button 1. When the button is pressed, the play function is run, which illuminates LED1 and streams the raw file from the SD card to analog pin A4 as a waveform. When playing is done, it turns off the LED.

     function play() {
       var f = E.openFile("test16ku.raw","r");
       var w = new Waveform(2048, {doubleBuffer:true});
       // load first bits of sound file
       w.buffer.set(f.read(w.buffer.length));
       w.buffer2.set(f.read(w.buffer.length));
       var fileBuf = f.read(w.buffer.length);
       // when one buffer finishes playing, load the next one
       w.on("buffer", function(buf) {
         buf.set(fileBuf);
         fileBuf = f.read(buf.length);
         if (fileBuf===undefined) {
           w.stop(); // end of file
           LED1.write(0);
         }
       });
       LED1.write(1); //turn on led1
       analogWrite(A4, 0.5);
       w.startOutput(A4,16000,{repeat:true}); 
     }
     
     function btnEvent() {
       if (digitalRead(BTN1) == 1)
         play(); 
     }
     
     setWatch(btnEvent, BTN1, true);
    
  • What I think happens is - since you do not debounce your button - that multiple play() run at the same time, and due to interference with time/phase constantly shifting funny sounds... (to verify that, put in our current code var cnt = 0; before the line function play(), and cnt++ after it. Then you upload the code and press the button. When it has finished playing, enter in console cnt and tell me what you got!

    Therefore, do something like this for watching your button:

    setWatch(play, BTN1, { repeat: false, edge:'xxxxx' , debounce:50});
    

    xxxxx is either rising or falling - both is working (if you do not hold on to the button until play has finished).

    You put this line as line 27 and you also insert it into your stop after line 14 (in which you switch your LED1 off).

    When this is working - with xxxx the way that the play starts only after you have released the button - you can then make it stop when pressing and releasing the button while it is playing.

    Looking forward to hear from you!

    PS: I did not work with files yet on Espruino, but I wonder if you need to do something like close(), do you?

  • Thank you, it is certainly a good thing to de-bounce the button.

    The problem is not related to bouncing though, because the exact same problem occurs if I only send the contents of the play function to the board and no button is involved. It really sounds like the sample output rate changes every few thousand samples... which is strange, since it is only set once in w.startOutput. Any other ideas?

  • Fort the stop play:

    Make this line

    var watch;
    

    the very first line in your code and insert after the line in which you turn the LED1 on these lines:

    watch = setWatch(function(){ // set the watch for stopping
        w.stop(); 
        setWatch(play, BTN1, { repeat: false, edge:'xxxxx' , debounce:50});
      }, BTN1, { repeat: false, edge:'xxxxx' , debounce:50});
    

    Then add in the stop before the line with w.stop(); this line:

    clearWatch(watch); // clears the watch for stopping
    
  • Interesting... no other idea right now...

  • ...but the code assumes that there is at least data available to fill three buffers (or at least the first two fully and the third partially)... because I do not see any checks before these three buffers are getting used. May be the implementation is tolerant to the case were not even the first buffer is full... ;) I'm also not sure if stopping at the moment when reading ahead the content for the next buffer switch is correct... the waveform may still have things to play off of the buffer to which it just has switched. @Gordon will for sure provide the needed insight here!

  • The sample is long enough to fill 3 buffers (30 kbytes should fill 15 buffers of 2048 bytes). Note that this code comes from the original Espruino documentation. Even if I copy it without any changes (such as the button interface), I get that problem. The JavaScript should be fine and it looks fine to me... so the problem is perhaps rooted even deeper (in the firmware, on a level below the JS interpreter)?

    I don't know what happens under the hood, so I can only speculate, but is it possible that something (reading from the SD?) is slowing down the waveform output loop (hence the lower output frequency) and then some regulative code in the firmware compares the actual output position to the supposed one and speeds up the output in order to "catch up" (hence the higher output frequency)?

    Not sure if it matters, but I have a bluetooth module soldered onto the board (which I am not using at the same time)

  • Update: I just flashed the latest firmware onto the board and the behavior changed. Now the sampling rate seems to remain constant. The original issue (changing sampling rate) is fixed :-D Instead, I now get a different problem: little bits of the audio are repeating. This looks very much like the buffers cannot be re-filled fast enough and the playback runs into old data before the new data is ready. That sucks, but it's much less mysterious.

    Some experimenting has shown that with a buffer size of 1024 samples, the maximum sampling rate achievable without audible artifacts is 10000 Hz. For a buffer size of 2048, it's 14000 Hz. I cannot make the buffer much bigger, since Espruino will run out of memory.

    The documentation metions output rates of 20 KHz... has anybody achieved better results than me, while streaming from an SD?

  • Oh yeah - Gordon fixed some issues with waveform recently, that'd be that.

    It might be loading the data off the SD card that's slowing it down? Also, what's the default SPI clock speed? I wonder if it's slow enough that the SPI data transfer time is an issue? You could try specifying a higher baud, and see if that helps.

    Can you post the latest code you're using?

  • I now get a different problem: little bits of the audio are repeating.

    Yes, that'll be the time it takes Espruino to load the data off the SD card. As @DrAzzy says, you might be able to bump the clock rate of the SD card's SPI up:

    require('fs').readdir(); // force SD card to get mounted
    SPI2.setup({sck:B13, miso:B14, mosi:B15, baud:4000000}); // now re-setup SPI
    

    But I don't think you'll get a huge amount better. While the documentation mentions 20KHz, that's when playing a file that's already loaded into RAM - trying to stream it off the SD card as well is substantially more painful for Espruino.

  • @Gordon, you mentioned a while ago on my memory manager thread something about DMA... could that help? Something like a module that let's you setup a DMA between two connected devices/'comm protocols'? It can evade the back and forth between fast firmware and slower source interpreted JS? The module should be an on-demand loadable module but would for need pieces in the firmware. The firmware pieces are like a switch board or clearing / routing house for data in transfer. I see the available firmware memory melt away like snow in summer...

  • DMA itself is relatively basic - 'read X bytes from Y and copy them to Z'. Handling FAT32 with it would be a no go. Potentially there's the ability to re-write bits of the FAT library to use DMA and become async, but that'd require a new filesystem API as well and is a big chunk of work :(

    So the other option is to use DMA for the Waveform output itself. That's definitely possible and could potentially be a second mode of operation for Waveforms. The problem I have is that when you move away from the current software solution, things are so interconnected that you can no longer hide the complexity from the user - which is what I've been trying to avoid with Espruino.

    If I did waveforms with DMA you wouldn't be allowed to do it if you were also using PWM on certain other pins because you need to hijack a timer from somewhere to drive DMA. Doing two waveforms would be even more tricky, and overlapping them on the same output would be impossible. Suddenly instead of copying 6 lines of JS from the website you've got to read a 1000 page STM32 reference manual.

    ... and if you're willing to read that manual you can actually set up DMA right now with the peek and poke functions. It's just that nobody ever does because it's such a nightmare :)

  • Have a look at VS1053 breakout boards, ideally with SD card. Programmable by SPI, there arduino libraries and examples.

  • Post a reply
    • Bold
    • Italics
    • Link
    • Image
    • List
    • Quote
    • code
    • Preview
About

Streamed waveform keeps changing sampling rate

Posted by Avatar for Dennis @Dennis

Actions