/* 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;
}