Elastic Sheep

Because elasticdog was already taken

Elastic Sheep header image 2

Teensy2 USB WAV player – Part 2

June 29th, 2010 · 17 Comments · Uncategorized

[Update Jan 6th, 2011 / An error in the capacitor unit (pF instead of nF) has been fixed in the schematics]

Long overdue post in my USB WAV player implementation series. Let’s go !

General architecture

Here are the main software building blocks of the application:

The left side is the USB Mass Storage implementation based on the LUFA library. It is enabled if the user press a predefined button at power-on (see Part 1).

The right side is the WAV playback application and its related blocks.

  • The Player block offers a high-level API to control the playback of files,
  • The Wav parser contains some simple WAV-header parsing code to determine what are the sampling rate and number of channels of a file; it also helps to locate the audio data start in the file,
  • The Dac block implements the digital to analog conversion to output sound to a speaker.

DAC block

The DAC block converts digital samples to an analog signal using Pulse-Width Modulation (PWM) and a low-pass RC filter.

The PWM is generated from the Timer4 of the ATMEGA32U4 in Fast PWM mode with an 8-bit resolution. Using OCR4A and OC4B we can produce two independent PWM signals.

The PWM clock is set to the highest possible value (PeripheralClock / 256 = 187500 Hz) to avoid a fundamental harmonic close to the audio signal. The RC filter has a cut-off frequency of about 22kHz corresponding to the Nyquist frequency for the 44.1kHz sampling rate.

Two buffers of 8-bit samples (mono or stereo) are used as input. An interrupt triggered by Timer0 at the sampling rate (8kHz, 16kHz, 44.1kHz) read samples for one buffer and update the Timer4 output compare registers. When the end of one buffer is reached, the routine switch to the other one and trigger and End Of Buffer interrupt.

The End Of Buffer interrupt by Timer0 by writing a 0 output compare value. In OCR0B. This immediately raise the TIMER0_COMPB_vect interrupt. As its priority is lower than TIMER0_COMPA_vect, it will not block the sample transfer while being served by the Wav player block (The idea has been borrowed from LadyAda WaveHc library)

The double buffer mechanism allows the Wav player block to refill one buffer while the other one is copied to the PWM generation. It can be viewed as a particular implementation of a circular buffer.

Player block

Here is the WAV player API:

  • player_start
  • player_stop
  • player_pause
  • player_resume

When started, the player parse the WAV header from a file then fill its buffers and start the DAC block. Subsequent data copy from the file to buffer are handled in the End Of Buffer interrupt:

When no more data can be read from the file, the End Of File callback is called to notify the client application. This callback is provided by the client when calling the Player_start API (notify_eof).

The routine also performs the sample conversion if the file contains 16-bit samples.

Player performance

The following sampling rate are currently supported: 8kHz, 16kHz, 22.05kHz and 44.1kHz. Mono and stereo files can be played. As the DAC plays only 8-bit samples, 16-bit per sample files are converted to 8-bit samples when reading from the audio file.

The playback has been successfully tested with files up to 44.1kHz 16-bit mono files with a 16MHz clock. With 44.1kHz 16-bit stereo files, underflows are happening (To be investigated later to see if it can be supported with some optimization…)

The circuit

The audio outputs are connected to an active amplifier through a stereo jack connector. Capacitors are placed after the RC filters to remove the DC components of the analog signal.

A stereo potentiometer allows to control the level of both audio channels.

Here is the circuit schematic:

Demo applications

I coded two demo applications to illustrate possible use cases:

  • Shuffle: the application automatically plays the files in the root directory of the SD card. When the button 0 is pushed, the application randomly plays the next file.
  • Trigger: the playback of a predefined file is assigned to each push-button.

As the playback is fully handled with interrupts, the mainloop in those demos is only responsible for the push-button polling and to start/stop the playback depending on the use case.

Testing the demos

Go in the apps/shuffle or apps/trigger subdirectory and type make.
Upload the resulting hex file with the Teensy loader.
Copy some WAV file to an SD card root directory and power-on the hardware.

Here is the result:

Shuffle playback demo

Button-triggered playback demo

Source code

The source code and demo binaries can be downloaded from bitbucket.org.

Source: http://bitbucket.org/elasticsheep/teensy2-usb-wavplayer/

Archive: http://bitbucket.org/elasticsheep/teensy2-usb-wavplayer/get/V2.zip

Binaries: shuffle-ref-V2.hextrigger-ref-V2.hex

To my few readers

Any comment is welcome: if you learned something, if the code has been useful to a project of yours or if there is an error, leave a message ;)

Tags: ·····

17 Comments so far ↓

  • Martin

    Where I can buy this?! :-)
    You should bundle this in a small box…

    I stumpled across this, searching for a small in-expensive one-button mp3/wav player – e.g. for laying on the desktop with “applause” samples. Or hanging on the wall besides an (anti)-idol-poster for playing a sample of that person/band/whatever.

  • The Chief Sheep

    @Martin

    Thanks for the compliment.

    But my experiments are not optimized enough for cost/performance to be productized yet ;)

  • Heinz

    It it possible, that Your Low Pass Filter is wrong calculated. 10 pF is much too small. Maybe it should be 10nF….

  • The Chief Sheep

    Hi Heinz,

    You are right. I made a mistake in all my schematics using pF instead of nF.

    The cutoff frequency of my RC filter is:
    fc = 1 / (2 * PI * R * C) = 21220 Hz
    with R = 750 ohms
    and C = 10E-9 F = 10 nF

    Thanks for raising the issue. I will update my schematics soon.
    Mathieu

  • Hohenheim

    Am i glad I found this. Ok first of all thanks a million for documenting your work and sharing it.
    Am just pretty much getting into the uC’s world and my C/C++ is noobish still.

    I got a question, what information are you sending the Teensy via serial (Rx/Tx)

    i’ve been working on mine for a couple of days now based on the arduino documentation i’ve found and adapting it to the teensy but no success yet

  • The Chief Sheep

    Hi Hohenheim,

    For this project, the serial interface is used to output debug messages with the printf_P function.

    The printf_P function prints characters using the stdout FILE, then stdout is mapped on the SerialStream_TxByte function to send characters to the TX port. Serial and SerialStream drivers are provided by the LUFA project located in vendor/LUFA_091223/LUFA/Drivers/Peripherals.

    With the Arduino library, you should be able send strings on the serial interface with the Serial.print() method instead.

    Mathieu

  • Khanz

    Hi, how difficult can be to use it as a WAV recorder?

  • The Chief Sheep

    Hi Khanz,

    The principles would be very close:

    * Instead of the Timer0 interrupt playing samples from buffers, it would read values from an analog pin and store them in a buffer0/buffer1 flip-flop.
    * Once a buffer is full it would be written to the SD card while the other one is being filled up, using the sdreader API.

    You would also need some code to write header information to a WAV file.

    I don’t think it should be too difficult starting from my WAV player example. Maybe a subject for a future article…

    BR
    Mathieu

  • KC

    Glad that I found this site. I am also working on a similar WAV player but with another processor. The PWM frequency is 195khz. I am also the using the same LPF filter.

    The sound is generally acceptable but there are a little crackling or “static” in the background.

    I don’t know what is the reason for the crackling sound, I was wondering is it due to the insufficient filtering of the simple RC LPF or the wiring of my hardware or my software or just the quality I would expect from the PWM DAC.

    I could not tell from your video, did your player give clear sound without any crackling?

  • Phill Rogers

    Is there any reason why you couldn’t use PWM output on pins PB6 & PB5 rather than PB6 & PC7, if you moved the SW1 as well?
    I.e. are there any timer dependancies why?

    Thanks,

  • The Chief Sheep

    Hi Phill,
    PB6 and PB5 don’t share the same timer (Timer1 and Timer4).
    I used 2 outputs (OC4A & OC4B) of Timer4 because it is a high-speed timer on the ATMEGA32U4.
    This timer allows PWM outputs with a base frequency far from the audible spectrum.
    BR,
    Mathieu

  • Merle

    Mathieu,

    Very nice design. I am taking an Easy Button from Staples and making it into a Crazy Buttion (Crazy Train song) for my manager. I think this might be the best thing I found to do it. It looks small enough to fit and robust enough to be able to play both the “That was easy” and “Crazy Train” all in one button.

    Thanks for your engineering.

    Merle

  • Joel

    Hi Mathieu,
    I am looking at implementing this wav player, but I only need mono not stereo. Do I simply just remove all left channel components from PC7 and pot switch from PB5? Is there any software changes I have to make? Also, instead of using a push button to tell what mode to go into (i.e. application or mass storage), can I just use the state of VBUS pin? So if high, then go into mass storage mode, and if low go into application. This pin can be read by the ATMEGA32U4 right? If so, I think I need to put a 20k pull-down on VBUS, so it’s not left floating when USB cable not plugged in. I see that Adafruit uses a 12-bit DAC for their wave player. Would you recommend going with an external DAC like they did or just use the internal PWM channel like you did?

    Thanks,
    Joel

  • The Chief Sheep

    Hi Joel,
    * When playing from a mono WAV file, the output is duplicated on both PC7 and PB6. So you can just unplug one of those output.
    If you need to reuse the pin, you can remove the following code in dac_init() to free PB6:

    80 /* Enable a second PWM channel */
    81 TCCR4A |= _BV(COM4B1) | _BV(PWM4B);
    82 OCR4B = 0;
    83
    84 /* Enable the OC4B output */
    85 DDRB |= _BV(PORTB6);

    and in TIMER0_COMPA_vect:

    188 if (dac.channels == CHANNELS_STEREO)
    189 {
    190 OCR4B = *dac.read_ptr++;
    191 }
    192 else
    193 {
    194 OCR4B = l_sample;
    195 }

    * Yes if your circuit has another source of power, then you can check the presence of VBUS in the USBSTA register.
    Also you can detect a change of VBUS with the VBUSTI interrupt source.
    You should not use a pull-down on VBUS as they are already provided inside the ATMEGA32U4 (see datasheet figure 21-13).

    * Using an external DAC is interesting if you need a greater audio quality AND if it uses a protocol that you can implement with a hardware block of the processor to spare cycles. My design and Adafruit’s design both uses a timer at the sampling frequency to push samples. But in my design, I just have to update 2 registers (OCR4A and OCR4B) while with the Adafruit DAC, the DAC protocol must be bit-banged for each sample… It would be more efficient with a DAC that would contain a FIFO and that can be filled block by block instead of sample by sample.
    Also note the PWM of the ATMEGA32U4 is a high speed PWM that allows to avoid high frequency aliasing. With the ATMEGA168 of an Arduino, the highest PWM speed is too close of the sampling frequency. In this case the DAC is a better choice for the sound quality.

    BR
    Mathieu

  • Joel

    Hi Mathieu,
    Thanks! This is very helpful to me. I do not need great sound quality, so I can omit the external DAC and save a couple of bucks. I just thought that maybe you might be running into occasional underflow problem and having intermittent sound issues because of it. I tried running make to compile source, but I just get a bunch of error messages. What compiler should I be using for Windows 7? I have the latest WinAVR and Atml Studio 6.0 installed.

    Thanks for your help,
    Joel

  • Joel

    Hi Mathieu,
    Is it possible/easy to run this project as an Arduino sketch?

  • The Chief Sheep

    Joel,
    * There are underflows with the current code playing a 44.1kHz stereo file. The data throughput is too important for the sample copy ISR. It could be hand-optimized in assembler but it would not be an important gap in quality compared to 22kHz stereo.
    * To build the project I am using the CrossPack-AVR package on a Mac. It contains avr-gcc and the avr-libc. This is equivalent to WinAVR on a PC.
    * I think it would be difficult to port this project as an Arduino sketch. It would be easier in this case to start the project using the Adafruit AFWAve library.
    BR
    Mathieu

Leave a Comment