Big Mouth Billy Bass automation library
Revision 0:84aaade0de8f, committed 2013-06-17
- Comitter:
- bikeNomad
- Date:
- Mon Jun 17 22:17:59 2013 +0000
- Child:
- 1:9b1f3eb204ac
- Commit message:
- Mostly working; hangs when reading actions
Changed in this revision
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/action.hpp Mon Jun 17 22:17:59 2013 +0000
@@ -0,0 +1,33 @@
+#ifndef __included_action_hpp
+#define __included_action_hpp
+
+#include "billybass.hpp"
+
+// 0.000000\t5.646050\thead
+// 0.064498\t1.069177\ttail
+// 0.357219\t0.580481\tmouth
+
+#define MAX_ACTION_LINE_LENGTH 30
+
+struct Action
+{
+ Action(float _time = -1.0,
+ bool _state = false,
+ DigitalOut *_out = 0,
+ char const *_outName = 0)
+ : actionTime(_time), desiredState(_state)
+ , output(_out), outputName(_outName)
+ {
+ }
+
+ bool operator < (Action const &other) const { return actionTime < other.actionTime; }
+
+ bool isValid() const { return actionTime >= 0.0 && output != 0; }
+
+ float actionTime;
+ bool desiredState;
+ DigitalOut *output;
+ char const *outputName;
+};
+
+#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/billybass.cpp Mon Jun 17 22:17:59 2013 +0000
@@ -0,0 +1,30 @@
+
+#include "billybass.hpp"
+#include <string.h>
+
+// class static
+BillyBass* BillyBass::fish[ MAX_FISH ];
+unsigned BillyBass::numFish;
+char const * BillyBass::mouthName = "mouth";
+char const * BillyBass::bodyName = "body";
+char const * BillyBass::tailName = "tail";
+
+DigitalOut *BillyBass::outputNamed(char const *_outputName, char const **_pName)
+{
+ DigitalOut *output = 0;
+
+ if (!strcmp(_outputName, mouthName)) {
+ output = &mouth;
+ if (_pName) *_pName = mouthName;
+ }
+ else if (!strcmp(_outputName, "head") || !strcmp(_outputName, bodyName)) {
+ output = &body;
+ if (_pName) *_pName = bodyName;
+ }
+ else if (!strcmp(_outputName, tailName)) {
+ output = &tail;
+ if (_pName) *_pName = tailName;
+ }
+
+ return output;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/billybass.hpp Mon Jun 17 22:17:59 2013 +0000
@@ -0,0 +1,45 @@
+#ifndef __included_billybass_hpp
+#define __included_billybass_hpp
+
+#include "mbed.h"
+#include "config.hpp"
+#include <list>
+#include <vector>
+#include <cmath>
+
+extern AnalogOut speaker;
+extern Serial pc;
+
+class BillyBass
+{
+public:
+
+ BillyBass(PinName tailPin, PinName mouthPin, PinName bodyPin) :
+ tail(tailPin), mouth(mouthPin), body(bodyPin)
+ {
+ tail.write(0);
+ mouth.write(0);
+ body.write(0);
+ if (numFish < MAX_FISH) fish[numFish++] = this;
+ // else error
+ }
+
+ // if *_pName, it will get the string name of the output
+ DigitalOut *outputNamed(char const *_outputName, char const **_pName = 0);
+
+ static BillyBass *bassNumber(unsigned which) { return (which >= numFish) ? 0 : fish[which]; }
+ static unsigned getNumFish() { return numFish; }
+
+protected:
+ static BillyBass* fish[ MAX_FISH ];
+ static unsigned numFish;
+ static char const * mouthName;
+ static char const * bodyName;
+ static char const * tailName;
+
+ DigitalOut tail;
+ DigitalOut mouth;
+ DigitalOut body;
+};
+
+#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/config.hpp Mon Jun 17 22:17:59 2013 +0000
@@ -0,0 +1,38 @@
+#ifndef __included_config_hpp
+#define __included_config_hpp
+
+// configuration for Billy Bass
+#define MAX_FISH 2
+
+// SD card layout: all files in BASS_DIRECTORY ("/sd/SD_Files/")
+//
+// Each pair of files in that directory are named like:
+// "<seqnum>_<fishnum>_filename.raw" (16-bit mono raw little-endian samples, sample rate=SAMPLE_RATE_HZ)
+// and
+// "<seqnum>_<fishnum>_filename.txt" (tab-separated text file with starttime,endtime,<mouth|head|body|tail>
+// text files are as exported from Audacity's label tracks
+//
+#define SD_NAME "sd"
+#define SD_ROOT "/" SD_NAME
+#define BASS_DIRECTORY SD_ROOT "/SD_Files"
+// length of BASS_DIRECTORY without NUL
+#define BASS_DIRECTORY_LENGTH 12
+#define MAX_BASENAME_LENGTH 30
+#define MAX_PATH_LEN \
+ BASS_DIRECTORY_LENGTH + 1 \
+ + MAX_BASENAME_LENGTH + 1
+
+// Sample configuration
+typedef int16_t Sample_t; // 16-bit raw, LE samples
+const float SAMPLE_RATE_HZ = 8000.0;
+const unsigned SAMPLE_PERIOD_USEC = (unsigned)(1.0e6/SAMPLE_RATE_HZ);
+
+// Player configuration
+const size_t BUFFER_SIZE = 512;
+const size_t SAMPLES_PER_BUFFER = BUFFER_SIZE / sizeof(Sample_t);
+const float SECONDS_PER_CHUNK = SAMPLES_PER_BUFFER / SAMPLE_RATE_HZ;
+
+#define SERIAL_BAUD 115200
+#define ANALOG_OUTPUT_BIAS 0x8000
+
+#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/player.hpp Mon Jun 17 22:17:59 2013 +0000
@@ -0,0 +1,154 @@
+#ifndef __included_player_hpp
+#define __included_player_hpp
+
+#include "billybass.hpp"
+
+class SongPlayer;
+
+struct SampleBuffer
+{
+ Sample_t volatile buf[ SAMPLES_PER_BUFFER ];
+ size_t volatile samplesRemaining;
+ Sample_t volatile * volatile nextSample;
+
+ bool isDone()
+ {
+ return !samplesRemaining;
+ }
+
+ float remainingDuration()
+ {
+ return samplesRemaining / SAMPLE_RATE_HZ;
+ }
+
+ // return true if we read any samples
+ bool loadFrom(FILE *fp)
+ {
+ samplesRemaining = fread((void*)buf, sizeof(Sample_t), SAMPLES_PER_BUFFER, fp);
+ nextSample = buf;
+ return samplesRemaining > 0;
+ }
+
+ Sample_t getNextSample()
+ {
+ --samplesRemaining;
+ return *++nextSample;
+ }
+
+ SampleBuffer() : samplesRemaining(0)
+ , nextSample(buf) {
+ }
+};
+
+struct SongPlayer
+{
+ SampleBuffer * volatile playing;
+ SampleBuffer * volatile loading;
+ SampleBuffer buffer[2];
+ FILE *fp;
+ size_t nChunks;
+ size_t volatile chunksRemaining;
+ float timeInSong;
+ Ticker sampleTicker;
+
+ SongPlayer() : playing(0)
+ , loading(0)
+ , fp(0)
+ , nChunks(0)
+ , chunksRemaining(0)
+ {
+ }
+
+ // interrupt handler
+ void playNextSample(void)
+ {
+ if (playing->samplesRemaining == 0)
+ swapBuffers();
+ // NOTE bias of 0xC000 requires normalizing to 75% of full scale
+ speaker.write_u16(static_cast<uint16_t>(playing->getNextSample() + 0x8000) / 2);
+ }
+
+ bool startSong(char const *name)
+ {
+ pc.printf("starting %s: ", name);
+ if (fp) fclose(fp);
+ fp = fopen(name, "rb");
+ pc.printf("opened, ");
+ if (!fp) return false;
+
+ pc.printf("seekend, ");
+ if (fseek(fp, 0, SEEK_END)) return false;
+
+ long fileSize = ftell(fp);
+ pc.printf("size=%d, ", fileSize);
+ if (fileSize < 0) return false;
+
+ if (fseek(fp, 0, SEEK_SET)) return false;
+
+ pc.printf("rewound, ");
+ chunksRemaining = nChunks = fileSize / BUFFER_SIZE;
+ loading = &buffer[0];
+ playing = &buffer[1];
+ pc.printf("chunks=%d expected=%f seconds\r\n", nChunks, nChunks * SECONDS_PER_CHUNK);
+ if (!loadNextChunk())
+ return false;
+
+ timeInSong = 0.0;
+ sampleTicker.attach_us(this, &SongPlayer::playNextSample, SAMPLE_PERIOD_USEC);
+ return true;
+ }
+
+ // swap loading/playing buffers;
+ // decrement chunksRemaining
+ void swapBuffers()
+ {
+ SampleBuffer * volatile tmp = playing;
+ if (tmp == buffer + 0)
+ playing = buffer + 1;
+ else
+ playing = buffer + 0;
+ loading = tmp;
+ if (chunksRemaining)
+ chunksRemaining--;
+ }
+
+ // get next chunk of file into *loading
+ // to prepare for eventual swap.
+ // returns true if more samples remain
+ bool loadNextChunk()
+ {
+ if (!chunksRemaining) return false;
+
+ bool notDone = loading->loadFrom(fp);
+ return notDone;
+ }
+
+ bool isDone()
+ {
+ return !chunksRemaining;
+ }
+
+ // look at loading buffer; load only if necessary.
+ bool loadIfNecessary()
+ {
+ if (loading->isDone())
+ {
+ timeInSong += SECONDS_PER_CHUNK;
+ return loadNextChunk();
+ }
+ else { return true; }
+ }
+
+ void playEntireSong(char const *name)
+ {
+ if (!startSong(name)) return;
+
+ while (!isDone())
+ {
+ loadIfNecessary();
+ }
+ sampleTicker.detach();
+ }
+};
+
+#endif
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/song.cpp Mon Jun 17 22:17:59 2013 +0000
@@ -0,0 +1,178 @@
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <ctype.h>
+#include <algorithm>
+
+#include "billybass.hpp"
+#include "song.hpp"
+
+// class static
+char const Song::directoryName[ BASS_DIRECTORY_LENGTH + 1 ] = BASS_DIRECTORY;
+// class static
+char const *Song::textExtension = "txt";
+// class static
+char const *Song::sampleExtension = "raw";
+// class static
+unsigned const Song::NO_FISH = MAX_FISH + 1;
+// class static
+std::list<Song*> Song::songs;
+
+// _name is relative to BASS_DIRECTORY
+// class static
+Song *Song::newSong(char const *_name)
+{
+ Song *s = new Song;
+ if (!s) {
+ pc.printf("new Song == 0\r\n");
+ return 0;
+ }
+ if (! s->parseFilename(_name)) {
+ pc.printf("parseFilename(%s) failed\r\n", _name);
+ goto on_error;
+ }
+ if (! s->readActions()) {
+ pc.printf("readActions(%s) failed\r\n", _name);
+ goto on_error;
+ }
+ songs.push_back(s);
+
+ return s;
+
+on_error:
+ delete s;
+ return 0;
+}
+
+// class static
+void Song::clearAllSongs()
+{
+ for (std::list<Song *>::const_iterator song = songs.begin(); song != songs.end(); song++)
+ delete *song;
+ songs.clear();
+}
+
+// class static
+unsigned Song::readAllSongs(DIR *bassDir)
+{
+ if (!bassDir) return 0;
+ clearAllSongs();
+
+ while (dirent *dir = bassDir->readdir()) {
+ pc.printf("Reading %s\r\n", dir->d_name);
+ unsigned namelen = strlen(dir->d_name);
+ if (namelen > 9 && !strcasecmp(dir->d_name + namelen - 3, sampleExtension)) {
+ Song *song = Song::newSong(dir->d_name);
+ if (!song) {
+ pc.printf("ERROR reading %s\r\n", dir->d_name);
+ } else {
+ song->print(pc);
+ }
+ }
+ }
+
+ return songs.size();
+}
+
+bool Song::parseFilename(char const *_name)
+{
+ if (strlen(_name) > MAX_BASENAME_LENGTH)
+ goto on_error;
+
+ strcpy(fullname, directoryName);
+ basename = fullname + BASS_DIRECTORY_LENGTH;
+ *basename++ = '/';
+ strcpy(basename, _name);
+
+ char *p;
+ long n = strtol(_name, &p, 10);
+ if (*p != '_' || n <= 0)
+ goto on_error;
+
+ sequenceNumber = (unsigned)(n - 1);
+
+ p++; // skip underscore
+ n = strtol(p, &p, 10);
+ if (*p != '_' || n <= 0 || n > MAX_FISH)
+ goto on_error;
+
+ whichFish = (unsigned)(n - 1);
+
+ p = strrchr(basename, '.');
+ if (!p)
+ goto on_error;
+
+ extension = p+1;
+
+ if (strcasecmp(extension, sampleExtension))
+ goto on_error;
+
+ return true;
+
+on_error:
+ whichFish = NO_FISH;
+ basename = 0;
+ return false;
+}
+
+bool Song::readActions()
+{
+ FILE *txtfile = fopen(getTextFileName(), "r");
+ if (!txtfile) {
+ pc.printf("can't open %s\r\n", getTextFileName());
+ return false;
+ }
+ bool retval = false;
+
+ BillyBass *bass = BillyBass::bassNumber(whichFish);
+ if (!bass) {
+ pc.printf("No bass!\r\n");
+ goto done;
+ }
+
+ // read actions from file
+ unsigned line = 1;
+ char actionLine[ MAX_ACTION_LINE_LENGTH + 1 ];
+ while (true) {
+ if (!fgets(actionLine, sizeof(actionLine), txtfile))
+ break;
+
+ // delete trailing whitespace
+ char *p = actionLine + strlen(actionLine) - 1;
+ while (isspace(*p)) *p-- = 0;
+
+ float startTime = strtof(actionLine, &p);
+ if (p == actionLine)
+ goto done;
+
+ char *q;
+ float endTime = strtof(p, &q);
+ if (q == p)
+ goto done;
+
+ while (isspace(*q)) q++;
+
+ char const *outName;
+ DigitalOut *out = bass->outputNamed(q, &outName);
+ if (!out) {
+ pc.printf("%s line %d: bad outname \"%s\"\r\n", getTextFileName(), line, q);
+ goto done;
+ }
+ pc.printf("%d add %f %f %s\r\n", line, startTime, endTime, outName);
+
+ actions.push_back(Action(startTime, true, out, outName));
+ actions.push_back(Action(endTime, false, out, outName));
+
+ line++;
+ }
+
+ std::sort(actions.begin(), actions.end()); // sort actions by time
+ retval = true;
+
+done:
+ if (!retval) {
+ pc.printf("Error reading action from \"%s\"\r\n", actionLine);
+ }
+ fclose(txtfile);
+ return retval;
+}
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/song.hpp Mon Jun 17 22:17:59 2013 +0000
@@ -0,0 +1,78 @@
+#ifndef __included_song_hpp
+#define __included_song_hpp
+
+#include <DirHandle.h>
+
+#include "billybass.hpp"
+#include "config.hpp"
+#include "action.hpp"
+
+class Song
+{
+public:
+
+ typedef std::vector<Action> actions_t;
+
+ // _name is relative to BASS_DIRECTORY
+ // return a pointer to a fully read-in Song if valid
+ // also adds new song to songs
+ static Song *newSong(char const *_name);
+ static std::list<Song*> songs;
+ static unsigned readAllSongs(DIR *bassDir);
+ static void clearAllSongs();
+
+ Song() : sequenceNumber(0), whichFish(NO_FISH), basename(0), extension(0) {
+ fullname[0] = 0;
+ actions.reserve(60);
+ }
+
+ bool isValid() const {
+ return basename != 0 && whichFish != NO_FISH;
+ }
+ bool parseFilename(char const *_name);
+ bool readActions();
+
+ unsigned getSequenceNumber() const {
+ return sequenceNumber;
+ }
+ unsigned getWhichFish() const {
+ return whichFish;
+ }
+ char const *getSampleFileName() {
+ if (!extension) return 0;
+ strcpy(extension, sampleExtension);
+ return fullname;
+ }
+ char const *getTextFileName() {
+ if (!extension) return 0;
+ strcpy(extension, textExtension);
+ return fullname;
+ }
+ actions_t const &getActions() const {
+ return actions;
+ }
+
+ void print(Serial &pc) {
+ pc.printf("%s fish=%u seq=%u\r\n", getSampleFileName(), whichFish, sequenceNumber);
+ for (actions_t::const_iterator action_it = actions.begin(); action_it != actions.end(); action_it++) {
+ Action const &action = *action_it;
+ pc.printf("%0.02f %s %d\r\n", action.actionTime, action.outputName, action.desiredState ? 1 : 0);
+ }
+ }
+
+protected:
+
+ static char const directoryName[ BASS_DIRECTORY_LENGTH + 1 ];
+ static unsigned const NO_FISH;
+ static char const *textExtension;
+ static char const *sampleExtension;
+
+ unsigned sequenceNumber; // 0-relative
+ unsigned whichFish; // 0-relative
+ char fullname[ MAX_PATH_LEN ]; // with directory name
+ char *basename; // points into fullname
+ char *extension; // points into fullname
+ actions_t actions;
+};
+
+#endif // __included_song_hpp
Ned Konz