/* Arduino WaveHC Library * Copyright (C) 2009 by William Greiman * * This file is part of the Arduino WaveHC Library * * This Library is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This Library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with the Arduino WaveHC Library. If not, see * . */ /** \mainpage Arduino WaveHC Library
Copyright © 2009 by William Greiman
\section Intro Introduction WaveHC is an Arduino library for the Adafruit Wave Shield. It can play uncompressed mono Wave(.WAV) files at sample rate up to 44.1 K samples per second. Only the high 12 bits of 16-bit files are used. Audio files are read from an SD flash memory card. Standard SD and high capacity SDHC flash memory cards are supported with FAT16 or FAT32 file systems. The WaveHC only supports short FAT 8.3 names. WaveHC does not support MMC flash cards. \section comment Bugs and Comments If you wish to report bugs or have comments, send email to fat16lib@sbcglobal.net. \section SDcard SD/SDHC Cards Arduinos access SD cards using the cards SPI protocol. PCs, Macs, and most consumer devices use the 4-bit parallel SD protocol. A card that functions well on A PC or Mac may not work well on the Arduino. Most cards have good SPI read performance but cards vary widely in how there SPI hardware interface is implemented. Newer card require very fast rise times for SPI signals. Version 1.0 of the Wave Shield may not work well with these cards. Ladyada's improved Version 1.1 works with almost all SD/SDHC cards. The default SPI clock rate is 8 Mhz. It may be helpful on Version 1.0 wave shields to reduce this to 4 Mhz. See SdReader::init() for details. SanDisk cards generally have good performance in the Version 1.0 Wave Shield. \section WaveHCClass WaveHC Usage See Ladyada's excellent tutorial on using WaveHC: http://www.ladyada.net/make/waveshield/libraryhc.html Also see the readme.txt file for instructions on installing WaveHC. Advanced users may need to edit the WavePinDefs.h file. WaveHC uses a slightly restricted form of short file names. Only printable ASCII characters are supported. No characters with code point values greater than 127 are allowed. Space is not allowed even though space was allowed in the API of early versions of DOS. Short names are limited to 8 characters followed by an optional period (.) and extension of up to 3 characters. The characters may be any combination of letters and digits. The following special characters are also allowed: $ % ' - _ @ ~ ` ! ( ) { } ^ # & Short names are always converted to upper case and their original case value is lost. \section HowTo How to Format and Prepare SD Cards for WaveHC WaveHC is optimized for contiguous files. It will only play 16-bit 44.1 K files if they are contiguous. All files copied to a newly formatted card will be contiguous. It is only possible to create a fragmented file if you delete a file from an SD and copy a larger file to the SD. You should use a freshly formatted SD card for best performance. FAT file systems become slower if many files have been created and deleted. This is because the directory entry for a deleted file is marked as deleted, but is not deleted. When a file is opened, these entries must be scanned to find the file to be opened, a flaw in the FAT design. Also files can become fragmented which causes reads to be slower. Microsoft operating systems support removable media formatted with a Master Boot Record, MBR, or formatted as a super floppy with a FAT Boot Sector in block zero. Microsoft operating systems expect MBR formatted removable media to have only one partition. The first partition should be used. Microsoft operating systems do not support partitioning SD flash cards. If you erase an SD card with a program like KillDisk, Most versions of Windows will format the card as a super floppy. The best way to restore an SD card's format is to use SDFormatter which can be downloaded from: http://www.sdcard.org/consumers/formatter/ SDFormatter aligns flash erase boundaries with file system structures which reduces write latency and file system overhead. SDFormatter does not have an option for FAT type so it may format small cards as FAT12. After the MBR is restored by SDFormatter you may need to reformat small cards that have been formatted FAT12 to force the volume type to be FAT16. If you reformat the SD card with an OS utility, choose a cluster size that will result in: 4084 < CountOfClusters && CountOfClusters < 65525 The volume will then be FAT16. If you are formatting an SD card on OS X or Linux, be sure to use the first partition. Format this partition with a cluster count in above range. \section References References Adafruit Industries: http://www.adafruit.com/ http://www.ladyada.net/make/waveshield/ The Arduino site: http://www.arduino.cc/ For more information about FAT file systems see: http://www.microsoft.com/whdc/system/platform/firmware/fatgen.mspx For information about using SD cards as SPI devices see: http://www.sdcard.org/developers/tech/sdcard/pls/Simplified_Physical_Layer_Spec.pdf The ATmega328 datasheet: http://www.atmel.com/dyn/resources/prod_documents/doc8161.pdf */ #include #include #include #include #include // verify program assumptions #if PLAYBUFFLEN != 256 && PLAYBUFFLEN != 512 #error PLAYBUFFLEN must be 256 or 512 #endif // PLAYBUFFLEN WaveHC *playing = 0; uint8_t buffer1[PLAYBUFFLEN]; uint8_t buffer2[PLAYBUFFLEN]; uint8_t *playend; ///< end position for current buffer uint8_t *playpos; ///< position of next sample uint8_t *sdbuff; ///< SD fill buffer uint8_t *sdend; ///< end of data in sd buffer // status of sd #define SD_READY 1 ///< buffer is ready to be played #define SD_FILLING 2 ///< buffer is being filled from DS #define SD_END_FILE 3 ///< reached end of file uint8_t sdstatus = 0; //------------------------------------------------------------------------------ // timer interrupt for DAC ISR(TIMER1_COMPA_vect) { if (!playing) return; if (playpos >= playend) { if (sdstatus == SD_READY) { // swap double buffers playpos = sdbuff; playend = sdend; sdbuff = sdbuff != buffer1 ? buffer1 : buffer2; sdstatus = SD_FILLING; // interrupt to call SD reader TIMSK1 |= _BV(OCIE1B); } else if (sdstatus == SD_END_FILE) { playing->stop(); return; } else { // count overrun error if not at end of file if (playing->remainingBytesInChunk) { playing->errors++; } return; } } uint8_t dh, dl; if (playing->BitsPerSample == 16) { // 16-bit is signed dh = 0X80 ^ playpos[1]; dl = playpos[0]; playpos += 2; } else { // 8-bit is unsigned dh = playpos[0]; dl = 0; playpos++; } #if DVOLUME uint16_t tmp = (dh << 8) | dl; tmp >>= playing->volume; dh = tmp >> 8; dl = tmp; #endif // DVOLUME // dac chip select low mcpDacCsLow(); // send DAC config bits mcpDacSdiLow(); mcpDacSckPulse(); // DAC A mcpDacSckPulse(); // unbuffered mcpDacSdiHigh(); mcpDacSckPulse(); // 1X gain mcpDacSckPulse(); // no SHDN // send high 8 bits mcpDacSendBit(dh, 7); mcpDacSendBit(dh, 6); mcpDacSendBit(dh, 5); mcpDacSendBit(dh, 4); mcpDacSendBit(dh, 3); mcpDacSendBit(dh, 2); mcpDacSendBit(dh, 1); mcpDacSendBit(dh, 0); // send low 4 bits mcpDacSendBit(dl, 7); mcpDacSendBit(dl, 6); mcpDacSendBit(dl, 5); mcpDacSendBit(dl, 4); // chip select high - done mcpDacCsHigh(); } //------------------------------------------------------------------------------ // this is the interrupt that fills the playbuffer ISR(TIMER1_COMPB_vect) { // turn off calling interrupt TIMSK1 &= ~_BV(OCIE1B); if (sdstatus != SD_FILLING) return; // enable interrupts while reading the SD sei(); int16_t read = playing->readWaveData(sdbuff, PLAYBUFFLEN); cli(); if (read > 0) { sdend = sdbuff + read; sdstatus = SD_READY; } else { sdend = sdbuff; sdstatus = SD_END_FILE; } } //------------------------------------------------------------------------------ /** create an instance of WaveHC. */ WaveHC::WaveHC(void) { fd = 0; } //------------------------------------------------------------------------------ /** * Read a wave file's metadata and initialize member variables. * * \param[in] f A open FatReader instance for the wave file. * * \return The value one, true, is returned for success and * the value zero, false, is returned for failure. Reasons * for failure include I/O error, an invalid wave file or a wave * file with features that WaveHC does not support. */ uint8_t WaveHC::create(FatReader &f) { // 18 byte buffer // can use this since Arduino and RIFF are Little Endian if (!DVOLUME) { Serial.println("DVOLUME must be set to non-zero in WaveHC.h"); return false; } union { struct { char id[4]; uint32_t size; char data[4]; } riff; // riff chunk struct { uint16_t compress; uint16_t channels; uint32_t sampleRate; uint32_t bytesPerSecond; uint16_t blockAlign; uint16_t bitsPerSample; uint16_t extraBytes; } fmt; // fmt data } buf; #if OPTIMIZE_CONTIGUOUS // set optimized read for contiguous files f.optimizeContiguous(); #endif // OPTIMIZE_CONTIGUOUS // must start with WAVE header if (f.read(&buf, 12) != 12 || strncmp(buf.riff.id, "RIFF", 4) || strncmp(buf.riff.data, "WAVE", 4)) { return false; } // next chunk must be fmt if (f.read(&buf, 8) != 8 || strncmp(buf.riff.id, "fmt ", 4)) { return false; } // fmt chunk size must be 16 or 18 uint16_t size = buf.riff.size; if (size == 16 || size == 18) { if (f.read(&buf, size) != (int16_t)size) { return false; } } else { // compressed data - force error buf.fmt.compress = 0; } if (buf.fmt.compress != 1 || (size == 18 && buf.fmt.extraBytes != 0)) { putstring_nl("Compression not supported"); return false; } Channels = buf.fmt.channels; if (Channels > 2) { putstring_nl("Not mono/stereo!"); return false; } else if (Channels > 1) { putstring_nl(" Warning stereo file!"); } BitsPerSample = buf.fmt.bitsPerSample; if (BitsPerSample > 16) { putstring_nl("More than 16 bits per sample!"); return false; } dwSamplesPerSec = buf.fmt.sampleRate; uint32_t clockRate = dwSamplesPerSec * Channels; uint32_t byteRate = clockRate * BitsPerSample / 8; #if RATE_ERROR_LEVEL > 0 if (clockRate > MAX_CLOCK_RATE || byteRate > MAX_BYTE_RATE) { putstring_nl("Sample rate too high!"); if (RATE_ERROR_LEVEL > 1) { return false; } } else if (byteRate > 44100 && !f.isContiguous()) { putstring_nl("High rate fragmented file!"); if (RATE_ERROR_LEVEL > 1) { return false; } } #endif // RATE_ERROR_LEVEL > 0 fd = &f; errors = 0; isplaying = 0; remainingBytesInChunk = 0; volume = 0; // position to data return readWaveData(0, 0) < 0 ? false : true; } //------------------------------------------------------------------------------ /*! * @brief Returns true if the player is paused else false. * @returns true if paused, false otherwise */ uint8_t WaveHC::isPaused(void) { cli(); uint8_t rtn = isplaying && !(TIMSK1 & _BV(OCIE1A)); sei(); return rtn; } //------------------------------------------------------------------------------ /** * Pause the player. */ void WaveHC::pause(void) { cli(); TIMSK1 &= ~_BV(OCIE1A); // disable DAC interrupt sei(); fd->volume()->rawDevice()->readEnd(); // redo any partial read on resume } //------------------------------------------------------------------------------ /** * Play a wave file. * * WaveHC::create() must be called before a file can be played. * * Check the member variable WaveHC::isplaying to monitor the status * of the player. */ void WaveHC::play(void) { // setup the interrupt as necessary int16_t read; playing = this; // fill the play buffer read = readWaveData(buffer1, PLAYBUFFLEN); if (read <= 0) return; playpos = buffer1; playend = buffer1 + read; // fill the second buffer read = readWaveData(buffer2, PLAYBUFFLEN); if (read < 0) return; sdbuff = buffer2; sdend = sdbuff + read; sdstatus = SD_READY; // its official! isplaying = 1; // Setup mode for DAC ports mcpDacInit(); // Set up timer one // Normal operation - no pwm not connected to pins TCCR1A = 0; // no prescaling, CTC mode TCCR1B = _BV(WGM12) | _BV(CS10); // Sample rate - play stereo interleaved OCR1A = F_CPU / (dwSamplesPerSec * Channels); // SD fill interrupt happens at TCNT1 == 1 OCR1B = 1; // Enable timer interrupt for DAC ISR TIMSK1 |= _BV(OCIE1A); } //------------------------------------------------------------------------------ /*! Read wave data. * * @brief Not for use in applications. Must be public so SD read ISR can access * it. Insures SD sectors are aligned with buffers. * @param buff pointer to the buffer where the data should be placed * @param len the number of bytes to read. * @returns the number of bytes that were actually read. */ int16_t WaveHC::readWaveData(uint8_t *buff, uint16_t len) { if (remainingBytesInChunk == 0) { struct { char id[4]; uint32_t size; } header; while (1) { if (fd->read(&header, 8) != 8) return -1; if (!strncmp(header.id, "data", 4)) { remainingBytesInChunk = header.size; break; } // if not "data" then skip it! if (!fd->seekCur(header.size)) { return -1; } } } // make sure buffers are aligned on SD sectors uint16_t maxLen = PLAYBUFFLEN - fd->readPosition() % PLAYBUFFLEN; if (len > maxLen) len = maxLen; if (len > remainingBytesInChunk) { len = remainingBytesInChunk; } int16_t ret = fd->read(buff, len); if (ret > 0) remainingBytesInChunk -= ret; return ret; } //------------------------------------------------------------------------------ /** Resume a paused player. */ void WaveHC::resume(void) { cli(); // enable DAC interrupt if (isplaying) TIMSK1 |= _BV(OCIE1A); sei(); } //------------------------------------------------------------------------------ /** * Reposition a wave file. * * \param[in] pos seek will attempt to position the file near \a pos. * \a pos is the byte number from the beginning of file. */ void WaveHC::seek(uint32_t pos) { // make sure buffer fill interrupt doesn't happen cli(); if (fd) { pos -= pos % PLAYBUFFLEN; if (pos < PLAYBUFFLEN) pos = PLAYBUFFLEN; // don't play metadata uint32_t maxPos = fd->readPosition() + remainingBytesInChunk; if (maxPos > fd->fileSize()) maxPos = fd->fileSize(); if (pos > maxPos) pos = maxPos; if (fd->seekSet(pos)) { // assumes a lot about the wave file remainingBytesInChunk = maxPos - pos; } } sei(); } //------------------------------------------------------------------------------ /** Set the player's sample rate. * * \param[in] samplerate The new sample rate in samples per second. * No checks are done on the input parameter. */ void WaveHC::setSampleRate(uint32_t samplerate) { if (samplerate < 500) samplerate = 500; if (samplerate > 50000) samplerate = 50000; // from ladayada's library. cli(); while (TCNT0 != 0) ; OCR1A = F_CPU / samplerate; sei(); } //------------------------------------------------------------------------------ /** Stop the player. */ void WaveHC::stop(void) { TIMSK1 &= ~_BV(OCIE1A); // turn off interrupt playing->isplaying = 0; playing = 0; }