SDMMCWavPlayer
Introduction
EasyPIC3 development board used for prototyping
Here is a simple project to create a Wav file player, with files being read from a Secure Digital or Multi Media Card (SD or MMC). The files are read using the SDFileSystem library and the bytes are stored in a circular buffer. A timer interrupt reads bytes from the buffer and sends them to a R-2R digital to analogue converter (DAC) on Port D. A basic filter is used to remove the high frequency noise from the output. In the prototype version of this project, the output is then fed to an external amplifier, although a simple op-amp amplifier could be constructed as part of the project.
The files are read sequentially in the order that they appear on the card. After the last file has been played, the code stops. The header of each file is checked to ensure that it is a Wav file of the correct format - only 8-bit, mono files of 16kHz sampling rate can be played with the attached code, although it would be easy to adapt the code to play files of a lower sampling rate. With the PIC running at 32MHz, it is not possible to go to higher sampling rates using this code.
An alphanumeric LCD is used to display the name of the file being played. It also shows a progress bar indicating the fraction of the file that has been played. There is much scope for adding to the sophistication of the code in this area.
Two LEDS on Port E are also used in the code - that on PORTE.0 indicates that a buffer under-run has occurred whilst reading a file, whilst that on PORTE.1 indicates that the buffer is full. The circular buffer is used to prevent glitches in the output whilst the SD card accesses a new cluster.
During testing, the sound quality was initially quite poor. However, by adjusting the resistors in the R-2R ladder carefully, the sound quality was much improved.
Extensions to the project might include:
- the ability to select the file to play using buttons
- the inclusion of play, pause and stop buttons
- the option to play files in a random order
All of the above would be relatively straightforward. There is ample code space still available, since on a PIC18F452 the code only occupies 6677 bytes of the 32768 available.
I hope that this simple project gives you some ideas.
Circuit Diagram
Project Code
{ ******************************************************************************** * Name : WavPlayer.BAS * * Author : S Wright * * Notice : Copyright (c) 2006 S Wright * * : All Rights Reserved * * Date : 25/10/2006 * * Version : 1.0 * * Notes : * * : * ******************************************************************************** } Device = 18F452 Clock = 32 Config OSC = HSPLL #option SD_SPI = MSSP #option SD_SUPPORT_SUB_DIRECTORIES = True #option LCD_DATA = PORTB.4 #option LCD_RS = PORTB.2 #option LCD_EN = PORTB.3 #option LCD_INIT_DELAY = 300 Include "SDFileSystem.bas" Include "LCD.bas" Const BufferSize = 500 // buffer to avoid glitches Const TimerReload = 65036 // alter for different sample rates Dim Timer As TMR1L.AsWord // alias to Timer1 Dim TimerOn As T1CON.Booleans(0) // start and stop Dim TimerEnabled As PIE1.Booleans(0) // enable interrupts Dim TimerInterrupt As PIR1.Booleans(0) // interrupt flag Dim File As String(13) Dim WritePointer, ReadPointer As Word // read/write pointers in Dim Buffer(BufferSize) As Byte // circular buffer Dim DataSize As LongWord // size of WAV data in file Dim Progress As LongWord // increment progress bar when zero Dim ProgressIncrement As LongWord // progress bar step { ******************************************************************************** * Name : OnTimer (Interupt) * * Purpose : Sends bytes to R-2R DAC on PORT D to play file * ******************************************************************************** } Interrupt OnTimer() // ISR Timer = TimerReload Save(0) // context save If ReadPointer = WritePointer Then // buffer empty PORTE.0 = 1 // set buffer empty indicator Else Inc(ReadPointer) // increment read pointer If ReadPointer = BufferSize Then // if end of buffer then... ReadPointer = 0 // move back to beginning of buffer EndIf PORTD = Buffer(ReadPointer) // move byte to PORTD (R-2R ladder DAC) EndIf TimerInterrupt = False // clear interrupt flag Restore End Interrupt { ******************************************************************************** * Name : ReadFmt * * Purpose : Reads header of WAV file and checks compatibility * ******************************************************************************** } Function ReadFmt() As Boolean // read fmt block of WAV file Dim Index As Byte ReadFmt = True Index = 0 Repeat // skip over RIFF header SD.ReadLongWord Inc(Index) Until Index = 3 If SD.ReadChar <> "f" Then // check in fmt block ReadFmt = False EndIf If SD.ReadChar <> "m" Then ReadFmt = False EndIf If SD.ReadChar <> "t" Then ReadFmt = False EndIf If SD.ReadChar <> " " Then ReadFmt = False EndIf SD.ReadLongWord // skip over fmt size If SD.ReadWord <> 1 Then // check PCM (un-compressed) file ReadFmt = False EndIf If SD.ReadWord <> 1 Then // check 1 channel (mono) file ReadFmt = False EndIf If SD.ReadLongWord <> 16000 Then // check sample rate (16kHz) ReadFmt = False EndIf Index = 0 Repeat // skip to data size SD.ReadByte Inc(Index) Until Index = 12 DataSize = SD.ReadLongWord SD.ReadLongWord // skip to data section ProgressIncrement = DataSize / 16 End Function { ******************************************************************************** * Name : ProgressBar * * Purpose : Updates progress bar during playback * ******************************************************************************** } Sub ProgressBar() Dec(Progress) If Progress = 0 Then Progress = ProgressIncrement LCD.Write("=") EndIf End Sub { ******************************************************************************** * Name : SoftStart * * Purpose : Ramps DAC output to new value to avoid clicks * ******************************************************************************** } Sub SoftStart(pNewValue As Byte) Dim Value As Byte Value = PORTD Repeat If Value < pNewValue Then Inc(Value) ElseIf Value > pNewValue Then Dec(Value) EndIf PORTD = Value DelayUS(20) Until Value = pNewValue End Sub { ******************************************************************************** * Name : PlayFile * * Purpose : Plays file * ******************************************************************************** } Sub PlayFile(pFile As String) Dim TempWritePointer As Word LCD.Cls LCD.Writeat(1, 1, "{ ", pFile, " }") LCD.WriteAt(2, 1, " OPENING FILE ") SD.OpenFile(pFile) Timer = TimerReload // set timer one ReadPointer = 0 // initialise pointers WritePointer = 0 If Not(ReadFmt()) Then // incompatible file format LCD.WriteAt(2, 1, "WRONG FILE TYPE!") Else LCD.WriteAt(2, 1, "----------------") Progress = ProgressIncrement LCD.MoveCursor(2,1) Repeat // fill buffer initially Inc(WritePointer) Buffer(WritePointer) = SD.ReadByte Dec(DataSize) ProgressBar() Until (WritePointer = BufferSize - 1) Or SD.EOF SoftStart(Buffer(1)) // ramp up to first byte to play TimerOn = True // start timer - play starts Repeat TempWritePointer = WritePointer // use a temp pointer in order Inc(TempWritePointer) // to prevent interrupt If TempWritePointer = BufferSize Then // detecting a buffer underun TempWritePointer = 0 // when buffer is full EndIf While TempWritePointer = ReadPointer PORTE.1 = 1 Wend PORTE.1 = 0 WritePointer = TempWritePointer Buffer(WritePointer) = SD.ReadByte // write next byte to buffer Dec(DataSize) ProgressBar() Until SD.EOF Or (DataSize = 0) EndIf SD.CloseFile Repeat // wait until buffer is empty Until ReadPointer = WritePointer TimerOn = False // stop timer PORTE = 0 End Sub // Main program... TRISD = 0 TRISE = 0 PORTE = 0 SoftStart(127) TimerOn = False // stop timer initially Enable(OnTimer) // assign the interrupt handler TimerEnabled = True // enable timer interrupts LCD.Cls LCD.WriteAt(1, 1, "PLEASE INSERT SD") Repeat Until SD.Init(spiOscDiv4) Repeat File = SD.Dir(dirNext, sdFile) // search for next file on SD card If File <> Null Then // file found PlayFile(File) EndIf Until File = Null LCD.Cls LCD.WriteAt(1, 5, "FINISHED")