diff --git a/Collateral/Edited teensy files/MovingAverage/MovingAverage.cpp b/Collateral/Edited teensy files/MovingAverage/MovingAverage.cpp deleted file mode 100644 index a076824..0000000 --- a/Collateral/Edited teensy files/MovingAverage/MovingAverage.cpp +++ /dev/null @@ -1,47 +0,0 @@ -/* - * MovingAverage.cpp - implementation - * Jouni Stenroos 2016 - */ - -#include -#include "MovingAverage.h" - -MovingAverage::MovingAverage(int nValues) { - if (nValues <= 1) { - _values = NULL; - return; - } - _values = new int[nValues]; - _nValues = nValues; - for (int i = 0; i < nValues; i++) { - _values[i] = 0; - } - _firstUse = true; - _curValue = 0; - _total = 0; -} - -int MovingAverage::average(int aValue) { - if (_values == NULL) - return 0; - if (_firstUse) { - /* First time fill all values with the value */ - for (int i = 0; i < _nValues; i++) { - _values[i] = aValue; - _total += aValue; - } - _firstUse = false; - _curValue++; - } else { - _total -= _values[_curValue]; - _total += aValue; - _values[_curValue++] = aValue; - if (_curValue >= _nValues) - _curValue = 0; - } - return (int)round((double)_total / (double)_nValues); -} - -int MovingAverage::getAverage() { - return (int)round((double)_total / (double)_nValues); -} diff --git a/Collateral/Edited teensy files/MovingAverage/MovingAverage.h b/Collateral/Edited teensy files/MovingAverage/MovingAverage.h deleted file mode 100644 index 00851c7..0000000 --- a/Collateral/Edited teensy files/MovingAverage/MovingAverage.h +++ /dev/null @@ -1,24 +0,0 @@ -/* - * MovingAverage.h - interface - * MovingAverage, a simple implementation of a running average of - * number of values. - * Jouni Stenroos 2016 - */ - -#ifndef _RUNNINGAVERAGE_h_ -#define _RUNNINGAVERAGE_h_ - -class MovingAverage { -public: - MovingAverage(int nValues); - int average(int value); - int getAverage(); -private: - int *_values; - int _nValues; - int _curValue; - bool _firstUse; - int _total; -}; - -#endif // _RUNNINGAVERAGE_h_ diff --git a/Collateral/Edited teensy files/README.md b/Collateral/Edited teensy files/README.md deleted file mode 100644 index 2b4aefb..0000000 --- a/Collateral/Edited teensy files/README.md +++ /dev/null @@ -1,17 +0,0 @@ -##Using the edited Teensy files to compile your own firmware - -RMPlaySDRaw is based on Teensy play_sd_raw, but with additions specific for RadioMusic. -MovingAverage is a very simple implementation of a moving average. - -These directories should be copied to the Arduino user library folder (on Mac: ~/Documents/Arduino/libraries) - -1. Adds a new function: playFrom(filename, startPoint) which starts the file at a particular point. It actually starts at the modulo of the start point vs the file size, so if you start at a point past the end of the file, it will start within the file, offset by the remainder. If the file is same as the previous one, a plain seek is performed instead of close/open ([More info](*https://github.com/TomWhitwell/RadioMusic/wiki/Troubleshooting-the-Radio-Music-module#what-does-the-reset-button-do)) -1. Adds a section to AudioPlaySdRaw::update that checks whether the file is playing normally (i.e. that it has read 256 bytes to play). If not, it calls hotswap callback. This enables hot swapping by allowing the callback to reboot the device.. Normally, pulling out the SD card causes the playback to get stuck in a loop. -1. Adds a new variable: fileOffset which returns the current position of the playing file in bytes. This enables the radio-style switching between playing files. -1. Adds a new function: preparePlayFrom(filename) which closes the previous running file and opens the new one but does not start playing it. -1. Adds a new function: pause() which disables playing but leaves the file open. - -###play_sd_raw.h - -1. Just adds new functions and variables: playFrom, fileOffset, hotswap_cb, - diff --git a/Collateral/Edited teensy files/RMPlaySDRaw/RMPlaySDRaw.cpp b/Collateral/Edited teensy files/RMPlaySDRaw/RMPlaySDRaw.cpp deleted file mode 100644 index 22b51c3..0000000 --- a/Collateral/Edited teensy files/RMPlaySDRaw/RMPlaySDRaw.cpp +++ /dev/null @@ -1,195 +0,0 @@ -/* Audio Library for Teensy 3.X - * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com - * - * Development of this audio library was funded by PJRC.COM, LLC by sales of - * Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop - * open source software by purchasing Teensy or other PJRC products. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice, development funding notice, and this permission - * notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#include "RMPlaySDRaw.h" -#include "spi_interrupt.h" - -void RMPlaySDRaw::begin(void) -{ - playing = false; - file_offset = 0; - file_size = 0; - _filePath[0] = 0; -} - - -bool RMPlaySDRaw::play(const char *filename) -{ - stop(); - strcpy(_filePath, filename); - AudioStartUsingSPI(); - __disable_irq(); - rawfile = SD.open(filename); - __enable_irq(); - if (!rawfile) { - //Serial.println("unable to open file"); - return false; - } - file_size = rawfile.size(); - file_offset = 0; - //Serial.println("able to open file"); - playing = true; - return true; -} - -bool RMPlaySDRaw::preparePlayFrom(const char *filename) -{ - if (strcmp(_filePath, filename) == 0) - return true; // Already prepared for this file. - strcpy(_filePath, filename); - __disable_irq(); - //Serial.println("pause() - Closing file."); - rawfile.close(); - rawfile = SD.open(filename); - __enable_irq(); - if (!rawfile) { - return false; - } - file_size = rawfile.size(); - file_offset = 0; - return true; -} - -bool RMPlaySDRaw::playFrom(const char *filename, unsigned long startPoint) -{ - // We use the same file, just seek inside it. - if (strcmp(_filePath, filename) == 0) { - Serial.print("Continuing on file "); - Serial.println(filename); - rawfile.seek(startPoint % file_size); - file_offset = startPoint; -// AudioStartUsingSPI(); - playing = true; - return true; - } - stop(); - strcpy(_filePath, filename); - AudioStartUsingSPI(); - __disable_irq(); - rawfile = SD.open(filename); - __enable_irq(); - if (!rawfile) { - Serial.println("NO: unable to open file"); - return false; - } - file_size = rawfile.size(); - rawfile.seek(startPoint % file_size); - file_offset = startPoint; - - Serial.println("YES: able to open file"); - playing = true; - return true; -} - - - -void RMPlaySDRaw::pause(void) { - __disable_irq(); - if (playing) { - playing = false; -// AudioStopUsingSPI(); - } - __enable_irq(); -} - -void RMPlaySDRaw::stop(void) -{ - __disable_irq(); - //Serial.println("stop() - Closing file."); - rawfile.close(); - if (playing) { - playing = false; - __enable_irq(); - AudioStopUsingSPI(); - } else { - __enable_irq(); - } -} - - -void RMPlaySDRaw::update(void) -{ - unsigned int i; - int n; - audio_block_t *block; - - // only update if we're playing - if (!playing) return; - - // allocate the audio blocks to transmit - block = allocate(); - if (block == NULL) - return; - - if (rawfile.available()) { - // we can read more data from the file... - if (bufAvail == 0) { - bufAvail = rawfile.read(audioBuffer, AUDIOBUFSIZE); - bufPos = 0; - if (bufAvail < 0) { - if (hotswap_cb) - hotswap_cb(); - return; - } - } - n = min(AUDIO_BLOCK_SAMPLES * 2, bufAvail); - memcpy(block->data, &(audioBuffer[bufPos]), n); - bufAvail -= n; - bufPos += n; - // ADD THIS SECTION TO ENABLE HOT SWAPPING - // read returns -1 on error. - // END OF NEW HOT SWAP SECTION - - file_offset += n; - for (i=n/2; i < AUDIO_BLOCK_SAMPLES; i++) { - block->data[i] = 0; - } - transmit(block); - } else { -// rawfile.close(); -// AudioStopUsingSPI(); - playing = false; - } - release(block); -} - -#define B2M (uint32_t)((double)4294967296000.0 / AUDIO_SAMPLE_RATE_EXACT / 2.0) // 97352592 - -uint32_t RMPlaySDRaw::positionMillis(void) -{ - return ((uint64_t)file_offset * B2M) >> 32; -} - -uint32_t RMPlaySDRaw::lengthMillis(void) -{ - return ((uint64_t)file_size * B2M) >> 32; -} - -uint32_t RMPlaySDRaw::fileOffset(void) -{ - return file_offset; -} - diff --git a/Collateral/Edited teensy files/RMPlaySDRaw/RMPlaySDRaw.h b/Collateral/Edited teensy files/RMPlaySDRaw/RMPlaySDRaw.h deleted file mode 100644 index 038f911..0000000 --- a/Collateral/Edited teensy files/RMPlaySDRaw/RMPlaySDRaw.h +++ /dev/null @@ -1,62 +0,0 @@ -/* Audio Library for Teensy 3.X - * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com - * - * Development of this audio library was funded by PJRC.COM, LLC by sales of - * Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop - * open source software by purchasing Teensy or other PJRC products. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice, development funding notice, and this permission - * notice shall be included in all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -#ifndef RMPlaySDraw_h_ -#define RMPlaySDraw_h_ - -#include "AudioStream.h" -#include "SD.h" -#define AUDIOBUFSIZE (AUDIO_BLOCK_SAMPLES * 8) - -class RMPlaySDRaw : public AudioStream -{ -public: - RMPlaySDRaw(void) : AudioStream(0, NULL) { begin(); } - void begin(void); - bool play(const char *filename); - bool playFrom(const char *filename, unsigned long start); - bool preparePlayFrom(const char *filename); - void stop(void); - void pause(void); - bool isPlaying(void) { return playing; } - uint32_t positionMillis(void); - uint32_t lengthMillis(void); - uint32_t fileOffset(void); - virtual void update(void); - - void (*hotswap_cb)(); -private: - File rawfile; - uint32_t file_size; - volatile uint32_t file_offset; - volatile bool playing; - unsigned char audioBuffer[AUDIOBUFSIZE]; // - volatile int bufAvail = 0; - volatile int bufPos = 0; - char _filePath[16]; -}; - -#endif diff --git a/RadioMusic/AnalogInput.cpp b/RadioMusic/AnalogInput.cpp new file mode 100644 index 0000000..6373071 --- /dev/null +++ b/RadioMusic/AnalogInput.cpp @@ -0,0 +1,151 @@ +#include "Arduino.h" + +// Combined CV + Pot + +// 2 Pins + +// 13 bit + 13 bit => LOW_VAL -> HIGH_VAL + +// Separate Analogue In + +// 13 bit => LOW_VAL -> HIGH_VAL + +// Mapping function isn't uniform, i.e. output may have a half step at top and bottom of range +#include "AnalogInput.h" +#include "RadioMusic.h" + +#ifdef DEBUG_INTERFACE +#define D(x) x +#else +#define D(x) +#endif + +AnalogInput::AnalogInput(uint pinIndex) { + + pin = pinIndex; +} + +void AnalogInput::setRange(float outLow, float outHigh, boolean quantiseOutput) { + quantise = quantiseOutput; + + // Check the range is the right way around + if(outLow > outHigh) { + outputLow = outHigh; + outputHigh = outLow; + } else { + outputLow = outLow; + outputHigh = outHigh; + } + + // Use range + 1 so that the top value has equal coverage across the input range + float range = (outputHigh - outputLow) + 1; + + inToOutRatio = (float) ADC_MAX_VALUE / range; + + if(range < 200 && quantise) { + hysteresis = true; + borderThreshold = inToOutRatio / 4; + D( + Serial.print("Input hysteresis for pin "); + Serial.print(pin); + Serial.print(" "); + Serial.print(borderThreshold); + Serial.print(" with range "); + Serial.println(range); + ); + } else { + borderThreshold = 16; + } + + // Keep the inverse ratio so we only multiply during update + inverseRatio = 1.0 / inToOutRatio; + +// currentValue = outLow; +} + +void AnalogInput::setAverage(boolean avg) { + average = avg; +} + +void AnalogInput::setSmoothSteps(int steps) { + smoothSteps = steps; +} + +float AnalogInput::getRatio() { + return inToOutRatio; +} + +boolean AnalogInput::update() { + + if(average) { + inputValue = 0; + for(int i=0;i borderThreshold) { + valueAtLastChange = inputValue; + currentValue = (inputValue * inverseRatio) + outputLow; + return true; + } + + } else { + inputValue = 0; + for(int i=0;i borderThreshold || newValue > START_VALUE) { + valueAtLastChange = inputValue; + currentValue = newValue; + D( + Serial.print("In Val "); + Serial.print(inputValue); + Serial.print(" "); + Serial.print(inverseRatio, 8); + Serial.print(" "); + Serial.println(newValue); + ); + return true; + } + } +// else { +// D( +// Serial.print(pin); +// Serial.print(" no change "); +// Serial.print(newValue); +// Serial.print(" "); +// Serial.print(currentValue); +// Serial.print(" "); +// Serial.print(inputValue); +// Serial.print(" "); +// Serial.println(valueAtLastChange); +// ); +// } + } else if(abs(inputValue - oldInputValue) > borderThreshold) { + currentValue = (inputValue * inverseRatio) + outputLow; + oldInputValue = inputValue; + return true; + } + + } + + return false; +} + +void AnalogInput::printDebug() { + Serial.print("I "); + Serial.print(pin); + Serial.print("\t"); + Serial.println(inputValue); +} + diff --git a/RadioMusic/AnalogInput.h b/RadioMusic/AnalogInput.h new file mode 100644 index 0000000..530c236 --- /dev/null +++ b/RadioMusic/AnalogInput.h @@ -0,0 +1,46 @@ +#ifndef AnalogInput_h +#define AnalogInput_h + +#include "Arduino.h" + +#define ADC_BITS 13 +#define ADC_MAX_VALUE (1 << ADC_BITS) + +#define START_VALUE -100 + +class AnalogInput { + public: + AnalogInput(uint pinIndex); + boolean update(); + void setRange(float outLow, float outHigh, boolean quantise); + void setAverage(boolean avg); + void setSmoothSteps(int steps); + + void printDebug(); + float getRatio(); + float currentValue = START_VALUE; + int32_t inputValue = 0; + uint16_t borderThreshold = 16; + private: + int pin; + float outputLow = 0.0; + float outputHigh = 1.0; + float inToOutRatio = 0.0; + float inverseRatio = 0.0; + + // Set out of range to trigger a change status on first call. + int32_t oldInputValue = -1000; + int32_t valueAtLastChange = -1000; + + // Use hysteresis thresholds at value boundaries + boolean hysteresis = false; + + // Clamp output to int + boolean quantise = false; + + // Smooth input + boolean average = false; + uint8_t smoothSteps = 40; +}; + +#endif diff --git a/RadioMusic/AudioEngine.cpp b/RadioMusic/AudioEngine.cpp new file mode 100644 index 0000000..c0e3000 --- /dev/null +++ b/RadioMusic/AudioEngine.cpp @@ -0,0 +1,253 @@ +#include "AudioEngine.h" +#include "Arduino.h" + +#include "RadioMusic.h" + +#ifdef DEBUG_ENGINE +#define D(x) x +#else +#define D(x) +#endif + +void AudioEngine::init(Settings& config) { + + AudioMemory(25); + + settings = &config; + + playRaw1.loopPlayback(settings->looping); + playRaw2.loopPlayback(settings->looping); + + mixer.gain(0, 1.0); + mixer.gain(1, 1.0); + + error = false; +} + +D( +void AudioEngine::test(AudioFileInfo& info1, AudioFileInfo& info2) { + currentPlayer->playFrom(&info1); + previousPlayer->playFrom(&info2); +} + +void AudioEngine::testSwap() { + pFadeOut->fadeOut(500); + pFadeIn->fadeIn(500); + prevAudioElapsed = 0; + if(pFadeOut == &fade1) { + pFadeOut = &fade2; + pFadeIn = &fade1; + } else { + pFadeOut = &fade1; + pFadeIn = &fade2; + } +} +); + +void AudioEngine::swap() { + // Swap players and fades. + if (currentPlayer == &playRaw1) { + currentPlayer = &playRaw2; + previousPlayer = &playRaw1; + pFadeOut = &fade1; + pFadeIn = &fade2; + D( + Serial.println("AE: Playing through player 2"); + ); + } else { + currentPlayer = &playRaw1; + previousPlayer = &playRaw2; + pFadeOut = &fade2; + pFadeIn = &fade1; + D( + Serial.println("AE: Playing through player 1"); + ); + } +} + +boolean AudioEngine::update() { + + if (currentPlayer->hasFinished()) { + if(settings->looping) { + currentPlayer->restart(); + return false; + } else { + // Flag EOF + return true; + } + } + + // Check if previously playing rawPlayer has reached fadeOut period and can be stopped. + // Or if MUTE is not active. + if ((prevAudioElapsed > settings->crossfadeTime || !settings->crossfade) && previousPlayer->isPlaying()) { + D(Serial.println("AE: Pause previous player");); + previousPlayer->stop(); + } + + if(currentPlayer->errors > MAX_ERRORS) { + D(Serial.println("Player reached max errors.");); + error = true; + } else if(currentPlayer->errors > 0) { + D(Serial.print("Error count ");Serial.println(currentPlayer->errors);); + } + return false; +} + +void AudioEngine::changeTo(AudioFileInfo* fileInfo, unsigned long start) { + + D( + Serial.print("AE: current file is now "); + Serial.println(fileInfo->name); + Serial.print("AE: Current player has "); + Serial.print(currentPlayer->rawfile.name()); + Serial.println(); + ); + + uint32_t pos = 0; + + if (settings->looping && currentFileInfo != NULL) { + D( + Serial.print("Elapsed "); + Serial.println(elapsed); + ); + if(settings->loopMode == LOOP_MODE_RADIO) { + currentFileInfo->startPlayFrom += ((elapsed * currentFileInfo->getSampleRate() * 2) / 1000); + if(currentFileInfo->startPlayFrom % currentFileInfo->getBytesPerSample() != 0) { + currentFileInfo->startPlayFrom -= currentFileInfo->startPlayFrom % currentFileInfo->getBytesPerSample(); + } + pos = (fileInfo->startPlayFrom + ((elapsed * fileInfo->getSampleRate()) / 1000)); + } else if(settings->loopMode == LOOP_MODE_CONTINUE) { + pos = currentPlayer->offset() * fileInfo->size; + } else if(settings->loopMode == LOOP_MODE_START_POINT) { + pos = (start * fileInfo->size) >> 13; + } + + } else { + // set start from arg + pos = (start * fileInfo->size) >> 13; + } + + if(pos % fileInfo->getBytesPerSample() != 0) { + pos -= pos % fileInfo->getBytesPerSample(); + } + + fileInfo->startPlayFrom = pos % fileInfo->size; + + D( + Serial.print("AE: start play from "); + Serial.println(fileInfo->startPlayFrom); + ); + + elapsed = 0; + currentFileInfo = fileInfo; + + // Re-apply speed limits + if(settings->pitchMode) { + setPlaybackSpeed(currentPlayer->playbackSpeed); + } + + if(settings->hardSwap) { + // If we allow all audio file types then no crossfades, just hard + // cut from one to the next + AudioNoInterrupts(); + currentPlayer->playFrom(currentFileInfo); // change audio + previousPlayer->stop(); + AudioInterrupts(); + } else { + + swap(); + + AudioNoInterrupts(); + currentPlayer->playFrom(currentFileInfo); // change audio + + if (settings->crossfade) { + // Do a crossfade. + D(Serial.print("Crossfade ");Serial.println(settings->crossfadeTime);); + pFadeOut->fadeOut(settings->crossfadeTime); + pFadeIn->fadeIn(settings->crossfadeTime); + // And reset the fade timer to let the previous file fade out before pausing it. + prevAudioElapsed = 0; + } else { + D(Serial.println("1ms xfade.");); + // Emulate no crossfade with 1ms fadeout/in + pFadeOut->fadeOut(2); + pFadeIn->fadeIn(2); + prevAudioElapsed = 0; + } + AudioInterrupts(); + } + + D( + Serial.print("AE: Current file "); + Serial.print(currentPlayer->rawfile.name()); + Serial.print(" .Prev file "); + Serial.print(previousPlayer->rawfile.name()); + Serial.println(); + ); + +} + +void AudioEngine::setPlaybackSpeed(float speed) { + // Limit speed on high bandwidth audio to not overload CPU + if(settings->anyAudioFiles && speed > 3.56) { + if(speed > 3.56) speed = 3.563595; + } else if(speed > 4.489) { + speed = 4.4898; + } + D( + Serial.print("AE: Set Playback Speed "); + Serial.println(speed,6); + Serial.print("AE: Bandwidth "); + Serial.print(currentFileInfo->getBandwidth()); + Serial.print(" "); + Serial.println(currentFileInfo->getSampleRate()); + ); + currentPlayer->playbackSpeed = speed; + previousPlayer->playbackSpeed = speed; +} + +// pos is from 0 -> 8192 +void AudioEngine::skipTo(uint32_t pos) { + uint32_t samplePos = ((float)pos / 8192.0) * (currentFileInfo->size / currentFileInfo->getBytesPerSample()); + D( + Serial.print("AE: Skip To "); + Serial.println(samplePos); + ); + currentFileInfo->startPlayFrom = (samplePos * currentFileInfo->getBytesPerSample()) % currentFileInfo->size; + currentPlayer->skipTo(currentFileInfo->startPlayFrom); + elapsed = 0; +} + +void AudioEngine::measure() { + Serial.print("AE: Buffer fills "); + Serial.print(playRaw1.bufferFills); + Serial.print("\t"); + Serial.print(playRaw2.bufferFills); + Serial.print(" .Updates "); + Serial.print(playRaw1.updates); + Serial.print("\t"); + Serial.print(playRaw2.updates); + Serial.print(" .CPU "); + Serial.print(playRaw1.processorUsageMax()); + Serial.print("\t"); + Serial.print(playRaw2.processorUsageMax()); + Serial.print(" . Errors "); + Serial.print(playRaw1.errors); + Serial.print("\t"); + Serial.println(playRaw2.errors); + + playRaw1.bufferFills = 0; + playRaw2.bufferFills = 0; + playRaw1.updates = 0; + playRaw2.updates = 0; + playRaw1.processorUsageMaxReset(); + playRaw2.processorUsageMaxReset(); +} + +float AudioEngine::getPeak() { + if (peak1.available()) { + return peak1.read(); + } else { + return 0; + } +} diff --git a/RadioMusic/AudioEngine.h b/RadioMusic/AudioEngine.h new file mode 100644 index 0000000..cda1ecc --- /dev/null +++ b/RadioMusic/AudioEngine.h @@ -0,0 +1,84 @@ +#ifndef AudioEngine_h +#define AudioEngine_h + +#include + +#include "SDPlayPCM.h" +#include "Settings.h" + +#define MAX_ERRORS 10 + +class AudioEngine { + public: + void test(AudioFileInfo& info1, AudioFileInfo& info2); + void testSwap(); + + SDPlayPCM *currentPlayer; + SDPlayPCM *previousPlayer; + AudioEffectFade *pFadeOut; + AudioEffectFade *pFadeIn; + + // Audio engine definitions. + SDPlayPCM playRaw1; + SDPlayPCM playRaw2; + AudioEffectFade fade1; + AudioEffectFade fade2; + AudioMixer4 mixer; + AudioAnalyzePeak peak1; + AudioOutputAnalog dac1; + AudioConnection patchCord1; + AudioConnection patchCord2; + AudioConnection patchCord3; + AudioConnection patchCord4; + AudioConnection patchCord5; + AudioConnection patchCord6; + + elapsedMillis prevAudioElapsed; + boolean eof = false; + + AudioEngine() : + patchCord1(playRaw1, fade1), + patchCord2(playRaw2, fade2), + patchCord3(fade1, 0, mixer, 0), + patchCord4(fade2, 0, mixer, 1), + patchCord5(mixer, 0, dac1, 0), + patchCord6(mixer, 0, peak1, 0){ + + playRaw1.playerID = 1; + playRaw2.playerID = 2; + currentPlayer = &playRaw1; + + previousPlayer = &playRaw2; + pFadeOut = &fade1; + pFadeIn = &fade2; + } + + void init(Settings& settings); + boolean update(); + void swap(); + void changeTo(AudioFileInfo* audioFileInfo, unsigned long start); + void printDebug(); + void skipTo(uint32_t time); + void setPlaybackSpeed(float speed); + void measure(); + float getPeak(); + AudioFileInfo* currentFileInfo; + boolean error = false; + + // Time elapsed since last switch / skip + elapsedMillis elapsed = 0; + + private: + uint16_t waitCount = 0; + boolean updateRequired = false; + AudioFileInfo* nextInfo; + Settings* settings; + +// boolean hardSwap = false; +// boolean looping = false; +// boolean mute = false; +// uint16_t declick = 0; + +}; + +#endif diff --git a/RadioMusic/AudioFileInfo.h b/RadioMusic/AudioFileInfo.h new file mode 100644 index 0000000..1aa7c02 --- /dev/null +++ b/RadioMusic/AudioFileInfo.h @@ -0,0 +1,113 @@ +#ifndef AudioFileInfo_h +#define AudioFileInfo_h + +#include "Arduino.h" + +#define CHANNELS_MASK B00000001 +#define MONO 0 +#define STEREO 1 + +#define BIT_DEPTH_MASK B00000110 +#define BIT_DEPTH_8 0 // 00 +#define BIT_DEPTH_16 1 // 01 +#define BIT_DEPTH_24 2 // 10 +#define BIT_DEPTH_32 3 // 11 + +#define SAMPLE_RATE_MASK B00111000 +#define SAMPLE_RATE_11 0 // 000 +#define SAMPLE_RATE_22 1 // 001 +#define SAMPLE_RATE_44 2 // 010 +#define SAMPLE_RATE_48 3 // 011 +#define SAMPLE_RATE_96 4 // 100 + +static uint32_t SAMPLE_RATES[5] = { 11025,22050,44100,48000,96000 }; +static uint8_t BIT_DEPTHS[4] = {8,16,24,32}; + +class AudioFileInfo { +public: + + String name; + // Size doesn't include the header for wav files, just the length of the audio + uint32_t size; + +// // Only accept 11025, 22050, 44100, 88,200, 48000 and 96000 +// uint32_t sampleRate; +// +// // Only allow mono or stereo +// uint16_t channels; +// +// // Only allow 8, 16, 24 and 32 bit data +// uint8_t bytesPerSample; + + // sampleRate * channels * bitDepth/8 +// uint32_t byteRate; + + // For wav files where the audio data starts + uint32_t dataOffset = 0; + + // Store where playback ended if we want to resume + uint32_t startPlayFrom = 0; + + void setChannels(uint8_t channels) { + if(channels == 2) { + format |= 1; + } + } + + uint16_t getChannels() { + return format & STEREO ? 2 : 1; + } + + // Return false is sample rate not supported + boolean setSampleRate(uint32_t sampleRate) { + if(sampleRate % 11025 == 0) { + uint8_t m = (sampleRate / 11025) - 1; + + if(m == 3) m = 2; + format |= m << 3; + } else if(sampleRate % 48000 == 0) { + if(sampleRate / 48000 == 1) { + format |= 3 << 3; + } else { + format |= 4 << 3; + } + } else { + return false; + } + return true; + } + + uint32_t getSampleRate() { + return SAMPLE_RATES[(format & SAMPLE_RATE_MASK) >> 3]; + } + + void setBitsPerSample(uint8_t bits) { + format |= ((bits >> 3) - 1) << 1; + } + + uint8_t getBytesPerSample() { + return BIT_DEPTHS[(format & BIT_DEPTH_MASK) >> 1] >> 3; + } + + // Bytes per second + uint32_t getBandwidth() { + return getSampleRate() * getBytesPerSample() * getChannels(); + } + + // Packed format, Big endian. + // Bit 0 Mono / Stereo + // Bits 1 + 2 : Bit depth + // 0 0 : 8 Bit + // 0 1 : 16 bit + // 1 0 : 24 bit + // 1 1 : 32 bit + // Bits 2 -> 5 : Sample Rate. 11, 22, 44, 48 and 96 + // 0 0 1 : 11 + // 0 1 0 : 22 + // 0 1 1 : 44 + // 1 0 0 : 48 + // 1 0 1 : 96 + uint8_t format = 0; +}; + +#endif diff --git a/RadioMusic/AudioSystemHelpers.h b/RadioMusic/AudioSystemHelpers.h new file mode 100644 index 0000000..d7dff4e --- /dev/null +++ b/RadioMusic/AudioSystemHelpers.h @@ -0,0 +1,47 @@ +#ifndef AudioSystemHelpers_h +#define AudioSystemHelpers_h + +#include +#include "RamMonitor.h" + +// REBOOT CODES +#define RESTART_ADDR 0xE000ED0C +#define READ_RESTART() (*(volatile uint32_t *)RESTART_ADDR) +#define WRITE_RESTART(val) ((*(volatile uint32_t *)RESTART_ADDR) = (val)) + +void reBoot(int delayTime) { + Serial.println("Rebooting"); +// Serial.end(); + if (delayTime > 0) + delay(delayTime); + WRITE_RESTART(0x5FA0004); +} + +RamMonitor ramMonitor; +elapsedMillis cpuCheckTimer; + +void checkCPU() { + if (cpuCheckTimer > 3000) { + int maxCPU = AudioProcessorUsageMax(); + + Serial.print("MaxCPU : "); + Serial.print(maxCPU); + Serial.print("\tUnallocated : "); + Serial.print(ramMonitor.unallocated()); + Serial.print("\tFree : "); + Serial.println(ramMonitor.free()); + + if(ramMonitor.warning_crash()) { + Serial.println("!!!!#### CRASH WARNING #####!!!!"); + } + + if(ramMonitor.warning_lowmem()) { + Serial.println("!!!!#### LOW MEMORY WARNING #####!!!!"); + } + + cpuCheckTimer = 0; + AudioProcessorUsageMaxReset(); + } +} + +#endif diff --git a/RadioMusic/DebugUtils.h b/RadioMusic/DebugUtils.h new file mode 100644 index 0000000..334bdfc --- /dev/null +++ b/RadioMusic/DebugUtils.h @@ -0,0 +1,18 @@ +#ifndef DebugUtils_h +#define DebugUtils_h + +#include "Arduino.h" + +void showAddress(void* thing) { + uint16_t ptr; + char string[]="0123456789ABCDEF"; // array of characters corresponding to numbers from 0 to 15 + + /* these lines go inside the loop */ + ptr = (uint16_t) &thing; // store 16-bit address of 'value' + Serial.write( string[ (ptr >> 12) & 0xF ] ); // Write out highest 4-bits of memory address + Serial.write( string[ (ptr >> 8) & 0xF ] ); + Serial.write( string[ (ptr >> 4) & 0xF ] ); + Serial.write( string[ (ptr >> 0) & 0xF ] ); // Write out lowest 4-bits of memory address +} + +#endif diff --git a/RadioMusic/FileScanner.cpp b/RadioMusic/FileScanner.cpp new file mode 100644 index 0000000..a65109a --- /dev/null +++ b/RadioMusic/FileScanner.cpp @@ -0,0 +1,253 @@ +#include "FileScanner.h" +#include "AudioFileInfo.h" + +#include "RadioMusic.h" + +#ifdef DEBUG_FILES +#define D(x) x +#else +#define D(x) +#endif + +// File name compare routine for qsort +int fileNameCompare(const void *a, const void *b) { + AudioFileInfo *sa = (AudioFileInfo *) a; + AudioFileInfo *sb = (AudioFileInfo *) b; + + return sa->name.compareTo(sb->name); +} + +FileScanner::FileScanner() { +} + +void FileScanner::sortFiles() { + for (int i = 0; i < BANKS; i++) { + if (numFilesInBank[i] > 0) { + qsort(&(fileInfos[i][0]), numFilesInBank[i], sizeof(AudioFileInfo), + fileNameCompare); + } + } +} + +void FileScanner::scan(File* root, Settings& settings) { + + onlyNativeFormat = !settings.anyAudioFiles; + + if(!onlyNativeFormat) { + maximumFilesPerBank = 32; + } + + if (SD.exists("config.txt")) { + D(Serial.println("Scan TipTop"); ); + getExtensionlessFilesInRoot(root); + // disable looping for now + // TODO : remove this when looping is fixed for 24 bit + settings.looping = false; + settings.hardSwap = true; + settings.pitchMode = true; + D( + Serial.print("Finished Tip Top with "); Serial.print(lastBankIndex); Serial.println(" active banks");); + } else { + D( + if(onlyNativeFormat) { + Serial.println("Scan Radio Music. 44/16 Only."); + } else { + Serial.println("Scan Radio Music. All supported formats."); + } + + ); + scanDirectory(root); + D(Serial.println("Scan finished"); ); + if (settings.sort) { + sortFiles(); + D(showSortedFiles()); + } + } +} + +void FileScanner::showSortedFiles() { + for (int i = 0; i < BANKS; i++) { + Serial.print("Bank "); + Serial.println(i); + if (numFilesInBank[i] > 0) { + for(int j=0;jopenNextFile(O_RDONLY); + D(Serial.println(currentFile.name()); ); + if (!currentFile) { + // no more files + break; + } + boolean addFile = false; + currentFilename = currentFile.name(); + if (!currentFile.isDirectory() && currentFilename.indexOf('.') == -1) { + // assume its a tip top style wav file + int numFiles = numFilesInBank[directoryNumber]; + + AudioFileInfo& fileInfo = fileInfos[directoryNumber][numFiles]; + addFile = processWavFile(¤tFile, fileInfo); + + if (addFile) { + fileInfo.dataOffset = 48; + fileInfo.setBitsPerSample(24); + fileInfo.size -= 6; + D( + if (fileInfo.size % 3 != 0) { + Serial.print("FS data size not aligned "); + Serial.println(fileInfo.size % 3); + } + ); + fileInfo.name = currentFile.name(); + numFilesInBank[directoryNumber]++; + } + if (numFilesInBank[directoryNumber] >= MAX_FILES) { + directoryNumber++; + if (directoryNumber >= BANKS) { + D(Serial.println("Max Files reached"); ); + currentFile.close(); + break; + } else { + D(Serial.print("Moved to bank "); Serial.println(directoryNumber);); + } + if (directoryNumber > lastBankIndex) { + lastBankIndex = directoryNumber; + } + } + } + + currentFile.close(); + } +} + +void FileScanner::scanDirectory(File* dir) { + + D(Serial.print("Scan Dir "); Serial.println(dir->name()); Serial.println();); + + while (true) { + + File currentFile = dir->openNextFile(FILE_READ); + if (!currentFile) { + D(Serial.print("No file from "); Serial.print(dir->name()); Serial.print("\t"); Serial.println(currentFile.name());); + break; + } else { + D(Serial.print("Current file is ");Serial.println(currentFile.name())); + } + currentFilename = currentFile.name(); + boolean addFile = false; + + if (currentFilename.startsWith("_") != 0) { + currentFile.close(); + continue; + } + + if (currentFile.isDirectory()) { + // Ignore OSX Spotlight and Trash Directories + if (currentFilename.startsWith("SPOTL") == 0 + && currentFilename.startsWith("TRASH") == 0) { + currentDirectory = currentFilename; + scanDirectory(¤tFile); + } + } else { + for (int i = 0; i < NUM_FILE_TYPES; ++i) { + if (currentFilename.endsWith(fileTypes[i])) { + int directoryNumber = currentDirectory.toInt(); + if (directoryNumber >= BANKS) { + currentFile.close(); + return; + } + if (directoryNumber > lastBankIndex) { + lastBankIndex = directoryNumber; + } + int numFiles = numFilesInBank[directoryNumber]; + + AudioFileInfo& fileInfo = + fileInfos[directoryNumber][numFiles]; + // wav / WAV + if (i == 2 || i == 3) { + addFile = processWavFile(¤tFile, fileInfo); + } else { + addFile = processRawFile(¤tFile, fileInfo); + } + if (addFile) { + + fileInfo.name = currentDirectory + "/" + currentFilename; + numFilesInBank[directoryNumber]++; + D( + Serial.print("Adding file "); + Serial.print(numFilesInBank[directoryNumber]); + Serial.print(" : "); + Serial.println(currentFilename); + ); + if (numFilesInBank[directoryNumber] == MAX_FILES) { + D(Serial.println("Max Files reached"); ); + currentFile.close(); + return; + } + } + } + } + } + + currentFile.close(); + } +} + +boolean FileScanner::processRawFile(File* rawFile, AudioFileInfo& fileInfo) { + D(Serial.print("Raw File "); Serial.println(rawFile->name()); Serial.println();); + + fileInfo.size = rawFile->size(); + fileInfo.setChannels(1); + fileInfo.setBitsPerSample(16); + fileInfo.setSampleRate(44100); + + D(Serial.print("Raw file format "); Serial.print(fileInfo.format); Serial.print("\t"); Serial.print(fileInfo.getChannels()); Serial.print("\t"); Serial.print(fileInfo.getBytesPerSample()); Serial.print("\t"); Serial.println(fileInfo.getSampleRate());); + return true; +} + +boolean FileScanner::processWavFile(File* wavFile, AudioFileInfo& fileInfo) { + D(Serial.print("Process Wav File "); Serial.println(wavFile->name()); Serial.println();); + + if (wavHeaderReader.read(wavFile, fileInfo)) { + D( + Serial.print("Size: "); + Serial.print(fileInfo.size); + Serial.print("\tSRate: "); + Serial.print(fileInfo.getSampleRate()); + Serial.print("\tChannels: "); + Serial.print(fileInfo.getChannels()); + Serial.print("\tBPS: "); + Serial.print(fileInfo.getBytesPerSample()); + Serial.print("\tOffset: "); + Serial.println(fileInfo.dataOffset); + ); + if (onlyNativeFormat) { + if (fileInfo.getChannels() == 1 && fileInfo.getBytesPerSample() == 2 + && fileInfo.getSampleRate() == 44100) { + return true; + } else { + return false; + } + } + // We don't handle 96/24 stereo now because its too memory intensive + if (fileInfo.getChannels() == 2 && fileInfo.getSampleRate() > 90000 + && fileInfo.getBytesPerSample() == 3) { + return false; + } + + // Only mono or stereo. + if (fileInfo.getChannels() == 1 || fileInfo.getChannels() == 2) { + return true; + } + } + + return false; +} diff --git a/RadioMusic/FileScanner.h b/RadioMusic/FileScanner.h new file mode 100644 index 0000000..8c11551 --- /dev/null +++ b/RadioMusic/FileScanner.h @@ -0,0 +1,45 @@ +#ifndef FileScanner_h +#define FileScanner_h + +#include +#include "AudioFileInfo.h" +#include "WavHeaderReader.h" +#include "Settings.h" + +#define BANKS 16 +#define MAX_FILES 48 + +// .raw and .wav but both lower and upper case +#define NUM_FILE_TYPES 4 + +class FileScanner { + public: + FileScanner(); + void scan(File* root, Settings& settings); + + int lastBankIndex = 0; + AudioFileInfo fileInfos[BANKS][MAX_FILES]; + int numFilesInBank[BANKS]; + + private: + uint8_t maximumFilesPerBank = MAX_FILES; + void scanDirectory(File* dir); + void sortFiles(); + void showSortedFiles(); + + void getExtensionlessFilesInRoot(File* root); + boolean processWavFile(File* wavFile, AudioFileInfo& fileInfo); + boolean processRawFile(File* wavFile, AudioFileInfo& fileInfo); + String fileTypes[4] = { + "RAW", "raw", "WAV", "wav" + }; + String currentDirectory = "0"; + + WavHeaderReader wavHeaderReader; + // If true only scan for 44k, 16bit mono files. + boolean onlyNativeFormat = false; + + String currentFilename; +}; + +#endif diff --git a/RadioMusic/Helpers.ino b/RadioMusic/Helpers.ino deleted file mode 100644 index 2dd6bf4..0000000 --- a/RadioMusic/Helpers.ino +++ /dev/null @@ -1,309 +0,0 @@ -////////////////////////////////////////// -// SCAN SD DIRECTORIES INTO ARRAYS ////// -///////////////////////////////////////// - - -void scanDirectory(File dir, int numTabs) { - - while(true) { - File entry = dir.openNextFile(); - if (! entry) { - // no more files - break; - } - String fileName = entry.name(); - - if (fileName.endsWith(FILE_TYPE) && fileName.startsWith("_") == 0){ - int intCurrentDirectory = CURRENT_DIRECTORY.toInt(); - if (intCurrentDirectory > ACTIVE_BANKS) ACTIVE_BANKS = intCurrentDirectory; - FILE_NAMES[intCurrentDirectory][FILE_COUNT[intCurrentDirectory]].name = entry.name(); - FILE_NAMES[intCurrentDirectory][FILE_COUNT[intCurrentDirectory]].size = entry.size(); - FILE_COUNT[intCurrentDirectory]++; - }; - // Ignore OSX Spotlight and Trash Directories - if (entry.isDirectory() && fileName.startsWith("SPOTL") == 0 && - fileName.startsWith("TRASH") == 0) { - CURRENT_DIRECTORY = entry.name(); - scanDirectory(entry, numTabs+1); - } - entry.close(); - } -} - - -char filename[18]; -// TAKE BANK AND CHANNEL AND RETURN PROPERLY FORMATTED PATH TO THE FILE -char* buildPath (int bank, int channel){ - sprintf(filename, "%d", bank); - strcat(filename, "/"); - strcat(filename, FILE_NAMES[bank][channel].name.c_str()); - return filename; -} - -void reBoot(int delayTime){ - if (delayTime > 0) - delay (delayTime); - WRITE_RESTART(0x5FA0004); -} - - -////////////////////////////////////////////// -////////VARIOUS DEBUG DISPLAY FUNCTIONS ////// -////////////////////////////////////////////// - -// SHOW VISUAL INDICATOR OF TRACK PROGRESS IN SERIAL MONITOR -void playDisplay(){ - int position = (playRaw1.fileOffset() % FILE_NAMES[PLAY_BANK][PLAY_CHANNEL].size) >> 21; - int size = FILE_NAMES[PLAY_BANK][PLAY_CHANNEL].size >> 21; - for (int i = 0; i < size; i++){ - if (i == position) Serial.print("|"); - else Serial.print("_"); - } - Serial.println(""); -} - - -// SHOW CURRENTLY PLAYING TRACK IN SERIAL MONITOR -void whatsPlaying (){ - Serial.print("Bank:"); - Serial.print(PLAY_BANK); - Serial.print(" Channel:"); - Serial.print(PLAY_CHANNEL); - Serial.print(" File:"); - Serial.println (charFilename); -} - - -void printFileList(){ - - for (int x = 0; x < BANKS; x++){ - Serial.print("Bank: "); - Serial.println(x); - - Serial.print (FILE_COUNT[x]); - Serial.print(" "); - Serial.print (FILE_TYPE); - Serial.println(" Files found"); - - for (int i = 0; i < FILE_COUNT[x]; i++){ - Serial.print (i); - Serial.print (") "); -// Serial.print (FILE_DIRECTORIES[x][i]); -// Serial.print(" | "); - Serial.print(FILE_NAMES[x][i].name); - Serial.print(" | "); - Serial.print(FILE_NAMES[x][i].size); - Serial.println(" | "); - } - } -} - - -void printSettings(){ - Serial.print("MUTE="); - Serial.println(MUTE); - Serial.print("DECLICK="); - Serial.println(DECLICK); - Serial.print("ShowMeter="); - Serial.println(ShowMeter); - Serial.print("meterHIDE="); - Serial.println(meterHIDE); - Serial.print("ChanPotImmediate="); - Serial.println(ChanPotImmediate); - Serial.print("ChanCVImmediate="); - Serial.println(ChanCVImmediate); - Serial.print("StartPotImmediate="); - Serial.println(StartPotImmediate); - Serial.print("StartCVImmediate="); - Serial.println(StartCVImmediate); -} - - - - - - - - - -// DISPLAY PEAK METER IN LEDS -void peakMeter(){ - if (peak1.available()) { - fps = 0; - float peakReading = peak1.read(); - int monoPeak = round ( peakReading * 4); - monoPeak = round (pow(2,monoPeak)); - ledWrite(monoPeak-1); // - } -} - - -// WRITE A 4 DIGIT BINARY NUMBER TO LED0-LED3 -void ledWrite(int n){ - digitalWrite(LED0, HIGH && (n & B00001000)); - digitalWrite(LED1, HIGH && (n & B00000100)); - digitalWrite(LED2, HIGH && (n & B00000010)); - digitalWrite(LED3, HIGH && (n & B00000001)); -} - - - - - -////////////////////////////////////////////// -////READ AND WRITE SETTINGS ON THE SD CARD /// -////VIA http://overskill.alexshu.com/?p=107 /// -////////////////////////////////////////////// - - - - -void readSDSettings(){ - char character; - String settingName; - String settingValue; - settingsFile = SD.open("settings.txt"); - if (settingsFile) { - while (settingsFile.available()) { - character = settingsFile.read(); - while(character != '='){ - settingName = settingName + character; - character = settingsFile.read(); - } - character = settingsFile.read(); - while(character != '\n'){ - settingValue = settingValue + character; - character = settingsFile.read(); - if(character == '\n'){ - /* - //Debuuging Printing - Serial.print("Name:"); - Serial.println(settingName); - Serial.print("Value :"); - Serial.println(settingValue); - */ - // Apply the value to the parameter - applySetting(settingName,settingValue); - // Reset Strings - settingName = ""; - settingValue = ""; - } - } - } - // close the file: - settingsFile.close(); - } - else { - // if the file didn't open, print an error: - Serial.println("error opening settings.txt"); - } -} -/* Apply the value to the parameter by searching for the parameter name - Using String.toInt(); for Integers - toFloat(string); for Float - toBoolean(string); for Boolean - */ -void applySetting(String settingName, String settingValue) { - - if(settingName == "MUTE") { - MUTE=toBoolean(settingValue); - } - - if(settingName == "DECLICK") { - DECLICK=settingValue.toInt(); - } - - if(settingName == "ShowMeter") { - ShowMeter=toBoolean(settingValue); - } - - if(settingName == "meterHIDE") { - meterHIDE=settingValue.toInt(); - } - - if(settingName == "ChanPotImmediate") { - ChanPotImmediate=toBoolean(settingValue); - } - - if(settingName == "ChanCVImmediate") { - ChanCVImmediate=toBoolean(settingValue); - } - - if(settingName == "StartPotImmediate") { - StartPotImmediate=toBoolean(settingValue); - } - - if(settingName == "StartCVImmediate") { - StartCVImmediate=toBoolean(settingValue); - } - - if(settingName == "StartCVDivider") { - StartCVDivider=settingValue.toInt();; - } - - if(settingName == "Looping") { - Looping=toBoolean(settingValue); - } - - if (settingName == "Sort") { - SortFiles = toBoolean(settingValue); - gotSort = true; - } - - -} -// converting string to Float -float toFloat(String settingValue){ - char floatbuf[settingValue.length()]; - settingValue.toCharArray(floatbuf, sizeof(floatbuf)); - float f = atof(floatbuf); - return f; -} -// Converting String to integer and then to boolean -// 1 = true -// 0 = false -boolean toBoolean(String settingValue) { - if(settingValue.toInt()==1){ - return true; - } - else { - return false; - } -} -// Writes A Configuration file -void writeSDSettings() { - // Delete the old One - SD.remove("settings.txt"); - // Create new one - settingsFile = SD.open("settings.txt", FILE_WRITE); - // writing in the file works just like regular print()/println() function - settingsFile.print("MUTE="); - settingsFile.println(MUTE); - settingsFile.print("DECLICK="); - settingsFile.println(DECLICK); - settingsFile.print("ShowMeter="); - settingsFile.println(ShowMeter); - settingsFile.print("meterHIDE="); - settingsFile.println(meterHIDE); - settingsFile.print("ChanPotImmediate="); - settingsFile.println(ChanPotImmediate); - settingsFile.print("ChanCVImmediate="); - settingsFile.println(ChanCVImmediate); - settingsFile.print("StartPotImmediate="); - settingsFile.println(StartPotImmediate); - settingsFile.print("StartCVImmediate="); - settingsFile.println(StartCVImmediate); - settingsFile.print("StartCVDivider="); - settingsFile.println(StartCVDivider); - settingsFile.print("Looping="); - settingsFile.println(Looping); - settingsFile.print("Sort="); - settingsFile.println(SortFiles); - - - - // close the file: - settingsFile.close(); - //Serial.println("Writing done."); -} - diff --git a/RadioMusic/Interface.cpp b/RadioMusic/Interface.cpp new file mode 100644 index 0000000..b797bc0 --- /dev/null +++ b/RadioMusic/Interface.cpp @@ -0,0 +1,264 @@ +#include "Interface.h" + +#include "Arduino.h" +#include "Bounce2.h" +#include "RadioMusic.h" + +#ifdef DEBUG_INTERFACE +#define D(x) x +#else +#define D(x) +#endif + +// SETUP VARS TO STORE CONTROLS +// A separate variable for tracking reset CV only +volatile boolean resetCVHigh = false; + +// Called by interrupt on rising edge, for RESET_CV pin +void resetcv() { + resetCVHigh = true; +} + +void Interface::init(int fileSize, int channels, const Settings& settings, PlayState* state) { + + analogReadRes(ADC_BITS); + pinMode(RESET_BUTTON, OUTPUT); + pinMode(RESET_CV, settings.resetIsOutput ? OUTPUT : INPUT); + + // Add an interrupt on the RESET_CV pin to catch rising edges + attachInterrupt(RESET_CV, resetcv, RISING); + + uint16_t bounceInterval = 5; + resetButtonBounce.attach(RESET_BUTTON); + resetButtonBounce.interval(bounceInterval); + + // make it backwards compatible with the old 10-bit cv and divider + startCVDivider = settings.startCVDivider * (ADC_MAX_VALUE / 1024); + + pitchMode = settings.pitchMode; + + if(pitchMode) { + quantiseRootCV = settings.quantiseRootCV; + quantiseRootPot = settings.quantiseRootPot; + + float lowNote = settings.lowNote + 0.5; + startCVInput.setRange(lowNote, lowNote + settings.noteRange, quantiseRootCV); + startPotInput.setRange(0.0,48, quantiseRootPot); + startCVInput.borderThreshold = 64; + startPotInput.borderThreshold = 64; + } else { + D(Serial.print("Set Start Range ");Serial.println(ADC_MAX_VALUE / startCVDivider);); + startPotInput.setRange(0.0, ADC_MAX_VALUE / startCVDivider, false); + startCVInput.setRange(0.0, ADC_MAX_VALUE / startCVDivider, false); + startPotInput.setAverage(true); + startCVInput.setAverage(true); + startCVInput.borderThreshold = 32; + startPotInput.borderThreshold = 32; + } + + channelPotImmediate = settings.chanPotImmediate; + channelCVImmediate = settings.chanCVImmediate; + + startPotImmediate = settings.startPotImmediate; + startCVImmediate = settings.startCVImmediate; + + setChannelCount(channels); + + playState = state; + buttonTimer = 0; + buttonHoldTime = 0; + buttonHeld = false; +} + +void Interface::setChannelCount(uint16_t count) { + channelCount = count; + channelCVInput.setRange(0, channelCount - 1, true); + channelPotInput.setRange(0, channelCount - 1, true); + D(Serial.print("Channel Count ");Serial.println(channelCount);); +} + +uint16_t Interface::update() { + + uint16_t channelChanged = updateChannelControls(); + uint16_t startChanged = pitchMode ? updateRootControls() : updateStartControls(); + + changes = channelChanged; + changes |= startChanged; + changes |= updateButton(); + + if(resetCVHigh || (changes & BUTTON_SHORT_PRESS)) { + changes |= RESET_TRIGGERED; + } + resetCVHigh = false; + + return changes; +} + +uint16_t Interface::updateChannelControls() { + + boolean channelCVChanged = channelCVInput.update(); + boolean channelPotChanged = channelPotInput.update(); + + uint16_t channelChanged = 0; + + if(channelCVChanged || channelPotChanged) { + int channel = (int) constrain(channelCVInput.currentValue + channelPotInput.currentValue, 0, channelCount - 1); + + if (channel != playState->currentChannel) { + D(Serial.print("Channel ");Serial.println(channel);); + playState->nextChannel = channel; + channelChanged |= CHANNEL_CHANGED; + if((channelPotImmediate && channelPotChanged) || (channelCVImmediate && channelCVChanged)) { + playState->channelChanged = true; + } + } else { + D( + Serial.print("Channel change flag but channel is the same: "); + Serial.print(channel); + Serial.print(" "); + Serial.print(channelCVInput.currentValue); + Serial.print(" "); + Serial.print(channelPotInput.currentValue); + Serial.print(" "); + Serial.println(playState->currentChannel); + ); + } + } + + return channelChanged; +} + +uint16_t Interface::updateStartControls() { + uint16_t changes = 0; + + boolean cvChanged = startCVInput.update(); + boolean potChanged = startPotInput.update(); + + if(potChanged) { + changes |= TIME_POT_CHANGED; + if(startPotImmediate) { + changes |= CHANGE_START_NOW; + } + } + + if(cvChanged) { + changes |= TIME_CV_CHANGED; + if(startCVImmediate) { + changes |= CHANGE_START_NOW; + } + } + + start = constrain(((startCVInput.currentValue * startCVDivider) + (startPotInput.currentValue * startCVDivider)),0,ADC_MAX_VALUE); + + if(changes) { +// D( +// Serial.print("Start "); +// Serial.print(start); +// Serial.print("\t"); +// Serial.print(startCVInput.currentValue); +// Serial.print("\t"); +// Serial.println(startPotInput.currentValue); +// ); + D(startPotInput.printDebug();); +// D(startCVInput.printDebug();); + } + return changes; +} + +// return bitmap of state of changes for CV, Pot and combined Note. +uint16_t Interface::updateRootControls() { + + uint16_t change = 0; + + boolean cvChanged = startCVInput.update(); + boolean potChanged = startPotInput.update(); + + // early out if no changes + if(!cvChanged && !potChanged) { + return change; + } + + float rootPot = startPotInput.currentValue; + float rootCV = startCVInput.currentValue; + + if(cvChanged) { + D( + Serial.println("CV Changed"); + ); + if(quantiseRootCV) { + rootNoteCV = floor(rootCV); + if(rootNoteCV != rootNoteCVOld) { + D( + Serial.print("CV ");Serial.println(startCVInput.inputValue); + ); + change |= ROOT_CV_CHANGED; + } + } else { + rootNoteCV = rootCV; + change |= ROOT_CV_CHANGED; + } + } + + if(potChanged) { + D( + Serial.println("Pot Changed"); + ); + if(quantiseRootPot) { + rootNotePot = floor(rootPot); + if(rootNotePot != rootNotePotOld) { + D( + Serial.print("Pot ");Serial.println(startPotInput.inputValue); + ); + change |= ROOT_POT_CHANGED; + } + } else { + rootNotePot = rootPot; + change |= ROOT_POT_CHANGED; + } + } + + rootNote = rootNoteCV + rootNotePot; + + // Flag note changes when the note index itself changes + if(floor(rootNote) != rootNoteOld) { + change |= ROOT_NOTE_CHANGED; + rootNoteOld = floor(rootNote); + } + + return change; +} + +uint16_t Interface::updateButton() { + + resetButtonBounce.update(); + uint16_t buttonState = 0; + + // Button pressed + if(resetButtonBounce.rose()) { + buttonTimer = 0; + buttonHeld = true; + } + + if(resetButtonBounce.fell()) { + buttonHeld = false; + // button has been held down for some time + if (buttonTimer >= SHORT_PRESS_DURATION && buttonTimer < LONG_PRESS_DURATION){ + buttonState |= BUTTON_SHORT_PRESS; + } else if(buttonTimer > LONG_PRESS_DURATION) { + buttonState |= BUTTON_LONG_RELEASE; + } + buttonTimer = 0; + } + + if(buttonHeld && buttonTimer >= LONG_PRESS_DURATION) { + buttonState |= BUTTON_LONG_PRESS; + + uint32_t diff = buttonTimer - LONG_PRESS_DURATION; + if(diff >= LONG_PRESS_PULSE_DELAY) { + buttonState |= BUTTON_PULSE; + buttonTimer = LONG_PRESS_DURATION; + } + } + + return buttonState; +} diff --git a/RadioMusic/Interface.h b/RadioMusic/Interface.h new file mode 100644 index 0000000..61be540 --- /dev/null +++ b/RadioMusic/Interface.h @@ -0,0 +1,97 @@ +#ifndef Interface_h +#define Interface_h + +#include "Arduino.h" + +#include +#include "PlayState.h" +#include "Settings.h" +#include "AnalogInput.h" + +#define CHAN_POT_PIN A9 // pin for Channel pot +#define CHAN_CV_PIN A6 // pin for Channel CV +#define TIME_POT_PIN A7 // pin for Time pot +#define TIME_CV_PIN A8 // pin for Time CV +#define RESET_BUTTON 8 // Reset button +#define RESET_CV 9 // Reset pulse input + +#define TIME_POT_CHANGED 1 +#define TIME_CV_CHANGED 1 << 1 +#define CHANNEL_CHANGED 1 << 2 +#define CHANGE_START_NOW 1 << 3 +#define BUTTON_SHORT_PRESS 1 << 4 +#define BUTTON_LONG_PRESS 1 << 5 +#define BUTTON_LONG_RELEASE 1 << 6 +#define BUTTON_PULSE 1 << 7 +#define RESET_TRIGGERED 1 << 8 +#define ROOT_CV_CHANGED 1 << 9 +#define ROOT_POT_CHANGED 1 << 10 +#define ROOT_NOTE_CHANGED 1 << 11 + +#define SHORT_PRESS_DURATION 10 +#define LONG_PRESS_DURATION 600 +// after LONG_PRESS_DURATION every LONG_PRESS_PULSE_DELAY milliseconds the update +// function will set BUTTON_PULSE +#define LONG_PRESS_PULSE_DELAY 600 + +#define SAMPLEAVERAGE 16 // How many values are read and averaged of pot/CV inputs each interface check. + +class Interface { +public: + boolean quantiseRootCV = true; + boolean quantiseRootPot = true; + + float rootNoteCV = 36; + float rootNotePot = 36; + float rootNote = 36; + + boolean buttonHeld = false; + unsigned long start = 0; + + elapsedMillis buttonHoldTime; + + PlayState* playState; + + Interface() : channelCVInput(CHAN_CV_PIN), + channelPotInput(CHAN_POT_PIN), + startCVInput(TIME_CV_PIN), + startPotInput(TIME_POT_PIN) + { + playState = NULL; + } + + void init(int fileSize, int channels, const Settings& settings, PlayState* state); + void setChannelCount(uint16_t count); + void setFileSize(uint32_t fileSize); + + uint16_t update(); + uint16_t updateButton(); +private: + AnalogInput channelCVInput; + AnalogInput channelPotInput; + AnalogInput startCVInput; + AnalogInput startPotInput; + + Bounce resetButtonBounce; + elapsedMillis buttonTimer = 0; + + uint16_t changes = 0; + + uint16_t startCVDivider = 1; + boolean channelPotImmediate = true; + boolean channelCVImmediate = true; + boolean startPotImmediate = true; + boolean startCVImmediate = true; + + uint16_t channelCount = 0; + float rootNoteOld = -100; + float rootNotePotOld = -100; + float rootNoteCVOld = -100; + + uint16_t updateChannelControls(); + uint16_t updateStartControls(); + uint16_t updateRootControls(); + boolean pitchMode = false; +}; + +#endif diff --git a/RadioMusic/Interface.ino b/RadioMusic/Interface.ino deleted file mode 100644 index 7fcfc18..0000000 --- a/RadioMusic/Interface.ino +++ /dev/null @@ -1,152 +0,0 @@ -////////////////////////////////////////// -// CHECK INTERFACE POTS BUTTONS ETC ////// -///////////////////////////////////////// - -// A moving average of previous 5 values -void checkInterface(){ - - int channel; - unsigned long time; - // This is set to true when chan changes and back to false - // when reset is performed the next time. - static boolean chanHasChanged = false; - boolean buttonUp = false; - boolean buttonDown = false; - - // READ & AVERAGE POTS - - int chanPot = 0; - int chanCV = 0; - int timPot = 0; - int timCV = 0; - - for (int i = 0; i < SAMPLEAVERAGE; i++){ - chanPot += analogRead(CHAN_POT_PIN); - chanCV += analogRead(CHAN_CV_PIN); - timPot += analogRead(TIME_POT_PIN); - timCV += analogRead(TIME_CV_PIN); - } - - chanPot = chanPot / SAMPLEAVERAGE; - chanCV = chanCV / SAMPLEAVERAGE; - timPot = timPot / SAMPLEAVERAGE; - timCV = timCV / SAMPLEAVERAGE; - - // Snap small values to zero. - if (timPot <= timHyst) - timPot = 0; - if (timCV <= timHyst) - timCV = 0; - - // IDENTIFY POT / CV CHANGES - - boolean chanPotChange = (abs(chanPot - chanPotOld) > chanHyst); - boolean chanCVChange = (abs(chanCV - chanCVOld) > chanHyst); - boolean timPotChange = (abs(timPot - timPotOld) > timHyst); - boolean timCVChange = (abs(timCV - timCVOld) > timHyst); - - // MAP INPUTS TO CURRENT SITUATION - - channel = chanPot + chanCV; - channel = constrain(channel, 0, 1023); - channel = map(channel,0,1024,0,FILE_COUNT[PLAY_BANK]); // Highest pot value = 1 above what's possible (ie 1023+1) and file count is one above the number of the last file (zero indexed) - - time = timPot + timCV; - time = constrain(time, 0U, 1023U); - time = (time / StartCVDivider) * StartCVDivider; // Quantizes start position - time = time * (FILE_NAMES[PLAY_BANK][PLAY_CHANNEL].size / 1023); - - // IDENTIFY AND DEPLOY RELEVANT CHANGES - - if (channel != PLAY_CHANNEL && chanPotChange) { - NEXT_CHANNEL = channel; - CHAN_CHANGED = ChanPotImmediate; - chanPotOld = chanPot; - }; - - if (channel != PLAY_CHANNEL && chanCVChange) { - NEXT_CHANNEL = channel; - CHAN_CHANGED = ChanCVImmediate; - chanCVOld = chanCV; - }; - - // chanHasChanged status has different lifespan from CHAN_CHANGED, - // it is set back to false when reset is performed next time. - if (CHAN_CHANGED) - chanHasChanged = true; - - // time pot or cv changes may cause reset if - // enabled in settings.txt - if (timPotChange){ - playhead = time; - RESET_CHANGED = StartPotImmediate; - timPotOld = timPot; - } - - if (timCVChange){ - playhead = time; - RESET_CHANGED = StartCVImmediate; - timCVOld = timCV; - } - - // Reset Button & CV - if ( resetSwitch.update() ) { - resetButton = resetSwitch.read(); -// Serial.println(resetButton); - } - // If button is up and was previously down, it's a button up event - if (!resetButton && prevResetButton) { - buttonUp = true; - } else if (resetButton && !prevResetButton) { - buttonDown = true; - bankTimer = 0; - } - prevResetButton = resetButton; - - if ((buttonDown || resetCVHigh) && !bankChangeMode) - { - // We must set the playhead on reset if we previously have changed the channel. - // But only once so that the further resets will guarantee to reset to the same spot again. - if (chanHasChanged) { - playhead = time; - chanHasChanged = false; - } - RESET_CHANGED = true; - buttonDown = false; -#if DEBUG - Serial.print("Timpot value: "); - Serial.println(timPot); - Serial.print("TimCV value: "); - Serial.println(timCV); -#endif - } - resetCVHigh = false; - - // Hold Reset Button to Change Bank -// bankTimer = bankTimer * digitalRead(RESET_BUTTON); - - if (buttonUp) { -// Serial.print("Button released, time = "); -// Serial.println(bankTimer); - - if (bankTimer > SPECOPSTIME) { - Serial.println("SPECOPS time!"); - } else if (bankTimer > HOLDTIME){ - bankChangeMode = !bankChangeMode; -#if DEBUG - Serial.print("bankChangeMode = "); - Serial.println(bankChangeMode); -#endif - } else if (bankChangeMode) { // Bankchange is executed on button up to prevent the extra change once you want out of this mode. - PLAY_BANK++; - if (PLAY_BANK > ACTIVE_BANKS) PLAY_BANK = 0; - if (NEXT_CHANNEL >= FILE_COUNT[PLAY_BANK]) NEXT_CHANNEL = FILE_COUNT[PLAY_BANK]-1; - CHAN_CHANGED = true; - bankTimer = 0; - meterDisplay = 0; - EEPROM.write(BANK_SAVE, PLAY_BANK); - } - buttonUp = false; - bankTimer = 0; - } -} diff --git a/RadioMusic/LedControl.cpp b/RadioMusic/LedControl.cpp new file mode 100644 index 0000000..929e691 --- /dev/null +++ b/RadioMusic/LedControl.cpp @@ -0,0 +1,61 @@ +#include "Arduino.h" +#include "LedControl.h" + +LedControl::LedControl() { + +} + +void LedControl::init() { + pinMode(RESET_LED, OUTPUT); + pinMode(LED0,OUTPUT); + pinMode(LED1,OUTPUT); + pinMode(LED2,OUTPUT); + pinMode(LED3,OUTPUT); +} + +// Set any combination of top row LEDS from the bottom 4 bits +// bit position is the opposite of light position +// e.g. if bottom 4 bits are 0001 the LEDs are * O O O +void LedControl::multi(uint8_t bits) { + digitalWrite(LED3, HIGH && (bits & 1)); + digitalWrite(LED2, HIGH && (bits & 2)); + digitalWrite(LED1, HIGH && (bits & 4)); + digitalWrite(LED0, HIGH && (bits & 8)); +} + +// Set a single LED on the top row, others will be unlit +void LedControl::single(int index) { + digitalWrite(LED3, HIGH && (index==0)); + digitalWrite(LED2, HIGH && (index==1)); + digitalWrite(LED1, HIGH && (index==2)); + digitalWrite(LED0, HIGH && (index==3)); +} + +void LedControl::showReset(boolean high) { + digitalWrite(RESET_LED, high ? HIGH : LOW); +} + +void LedControl::flash() { + flashingBank = true; + bankFlashTimer = 0; +} + +// Set the bank and top row index. +// Different banks have different flashing rates +// Only supports 3 banks for now +void LedControl::bankAndSingle(int bank, int index) { + // Flash waveform LEDs for custom waves + if(bank > 0 && bank < 3) { + if(bankFlashTimer >= bankLedFlashTimes[bank]) { + bankFlashTimer = 0; + flashingBank = !flashingBank; + if(flashingBank) { + single(index % 4); + } else { + single(15); + } + } + } else { + single(index % 4); + } +} diff --git a/RadioMusic/LedControl.h b/RadioMusic/LedControl.h new file mode 100644 index 0000000..e1e1544 --- /dev/null +++ b/RadioMusic/LedControl.h @@ -0,0 +1,32 @@ +#ifndef LedControl_h +#define LedControl_h + +/** + * Chord Organ / Radio Music LED control + * + * 4 LEDs across the top and 1 reset LED. + * + */ +#define LED0 6 +#define LED1 5 +#define LED2 4 +#define LED3 3 +#define RESET_LED 11 // Reset LED indicator + +class LedControl { + + public: + LedControl(); + void init(); + void single(int index); + void multi(uint8_t bits); + void bankAndSingle(int bank, int index); + void flash(); + void showReset(boolean high); + private: + boolean flashingBank = false; + elapsedMillis bankFlashTimer = 0; + uint16_t bankLedFlashTimes[3] = {400,100,30}; + +}; +#endif diff --git a/RadioMusic/PlayState.cpp b/RadioMusic/PlayState.cpp new file mode 100644 index 0000000..57dcb98 --- /dev/null +++ b/RadioMusic/PlayState.cpp @@ -0,0 +1,16 @@ + + +#include "PlayState.h" + +#include "Arduino.h" + +void PlayState::printDebug() { + Serial.print("Play State: "); + Serial.print(currentChannel); + Serial.print("\t"); + Serial.print(nextChannel); + Serial.print("\t"); + Serial.print(channelChanged); + Serial.print("\t"); + +} diff --git a/RadioMusic/PlayState.h b/RadioMusic/PlayState.h new file mode 100644 index 0000000..c7c296f --- /dev/null +++ b/RadioMusic/PlayState.h @@ -0,0 +1,16 @@ +#ifndef PlayState_h +#define PlayState_h + +#include "Arduino.h" + +class PlayState { +public: + uint8_t bank = 0; + uint8_t currentChannel = 0; + uint8_t nextChannel = 0; + boolean channelChanged = true; + + void printDebug(); +}; + +#endif diff --git a/RadioMusic/README.MD b/RadioMusic/README.MD new file mode 100644 index 0000000..9effd26 --- /dev/null +++ b/RadioMusic/README.MD @@ -0,0 +1,14 @@ + +Radio Music New Firmware + +Extra files are no longer needed + +To build this firmware and upload to your Radio Music + +1. Install Arduino IDE : https://www.arduino.cc/en/Main/Software +2. Install Teensyduino : https://www.pjrc.com/teensy/td_download.html +3. Get a copy of the source code : https://github.com/Normalised/RadioMusic/archive/master.zip +4. Unzip the source code somewhere +5. Connect the Radio Music to your computer via USB. If you are on OSX or Win 10 you won't need drivers. +5. Open RadioMusic.ino in Arduino IDE +6. Click the upload button : ![Upload](https://www.arduino.cc/en/uploads/Guide/export.png) diff --git a/RadioMusic/RadioMusic.h b/RadioMusic/RadioMusic.h new file mode 100644 index 0000000..6d60055 --- /dev/null +++ b/RadioMusic/RadioMusic.h @@ -0,0 +1,22 @@ +#ifndef RadioMusic_h +#define RadioMusic_h + +// Debug Flags +//#define DEBUG +//#define DEBUG_STARTUP +//#define DEBUG_SETTINGS +//#define DEBUG_INTERFACE +//#define DEBUG_WAV +//#define DEBUG_ENGINE +//#define DEBUG_FILES +//#define DEBUG_PCM_PLAYER +//#define DEBUG_AUDIO_BUFFER + +// Regularly check CPU and Memory +//#define CHECK_CPU + +// Test config modes +//#define TEST_RADIO_MODE +//#define TEST_DRUM_MODE + +#endif diff --git a/RadioMusic/RadioMusic.ino b/RadioMusic/RadioMusic.ino index 0c61526..c5401ec 100644 --- a/RadioMusic/RadioMusic.ino +++ b/RadioMusic/RadioMusic.ino @@ -1,5 +1,5 @@ /* -RADIO MUSIC + RADIO MUSIC https://github.com/TomWhitwell/RadioMusic Audio out: Onboard DAC, teensy3.1 pin A14/DAC @@ -36,404 +36,396 @@ RADIO MUSIC - Some refactoring and organization of code. */ -#include #include -#include -#include #include #include #include +#include "RadioMusic.h" +#include "AudioSystemHelpers.h" +#include "Settings.h" +#include "LedControl.h" +#include "FileScanner.h" +#include "AudioEngine.h" +#include "Interface.h" +#include "PlayState.h" + +#ifdef DEBUG +#define D(x) x +#else +#define D(x) +#endif -// Setting to 1 enables many debugging prints on serial console -#define DEBUG 0 - -// OPTIONS TO READ FROM THE SD CARD, WITH DEFAULT VALUES -boolean MUTE = false; // Crossfade clicks when changing channel / position, - // at cost of speed. Fade speed is set by DECLICK -unsigned int DECLICK= 25; // milliseconds of fade in/out on switching -boolean ShowMeter = true; // Does the VU meter appear? -int meterHIDE = 2000; // how long to show the meter after bank change in Milliseconds -boolean ChanPotImmediate = true; // Settings for Pot / CV response. -boolean ChanCVImmediate = true; // TRUE means it jumps directly when you move or change. -boolean StartPotImmediate = false; // FALSE means it only has an effect when RESET is pushed - // or triggered -boolean StartCVImmediate = false; -int StartCVDivider = 2; // Changes sensitivity of Start control. 1 = most sensitive, - // 512 = lest sensitive (i.e only two points) -boolean Looping = true; // When a file finishes, start again from the beginning -boolean SortFiles = true; // By default we sort the directory contents. - -boolean gotSort = false; // true if settings.txt exists and contains Sort setting. - // We don't want to turn on sorting on disks that - // already have settings.txt, the user might have - // done efforts to do fat sorting on it. -File settingsFile; +// Press reset button to reboot +//#define RESET_TO_REBOOT +//#define ENGINE_TEST + +#define EEPROM_BANK_SAVE_ADDRESS 0 + +#define FLASHTIME 10 // How long do LEDs flash for? +#define SHOWFREQ 250 // how many millis between serial Debug updates + +#define peakFPS 30 // FRAMERATE FOR PEAK METER + +#define SD_CARD_CHECK_DELAY 20 -// Audio engine definitions. -RMPlaySDRaw playRaw1; -RMPlaySDRaw playRaw2; -AudioEffectFade fade1; -AudioEffectFade fade2; -AudioMixer4 mixer; -AudioAnalyzePeak peak1; -AudioOutputAnalog dac1; -AudioConnection patchCord1(playRaw1, fade1); -AudioConnection patchCord2(playRaw2, fade2); -AudioConnection patchCord3(fade1, 0, mixer, 0); -AudioConnection patchCord4(fade2, 0, mixer, 1); -AudioConnection patchCord5(mixer, 0, dac1, 0); -AudioConnection patchCord6(mixer, 0, peak1, 0); - -// Pointers to current and previous player -RMPlaySDRaw *pRawPlayer = &playRaw1; -RMPlaySDRaw *pPrevRawPlayer = &playRaw2; -AudioEffectFade *pFadeOut = &fade1; -AudioEffectFade *pFadeIn = &fade2; - -// REBOOT CODES -#define RESTART_ADDR 0xE000ED0C -#define READ_RESTART() (*(volatile uint32_t *)RESTART_ADDR) -#define WRITE_RESTART(val) ((*(volatile uint32_t *)RESTART_ADDR) = (val)) - -// SETUP VARS TO STORE DETAILS OF FILES ON THE SD CARD -#define MAX_FILES 75 -#define BANKS 16 - -typedef struct FileInfo_s { - String name; - unsigned long size; -} FileInfo_t; - -int ACTIVE_BANKS; -String FILE_TYPE = "RAW"; -FileInfo_t FILE_NAMES [BANKS][MAX_FILES]; -int FILE_COUNT[BANKS]; -String CURRENT_DIRECTORY = "0"; -File root; - -// SETUP VARS TO STORE CONTROLS -#define CHAN_POT_PIN A9 // pin for Channel pot -#define CHAN_CV_PIN A6 // pin for Channel CV -#define TIME_POT_PIN A7 // pin for Time pot -#define TIME_CV_PIN A8 // pin for Time CV -#define RESET_BUTTON 8 // Reset button -#define RESET_LED 11 // Reset LED indicator -#define RESET_CV 9 // Reset pulse input - -boolean CHAN_CHANGED = true; -boolean RESET_CHANGED = false; -boolean EOF_REACHED = false; -// A separate variable for tracking reset CV only -volatile boolean resetCVHigh = false; - -Bounce resetSwitch = Bounce( RESET_BUTTON, 20 ); // Bounce setup for Reset -int PLAY_CHANNEL; -int NEXT_CHANNEL; -unsigned long playhead; -char* charFilename; - -// BANK SWITCHER SETUP -#define BANK_BUTTON 2 // Bank Button -#define LED0 6 -#define LED1 5 -#define LED2 4 -#define LED3 3 -Bounce bankSwitch = Bounce( BANK_BUTTON, 20 ); -int PLAY_BANK = 0; -#define BANK_SAVE 0 - -// CHANGE HOW INTERFACE REACTS -int chanHyst = 5; // how many steps to move before making a change (out of 1024 steps on a reading) -int timHyst = 7; - -int chanPotOld; -int chanCVOld; -int timPotOld; -int timCVOld; - -#define FLASHTIME 10 // How long do LEDs flash for? -#define HOLDTIME 1300 // How many millis to hold a button to get bank change mode? -#define SHOWFREQ 250 // how many millis between serial Debug updates -#define CHECKFREQ 5 // how often to check the interface in Millis -#define SAMPLEAVERAGE 40 // How many values are read and averaged of pot/CV inputs each interface check. - -// Special ops undefined at the moment. Swapping between loop divider and main program in the future. -#define SPECOPSTIME 5000 // How many millis to hold a button to get specops function // ////////// // TIMERS -// ////////// +// ////////// -// elapsedMillis is a special variable in Teensy - increments every millisecond -elapsedMillis timChanged; elapsedMillis showDisplay; elapsedMillis resetLedTimer = 0; -elapsedMillis bankTimer = 0; elapsedMillis ledFlashTimer = 0; -elapsedMillis checkI = 0; // check interface -// CONTROL THE PEAK METER DISPLAY -elapsedMillis meterDisplay; // Counter to hide MeterDisplay after bank change -elapsedMillis fps; // COUNTER FOR PEAK METER FRAMERATE -elapsedMillis prevAudioElapsed; -#define peakFPS 12 // FRAMERATE FOR PEAK METER - -// DEBUG STUFF -elapsedMillis debugTimer; // Used for measuring performance. -elapsedMillis debugInterval; // How often debug messages are printed - -int prevBankTimer = 0; + +elapsedMillis meterDisplayDelayTimer; // Counter to hide MeterDisplay after bank change +elapsedMillis peakDisplayTimer; // COUNTER FOR PEAK METER FRAMERATE + + +int prevBankTimer = 0; boolean flashLeds = false; -boolean resetButton = false; -boolean prevResetButton = false; boolean bankChangeMode = false; +File settingsFile; + +Settings settings("SETTINGS.TXT"); +LedControl ledControl; +FileScanner fileScanner; +AudioEngine audioEngine; +Interface interface; +PlayState playState; +int NO_FILES = 0; +uint8_t noFilesLedIndex = 0; -void hotswap_callback(); +uint8_t rebootCounter = 0; -// File name compare routine for qsort -int fileNameCompare(const void *a, const void *b) { - FileInfo_t *sa = (FileInfo_t *)a; - FileInfo_t *sb = (FileInfo_t *)b; +void setup() { - return sa->name.compareTo(sb->name); +#ifdef DEBUG_STARTUP + while( !Serial ); + Serial.println("Starting"); +#endif // DEBUG_STARTUP + + ledControl.init(); + ledControl.single(playState.bank); + + // SD CARD SETTINGS FOR AUDIO SHIELD + SPI.setMOSI(7); + SPI.setSCK(14); + + boolean hasSD = openSDCard(); + if(!hasSD) { + Serial.println("Rebooting"); + reBoot(0); + } + + settings.init(hasSD); + + File root = SD.open("/"); + fileScanner.scan(&root, settings); + + getSavedBankPosition(); + + audioEngine.init(settings); + + int numFiles = 0; + for(int i=0;i<=fileScanner.lastBankIndex;i++) { + numFiles += fileScanner.numFilesInBank[i]; + } + D(Serial.print("File Count ");Serial.println(numFiles);); + + if(numFiles == 0) { + NO_FILES = 1; + D(Serial.println("No files");); + ledFlashTimer = 0; + } else if(fileScanner.numFilesInBank[playState.bank] == 0) { + D(Serial.println("Empty bank");); + while(fileScanner.numFilesInBank[playState.bank] == 0) { + playState.bank++; + if(playState.bank == fileScanner.lastBankIndex) { + playState.bank = 0; + } + } + D(Serial.print("Set bank to ");Serial.println(playState.bank);); + } + + interface.init(fileScanner.fileInfos[playState.bank][0].size, fileScanner.numFilesInBank[playState.bank], settings, &playState); + + D(Serial.println("--READY--");); } -void setup() { +void getSavedBankPosition() { + // CHECK FOR SAVED BANK POSITION + int a = 0; + a = EEPROM.read(EEPROM_BANK_SAVE_ADDRESS); + if (a >= 0 && a <= fileScanner.lastBankIndex) { + D( + Serial.print("Using bank from EEPROM "); + Serial.print(a); + Serial.print(" . Active banks "); + Serial.println(fileScanner.lastBankIndex); + + ); + playState.bank = a; + playState.channelChanged = true; + } else { + EEPROM.write(EEPROM_BANK_SAVE_ADDRESS, 0); + }; +} - //PINS FOR BANK SWITCH AND LEDS - pinMode(BANK_BUTTON,INPUT); - pinMode(RESET_BUTTON, INPUT); - pinMode(RESET_CV, INPUT); - pinMode(RESET_LED, OUTPUT); - pinMode(LED0,OUTPUT); - pinMode(LED1,OUTPUT); - pinMode(LED2,OUTPUT); - pinMode(LED3,OUTPUT); - ledWrite(PLAY_BANK); - - // START SERIAL MONITOR - Serial.begin(38400); - - // MEMORY REQUIRED FOR AUDIOCONNECTIONS - AudioMemory(5); - // SD CARD SETTINGS FOR AUDIO SHIELD - SPI.setMOSI(7); - SPI.setSCK(14); - - // OPEN SD CARD - int crashCountdown = 0; - - if (!(SD.begin(10))) { - while (!(SD.begin(10))) { - ledWrite(15); - delay(100); - ledWrite(0); - delay(100); - crashCountdown++; - if (crashCountdown > 6) - reBoot(500); - } - } - - // READ SETTINGS FROM SD CARD - - root = SD.open("/"); - - if (SD.exists("settings.txt")) { - readSDSettings(); - // There was a settings.txt but no Sort parameter in it. - // Let's turn sorting off. - if (gotSort == false) - SortFiles = false; - } - else { - writeSDSettings(); - }; - - // OPEN SD CARD AND SCAN FILES INTO DIRECTORY ARRAYS - scanDirectory(root, 0); - - // Sort files alphabetically in each bank. - if (SortFiles) { - for (int i = 0; i < BANKS; i++) { - if (FILE_COUNT[i] > 0) - qsort(&(FILE_NAMES[i][0]), FILE_COUNT[i], sizeof(FileInfo_t), fileNameCompare); - } - } - - - // CHECK FOR SAVED BANK POSITION - int a = 0; - a = EEPROM.read(BANK_SAVE); - if (a >= 0 && a <= ACTIVE_BANKS){ - PLAY_BANK = a; - CHAN_CHANGED = true; - } - else { - EEPROM.write(BANK_SAVE,0); - }; - playRaw1.hotswap_cb = hotswap_callback; - playRaw2.hotswap_cb = hotswap_callback; - mixer.gain(0, 1.0); - mixer.gain(1, 1.0); - // Add an interrupt on the RESET_CV pin to catch rising edges - attachInterrupt(RESET_CV, resetcv, RISING); - Serial.print("Free ram: "); - Serial.println(FreeRam()); - - debugInterval = 0; +boolean openSDCard() { + if (!(SD.begin(SS))) { + + Serial.println("No SD."); + while (!(SD.begin(SS))) { + ledControl.single(15); + delay(SD_CARD_CHECK_DELAY); + ledControl.single(rebootCounter % 4); + delay(SD_CARD_CHECK_DELAY); + rebootCounter++; + Serial.print("Crash Countdown "); + Serial.println(rebootCounter); + if (rebootCounter > 4) { + return false; + } + } + } + return true; } -// Called by interrupt on rising edge, for RESET_CV pin -void resetcv() { - resetCVHigh = true; +void loop() { + + #ifdef CHECK_CPU + checkCPU(); +// audioEngine.measure(); + #endif + + if(NO_FILES) { + // TODO : Flash the lights to show there are no files + if(ledFlashTimer > 100) { + ledControl.single(noFilesLedIndex); + noFilesLedIndex++; + noFilesLedIndex %= 4; + ledFlashTimer = 0; + rebootCounter ++; + // Wait 3 seconds and reboot + if(rebootCounter == 30) { + reBoot(0); + } + } + return; + } + + updateInterfaceAndDisplay(); + + audioEngine.update(); + + if(audioEngine.error) { + // Too many read errors, reboot + Serial.println("Audio Engine errors. Reboot"); + reBoot(0); + } + + if (playState.channelChanged) { + D( + Serial.print("RM: Going to next channel : "); + if(playState.channelChanged) Serial.print("RM: Channel Changed. "); + Serial.println(""); + ); + + playState.currentChannel = playState.nextChannel; + + AudioFileInfo* currentFileInfo = &fileScanner.fileInfos[playState.bank][playState.nextChannel]; + + audioEngine.changeTo(currentFileInfo, interface.start); + playState.channelChanged = false; + + resetLedTimer = 0; + + } + } -// Called from play_sd_raw hotswap code. -void hotswap_callback() -{ -#if DEBUG - Serial.println("Hotswap called"); -#endif - // Reboot instantly to prevent any audio engine operations. - reBoot(0); +void updateInterfaceAndDisplay() { + + uint16_t changes = checkInterface(); + updateDisplay(changes); } -//////////////////////////////////////////////////// -///////////////MAIN LOOP////////////////////////// -//////////////////////////////////////////////////// +void updateDisplay(uint16_t changes) { + if (showDisplay > SHOWFREQ) { + showDisplay = 0; + } + if (bankChangeMode) { + ledControl.showReset(1);// Reset led is on continuously when in bank change mode.. + if(!flashLeds) { + ledControl.multi(playState.bank); + } + + } else { + ledControl.showReset(resetLedTimer < FLASHTIME); // flash reset LED + } + + if (flashLeds) { + if (ledFlashTimer < FLASHTIME * 4) { + ledControl.multi(0x0F); + } else if(ledFlashTimer < FLASHTIME * 8) { + ledControl.multi(0); + } else { + ledFlashTimer = 0; + } + } else if (settings.showMeter && !bankChangeMode) { + peakMeter(); + } +} -void loop() { - ////////////////////////////////////////// - // IF FILE ENDS, MARK IT FOR RESTART FROM THE BEGINNING - ////////////////////////////////////////// - if (!pRawPlayer->isPlaying() && Looping){ - EOF_REACHED = true; - } - - // Check if previously playing rawPlayer has reached fadeOut period and can be paused. - // Or if MUTE is not active. - if ((prevAudioElapsed > DECLICK || !MUTE) && pPrevRawPlayer->isPlaying()) { - // Pause the previous audio device... - pPrevRawPlayer->pause(); - // ...and prepare it for playing the same sound as the active player is playing. - // This pretty much guarantees a glitch-free reset, if such is going to happen. - // If the player is already prepared for this file, the prepare does nothing. - AudioNoInterrupts(); - pPrevRawPlayer->preparePlayFrom(charFilename); - AudioInterrupts(); - } - - ////////////////////////////////////////// - ////////REACT TO ANY CHANGES ///////////// - ////////////////////////////////////////// - - if (CHAN_CHANGED || RESET_CHANGED || EOF_REACHED){ - - charFilename = buildPath(PLAY_BANK,NEXT_CHANNEL); - PLAY_CHANNEL = NEXT_CHANNEL; - - if (!EOF_REACHED && !RESET_CHANGED && Looping) { - // Carry on from previous position, unless reset pressed or time selected - playhead = pRawPlayer->fileOffset(); - } - playhead = (playhead / 16) * 16; // scale playhead to 16 step chunks -#if DEBUG - Serial.print("Playhead: "); - Serial.println(playhead); -#endif - // Swap players and fades. - if (pRawPlayer == &playRaw1) { - pRawPlayer = &playRaw2; - pPrevRawPlayer = &playRaw1; - pFadeOut = &fade1; - pFadeIn = &fade2; -#if DEBUG - Serial.println("Playing through player 1"); -#endif - } else { - pRawPlayer = &playRaw1; - pPrevRawPlayer = &playRaw2; - pFadeOut = &fade2; - pFadeIn = &fade1; -#if DEBUG - Serial.println("Playing through player 2"); -#endif - } - - // We have two audio players, without this call SD-operations may hang the program. - AudioNoInterrupts(); - // The file is marked for reaching end of file. - // Start from 0 but preserve playhead for future resets. - if (EOF_REACHED) - pRawPlayer->playFrom(charFilename, 0); // change audio - else - pRawPlayer->playFrom(charFilename,playhead); // change audio - AudioInterrupts(); - - if (MUTE) { - // Do a crossfade. - pFadeOut->fadeOut(DECLICK); - pFadeIn->fadeIn(DECLICK); - // And reset the fade timer to let the previous file fade out before pausing it. - prevAudioElapsed = 0; - } else { - // Emulate no crossfade with 1ms fadeout/in - pFadeOut->fadeOut(1); - pFadeIn->fadeIn(1); - prevAudioElapsed = 0; - } - ledWrite(PLAY_BANK); - - CHAN_CHANGED = false; - RESET_CHANGED = false; - EOF_REACHED = false; - - resetLedTimer = 0; // turn on Reset LED -#if DEBUG - whatsPlaying(); +// INTERFACE // + +uint16_t checkInterface() { + + uint16_t changes = interface.update(); + + #ifdef RESET_TO_REBOOT + if (changes & BUTTON_SHORT_PRESS) { + reBoot(0); + } + #endif + + // BANK MODE HANDLING + if((changes & BUTTON_LONG_PRESS) && !bankChangeMode) { + D(Serial.println("Enter bank change mode");); + bankChangeMode = true; + nextBank(); +// ledFlashTimer = 0; + } else if((changes & BUTTON_LONG_RELEASE) && bankChangeMode) { + D(Serial.println("Exit bank change mode");); + flashLeds = false; + bankChangeMode = false; + } + + if(changes & BUTTON_PULSE) { +// flashLeds = false; + if(bankChangeMode) { + D(Serial.println("BUTTON PULSE");); + nextBank(); + } else { + D(Serial.println("Button Pulse but not in bank mode");); + } + + } + + boolean resetTriggered = changes & RESET_TRIGGERED; + + bool skipToStartPoint = false; + bool speedChange = false; + + if(settings.pitchMode) { + + if(resetTriggered) { + skipToStartPoint = true; + } + + if((changes & (ROOT_NOTE_CHANGED | ROOT_POT_CHANGED | ROOT_CV_CHANGED) ) || resetTriggered) { + speedChange = true; + } + + } else { + + if((changes & CHANGE_START_NOW) || resetTriggered) { + skipToStartPoint = true; + } + } + + if(resetTriggered) { + if((changes & CHANNEL_CHANGED) || playState.nextChannel != playState.currentChannel) { + playState.channelChanged = true; + } + } + + if(speedChange) doSpeedChange(); + if(skipToStartPoint && !playState.channelChanged) { + if(settings.pitchMode) { + audioEngine.skipTo(0); + } else { + D(Serial.print("Skip to ");Serial.println(interface.start);); + audioEngine.skipTo(interface.start); + } + + } + + return changes; +} + +void doSpeedChange() { + float speed = 1.0; + speed = interface.rootNote - settings.rootNote; + D(Serial.print("Root ");Serial.println(interface.rootNote);); + speed = pow(2,speed / 12); + + audioEngine.setPlaybackSpeed(speed); +} + +void nextBank() { + + if(fileScanner.lastBankIndex == 0) { + D(Serial.println("Only 1 bank.");); + return; + } + playState.bank++; + if (playState.bank > fileScanner.lastBankIndex) { + playState.bank = 0; + } + if(fileScanner.numFilesInBank[playState.bank] == 0) { + D(Serial.print("No file in bank ");Serial.println(playState.bank);); + nextBank(); + } + + if (playState.nextChannel >= fileScanner.numFilesInBank[playState.bank]) + playState.nextChannel = fileScanner.numFilesInBank[playState.bank] - 1; + interface.setChannelCount(fileScanner.numFilesInBank[playState.bank]); + playState.channelChanged = true; + + D( + Serial.print("RM: Next Bank "); + Serial.println(playState.bank); + ); + + meterDisplayDelayTimer = 0; + EEPROM.write(EEPROM_BANK_SAVE_ADDRESS, playState.bank); +} + +#ifdef ENGINE_TEST +boolean tested = false; +int testIndex = 0; + +void engineTest() { + + if(!tested) { + audioEngine.test(fileScanner.fileInfos[playState.bank][0],fileScanner.fileInfos[playState.bank][1]); + tested = true; + } + + uint8_t changes = interface.update(); + + if(changes & BUTTON_SHORT_PRESS) { + testIndex += 2; + if(testIndex >= fileScanner.numFilesInBank[playState.bank]) { + Serial.println("Back to start"); + testIndex = 0; + } + audioEngine.test(fileScanner.fileInfos[playState.bank][testIndex],fileScanner.fileInfos[playState.bank][testIndex+1]); + } + + return; +} #endif - } else if (bankChangeMode && !resetButton) { - ledWrite(PLAY_BANK); - } - - - - ////////////////////////////////////////// - // CHECK INTERFACE & UPDATE DISPLAYS///// - ////////////////////////////////////////// - - if (checkI >= CHECKFREQ){ - checkInterface(); - checkI = 0; - } - - if (showDisplay > SHOWFREQ){ - showDisplay = 0; - } - if (bankChangeMode) - digitalWrite(RESET_LED, 1); // Reset led is on continuosly when in bank change mode.. - else - digitalWrite(RESET_LED, resetLedTimer < FLASHTIME); // flash reset LED - - // Flashing of top leds has priority. It's executed when reset button is held and - // certain time has passed. - if (flashLeds) { - if (ledFlashTimer < FLASHTIME * 4) - ledWrite(0x0F); - else - flashLeds = false; - // Next in priority is checking if the reset button is held. - // If holdtime has just exceeded, the top row of leds must be flashed. - // See above. - } else if (resetButton) { // The button is currently pressed - if (prevBankTimer < HOLDTIME && bankTimer >= HOLDTIME) { // We reached the hold time - flashLeds = true; - ledFlashTimer = 0; - } - prevBankTimer = bankTimer; - ledWrite(0x00); - // Finally if above conditions are not met, we check if peak meter should be displayed. - } else if (fps > 1000/peakFPS && meterDisplay > (unsigned int)meterHIDE && ShowMeter && !bankChangeMode) { - peakMeter(); // CALL PEAK METER - } + +void peakMeter() { + if( (peakDisplayTimer < 50) || (meterDisplayDelayTimer < settings.meterHide) ) return; + + float peakReading = audioEngine.getPeak(); + int monoPeak = round(peakReading * 4); + monoPeak = round(pow(2, monoPeak)); +// D(Serial.print("Peak ");Serial.print(peakReading,4);Serial.print(" ");Serial.println(monoPeak);); + ledControl.multi(monoPeak - 1); + peakDisplayTimer = 0; } + diff --git a/RadioMusic/Radio_Music_Next.txt b/RadioMusic/Radio_Music_Next.txt new file mode 100644 index 0000000..5e42253 --- /dev/null +++ b/RadioMusic/Radio_Music_Next.txt @@ -0,0 +1,34 @@ +New Stuff +--------- + +Banks / Files +------------- + +At the moment its 16 banks of 48 files because of memory limitations. Optimising will increase this at some point. + +Pitch Control +------------- + +add 'pitchMode=1' to the config. + +In pitchMode any retrigger (either button or RESET input) will play the file from the start. + +Wav Files +--------- + +There is a new setting 'anyAudioFiles=1' which will read pretty much any 16 and 24 bit files. + +BUT this will turn off crossfading so any channel changes are hard cuts. + +If the setting is not in the file or is 0 then it will read .raw files and only those .wav files +that are 44k 16bit mono. + +Tip Top Format +-------------- + +If it finds 'config.txt' in the root of the card then it will scan for all +extensionless files in the root and assume they are 24/96 tip top format files. + +It will only scan 128 of the files because of our bank / file limit. + + diff --git a/RadioMusic/RamMonitor.h b/RadioMusic/RamMonitor.h new file mode 100644 index 0000000..5781701 --- /dev/null +++ b/RadioMusic/RamMonitor.h @@ -0,0 +1,209 @@ +// Teensy 3.x RAM Monitor +// copyright by Adrian Hunt (c) 2015 - 2016 +// +// simplifies memory monitoring; providing both "raw" +// memory information and with frequent calls to the +// run() function, adjusted information with simulated +// stack allocations. memory is also monitored for low +// memory state and stack and heap crashes. +// +// raw memory information methods: +// +// int32_t unallocated() const; +// calculates space between heap and current stack. +// will return negitive if heap and stack currently +// overlap, corruption is very likely. +// +// uint32_t stack_used() const; +// calculates the current stack size. +// +// uint32_t heap_total() const; +// return the heap size. +// +// uint32_t heap_used() const; +// returns allocated heap. +// +// uint32_t heap_free() const; +// returns unused heap. +// +// int32_t free() const; +// calculates total free ram; unallocated and unused +// heap. note that this uses the current stack size. +// +// uint32_t total() const; +// returns total physical ram. +// +// extended memory information. These methods require +// the RamMonitor object to be initialized and the +// run() method called regularly. +// +// uint32_t stack_total() const; +// returns the memory required for the stack. this +// is determind by historical stack usage. +// +// int32_t stack_free() const; +// returns stack space that can be used before the +// stack grows and total size is increased. +// +// int32_t adj_unallocd() const; +// calculates unallocated memory, reserving space +// for the stack. +// +// int32_t adj_free() const; +// calculates total free ram by using adjusted +// unallocated and unused heap. +// +// bool warning_lowmem() const; +// bool warning_crash() const; +// return warning states: low memory is flagged when +// adjusted unallocated memory is below a set value. +// crash is flagged when reserved stack space over- +// laps heap and there is a danger of corruption. +// +// void initialize(); +// initializes the RamMonitor object enabling stack +// monitoring and the extended information methods. +// +// void run(); +// detects stack growth and updates memory warnings. +// this function must be called regulary. +// +// when using the extended memory information methods, +// a single RamMonitor object should be create at +// global level. two static constants define values +// that control stack allocation step size and the low +// memory warning level. these values are in bytes. +// the stack allocation step must be divisable by 4. +// +// static const uint16_t STACKALLOCATION; +// static const uint16_t LOWMEM; +// + +#ifndef RAMMONITOR_H +#define RAMMONITOR_H "1.0" + +#include +#include + +extern int* __brkval; // top of heap (dynamic ram): grows up towards stack +extern char _estack; // bottom of stack, top of ram: stack grows down towards heap + +class RamMonitor { +private: + typedef uint32_t MemMarker; + typedef uint8_t MemState; + + // user defined consts + static const uint16_t STACKALLOCATION = 1024; // stack allocation step size: must be 32bit boundries, div'able by 4 + static const uint16_t LOWMEM = 4096; // low memory warning: 4kb (less than between stack and heap) + + // internal consts + static const uint32_t HWADDRESS_RAMSTART = +#if defined(__MK20DX256__) + 0x1FFF8000; // teensy 3.1 (? 3.2 ?) +#elif defined(__MKL26Z64__) + 0x????????; // teensy LC +#else + 0x1FFFE000; // teensy 3.0 +#endif + static const MemMarker MEMMARKER = 0x524D6D6D; // chars RMmm ... Ram Monitor memory marker + static const uint16_t MARKER_STEP = STACKALLOCATION / sizeof(MemMarker); + + static const MemState msOk = 0; + static const MemState msLow = 1; + static const MemState msCrash = 2; + + MemMarker* _mlastmarker; // last uncorrupted memory marker + MemState _mstate; // detected memory state + + void _check_stack() { + int32_t free; + + // skip markers already comsumed by the stack + free = ((char*) &free) - ((char*) _mlastmarker); + if(free < 0) { + int32_t steps; + + steps = free / STACKALLOCATION; // note steps will be negitive + if(free % STACKALLOCATION) + --steps; + + _mlastmarker += MARKER_STEP * steps; + }; + + // check last marker and move if corrupted + while((*_mlastmarker != MEMMARKER) && (_mlastmarker >= (MemMarker*) __brkval)) + _mlastmarker -= MARKER_STEP; + }; +public: + int32_t unallocated() const { char tos; return &tos - (char*) __brkval; }; // calcs space between heap and stack (current): will be negitive if heap/stack crash + uint32_t stack_used() const { char tos; return &_estack - &tos; }; // calcs stack size (current): grows into unallocated + uint32_t heap_total() const { return mallinfo().arena; }; // returns heap size: grows into unallocated + uint32_t heap_used() const { return mallinfo().uordblks; }; // returns heap allocated + uint32_t heap_free() const { return mallinfo().fordblks; }; // returns free heap + + int32_t free() const { return unallocated() + heap_free(); }; // free ram: unallocated and unused heap + uint32_t total() const { return &_estack - (char*) HWADDRESS_RAMSTART; }; // physical ram + + // these functions (along with initialize and run) + // create the ellusion of stack allocation. + uint32_t stack_total() { // uses memory markers to "alloc" unallocated + _check_stack(); + return &_estack - (char*) _mlastmarker; + }; + + int32_t stack_free() { // calc stack usage before next marker corruption + char tos; + + _check_stack(); + return &tos - (char*) _mlastmarker; + }; + + int32_t adj_unallocd() { // calcs space between heap and "alloc'd" stack: will be negitive if heap/stack crash + _check_stack(); + return ((char*) _mlastmarker) - (char*) __brkval; + }; + + int32_t adj_free() { return adj_unallocd() + heap_free(); }; // free ram: unallocated and unused heap + + bool warning_lowmem() const { return (_mstate & msLow); }; // returns true when unallocated memory is < LOWMEM + bool warning_crash() const { return (_mstate & msCrash); }; // returns true when stack is in danger of overwriting heap + + void initialize() { + MemMarker* marker = (MemMarker*) &_estack; // top of memory + int32_t size; + int32_t steps; + + // skip current stack; + size = &_estack - (char*) ▮ // current stack size: marker address is tos + steps = size / STACKALLOCATION; + if(size % STACKALLOCATION) + ++steps; + + marker -= MARKER_STEP * steps; + + // record current top of stack + _mlastmarker = marker; + _mstate = msOk; + + // mark unused ram between top of stack and top of heap + while(marker >= (MemMarker*) __brkval) { + *marker = MEMMARKER; // write memory marker + marker -= MARKER_STEP; + }; + }; + + void run() { + int32_t unallocd = adj_unallocd(); // calls _check_stack() internally + + if(unallocd < 0) + _mstate = msCrash | msLow; + else if(unallocd < LOWMEM) + _mstate = msLow; + else + _mstate = msOk; + }; + +}; + +#endif diff --git a/RadioMusic/SDPlayPCM.cpp b/RadioMusic/SDPlayPCM.cpp new file mode 100644 index 0000000..54631a1 --- /dev/null +++ b/RadioMusic/SDPlayPCM.cpp @@ -0,0 +1,866 @@ +/* Audio Library for Teensy 3.X + * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com + * + * Development of this audio library was funded by PJRC.COM, LLC by sales of + * Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop + * open source software by purchasing Teensy or other PJRC products. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice, development funding notice, and this permission + * notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include "SDPlayPCM.h" + +#include "spi_interrupt.h" +#include "RadioMusic.h" + +#ifdef DEBUG_PCM_PLAYER +#define D(x) x +#else +#define D(x) +#endif + +#ifdef DEBUG_AUDIO_BUFFER +#define B(x) x +#else +#define B(x) +#endif + + +void SDPlayPCM::begin(void) { +// AudioStartUsingSPI(); + playing = false; + finished = false; + errors = 0; + // Set a dummy filename so it doesn't match anything at startup. + filename = "--------"; +} + +void SDPlayPCM::loopPlayback(bool loop) { + __disable_irq() + ; + looping = loop; + __enable_irq() + ; +} + +bool SDPlayPCM::changeFileTo(AudioFileInfo* info, bool closeFirst) { + filename = info->name; +// AudioStopUsingSPI(); + __disable_irq() + ; + if (closeFirst) { + rawfile.close(); + } + rawfile = SD.open(info->name.c_str()); + + dataSize = info->size; + bytesLeftInFile = info->size; + dataOffset = info->dataOffset; + channels = info->getChannels(); + bytesPerSample = info->getBytesPerSample(); + sampleRateSpeed = (float) info->getSampleRate() / 44100.0; + bytesAvailable = 0; + readPositionInBytes = 0; + bufferFillPosition = 0; + finished = false; + readError = false; + __enable_irq() + ; +// AudioStartUsingSPI(); + D( + if (dataSize % bytesPerSample != 0) { + debugHeader(); + Serial.print("!!!! Data size not aligned : "); + Serial.print(dataSize); + } + if(info->startPlayFrom != 0) { + Serial.print("!!! Start play from is not 0 : "); + Serial.println(info->startPlayFrom); + } + debugHeader(); + Serial.print("Change File To "); + Serial.print(info->name); + Serial.print(". BPS "); + Serial.print(bytesPerSample); + Serial.print(". BLIF "); + Serial.print(bytesLeftInFile); + Serial.print(" .SampleRateSpeed "); + Serial.print(sampleRateSpeed); + Serial.print(" .Bytes Per Block "); + Serial.println( + ceil(AUDIO_BLOCK_SAMPLES * speed * bytesPerSample * channels)); + ); + return true; +} + +bool SDPlayPCM::skipTo(uint32_t dataPosition) { + if(!rawfile) return false; + uint32_t pos = dataOffset + dataPosition; + AudioStopUsingSPI(); + __disable_irq() + ; + boolean didseek = rawfile.seek(pos); + bytesLeftInFile = dataSize - dataPosition; + readPositionInBytes = 0; + bytesAvailable = 0; + bufferFillPosition = 0; + readError = false; + playing = true; + finished = false; + __enable_irq() + ; + AudioStartUsingSPI(); + if(!didseek) { + D( + debugHeader(); + Serial.println("!!! didseek is false"); + ); + return false; + } + return true; +} + +bool SDPlayPCM::playFrom(AudioFileInfo* info) { + + uint32_t pos = info->dataOffset + info->startPlayFrom; + + D( + debugHeader(); + Serial.print("Play "); + Serial.print(info->name); + Serial.print(" from "); + Serial.print(info->startPlayFrom); + Serial.print(" starting from "); + Serial.println(pos); + ); + + + // We use the same file, just seek inside it. + if (info->name.compareTo(filename) == 0) { + D( + debugHeader(); + Serial.print("Continuing on file "); + Serial.print(filename); + Serial.print(" from "); + Serial.print(info->startPlayFrom); + Serial.print(" of "); + Serial.print(info->size); + Serial.print(" with "); + Serial.print(info->getBytesPerSample()); + Serial.print(" bps. File size "); + Serial.println(rawfile.size()); + ); + + updateRequired = false; + return skipTo(info->startPlayFrom); + + } else { + D( + Serial.print("Different name "); + Serial.println(filename); + ); + } + + //if(playing) stop(); + +// if(!playing) AudioStartUsingSPI(); + + if (!changeFileTo(info, true)) { + updateRequired = false; + return false; + } else { + D( + debugHeader(); + Serial.print("Change OK : "); + Serial.println(rawfile.name()); + ); + } + + skipTo(info->startPlayFrom); + +// __disable_irq() +// ; +// if(!rawfile.seek(pos)) { +// debugHeader(); +// Serial.println("Seek failed"); +// } +// bytesLeftInFile = info->size - info->startPlayFrom; +// playing = true; +// finished = false; +// __enable_irq() +// ; + + D( + debugHeader(); + Serial.print("Open. RPIB "); + Serial.print(readPositionInBytes); + Serial.print(" .InfoSize "); + Serial.print(info->size); + Serial.print(" .BLIF "); + Serial.print(bytesLeftInFile); + Serial.print(". File pos "); + Serial.print(rawfile.position()); + Serial.print(". File size "); + Serial.print(rawfile.size()); + Serial.print(". Avail "); + Serial.println(fileAvailable()); + ); + updateRequired = false; + return true; +} + +void SDPlayPCM::restart() { + + D( + debugHeader(); + Serial.println("Restart"); + ); + __disable_irq() + ; + rawfile.seek(dataOffset); + bytesLeftInFile = dataSize; + playing = true; + finished = false; + __enable_irq() + ; +} + +void SDPlayPCM::stop(void) { + __disable_irq() + ; + D( + debugHeader(); + Serial.println("Stop"); + ); + if (playing) { + playing = false; + __enable_irq() + ; +// AudioStopUsingSPI(); + } else { + __enable_irq() + ; + } +} + +void SDPlayPCM::update(void) { + + uint16_t n = AUDIO_BLOCK_SAMPLES; + audio_block_t *block; + uint16_t i = 0; + + if(errors > 100) return; + + // only update if we're playing + if (!playing) + return; + + if(!looping) { + if(finished || bytesLeftInFile == 0) { + D( + debugHeader(); + Serial.println("File finished"); + ); + playing = false; + finished = true; + return; + } + } + + // allocate the audio blocks to transmit + block = allocate(); + if (block == NULL) + return; + + inUpdate = true; + read = 0; + speed = playbackSpeed * sampleRateSpeed; + bytesRequired = ceil(AUDIO_BLOCK_SAMPLES * speed) * bytesPerSample * channels; + + if (bytesRequired > AUDIOBUFSIZE) { + bytesRequired = AUDIOBUFSIZE; + D( + debugHeader(); + Serial.println("!!! Bytes Required is bigger than the audio buffer.."); + ); + } + + if (bytesAvailable < bytesRequired) { + + bool bufferReadOk = false; + if (bytesRequired + < AUDIO_BLOCK_SAMPLES * bytesPerSample * channels) { + bufferReadOk = fillBuffer(AUDIO_BLOCK_SAMPLES * bytesPerSample * channels); + } else { + bufferReadOk = fillBuffer(bytesRequired); + } +B( + if (read % bytesPerSample != 0) { + debugHeader(); + Serial.print("Read not aligned "); + Serial.print(read % bytesPerSample); + Serial.print("\t"); + Serial.print(read); + Serial.print("\t"); + Serial.println(bytesRequired); + } +); + + if (read >= 0 && read < bytesRequired) { + B( + if (bytesAvailable % bytesPerSample != 0) + { + debugHeader(); + Serial.println("Bytes available not aligned"); + } + + if (read % bytesPerSample != 0) { + debugHeader(); + Serial.print("Read not aligned "); + Serial.print(read % bytesPerSample); + Serial.print("\t"); + Serial.print(read); + Serial.print("\t"); + Serial.println(bytesRequired); + } + if(!finished) { + debugHeader(); + Serial.print("Not enough data from fill buffer "); + Serial.print(bytesRequired); + Serial.print("\t"); + Serial.print(bytesAvailable); + Serial.print("\t"); + Serial.println(fileAvailable()); + } else { + debugHeader(); + Serial.print("File finished "); + Serial.print(bytesRequired); + Serial.print("\t"); + Serial.print(bytesAvailable); + Serial.print("\t"); + Serial.println(fileAvailable()); + } + ); + n = min(AUDIO_BLOCK_SAMPLES, + floor(bytesAvailable / (bytesPerSample * channels)) + / speed); + } else if (readError || !bufferReadOk) { + D(Serial.print("Read error. ");Serial.println(errors);); + errors++; + readError = false; + // Fill block with zero. + for (i = 0; i < AUDIO_BLOCK_SAMPLES; i++) { + block->data[i] = 0; + } + transmit(block); + release(block); + updates++; + return; + } + + } + + int16_t* out = block->data; + + l0 = 0; + lowSamplePos = 0; + float sp = speed * channels; + + // Check for buffer wrapping here so we don't do it 128 times in the loop. + boolean bufferWrap = ((n*sp) * bytesPerSample) + readPositionInBytes >= AUDIOBUFSIZE; + if (bytesPerSample == 2) { + // 16 bit copy + if(bufferWrap) { + for (i = 0; i < n; i++) { + lowSamplePos = (i * speed); + l0 = readPositionInBytes + (lowSamplePos << channels); + if (l0 >= AUDIOBUFSIZE) l0 -= AUDIOBUFSIZE; + memcpy(out++, &audioBuffer[l0],2); + } + } else { + // We won't wrap the buffer edge so don't check + for (i = 0; i < n; i++) { + lowSamplePos = (i * speed); + l0 = readPositionInBytes + (lowSamplePos << channels); + memcpy(out++, &audioBuffer[l0],2); + } + } + } else if (bytesPerSample == 3) { + + if(channels == 1) { + if(bufferWrap) { + // 24 bit copy. + for (i = 0; i < n; i++) { + lowSamplePos = (i * speed); + l0 = readPositionInBytes + (lowSamplePos * 3); + if (l0 >= AUDIOBUFSIZE) l0 -= AUDIOBUFSIZE; + *out++ = (audioBuffer[l0] | (audioBuffer[++l0] << 8) | (audioBuffer[++l0] << 16)) >> 8; + } + } else { + // 24 bit copy. + for (i = 0; i < n; i++) { + lowSamplePos = (i * speed); + l0 = readPositionInBytes + (lowSamplePos * 3); + *out++ = (audioBuffer[l0] | (audioBuffer[++l0] << 8) | (audioBuffer[++l0] << 16)) >> 8; + } + } + } else { + if(bufferWrap) { + // 24 bit copy. + for (i = 0; i < n; i++) { + lowSamplePos = (i * speed); + l0 = readPositionInBytes + ((lowSamplePos * 3) << 1); + if (l0 >= AUDIOBUFSIZE) l0 -= AUDIOBUFSIZE; + *out++ = (audioBuffer[l0] | (audioBuffer[++l0] << 8) | (audioBuffer[++l0] << 16)) >> 8; + } + } else { + // 24 bit copy. + for (i = 0; i < n; i++) { + lowSamplePos = (i * speed); + l0 = readPositionInBytes + ((lowSamplePos * 3) << 1); + *out++ = (audioBuffer[l0] | (audioBuffer[++l0] << 8) | (audioBuffer[++l0] << 16)) >> 8; + } + } + + } + } + + if (n != AUDIO_BLOCK_SAMPLES) { + B( + debugHeader(); + Serial.print("n != BLOCK : "); + Serial.println(n); + ); + + // There wasn't enough data earlier to fill the audio block + + bytesUsed = (n * sp) * bytesPerSample; + // Fill rest of block with zeros. + for (i = n; i < AUDIO_BLOCK_SAMPLES; i++) { + *out++ = 0; + } + if (!looping) + finished = true; + } else { + bytesUsed = bytesRequired; + } + + readPositionInBytes += bytesUsed; + readPositionInBytes %= AUDIOBUFSIZE; + + bytesAvailable -= bytesUsed; + + + transmit(block); + release(block); + inUpdate = false; + updates++; + if(readError) errors++; + readError = false; +} + +bool SDPlayPCM::fillBuffer(int32_t requiredBytes) { + +// How many bytes until the end of the buffer + spaceLeftInBuffer = AUDIOBUFSIZE - bufferFillPosition; + + read = 0; + + int32_t read2 = 0; + + B( + bool seekRequired = false; + int seek = 0; + int fillStyle = 0; + + uint32_t avail = fileAvailable(); + + if(bytesLeftInFile > avail) { + debugHeader(); + Serial.print("BLIF > Available at start. "); + Serial.print(bytesLeftInFile); + Serial.print(" > "); + Serial.println(avail); + if(bytesLeftInFile % bytesPerSample != 0) { + Serial.println("BLIF not aligned"); + } + if(avail % bytesPerSample != 0) { + Serial.println("Available not aligned"); + } else { + bytesLeftInFile = avail; + } + } + ); + + // Is there enough left to read from the file? + if (bytesLeftInFile < requiredBytes - bytesAvailable) { + B( + debugHeader(); + Serial.print("Not enough data in file. "); + Serial.print(bytesLeftInFile); + Serial.print(" left. \t"); + Serial.print(bytesRequired); + Serial.print(" Reqd\t"); + Serial.print(bytesAvailable); + Serial.print(" avail after "); + Serial.print(bufferFills); + Serial.print(" fills. Left in buf "); + Serial.println(spaceLeftInBuffer); + ); + + if (!looping) { + // we're not looping and the file will run out + // so only attempt to read the rest of the file + B( + debugHeader(); + Serial.print("Changing required to "); + Serial.println(bytesLeftInFile); + ); + requiredBytes = bytesLeftInFile; + } else { + B( + debugHeader(); + Serial.println("Seek required during fillBuffer"); + seekRequired = true; + seek = 1; + ); + } + } + + if (requiredBytes > spaceLeftInBuffer) { + // If we read all the data we need it will go across the buffer boundary + + if (spaceLeftInBuffer <= bytesLeftInFile) { + // Fill to end of buffer + read = rawfile.read(&(audioBuffer[bufferFillPosition]), + spaceLeftInBuffer); + if (read == -1 && !readError) { + B( + Serial.println("SDP: Read Error 1"); + ); + readError = true; + goto endfill; + } + bufferFillPosition = 0; + + if (requiredBytes <= bytesLeftInFile) { + B(fillStyle = 1;); + // Not enough buffer space, but enough left in file. No seek required + // Fill buffer from start + if (requiredBytes - read > 0) { + read2 = rawfile.read(audioBuffer, requiredBytes - read); + if (read2 == -1 && !readError) { + readError = true; + B(Serial.println("SDP: Read Error 2");); + goto endfill; + } + read += read2; + bufferFillPosition = read2; + } else { + bufferFillPosition = 0; + } + bytesLeftInFile -= read; + + } else { + // There is enough data in the file to fill the end of the buffer + // BUT NOT enough in the file to get all the data we need. Seek is required + + // We've already filled to the end of the buffer + + // now fill from start of buffer with what's left in the file + if (bytesLeftInFile - read > 0) { + read2 = rawfile.read(audioBuffer, bytesLeftInFile - read); + if (read2 == -1 && !readError) { + B( + Serial.println("SDP: Read Error 3"); + ); + readError = true; + goto endfill; + } + read += read2; + bufferFillPosition = read2; + } + + // Seek to start of file + rawfile.seek(dataOffset); + + // get the bit thats left + int32_t lastBit = requiredBytes - read; + B( + fillStyle = 2; + seekRequired = true; + seek = 0; + + if (lastBit <= 0) { + Serial.print("SDP: Last bit not +ve "); + Serial.println(lastBit); + } else { + debugHeader(); + Serial.print("Last bit "); + Serial.println(lastBit); + } + ); + int32_t read3 = rawfile.read(&(audioBuffer[bufferFillPosition]), + lastBit); + if (read3 == -1 && !readError) { + B( + Serial.println("SDP: Read error 4"); + ); + readError = true; + goto endfill; + } + + + read += read3; + bytesLeftInFile = dataSize - lastBit; + bufferFillPosition += lastBit; + } + } else { + // Buffer has enough space to read the rest of the file. + B( + fillStyle = 3; + seekRequired = true; + seek = 0; + ); + + // Get the rest from the file + read = rawfile.read(&(audioBuffer[bufferFillPosition]), + bytesLeftInFile); + if (read == -1 && !readError) { + B( + Serial.println("SDP: Read Error 5"); + ); + readError = true; + goto endfill; + } + // go to start of file + rawfile.seek(dataOffset); + + spaceLeftInBuffer -= read; + + // read to end of buffer + read2 = rawfile.read(&(audioBuffer[bufferFillPosition + read]), + spaceLeftInBuffer); + if (read2 == -1 && !readError) { + B( + Serial.println("SDP: Read Error 6"); + ); + readError = true; + goto endfill; + } + + read += read2; + + // Fill buffer from start with amount that is left + int32_t read3 = rawfile.read(audioBuffer, requiredBytes - read); + if (read3 == -1 && !readError) { + B( + Serial.println("SDP: Read Error 7"); + ); + readError = true; + goto endfill; + } + + read += read3; + bytesLeftInFile = dataSize - (read2 + read3); + bufferFillPosition = read3; + } + } else { + + // There's enough space in the buffer, is there enough in the file + if (bytesLeftInFile >= requiredBytes) { +// Serial.println("Fill style 4."); + // Enough in file, just read it + read = rawfile.read(&(audioBuffer[bufferFillPosition]), requiredBytes); + if (read == -1 && !readError) { + B( + fillStyle = 4; + debugHeader(); + Serial.println("Read Error 8"); + ); + readError = true; + goto endfill; + } else { + B( + fillStyle = 4; + if(read != requiredBytes) { + debugHeader(); + Serial.print("Read != Required. BLIF "); + Serial.print(bytesLeftInFile); + Serial.print("\t File avail "); + Serial.print(fileAvailable()); + Serial.print("\t File pos "); + Serial.println(rawfile.position()); + } + ); + bytesLeftInFile -= read; + bufferFillPosition += read; + } + } else { + + // Not enough in file + read = rawfile.read(&(audioBuffer[bufferFillPosition]), + bytesLeftInFile); + if (read == -1 && !readError) { + B( + fillStyle = 5; + debugHeader(); + Serial.println("Read Error 9"); + ); + readError = true; + goto endfill; + } + + if (looping) { + // If we're looping, seek back to start and fill from there + rawfile.seek(dataOffset); + + read2 = rawfile.read(&(audioBuffer[bufferFillPosition + read]), + requiredBytes - bytesLeftInFile); + if (read2 == -1) { + B( + seekRequired = true; + seek = 0; + debugHeader(); + Serial.println("Read Error 10"); + ); + readError = true; + goto endfill; + } + + read += read2; + bytesLeftInFile = dataSize - read2; + bufferFillPosition += read; + } else { + // otherwise flag playback finished + finished = true; + } + + } + } + + endfill: + + B( + + if(bytesLeftInFile > (fileAvailable())) { + debugHeader(); + Serial.print("BLIF > AVAIL. "); + Serial.print(bytesLeftInFile); + Serial.print(" != "); + Serial.print(fileAvailable()); + Serial.print(". Read "); + Serial.print(read); + Serial.print(". Style "); + Serial.print(fillStyle); + Serial.print(" Name. "); + Serial.println(rawfile.name()); + Serial.print(". File Pos "); + Serial.println(rawfile.position()); + if((fileAvailable()) % bytesPerSample == 0) { + debugHeader(); + Serial.println("Avail2 not aligned"); + } + } + + if(read == -1) { + debugHeader(); + Serial.print("Read error. Fill style "); + Serial.println(fillStyle); + if(avail != (fileAvailable())) { + debugHeader(); + Serial.print("Avail changed from "); + Serial.print(avail); + Serial.print(" to "); + Serial.println(fileAvailable()); + } + } else if(read > requiredBytes) { + debugHeader(); + Serial.print("Read too much "); + Serial.print(read); + Serial.print(" wanted "); + Serial.print(requiredBytes); + Serial.print(" fill style was "); + Serial.println(fillStyle); + } else if(read < requiredBytes) { + debugHeader(); + Serial.print("Didn't read enough "); + Serial.print(read); + Serial.print(" wanted "); + Serial.print(requiredBytes); + Serial.print(" fill style was "); + Serial.println(fillStyle); + } + + if(seekRequired) { + if(seek) { + debugHeader(); + Serial.print("Seek required but seek is high. "); + Serial.print("Fill style "); + Serial.println(fillStyle); + } else { + debugHeader(); + Serial.print("Seek completed. "); + Serial.print("Fill style "); + Serial.println(fillStyle); + } + } + + ); + + if(read == -1 || readError) { + return false; + } else { + bytesAvailable += read; + bufferFillPosition %= AUDIOBUFSIZE; + bufferFills++; + if(bytesLeftInFile == 0) finished = true; + return true; + } +} + +// Progress in file scaled from 0 to 1 +float SDPlayPCM::offset(void) { + // For now fudge it a bit and shift it forward in time by 2 blocks. + uint32_t bytes = bytesLeftInFile <= (bytesRequired * 2) ? bytesLeftInFile : bytesLeftInFile - (bytesRequired * 2); + float off = (float)(dataSize - bytes) / dataSize; + D( + debugHeader(); + Serial.print("Offset. Size "); + Serial.print(dataSize); + Serial.print("\t"); + Serial.print(bytesLeftInFile); + Serial.print("\t"); + Serial.println(off,4); + ); + return off; +} + +// Note rawfile.available() is clamped to 16 bit signed int +// not sure why the SD library does that. +// Just do the same as SD does but don't clamp the result. +uint32_t SDPlayPCM::fileAvailable() { + return rawfile.size() - rawfile.position(); +} + +void SDPlayPCM::debugHeader() { + Serial.print("SDP:"); + Serial.print(playerID); + Serial.write(' '); +} diff --git a/RadioMusic/SDPlayPCM.h b/RadioMusic/SDPlayPCM.h new file mode 100644 index 0000000..65310a3 --- /dev/null +++ b/RadioMusic/SDPlayPCM.h @@ -0,0 +1,118 @@ +/* Audio Library for Teensy 3.X + * Copyright (c) 2014, Paul Stoffregen, paul@pjrc.com + * + * Development of this audio library was funded by PJRC.COM, LLC by sales of + * Teensy and Audio Adaptor boards. Please support PJRC's efforts to develop + * open source software by purchasing Teensy or other PJRC products. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice, development funding notice, and this permission + * notice shall be included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef RMPlaySDraw_h_ +#define RMPlaySDraw_h_ + +#include "AudioStream.h" +#include +#include "AudioFileInfo.h" + +// Audio system AUDIO_BLOCK_SAMPLES is 128 and runs at 44k 16bit +// This means we need 2 BLOCKS of bytes for a block 44k 16 +// 3 for 44k 24bit +// 6.54 for 96k 24bit +// TODO : Use 2 buffers. 1 for multiples of 44k and 1 for multiples of 48k +// If there is a reasonable buffer size which is byte aligned at 48k +// e.g. an 836 byte buffer would have enough samples to fill 3 blocks for 48/16 -> 44/16 +// NOTE : Make sure this is a factor of 2 AND 3 otherwise 24-bit samples will be split +// across the buffer boundary and that will make things go wrong. +#define AUDIOBUFSIZE (AUDIO_BLOCK_SAMPLES * 27) + +class SDPlayPCM: public AudioStream { +public: + SDPlayPCM(void) : AudioStream(0, NULL) { begin(); } + void begin(void); + bool playFrom(AudioFileInfo* info); + bool hasFinished(void) { return finished; } + void restart(); + bool isPlaying(void) { return playing; } + void stop(void); + void loopPlayback(bool loop); + + bool skipTo(uint32_t dataOffset); + float offset(void); + + virtual void update(void); + + uint8_t playerID = 0; + + volatile bool readError = false; + volatile float playbackSpeed = 1.0; + + volatile uint16_t bufferFills = 0; + volatile uint16_t updates = 0; + volatile uint16_t errors = 0; + + File rawfile; + volatile bool inUpdate = false; + volatile bool updateRequired = false; + +private: + bool changeFileTo(AudioFileInfo* info, bool closeFirst); + + bool fillBuffer(int32_t requiredBytes); + void fastFillBuffer(); + void debugHeader(); + uint32_t fileAvailable(); + + // audioBuffer is a ring buffer + unsigned char audioBuffer[AUDIOBUFSIZE]; + + // Speed to play different SRs at normal speed. + // 44100 is 1.0 + volatile float sampleRateSpeed = 1.0; + // Playing speed independent of sample rate. + // 1.0 is always original speed + volatile float speed = 1.0; + volatile bool playing; + volatile bool finished; + volatile bool looping; + volatile int bytesPerSample = 2; + volatile int32_t bytesAvailable = 0; + volatile int32_t readPositionInBytes = 0; + volatile uint32_t bufferFillPosition = 0; + + String filename; + uint32_t dataSize = 0; + uint32_t dataOffset = 0; + uint16_t channels = 1; + + int32_t bytesLeftInFile = 0; + + uint32_t l0 = 0; + uint32_t lowSamplePos = 0; + uint32_t bytesUsed = 0; + uint16_t spaceLeftInBuffer = 0; + int32_t bytesRequired = 0; + int32_t read = 0; + +// // Fixed point phase. 5 bit integer 11 bit fractional +// // This is phase in samples, not bytes. +// uint32_t phaseIncrement = 0; +}; + +#endif diff --git a/RadioMusic/Settings.cpp b/RadioMusic/Settings.cpp new file mode 100644 index 0000000..0e892af --- /dev/null +++ b/RadioMusic/Settings.cpp @@ -0,0 +1,298 @@ +#include +#include "Settings.h" + +#include "RadioMusic.h" + +#ifdef DEBUG_SETTINGS +#define D(x) x +#else +#define D(x) +#endif + +Settings::Settings(const char* filename) { + _filename = filename; +} + +void Settings::init(boolean hasSD) { + + if (!hasSD) { + // Configure defaults + copyDefaults(); + } else { + if (SD.exists(_filename)) { + read(); + } else { + write(); + read(); + }; + } +} + +void Settings::copyDefaults() { + +} + +void Settings::read() { + + D(Serial.println("Reading settings.txt");); + + char character; + String settingName; + String settingValue; + settingsFile = SD.open("settings.txt"); + + uint8_t NAME = 1; + uint8_t VALUE = 2; + uint8_t state = NAME; + + if (settingsFile) { + while (settingsFile.available()) { + character = settingsFile.read(); + if(state == NAME) { + if(character == '=') { + state = VALUE; + } else { + settingName = settingName + character; + } + } else if(state == VALUE) { + if(character == '\n') { + applySetting(settingName, settingValue); + settingName = ""; + settingValue = ""; + state = NAME; + } else { + settingValue = settingValue + character; + } + } + } + if(settingName.length() > 0 && settingValue.length() > 0) { + applySetting(settingName, settingValue); + } + // close the file: + settingsFile.close(); + } else { + // if the file didn't open, print an error: + Serial.println("error opening settings.txt"); + } + // Do test settings here + +// crossfade = true; +// crossfadeTime = 1000; +// looping = false; +// anyAudioFiles = true; +// hardSwap = true; +// chanPotImmediate = false; +// chanCVImmediate = false; +// quantizeNote = true; +// startCVImmediate = true; +// startPotImmediate = true; +// quantiseRootPot = true; +// quantiseRootCV = true; +// pitchMode = true; +// startCVDivider = 1; + +#ifdef TEST_RADIO_MODE + radioMode(); +#endif + +#ifdef TEST_DRUM_MODE + drumMode(); +#endif + + if(!loopMode) { + if(pitchMode) { + loopMode = LOOP_MODE_START_POINT; + } else { + loopMode = LOOP_MODE_RADIO; + } + } + + D(Serial.print("Loop mode ");Serial.println(loopMode);); + + if(anyAudioFiles || pitchMode) { + hardSwap = true; + } +} + +void Settings::drumMode() { + crossfade=0; + crossfadeTime=100; + showMeter=1; + meterHide=2000; + chanPotImmediate=1; + chanCVImmediate=1; + startPotImmediate=1; + startCVImmediate=1; + startCVDivider=1; + looping=1; + sort=1; + pitchMode=1; + hardSwap = true; + anyAudioFiles = true; + loopMode = LOOP_MODE_START_POINT; +} + +void Settings::radioMode() { + crossfade=1; + crossfadeTime=100; + showMeter=1; + meterHide=2000; + chanPotImmediate=1; + chanCVImmediate=1; + startPotImmediate=0; + startCVImmediate=0; + startCVDivider=2; + looping=1; + sort=1; + pitchMode=0; + hardSwap = false; + anyAudioFiles = false; + loopMode = LOOP_MODE_RADIO; +} +/* Apply the value to the parameter by searching for the parameter name + Using String.toInt(); for Integers + toFloat(string); for Float + toBoolean(string); for Boolean + */ +void Settings::applySetting(String settingName, String settingValue) { + + D( + Serial.print(settingName); + Serial.print(" -> "); + Serial.println(settingValue); + ); + + if (settingName.equalsIgnoreCase("mute") || settingName.equalsIgnoreCase("crossfade")) { + crossfade = toBoolean(settingValue); + } + + if (settingName.equalsIgnoreCase("declick") || settingName.equalsIgnoreCase("crossfadeTime")) { + crossfadeTime = settingValue.toInt(); + } + + if (settingName.equalsIgnoreCase("showMeter")) { + showMeter = toBoolean(settingValue); + } + + if (settingName.equalsIgnoreCase("meterHide")) { + meterHide = settingValue.toInt(); + } + + if (settingName.equalsIgnoreCase("chanPotImmediate")) { + chanPotImmediate = toBoolean(settingValue); + } + + if (settingName.equalsIgnoreCase("chanCVImmediate")) { + chanCVImmediate = toBoolean(settingValue); + } + + if (settingName.equalsIgnoreCase("startPotImmediate")) { + startPotImmediate = toBoolean(settingValue); + } + + if (settingName.equalsIgnoreCase("startCVImmediate")) { + startCVImmediate = toBoolean(settingValue); + } + + if (settingName.equalsIgnoreCase("startCVDivider")) { + startCVDivider = settingValue.toInt(); + } + + if (settingName.equalsIgnoreCase("looping")) { + looping = toBoolean(settingValue); + } + + if (settingName.equalsIgnoreCase("sort")) { + sort = toBoolean(settingValue); + } + + if(settingName.equalsIgnoreCase("anyAudioFiles")) { + anyAudioFiles = toBoolean(settingValue); + } + + if(settingName.equalsIgnoreCase("pitchMode")) { + pitchMode = toBoolean(settingValue); + } + + if(settingName.equalsIgnoreCase("hardSwap")) { + hardSwap = toBoolean(settingValue); + } + + if(settingName.equalsIgnoreCase("noteRange")) { + noteRange = settingValue.toInt(); + if(noteRange < 1) noteRange = 1; + if(noteRange > 72) noteRange = 72; + } + + if(settingName.equalsIgnoreCase("rootNote")) { + rootNote = settingValue.toInt(); + if(rootNote < lowNote) rootNote = lowNote; + if(rootNote > 96) rootNote = 96; + } + + if(settingName.equalsIgnoreCase("loopMode")) { + loopMode = settingValue.toInt(); + } + + if(settingName.equalsIgnoreCase("quantiseNoteCV") || settingName.equalsIgnoreCase("quantizeNoteCV")) { + quantiseRootCV = toBoolean(settingValue); + } + if(settingName.equalsIgnoreCase("quantiseNotePot") || settingName.equalsIgnoreCase("quantizeNotePot")) { + quantiseRootPot = toBoolean(settingValue); + } + +} + +// converting string to Float +float Settings::toFloat(String settingValue) { + char floatbuf[settingValue.length()]; + settingValue.toCharArray(floatbuf, sizeof(floatbuf)); + float f = atof(floatbuf); + return f; +} + +// Converting String to integer and then to boolean +// 1 = true +// 0 = false +boolean Settings::toBoolean(String settingValue) { + if (settingValue.toInt() == 1) { + return true; + } else { + return false; + } +} + +void Settings::write() { + Serial.println("Settings file not found, writing new settings"); + + // Delete the old One + SD.remove("settings.txt"); + // Create new one + settingsFile = SD.open("settings.txt", FILE_WRITE); + settingsFile.print("crossfade="); + settingsFile.println(crossfade); + settingsFile.print("crossfadeTime="); + settingsFile.println(crossfadeTime); + settingsFile.print("showMeter="); + settingsFile.println(showMeter); + settingsFile.print("meterHide="); + settingsFile.println(meterHide); + settingsFile.print("chanPotImmediate="); + settingsFile.println(chanPotImmediate); + settingsFile.print("chanCVImmediate="); + settingsFile.println(chanCVImmediate); + settingsFile.print("startPotImmediate="); + settingsFile.println(startPotImmediate); + settingsFile.print("startCVImmediate="); + settingsFile.println(startCVImmediate); + settingsFile.print("startCVDivider="); + settingsFile.println(startCVDivider); + settingsFile.print("looping="); + settingsFile.println(looping); + settingsFile.print("sort="); + settingsFile.println(sort); + settingsFile.print("pitchMode="); + settingsFile.println(pitchMode); + // close the file: + settingsFile.close(); +} diff --git a/RadioMusic/Settings.h b/RadioMusic/Settings.h new file mode 100644 index 0000000..dcc1164 --- /dev/null +++ b/RadioMusic/Settings.h @@ -0,0 +1,75 @@ +#ifndef Settings_h +#define Settings_h + +#include + +// Classic Radio Music mode +// when true audio appears to play in the background as channels are switched. +// when false samples use the start time from CV / Pot EXCEPT in pitch mode where +// they always go back to the start. +#define LOOP_MODE_RADIO 1 +// Looper Mode +// Try to keep playback position as samples are switched +#define LOOP_MODE_CONTINUE 2 +// Use start point from interface +#define LOOP_MODE_START_POINT 3 + +class Settings { +public: + Settings(const char* filename); + void init(boolean hasSD); + void read(); + void write(); + float toFloat(String settingValue); + boolean toBoolean(String settingValue); + + uint8_t lowNote = 36; + uint8_t noteRange = 39; + + // Which note number is normal playback speed. + uint8_t rootNote = 60; + + boolean quantiseRootCV = true; + boolean quantiseRootPot = true; + + uint16_t crossfadeTime = 25; // milliseconds of fade in/out on switching + uint16_t meterHide = 2000; // how long to show the meter after bank change in Milliseconds + uint16_t startCVDivider = 2; // Changes sensitivity of Start control. 1 = most sensitive 512 = least sensitive (i.e only two points) + boolean crossfade = false; // Crossfade clicks when changing channel / position at cost of speed. Fade speed is set by DECLICK + boolean showMeter = true; // Does the VU meter appear? + + boolean chanPotImmediate = true; // Settings for Pot / CV response. + boolean chanCVImmediate = true; // TRUE means it jumps directly when you move or change. + + boolean startPotImmediate = false; // FALSE means it only has an effect when RESET is pushed or triggered + boolean startCVImmediate = false; + + boolean looping = false; // When a file finishes, start again from the beginning + + boolean sort = true; // By default we sort the directory contents. + + // Use start pot and cv to control speed instead of start point + boolean pitchMode = false; + + // If this is true we'll read any .wav files and try to play them + // if not we'll only play 44khz, 16bit mono files + boolean anyAudioFiles = true; + + // Use reset CV as an output + boolean resetIsOutput = false; + + // Only overrides if true. Force no crossfades. + boolean hardSwap = false; + + uint8_t loopMode = 0; + +private: + const char* _filename; + File settingsFile; + void radioMode(); + void drumMode(); + void copyDefaults(); + void applySetting(String name, String value); +}; + +#endif diff --git a/RadioMusic/Tuning.cpp b/RadioMusic/Tuning.cpp new file mode 100644 index 0000000..f7c877e --- /dev/null +++ b/RadioMusic/Tuning.cpp @@ -0,0 +1,309 @@ +#include +#include "Tuning.h" + +//#define DEBUG_TUNING + +Tuning::Tuning(const char* filename) { + _filename = filename; + numRatios = 0; +} + +boolean Tuning::init() { + boolean exists = SD.exists(_filename); +#ifdef DEBUG_TUNING + Serial.print("Checking for tuning file "); + Serial.println(_filename); + Serial.print("Exists "); + Serial.println(exists); +#endif + if(exists) { + return read(); + } else { + return false; + } +} + +// Return true if we successfully read a tuning file, false otherwise +boolean Tuning::read() { + + scalaFile = SD.open(_filename); + + char character; + + boolean inComment = false; + + int numEntries = 0; + ratios[0] = 1.0; + numRatios = 1; + + int DESCRIPTION_STATE = 1; + int ENTRIES_STATE = 2; + int RATIO_STATE = 3; + + int state = DESCRIPTION_STATE; + + String currentLine = ""; + + String description; + + while (scalaFile.available()) { + character = scalaFile.read(); + + // If we're in a comment just keep reading until we hit the next line + if(inComment) { + if(character == '\n') { + inComment = false; + } + continue; + } + if(character == '!') { + // Line is a comment, ignore until end + inComment = true; + if(state == RATIO_STATE && currentLine.length() > 1) { + // If a comment has started on a ratio line, we can process the ratio + if(addRatio(¤tLine)) { + break; + } + currentLine = ""; + } + continue; + } else { + inComment = false; + } + + if(state == DESCRIPTION_STATE) { + if(character == '\n') { + state = ENTRIES_STATE; + if(currentLine.length() == 0) { + description = "No Info"; + } else { + description = currentLine; + } + #ifdef DEBUG_TUNING + Serial.println("End description"); + Serial.println(description); + #endif + + currentLine = ""; + } else { + currentLine += character; + } + } else if(state == ENTRIES_STATE) { + if(character == '\n') { + state = RATIO_STATE; + numEntries = currentLine.toInt(); + if(numEntries == 0) { + return false; + } + #ifdef DEBUG_TUNING + Serial.print("End num entries: "); + Serial.println(numEntries); + #endif + + currentLine = ""; + } else { + currentLine += character; + } + } else if(state == RATIO_STATE) { + if(character == '\n') { + #ifdef DEBUG_TUNING + Serial.print("Got ratio"); + Serial.println(currentLine); + #endif + + if(addRatio(¤tLine)) { + break; + } + currentLine = ""; + } else { + currentLine += character; + } + } + } + + scalaFile.close(); + + #ifdef DEBUG_TUNING + if(numEntries != numRatios) { + Serial.print("Entries and Ratio Count not equal. "); + Serial.print(numEntries); + Serial.print(" vs "); + Serial.println(numRatios); + } + #endif + + if(numRatios > 1) { + return true; + } + + return false; +} + +// Returns boolean to indicate stopping condition has been met. +// return false means keep adding, return true means dont add any more ratios +boolean Tuning::addRatio(String* ratioText) { + float r = processRatio(ratioText); + Serial.print("Add ratio "); + Serial.print(numRatios); + Serial.print(" -> "); + Serial.println(r,4); + + ratios[numRatios] = r; + if(r < 2.0) { + numRatios++; + } else { + // At or over the octave, stop here. + return true; + } + + if(numRatios > 127) { + Serial.println("WARN. Enough ratios. Discarding extras. Octave scaling is probably broken"); + return true; + } + return false; +} +/** + * Valid Pitch Lines + + 81/64 + 408.0 + 408. + 5 + -5.0 + 10/20 + 100.0 cents + 100.0 C# + 5/4 E\ + + Note : They can also have comments placed after them. + */ +float Tuning::processRatio(String* ratioText) { + float ratio = 0.0; + + if(ratioText->indexOf(".") > 0) { + int spacePos = ratioText->indexOf(" "); + float cents = 0.0; + if(spacePos > 0) { + String centsSubstring = ratioText->substring(0,spacePos); + +#ifdef DEBUG_TUNING + Serial.print("Cents Substring:"); + Serial.print(centsSubstring); + Serial.println(":"); +#endif + + cents = centsSubstring.toFloat(); + } else { + cents = ratioText->toFloat(); + } + + ratio = pow(2,(cents/100.0)/12.0); + +#ifdef DEBUG_TUNING + Serial.print("Ratio is cents "); + Serial.println(cents); +#endif + + } else if(ratioText->indexOf("/") > 0) { + int slashPos = ratioText->indexOf("/"); + float numerator = ratioText->substring(0,slashPos).toFloat(); + float denominator = ratioText->substring(slashPos+1).toFloat(); + if(numerator != 0 && denominator != 0) { + ratio = numerator / denominator; + } + +#ifdef DEBUG_TUNING + Serial.print("Ratio is ratio "); + Serial.print(numerator); + Serial.print("/"); + Serial.print(denominator); + Serial.print(" = "); + Serial.println(ratio,4); +#endif + + } else { + ratio = ratioText->toFloat(); +// Serial.println("Ratio is float"); + } +// Serial.println(ratio); + return ratio; +} + +float* Tuning::createNoteMap() { + + #ifdef DEBUG_TUNING + Serial.print("Create note map for "); + Serial.print(numRatios); + Serial.println(" ratios."); + Serial.println("i\tRel\t\tOct\t\tIndex\t\tBase\t\tRatio\t\tFreq"); + #endif + + if(numRatios > 2) { + int centerNote = 69; + float centerFrequency = 440.0; + // Whilst this looks bad, the value at this position is the final octave scaling value + // from the tuning file + float octaveSize = ratios[numRatios]; + if(octaveSize <= 0.00000001) { + octaveSize = 1.0; + Serial.println("Octave ratio was zero, forcing to 1."); + } +#ifdef DEBUG_TUNING + Serial.print("Octave Size "); + Serial.println(octaveSize,8); +#endif + int notesPerOctave = numRatios; + + float octaveBaseFreq = 0.0; + int indexInOctave = 0; + float noteRatio = 0.0; + int octavesAway = 0; + float octaveFactor; + int relativeNoteIndex = 0; + for(int i=0;i<128;i++) { + relativeNoteIndex = i - centerNote; + + octavesAway = abs(floor((float)relativeNoteIndex / (float)notesPerOctave)); + indexInOctave = relativeNoteIndex % notesPerOctave; + octaveFactor = pow(octaveSize, octavesAway); + + if(relativeNoteIndex < 0) { + octaveBaseFreq = centerFrequency / octaveFactor; + indexInOctave = (indexInOctave + notesPerOctave) % notesPerOctave; + } else { + octaveBaseFreq = centerFrequency * octaveFactor; + } + + noteRatio = ratios[indexInOctave]; + pitchValues[i] = octaveBaseFreq * noteRatio; + + #ifdef DEBUG_TUNING + Serial.print(i); + Serial.print("\t"); + Serial.print(relativeNoteIndex); + Serial.print("\t\t"); + Serial.print(octavesAway); + Serial.print("\t\t"); + Serial.print(indexInOctave); + Serial.print("\t\t"); + Serial.print(octaveBaseFreq); + Serial.print("\t\t"); + Serial.print(noteRatio); + Serial.print("\t\t"); + Serial.println(pitchValues[i]); + #endif + } + + } else { + // Create standard 12-tet note map + for(int i=0;i<128;i++) { + pitchValues[i] = getStandardFreq(i); + } + } + + return pitchValues; +} + +float Tuning::getStandardFreq(float note) { + return 440.0*(pow (1.059463094359,note - 69)); +} diff --git a/RadioMusic/Tuning.h b/RadioMusic/Tuning.h new file mode 100644 index 0000000..1a2bbe2 --- /dev/null +++ b/RadioMusic/Tuning.h @@ -0,0 +1,24 @@ +#ifndef Tuning_h +#define Tuning_h + +#include + +class Tuning { + + public: + Tuning(const char* filename); + boolean init(); + boolean read(); + float pitchValues[128]; + float* createNoteMap(); + float getStandardFreq(float note); + private: + const char* _filename; + float ratios[128]; + int numRatios; + File scalaFile; + float processRatio(String* ratioLine); + boolean addRatio(String* ratioLine); +}; + +#endif diff --git a/RadioMusic/WavHeaderReader.cpp b/RadioMusic/WavHeaderReader.cpp new file mode 100644 index 0000000..40e3d3e --- /dev/null +++ b/RadioMusic/WavHeaderReader.cpp @@ -0,0 +1,132 @@ +#include "WavHeaderReader.h" + +#include +#include + +#include "RadioMusic.h" + +#ifdef DEBUG_WAV +#define D(x) x +#else +#define D(x) +#endif + +boolean WavHeaderReader::read(File* file, AudioFileInfo& info) { + + waveFile = file; + + if (waveFile->available()) { + D( + Serial.print("Bytes available "); + Serial.println(waveFile->available()); + ); + uint32_t chunkSize = 0; + + // 'RIFF' + if (!waveFile->seek(4)) { + D(Serial.println("Seek past RIFF failed"); ); + return false; + } else { + D(Serial.print("Cur Pos "); Serial.println(waveFile->position());); + } + + // File Size 4 bytes + uint32_t fileSize = readLong(); + D(Serial.print("File size "); Serial.println(fileSize);); + // 'WAVE' as little endian uint32 is 1163280727 + // If chunk ID isnt 'WAVE' stop here. + if (readLong() != 1163280727) { + D(Serial.println("Chunk ID not WAVE"); ); + return false; + } + + uint32_t nextID = readLong(); + // Keep skipping chunks until we hit 'fmt ' + while (nextID != 544501094) { + chunkSize = readLong(); + D(Serial.print("Skipping "); Serial.println(chunkSize);); + waveFile->seek(waveFile->position() + chunkSize); + nextID = readLong(); + } + + if (!waveFile->available()) { + D(Serial.println("Skipped whole file"); ); + return false; + } + + D(Serial.print("Found fmt chunk at "); Serial.println(waveFile->position());); + // Next block is fmt sub chunk + // subchunk ID : 'fmt ' (note the space) 4 bytes + // fmt subchunk Size : 4 bytes. + chunkSize = readLong(); + D(Serial.print("Format Chunk Size "); Serial.println(chunkSize);); + // Audio Format 2 bytes : 1 = PCM + uint16_t format = readShort(); + + // NumChannels 2 bytes + info.setChannels(readShort()); + + D(Serial.print("Is Stereo : "); Serial.println(info.format & STEREO);); + // SampleRate 4 bytes + if(!info.setSampleRate(readLong())) { + return false; + } + + // ByteRate 4 bytes + //info.byteRate = readLong(); + uint32_t byteRate = readLong(); + + // BlockAlign 2 bytes + uint16_t blockAlign = readShort(); + + // BitsPerSample 2 bytes + uint16_t bitsPerSample = readShort(); + if (bitsPerSample % 8 != 0) { + D(Serial.print("Unsupported bit depth "); Serial.println(bitsPerSample);); + return false; + } + info.setBitsPerSample(bitsPerSample); + + if (chunkSize > 16) { + D(Serial.print("Format chunk is extended "); Serial.println(chunkSize);); + waveFile->seek(waveFile->position() + chunkSize - 16); + } + + // 'data' as little endian uint32 is 1635017060 + // read the chunk ID as a uint32 rather than doing strcmp + while (readLong() != 1635017060) { + chunkSize = readLong(); + waveFile->seek(waveFile->position() + chunkSize); + } + + chunkSize = readLong(); + D(Serial.print("WAV data length "); Serial.println(chunkSize);); + info.size = chunkSize; + info.dataOffset = waveFile->position(); + } else { + D(Serial.println("File not available"); ); + } + return true; +} + +uint16_t WavHeaderReader::readShort() { + uint16_t val = waveFile->read(); + val = waveFile->read() << 8 | val; + return val; +} + +// Wav files are little endian +uint32_t WavHeaderReader::readLong() { + + int32_t val = waveFile->read(); + if (val == -1) { + D(Serial.println("Long read error. 1"); ); + } + for (byte i = 8; i < 32; i += 8) { + val = waveFile->read() << i | val; + if (val == -1) { + D(Serial.print("Long read error "); Serial.println(i);); + } + } + return val; +} diff --git a/RadioMusic/WavHeaderReader.h b/RadioMusic/WavHeaderReader.h new file mode 100644 index 0000000..a48c8f8 --- /dev/null +++ b/RadioMusic/WavHeaderReader.h @@ -0,0 +1,18 @@ +#ifndef WavHeaderReader_h +#define WavHeaderReader_h + +#include "SD.h" +#include "AudioFileInfo.h" + +class WavHeaderReader { +public: + // Return true if header was read successfully + boolean read(File* file, AudioFileInfo& info); +private: + uint32_t readLong(); + uint16_t readShort(); + + File* waveFile; +}; + +#endif diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBL b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBL similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBL rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBL diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBO b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBO similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBO rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBO diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBP b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBP similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBP rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBP diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBS b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBS similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GBS rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GBS diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GML b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GML similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GML rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GML diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTL b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTL similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTL rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTL diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTO b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTO similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTO rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTO diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTP b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTP similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTP rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTP diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTS b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTS similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.GTS rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.GTS diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.OUT b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.OUT similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.OUT rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.OUT diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.TXT b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.TXT similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB /RadioMusic_B.TXT rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music B Rev2 GRB/RadioMusic_B.TXT diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBL b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBL similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBL rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBL diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBO b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBO similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBO rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBO diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBP b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBP similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBP rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBP diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBS b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBS similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GBS rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GBS diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GML b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GML similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GML rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GML diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTL b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTL similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTL rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTL diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTO b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTO similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTO rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTO diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTP b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTP similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTP rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTP diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTS b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTS similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.GTS rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.GTS diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.OUT b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.OUT similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.OUT rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.OUT diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.TXT b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.TXT similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB /RadioMusic_F.TXT rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Radio Music F Rev2 GRB/RadioMusic_F.TXT diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_B Rev2.brd b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_B Rev2.brd similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_B Rev2.brd rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_B Rev2.brd diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_B Rev2.sch b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_B Rev2.sch similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_B Rev2.sch rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_B Rev2.sch diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_F Rev2.brd b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_F Rev2.brd similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_F Rev2.brd rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_F Rev2.brd diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_F Rev2.sch b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_F Rev2.sch similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_F Rev2.sch rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_F Rev2.sch diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_Panel.brd b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_Panel.brd similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_Panel.brd rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_Panel.brd diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_Panel.sch b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_Panel.sch similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /RadioMusic_Panel.sch rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/RadioMusic_Panel.sch diff --git a/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /TomW.lbr b/RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/TomW.lbr similarity index 100% rename from RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive /TomW.lbr rename to RadioMusicHardware/Gerbers/Rev 2 Gerbers Dec 2014/Rev 2 Eagle Archive/TomW.lbr