Big Mouth Billy Bass automation library

Dependents:   BillyBass_with_SD

Files at this revision

API Documentation at this revision

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

action.hpp Show annotated file Show diff for this revision Revisions of this file
billybass.cpp Show annotated file Show diff for this revision Revisions of this file
billybass.hpp Show annotated file Show diff for this revision Revisions of this file
config.hpp Show annotated file Show diff for this revision Revisions of this file
player.hpp Show annotated file Show diff for this revision Revisions of this file
song.cpp Show annotated file Show diff for this revision Revisions of this file
song.hpp Show annotated file Show diff for this revision Revisions of this file
--- /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