diff --git a/TimeMachine/TimeMachine.cpp b/TimeMachine/TimeMachine.cpp index 270d286..5980121 100644 --- a/TimeMachine/TimeMachine.cpp +++ b/TimeMachine/TimeMachine.cpp @@ -1,6 +1,15 @@ #include "daisy_patch_sm.h" #include "daisysp.h" -#include "dsp.h" + +#include "dsp/stereo_time_machine.h" +#include "dsp/slew.h" +#include "dsp/clock_rate_detector.h" +#include "dsp/continuous_schmidt.h" + +#include "ui/leds.h" +#include "ui/calibrator.h" +#include "ui/ui.h" + #include "time_machine_hardware.h" using namespace daisy; @@ -10,27 +19,9 @@ using namespace std; #define TIME_SECONDS 150 #define BUFFER_WIGGLE_ROOM_SAMPLES 1000 - +#define DEVELOPMENT_MODE true #define LINEAR_TIME false -//Setting Struct containing parameters we want to save to flash -struct CalibrationData { - float timeCvOffset = 0.0; - float skewCvOffset = 0.0; - float feedbackCvOffset = 0.0; - int calibrated = false; - - //Overloading the != operator - //This is necessary as this operator is used in the PersistentStorage source code - bool operator!=(const CalibrationData& a) const { - return !( - a.timeCvOffset==skewCvOffset && \ - a.skewCvOffset==skewCvOffset && \ - a.feedbackCvOffset==feedbackCvOffset && \ - a.calibrated==calibrated - ); - } -}; // init buffers - add an extra second just in case we somehow end up slightly beyond max time // due to precision loss in floating point arithmetic (maybe use doubles for time values???) @@ -38,318 +29,309 @@ float DSY_SDRAM_BSS bufferLeft[48000 * TIME_SECONDS + BUFFER_WIGGLE_ROOM_SAMPLES float DSY_SDRAM_BSS bufferRight[48000 * TIME_SECONDS + BUFFER_WIGGLE_ROOM_SAMPLES]; TimeMachineHardware hw; -PersistentStorage CalibrationDataStorage(hw.qspi); +Ui ui; GateIn gate; -Led leds[9]; +Leds leds; + +// Keep track of the agreement between the random sequence sent to the +// switch and the value read by the ADC. +uint32_t normalization_detection_count_ = 0; +uint32_t normalization_probe_state_ = 0; + +const uint8_t kNumNormalizedChannels = 4; +const uint8_t kProbeSequenceDuration = 32; +uint8_t normalization_probe_mismatches_[kNumNormalizedChannels] = {0, 0, 0, 0}; +bool is_patched_[kNumNormalizedChannels] = {false, false, false, false}; +int normalized_channels_[kNumNormalizedChannels] = { + VCA_1_CV, + VCA_2_CV, + VCA_3_CV, + VCA_4_CV +}; +float modulation_values_[kNumNormalizedChannels] = { + 0.0, 0.0, 0.0, 0.0 +}; + StereoTimeMachine timeMachine; -ClockRateDetector clockRateDetector; -ContSchmidt timeKnobSchmidt; -ContSchmidt timeCvSchmidt; - -Slew timeKnobSlew; -Slew feedbackKnobSlew; -Slew distributionKnobSlew; -Slew timeCvSlew; -Slew feedbackCvSlew; -Slew distributionCvSlew; - -// global storage for CV/knobs so we don't get them twice to print diagnostics -float timeCv = 0.0; -float feedbackCv = 0.0; -float skewCv = 0.0; -float timeKnob = 0.0; -float feedbackKnob = 0.0; -float skewKnob = 0.0; -float drySlider = 0.0; -float delaySliders = 0.0; + +PersistentStorage calibrationDataStorage(hw.qspi); +Calibrator calibrator; + + // calibration offsets for CV float timeCvOffset = 0.0; float feedbackCvOffset = 0.0; float skewCvOffset = 0.0; +float normalized_offsets_[kNumNormalizedChannels] = {0.0, 0.0, 0.0, 0.0}; + float finalTimeValue = 0.0; float finalDistributionValue = 0.0; float finalFeedbackValue = 0.0; -// delay setting LEDs for startup sequences -bool setLeds = false; - CpuLoadMeter cpuMeter; int droppedFrames = 0; +int audioCallBackRun = 0; + +// if modulation is patched, then slider acts as attenuverter for modulation +// if modulation is unpatched, slider is +// @sliderIdx is between 1 and 8 (incl.); +float readHeadAmp(int sliderIdx, Ui ui_local) { + float sliderAmpValue = ui_local.sliderAmpValues_[sliderIdx - 1]; + // 4 modulation inputs + int modulationIndex = (sliderIdx - 1) / 2; + + if (!is_patched_[modulationIndex]) { + return sliderAmpValue; + } + // between -1 & 1 + // -1 is silent, 1 is max volume + float modulationValue = modulation_values_[modulationIndex]; + + return sliderAmpValue * modulationValue; +} // called every N samples (search for SetAudioBlockSize) void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) -{ +{ // cpu meter measurements start cpuMeter.OnBlockStart(); droppedFrames++; - - // process controls - hw.ProcessAllControls(); - - // populate/update global CV/knob vars (time is slewed to reduce noise at large time values) - timeKnob = minMaxKnob(1.0 - hw.GetAdcValue(TIME_KNOB), 0.0008); - feedbackKnob = fourPointWarp(1.0 - minMaxKnob(hw.GetAdcValue(FEEDBACK_KNOB), 0.028)); - skewKnob = fourPointWarp(1.0 - minMaxKnob(hw.GetAdcValue(SKEW_KNOB), 0.0008)); - - timeCv = clamp(hw.GetAdcValue(TIME_CV) - timeCvOffset, -1, 1); - feedbackCv = clamp(hw.GetAdcValue(FEEDBACK_CV) - feedbackCvOffset, -1, 1); - skewCv = clamp(hw.GetAdcValue(SKEW_CV) - skewCvOffset, -1, 1); - - drySlider = minMaxSlider(1.0 - hw.GetAdcValue(DRY_SLIDER)); - - // calculate time based on clock if present, otherwise simple time - float time = 0.0; - if(clockRateDetector.GetInterval() > 0.0) { - // 12 quantized steps for knob, 10 for CV (idk what these quanta should actually be) - // time doubles and halves with each step, they are additive/subtractive - float timeCoef = pow(2.0, (timeKnobSchmidt.Process((1.0-timeKnob)*12)) + (timeCvSchmidt.Process(timeCv*10))) / pow(2.0, 6.0); - time = clockRateDetector.GetInterval() / timeCoef; - // make sure time is a power of two less than the max time available in the buffer - while(time > TIME_SECONDS) time *= 0.5; - } else { - // time linear with knob, scaled v/oct style with CV - time = pow(timeKnobSlew.Process(timeKnob), 2.0) * 8.0 / pow(2.0, timeCvSlew.Process(timeCv) * 5.0); - } - - // force time down to a max value (taking whichever is lesser, the max or the time) - time = std::min((float)TIME_SECONDS, time); - // condition feedback knob to have deadzone in the middle, add CV - float feedback = clamp(fourPointWarp(feedbackKnobSlew.Process(feedbackKnob)) * 2.0 + feedbackCvSlew.Process(feedbackCv), 0, 3); - // condition distribution knob value to have deadzone in the middle, add CV - float distribution = fourPointWarp(distributionKnobSlew.Process(skewKnob)) + distributionCvSlew.Process(skewCv); - - finalTimeValue = time; - finalFeedbackValue = feedback; - finalDistributionValue = distribution; + audioCallBackRun++; + if (audioCallBackRun > 10000000) { audioCallBackRun = 0; } + //hw.PrintLine("AUDIO_CALLBACK_CPU_BLOCK_OK"); + + //ui.ProcessAllControls(); + //hw.PrintLine("AUDIO_CALLBACK_CONTROLS_PROCESSED"); for(int i=0; i<9; i++) { - if(i<8) { - // set LEDs based on loudness for last 8 sliders - float loudness = timeMachine.timeMachineLeft.readHeads[i].loudness.Get(); - loudness = max(loudness, timeMachine.timeMachineRight.readHeads[i].loudness.Get()); - if(setLeds) { - leds[i+1].Set(loudness); - leds[i+1].Update(); - } - } else { - // set LEDs based on loudness for first slider - float loudness = timeMachine.timeMachineLeft.loudness.Get(); - loudness = max(loudness, timeMachine.timeMachineRight.loudness.Get()); - if(setLeds) { - leds[0].Set(loudness); - leds[0].Update(); - } - } + float loudnessLeft = timeMachine.timeMachineLeft.GetLoudness(i); + float loudnessRight = timeMachine.timeMachineRight.GetLoudness(i); + + leds.Set(i, max(loudnessLeft, loudnessRight)); } // set time machine dry slider value, feedback, "blur" which is semi-deprecated - timeMachine.Set(drySlider, feedback, feedback); // controlling "blur" with feedback now??? + timeMachine.Set(ui.drySlider, ui.feedback, ui.feedback); // controlling "blur" with feedback now??? + + //hw.PrintLine("AUDIO_CALLBACK_DRY_SET"); for(int i=1; i<9; i++) { // let last 8 slider time/amp/blur values for left channel time machine instance timeMachine.timeMachineLeft.readHeads[i-1].Set( - spread((i / 8.0), distribution) * time, - max(0.0f, minMaxSlider(1.0f - hw.GetSliderValue(i))), - max(0., feedback-1.0) + spread((i / 8.0), ui.distribution) * ui.time, + readHeadAmp(i, ui), + max(0., ui.feedback-1.0) ); // let last 8 slider time/amp/blur values for right channel time machine instance timeMachine.timeMachineRight.readHeads[i-1].Set( - spread((i / 8.0), distribution) * time, - max(0.0f, minMaxSlider(1.0f - hw.GetSliderValue(i))), - max(0., feedback-1.0) + spread((i / 8.0), ui.distribution) * ui.time, + readHeadAmp(i, ui), + max(0., ui.feedback-1.0) ); } + for (size_t i = 0; i < size; i++) { // process gate for clock rate detector at audio rate (per-sample) so it calculates clock correctly - clockRateDetector.Process(hw.gate_in_2.State()); + ui.ProcessClockRate(hw.gate_in_2.State()); + // process input into time machine float* output = timeMachine.Process(in[0][i], in[1][i]); // set hardware output to time machine output - out[0][i] = output[0]; - out[1][i] = output[1]; + out[0][i] = 0.0; //output[0]; + out[1][i] = 0.0; //output[1]; } + //hw.PrintLine("AUDIO_CALLBACK_OUTPUT_DONE"); + //cpu meter measurement stop cpuMeter.OnBlockEnd(); droppedFrames--; } -bool shouldCalibrate() { - bool shouldCalibrate = \ - (hw.GetAdcValue(SKEW_CV) < 0.01) && \ - (hw.GetAdcValue(TIME_CV) < 0.01) && \ - (hw.GetAdcValue(FEEDBACK_CV) < 0.01) && \ - hw.gate_in_2.State(); - for(int i=0; i<9; i++) { - shouldCalibrate &= hw.GetSliderValue(i) < 0.01; - } - shouldCalibrate &= minMaxKnob(1.0 - hw.GetAdcValue(TIME_KNOB)) > 0.95; - shouldCalibrate &= minMaxKnob(1.0 - hw.GetAdcValue(SKEW_KNOB)) > 0.95; - shouldCalibrate &= minMaxKnob(1.0 - hw.GetAdcValue(FEEDBACK_KNOB)) > 0.95; - return shouldCalibrate; + +void DetectNormalization() { + bool expected_value = normalization_probe_state_ >> 31; + for (int i = 0; i < kNumNormalizedChannels; ++i) { + int channel = normalized_channels_[i]; + float value = hw.GetAdcValue(channel); + bool read_value; + if (value > 4.95) { + read_value = true; + } else if (value < 0.05) { + read_value = false; + } + else { + ++normalization_probe_mismatches_[i]; + continue; + } + + if (expected_value != read_value) { + ++normalization_probe_mismatches_[i]; + } + } + + ++normalization_detection_count_; + if (normalization_detection_count_ == kProbeSequenceDuration) { + normalization_detection_count_ = 0; + for (int i = 0; i < kNumNormalizedChannels; ++i) { + is_patched_[i] = normalization_probe_mismatches_[i] >= 2; + normalization_probe_mismatches_[i] = 0; + } + } + + normalization_probe_state_ = 1103515245 * normalization_probe_state_ + 12345; + hw.WriteNormalization(normalization_probe_state_ >> 31); } int main(void) { + // init time machine hardware hw.Init(); - hw.SetAudioBlockSize(4); // number of samples handled per callback + + //hw.SetAudioBlockSize(4); // number of samples handled per callback + hw.PrintLine("AUDIO_INITIALIZED"); + + ui.Init(hw); + hw.PrintLine("UI_INITIALIZED"); + + calibrator.Init(calibrationDataStorage); + hw.PrintLine("CALIBRATOR_INITIALIZED"); dsy_gpio_pin gatePin = DaisyPatchSM::B9; gate.Init(&gatePin); // initialize LEDs - leds[0].Init(DaisyPatchSM::D1, false); - leds[1].Init(DaisyPatchSM::D2, false); - leds[2].Init(DaisyPatchSM::D3, false); - leds[3].Init(DaisyPatchSM::D4, false); - leds[4].Init(DaisyPatchSM::D5, false); - // hacky selector for my weird dev version (Eris) - #if BODGE - leds[5].Init(DaisyPatchSM::B8, false); - #else - leds[5].Init(DaisyPatchSM::A9, false); - #endif - leds[6].Init(DaisyPatchSM::D10, false); - leds[7].Init(DaisyPatchSM::D7, false); - leds[8].Init(DaisyPatchSM::D6, false); - + leds.Init( + DaisyPatchSM::D1, + DaisyPatchSM::D2, + DaisyPatchSM::D3, + DaisyPatchSM::D4, + DaisyPatchSM::D5, + DaisyPatchSM::A9, + DaisyPatchSM::D10, + DaisyPatchSM::D7, + DaisyPatchSM::D6); + + hw.PrintLine("LEDS_INITIALIZED"); + // set sample rate hw.SetAudioSampleRate(SaiHandle::Config::SampleRate::SAI_48KHZ); - // init slew limiter for time (we should tune this more delibrately) - timeKnobSlew.Init(0.5, 0.0005); - feedbackKnobSlew.Init(0.5, 0.0005); - distributionKnobSlew.Init(0.5, 0.0005); - timeCvSlew.Init(0.5, 0.0005); - feedbackCvSlew.Init(0.5, 0.0005); - distributionCvSlew.Init(0.5, 0.0005); - - // init clock rate detector - clockRateDetector.Init(hw.AudioSampleRate()); + hw.PrintLine("SAMPELRATE: %d", hw.AudioSampleRate()); // init time machine - timeMachine.Init(hw.AudioSampleRate(), TIME_SECONDS + (((float)BUFFER_WIGGLE_ROOM_SAMPLES) * 0.5 / hw.AudioSampleRate()), bufferLeft, bufferRight); - - // load calibration data, using sensible defaults - CalibrationDataStorage.Init({0.0f, 0.0f, 0.0f, false}); - CalibrationDataStorage.GetSettings(); - CalibrationData &savedCalibrationData = CalibrationDataStorage.GetSettings(); + timeMachine.Init( + hw.AudioSampleRate(), + TIME_SECONDS + (((float)BUFFER_WIGGLE_ROOM_SAMPLES) * 0.5 / hw.AudioSampleRate()), + bufferLeft, + bufferRight); + + hw.PrintLine("SAMPELRATE: %d", hw.AudioSampleRate()); + hw.PrintLine("MAXDELAY: %d", TIME_SECONDS + (((float)BUFFER_WIGGLE_ROOM_SAMPLES) * 0.5 / hw.AudioSampleRate())); + hw.PrintLine("SAMPELRATE: %d", hw.AudioSampleRate()); // init cpu meter cpuMeter.Init(hw.AudioSampleRate(), hw.AudioBlockSize()); // start time machine hardware audio and logging hw.StartAudio(AudioCallback); + hw.PrintLine("AUDIO_CALLBACK_STARTED"); + //leds.InitStartupSequence(); - // LED startup sequence - int ledSeqDelay = 100; - for(int i=0; i<9; i++) { - for(int j=0; j<9; j++) { - leds[j].Set(j == i ? 1.0 : 0.0); - leds[j].Update(); - } - hw.PrintLine("%d", i); - System::Delay(ledSeqDelay); - } + // Calibrate if needed + //calibrator.Calibrate(hw, leds); - if(shouldCalibrate()) { + //TODO: CALIBRATION OFFSETS NEED INJECTING TO UI + // timeCvOffset = savedCalibrationData.timeCvOffset; + // skewCvOffset = savedCalibrationData.skewCvOffset; + // feedbackCvOffset = savedCalibrationData.feedbackCvOffset; + // normalized_offsets_[0] = savedCalibrationData.vca1CvOffset; + // normalized_offsets_[1] = savedCalibrationData.vca2CvOffset; + // normalized_offsets_[2] = savedCalibrationData.vca3CvOffset; + // normalized_offsets_[3] = savedCalibrationData.vca4CvOffset; - bool calibrationReady = true; - // do reverse LED startup sequence while - // checking that we definitely want to calibrate - for(int i=0; i<(5000/ledSeqDelay); i++) { - for(int j=0; j<9; j++) { - leds[j].Set(j == (8 - (i%9)) ? 1.0 : 0.0); - leds[j].Update(); - } - System::Delay(ledSeqDelay); - calibrationReady &= shouldCalibrate(); - if(!calibrationReady) break; - } - - if(calibrationReady) { - // perform calibration routine - int numSamples = 128; - for(int i = 0; i < numSamples; i++) { - // accumulate cv values - savedCalibrationData.timeCvOffset += timeCv; - savedCalibrationData.skewCvOffset += skewCv; - savedCalibrationData.feedbackCvOffset += feedbackCv; - // wait 10ms - System::Delay(10); - // set LEDs - for(int ledIndex=0; ledIndex<9; ledIndex++) { - leds[ledIndex].Set(i % 8 < 4 ? 1.0f : 0.0f); - leds[ledIndex].Update(); - } - } + leds.StartUi(); + + hw.StartLog(true); + + while(1) { + //DetectNormalization(); + ui.ProcessAllControls(); + + if (DEVELOPMENT_MODE) { + // print diagnostics + hw.PrintLine("TIME_CV: " FLT_FMT(6), FLT_VAR(6, ui.timeCv)); + hw.PrintLine("FEEDBACK_CV: " FLT_FMT(6), FLT_VAR(6, ui.feedbackCv)); + hw.PrintLine("SKEW_CV: " FLT_FMT(6), FLT_VAR(6, ui.skewCv)); - // divide CVs by number of samples taken to get average - savedCalibrationData.timeCvOffset = savedCalibrationData.timeCvOffset / ((float)numSamples); - savedCalibrationData.skewCvOffset = savedCalibrationData.skewCvOffset / ((float)numSamples); - savedCalibrationData.feedbackCvOffset = savedCalibrationData.feedbackCvOffset / ((float)numSamples); + if (is_patched_[0]) { + hw.PrintLine("VCA_1_CV: " FLT_FMT(6), FLT_VAR(6, ui.vca1Cv)); + } else { + hw.PrintLine("VCA_1_CV is unpatched!"); + } - // set calibrated value to true - savedCalibrationData.calibrated = true; + if (is_patched_[1]) { + hw.PrintLine("VCA_2_CV: " FLT_FMT(6), FLT_VAR(6, ui.vca2Cv)); + } else { + hw.PrintLine("VCA_2_CV is unpatched!"); + } + + if (is_patched_[2]) { + hw.PrintLine("VCA_3_CV: " FLT_FMT(6), FLT_VAR(6, ui.vca3Cv)); + } else { + hw.PrintLine("VCA_3_CV is unpatched!"); + } + + if (is_patched_[3]) { + hw.PrintLine("VCA_4_CV: " FLT_FMT(6), FLT_VAR(6, ui.vca4Cv)); + } else { + hw.PrintLine("VCA_4_CV is unpatched!"); + } - // save calibration data - CalibrationDataStorage.Save(); - } - } + hw.PrintLine("TIME_KNOB: " FLT_FMT(6), FLT_VAR(6, ui.timeKnob)); + hw.PrintLine("FEEDBACK_KNOB: " FLT_FMT(6), FLT_VAR(6, ui.feedbackKnob)); + hw.PrintLine("SKEW_KNOB: " FLT_FMT(6), FLT_VAR(6, ui.skewKnob)); + hw.PrintLine("GATE IN: %d", hw.gate_in_2.State()); - timeCvOffset = savedCalibrationData.timeCvOffset; - skewCvOffset = savedCalibrationData.skewCvOffset; - feedbackCvOffset = savedCalibrationData.feedbackCvOffset; + hw.PrintLine("GATE IN: %d", hw.gate_in_1.State()); + hw.PrintLine("CV IN 1: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_4))); + hw.PrintLine("CV IN 2: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_5))); + hw.PrintLine("CV IN 3: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_6))); + hw.PrintLine("CV IN 4: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_7))); - hw.StartLog(); + // hw.PrintLine("TIME_CAL: " FLT_FMT(6), FLT_VAR(6, savedCalibrationData.timeCvOffset)); + // hw.PrintLine("FEEDBACK_CAL: " FLT_FMT(6), FLT_VAR(6, savedCalibrationData.feedbackCvOffset)); + // hw.PrintLine("SKEW_CAL: " FLT_FMT(6), FLT_VAR(6, savedCalibrationData.skewCvOffset)); + // hw.PrintLine("CALIBRATED: %d", savedCalibrationData.calibrated); - setLeds = true; + hw.PrintLine("FINAL TIME: " FLT_FMT(6), FLT_VAR(6, finalTimeValue)); + hw.PrintLine("FINAL DISTRIBUTION: " FLT_FMT(6), FLT_VAR(6, ui.distribution)); + hw.PrintLine("FINAL FEEDBACK: " FLT_FMT(6), FLT_VAR(6, finalFeedbackValue)); - while(1) { + hw.PrintLine("CPU AVG: " FLT_FMT(6), FLT_VAR(6, cpuMeter.GetAvgCpuLoad())); + hw.PrintLine("CPU MIN: " FLT_FMT(6), FLT_VAR(6, cpuMeter.GetMinCpuLoad())); + hw.PrintLine("CPU MAX: " FLT_FMT(6), FLT_VAR(6, cpuMeter.GetMaxCpuLoad())); + + hw.PrintLine("DROPPED FRAMES: %d", droppedFrames); + hw.PrintLine("AUDIO CALLBACK RUN: %d", audioCallBackRun); + hw.PrintLine("SAMPELRATE: %d", hw.AudioSampleRate()); - // print diagnostics - hw.PrintLine("TIME_CV: " FLT_FMT(6), FLT_VAR(6, timeCv)); - hw.PrintLine("FEEDBACK_CV: " FLT_FMT(6), FLT_VAR(6, feedbackCv)); - hw.PrintLine("SKEW_CV: " FLT_FMT(6), FLT_VAR(6, skewCv)); - hw.PrintLine("TIME_KNOB: " FLT_FMT(6), FLT_VAR(6, timeKnob)); - hw.PrintLine("FEEDBACK_KNOB: " FLT_FMT(6), FLT_VAR(6, feedbackKnob)); - hw.PrintLine("SKEW_KNOB: " FLT_FMT(6), FLT_VAR(6, skewKnob)); - hw.PrintLine("GATE IN: %d", hw.gate_in_2.State()); - - hw.PrintLine("GATE IN: %d", hw.gate_in_1.State()); - hw.PrintLine("CV IN 1: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_4))); - hw.PrintLine("CV IN 2: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_5))); - hw.PrintLine("CV IN 3: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_6))); - hw.PrintLine("CV IN 4: " FLT_FMT(6), FLT_VAR(6, hw.GetAdcValue(CV_7))); - - hw.PrintLine("TIME_CAL: " FLT_FMT(6), FLT_VAR(6, savedCalibrationData.timeCvOffset)); - hw.PrintLine("FEEDBACK_CAL: " FLT_FMT(6), FLT_VAR(6, savedCalibrationData.feedbackCvOffset)); - hw.PrintLine("SKEW_CAL: " FLT_FMT(6), FLT_VAR(6, savedCalibrationData.skewCvOffset)); - hw.PrintLine("CALIBRATED: %d", savedCalibrationData.calibrated); - - hw.PrintLine("FINAL TIME: " FLT_FMT(6), FLT_VAR(6, finalTimeValue)); - hw.PrintLine("FINAL DISTRIBUTION: " FLT_FMT(6), FLT_VAR(6, finalDistributionValue)); - hw.PrintLine("FINAL FEEDBACK: " FLT_FMT(6), FLT_VAR(6, finalFeedbackValue)); - - hw.PrintLine("CPU AVG: " FLT_FMT(6), FLT_VAR(6, cpuMeter.GetAvgCpuLoad())); - hw.PrintLine("CPU MIN: " FLT_FMT(6), FLT_VAR(6, cpuMeter.GetMinCpuLoad())); - hw.PrintLine("CPU MAX: " FLT_FMT(6), FLT_VAR(6, cpuMeter.GetMaxCpuLoad())); - - hw.PrintLine("DROPPED FRAMES: %d", droppedFrames); - - for(int i=0; i<9; i++) { - hw.PrintLine("%d: " FLT_FMT(6), i, FLT_VAR(6, minMaxSlider(1.0 - hw.GetSliderValue(i)))); - } - hw.PrintLine(""); - System::Delay(250); + for(int i=0; i<9; i++) { + hw.PrintLine("%d: " FLT_FMT(6), i, FLT_VAR(6, minMaxSlider(1.0 - hw.GetSliderValue(i)))); + } + + hw.PrintLine(""); + System::Delay(250); + } + } } diff --git a/TimeMachine/dsp.h b/TimeMachine/dsp.h deleted file mode 100644 index 1602f42..0000000 --- a/TimeMachine/dsp.h +++ /dev/null @@ -1,375 +0,0 @@ -#include "daisysp.h" -#include "biquad.h" - -int wrap_buffer_index(int x, int size) { - while(x >= size) x -= size; - while(x < 0) x += size; - return x; -} - -int seconds_to_samples(float x, float sampleRate) { - return (int)(x * sampleRate); -} - -float mix(float x, float a, float b) { - return x*(1-x) + b*x; -} - -float clamp(float x, float a, float b) { - return std::max(a,std::min(b,x)); -} - -float fourPointWarp(float x, - float ai=0.0, - float av=0.0, - float bi=0.45, - float bv=0.5, - float ci=0.55, - float cv=0.5, - float di=1.0, - float dv=1.0) { - if(x < ai) { - return av; - } else if(x < bi) { - x = (x - ai) / (bi - ai); - return av * (1.0 - x) + bv * x; - } else if(x < ci) { - x = (x - bi) / (ci - bi); - return bv * (1.0 - x) + cv * x; - } else if(x < di) { - x = (x - ci) / (di - ci); - return cv * (1.0 - x) + dv * x; - } else { - return dv; - } -} - -float minMaxKnob(float in, float dz=0.002) { - in = in - dz * 0.5; - in = in * (1.0 + dz); - return std::min(1.0f, std::max(0.0f, in)); -} - -float minMaxSlider(float in, float dz=0.002) { - return minMaxKnob(in, dz); -} - -float softClip(float x, float kneeStart=0.9, float kneeCurve=5.0) { - float linPart = clamp(x, -kneeStart, kneeStart); - float clipPart = x - linPart; - clipPart = atan(clipPart * kneeCurve) / kneeCurve; - return linPart + clipPart; -} - -float spread(float x, float s, float e=2.5) { - s = clamp(s, 0.0, 1.0); - if(s > 0.5) { - s = (s-0.5)*2.0; - s = s*e+1.0; - return 1.0 - pow(1.0-x, s); - } else if(s < 0.5) { - s = 1.0-(s*2.0); - s = s*e+1.0; - return pow(x, s); - } else { - return x; - } -} - -class PreciseSlew -{ - public: - PreciseSlew() {} - ~PreciseSlew() {} - void Init(float sample_rate, float htime) { - lastVal = 0; - prvhtim_ = -100.0; - htime_ = htime; - - sample_rate_ = sample_rate; - onedsr_ = 1.0 / sample_rate_; - } - float Process(float in) { - if(prvhtim_ != htime_) - { - c2_ = pow(0.5, onedsr_ / htime_); - c1_ = 1.0 - c2_; - prvhtim_ = htime_; - } - return lastVal = c1_ * in + c2_ * lastVal; - } - inline void SetHtime(float htime) { htime_ = htime; } - inline float GetHtime() { return htime_; } - float htime_; - float c1_, c2_, lastVal, prvhtim_; - float sample_rate_, onedsr_; -}; - - -class Slew { -public: - double lastVal = 0.0; - double coef = 0.001; - double noiseFloor = 0.0; - double noiseCoef = 0.0; - int settleSamples = 0; - int settleSamplesThreshold = 96; - void Init(double coef = 0.001, double nf=0.0) { - this->coef = coef; - this->noiseFloor = nf; - } - float Process(float x) { - double c = coef; - double d = (x - lastVal); - // if we've set a noise floor - if(noiseFloor > 0.0) { - // if we're under the noise floor - if(abs(d) < noiseFloor) { - // if the input needs to settle - if(settleSamples < settleSamplesThreshold) { - // keep sampling - settleSamples++; - // if the input is done settling - } else { - // don't change the value - d = 0.0; - } - // if we're over the noise floor - } else { - // reset the settle wait - settleSamples = 0; - } - } - lastVal = lastVal + d * c; - return lastVal; - } -}; - -class ContSchmidt { -public: - float val = 0.0; - float Process(float x, float h=0.333) { - float i, f; - float sign = x < 0.0 ? -1.0 : 1.0; - x = abs(x); - f = modf(x, &i); - if(f < h) val = i; - if(f > 1.0 - h) val = i + 1; - return val * sign; - } -}; - -class UltraSlowDCBlocker { -public: - Slew slew; - void Init(float coef = 0.00001) { - slew.Init(coef); - } - float Process(float x) { - return x - slew.Process(x); - } -}; - -class LoudnessDetector { -public: - Slew slew; - float lastVal = 0; - void Init() { slew.Init(); } - float Get() { return this->lastVal; } - float Process(float x) { - lastVal = slew.Process(abs(x)); - return x; - } -}; - -class Limiter { -public: - float gainCoef; - float attackCoef; - float releaseCoef; - void Init(float sampleRate) { - gainCoef = 1; - releaseCoef = 16.0 / sampleRate; - } - float Process(float in) { - float targetGainCoef = 1.0 / std::max(abs(in), (float)1.0); - if(targetGainCoef < gainCoef) { - gainCoef = targetGainCoef; - } else { - gainCoef = gainCoef * (1.0 - releaseCoef) + targetGainCoef*releaseCoef; - } - return in * gainCoef; - } -}; - -class ReadHead { -public: - LoudnessDetector loudness; - float* buffer; - int bufferSize; - float delayA = 0.0; - float delayB = 0.0; - float targetDelay = -1; - float ampA = 0.0; - float ampB = 0.0; - float targetAmp = -1; - float sampleRate; - float phase = 1.0; - float delta; - float blurAmount; - void Init(float sampleRate, float* buffer, int bufferSize) { - this->sampleRate = sampleRate; - this->delta = 5.0 / sampleRate; - this->buffer = buffer; - this->bufferSize = bufferSize; - this->blurAmount = 0.0; - } - void Set(float delay, float amp, float blur = 0) { - this->targetDelay = delay; - this->targetAmp = amp; - this->blurAmount = blur; - } - float Process(float writeHeadPosition) { - if(phase >= 1.0 && (targetDelay >= 0.0 || targetAmp > 0.0)) { - delayA = delayB; - delayB = targetDelay; - targetDelay = -1.0; - ampA = ampB; - ampB = targetAmp; - targetAmp = -1.0; - phase = 0.0; - delta = (5.0 + daisy::Random::GetFloat(-blurAmount, blurAmount)) / sampleRate; - } - float outputA = this->buffer[wrap_buffer_index(writeHeadPosition - seconds_to_samples(this->delayA, this->sampleRate), bufferSize)]; - float outputB = this->buffer[wrap_buffer_index(writeHeadPosition - seconds_to_samples(this->delayB, this->sampleRate), bufferSize)]; - float output = ((1 - phase) * outputA) + (phase * outputB); - float outputAmp = ((1 - phase) * ampA) + (phase * ampB); - phase = phase <= 1.0 ? phase + delta : 1.0; - return loudness.Process(output) * outputAmp; - } -}; - -float defaultBlurFunc(float x, float mix) { return x; } -class TimeMachine { -public: - ReadHead readHeads[8]; - LoudnessDetector loudness; - float sampleRate; - float* buffer; - int bufferSize; - int writeHeadPosition; - float dryAmp; - float feedback; - float blur; - Slew dryAmpSlew; - Slew feedbackSlew; - Slew ampCoefSlew; - Slew blurSlew; - Limiter outputLimiter; - Limiter feedbackLimiter; - UltraSlowDCBlocker dcblk; - daisysp::Compressor compressor; - float (*blurFunc)(float, float) = nullptr; - void Init(float sampleRate, float maxDelay, float* buffer) { - this->sampleRate = sampleRate; - this->bufferSize = seconds_to_samples(maxDelay, sampleRate); - this->buffer = buffer; - for(int i=0; idryAmp = dryAmp; - this->feedback = feedback; - this->blur = blur; - } - void SetBlurFunc(float (*f)(float, float)) { - blurFunc = f; - } - float Process(float in) { - float out = 0; - - float ampCoef = 0.0; - for(int i=0; i<8; i++) ampCoef += readHeads[i].targetAmp; - ampCoef = ampCoefSlew.Process(1.0 / std::max(1.0f, ampCoef)); - - buffer[writeHeadPosition] = loudness.Process(in); - - for(int i=0; i<8; i++) out += readHeads[i].Process(writeHeadPosition); - out = dcblk.Process(out); - out = compressor.Process(out, buffer[writeHeadPosition] + out); - - buffer[writeHeadPosition] = -(feedbackLimiter.Process(buffer[writeHeadPosition] + (out * feedbackSlew.Process(feedback) * ampCoef))); - out = outputLimiter.Process(out + in * dryAmpSlew.Process(dryAmp)); - writeHeadPosition = wrap_buffer_index(writeHeadPosition + 1, bufferSize); - - return out; - } -}; - -class StereoTimeMachine { -public: - TimeMachine timeMachineLeft; - TimeMachine timeMachineRight; - float outputs[2]; - void Init(float sampleRate, float maxDelay, float* bufferLeft, float* bufferRight) { - timeMachineLeft.Init(sampleRate, maxDelay, bufferLeft); - timeMachineRight.Init(sampleRate, maxDelay, bufferRight); - } - void Set(float dryAmp, float feedback, float blur=0.0) { - timeMachineLeft.Set(dryAmp, feedback, blur); - timeMachineRight.Set(dryAmp, feedback, blur); - } - float* Process(float inLeft, float inRight) { - outputs[0] = timeMachineLeft.Process(inLeft); - outputs[1] = timeMachineRight.Process(inRight); - return outputs; - } -}; - -class ClockRateDetector { -public: - int samplesSinceLastClock; - int lastIntervalInSamples; - bool lastVal; - float sampleRate; - ClockRateDetector() { - samplesSinceLastClock = 0; - lastIntervalInSamples = 0; - lastVal = false; - } - void Init(int sr) { sampleRate = sr; } - bool isStale() { - return samplesSinceLastClock > sampleRate * 2; - } - float GetInterval() { - float interval = lastIntervalInSamples / sampleRate; - return isStale() ? 0.0 : interval; - } - void Process(bool triggered) { - if(triggered && lastVal != triggered) { - if(isStale()) { - lastIntervalInSamples = samplesSinceLastClock; - } else { - lastIntervalInSamples = (lastIntervalInSamples + samplesSinceLastClock) * 0.5; - } - samplesSinceLastClock = 0; - } else { - samplesSinceLastClock++; - } - lastVal = triggered; - } -}; \ No newline at end of file diff --git a/TimeMachine/biquad.h b/TimeMachine/dsp/biquad.h similarity index 100% rename from TimeMachine/biquad.h rename to TimeMachine/dsp/biquad.h diff --git a/TimeMachine/dsp/clock_rate_detector.h b/TimeMachine/dsp/clock_rate_detector.h new file mode 100644 index 0000000..a3f3e3d --- /dev/null +++ b/TimeMachine/dsp/clock_rate_detector.h @@ -0,0 +1,41 @@ +#include "daisysp.h" + +#ifndef CLOCK_RATE_DETECTOR_H_ +#define CLOCK_RATE_DETECTOR_H_ + +class ClockRateDetector { +public: + int samplesSinceLastClock; + int lastIntervalInSamples; + bool lastVal; + float sampleRate; + ClockRateDetector() { + samplesSinceLastClock = 0; + lastIntervalInSamples = 0; + lastVal = false; + } + void Init(int sr) { sampleRate = sr; } + + bool isStale() { + return samplesSinceLastClock > sampleRate * 2; + } + float GetInterval() { + float interval = lastIntervalInSamples / sampleRate; + return isStale() ? 0.0 : interval; + } + void Process(bool triggered) { + if(triggered && lastVal != triggered) { + if(isStale()) { + lastIntervalInSamples = samplesSinceLastClock; + } else { + lastIntervalInSamples = (lastIntervalInSamples + samplesSinceLastClock) * 0.5; + } + samplesSinceLastClock = 0; + } else { + samplesSinceLastClock++; + } + lastVal = triggered; + } +}; + +#endif // CLOCK_RATE_DETECTOR_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/continuous_schmidt.h b/TimeMachine/dsp/continuous_schmidt.h new file mode 100644 index 0000000..1afcc81 --- /dev/null +++ b/TimeMachine/dsp/continuous_schmidt.h @@ -0,0 +1,20 @@ +#include "daisysp.h" + +#ifndef CONT_SCHMIDT_H_ +#define CONT_SCHMIDT_H_ + +class ContSchmidt { +public: + float val = 0.0; + float Process(float x, float h=0.333) { + float i, f; + float sign = x < 0.0 ? -1.0 : 1.0; + x = abs(x); + f = modf(x, &i); + if(f < h) val = i; + if(f > 1.0 - h) val = i + 1; + return val * sign; + } +}; + +#endif // CONT_SCHMIDT_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/limiter.h b/TimeMachine/dsp/limiter.h new file mode 100644 index 0000000..5ca43a9 --- /dev/null +++ b/TimeMachine/dsp/limiter.h @@ -0,0 +1,28 @@ +#include "daisysp.h" + +#ifndef LIMITER_H_ +#define LIMITER_H_ +namespace oam { + + class Limiter { + public: + float gainCoef; + float releaseCoef; + void Init(float sampleRate) { + gainCoef = 1; + releaseCoef = 16.0 / sampleRate; + } + float Process(float in) { + float targetGainCoef = 1.0 / std::max(std::abs(in), (float)1.0); + if(targetGainCoef < gainCoef) { + gainCoef = targetGainCoef; + } else { + gainCoef = gainCoef * (1.0 - releaseCoef) + targetGainCoef*releaseCoef; + } + return in * gainCoef; + } + }; +} + + +#endif // LIMITER_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/loudness_detector.h b/TimeMachine/dsp/loudness_detector.h new file mode 100644 index 0000000..c9bbda9 --- /dev/null +++ b/TimeMachine/dsp/loudness_detector.h @@ -0,0 +1,19 @@ +#include "daisysp.h" +#include "slew.h" + +#ifndef LOUDNESS_DETECTOR_H_ +#define LOUDNESS_DETECTOR_H_ + +class LoudnessDetector { +public: + Slew slew; + float lastVal = 0; + void Init() { slew.Init(); } + float Get() { return this->lastVal; } + float Process(float x) { + lastVal = slew.Process(abs(x)); + return x; + } +}; + +#endif // LOUDNESS_DETECTOR_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/precise_slew.h b/TimeMachine/dsp/precise_slew.h new file mode 100644 index 0000000..c6321d4 --- /dev/null +++ b/TimeMachine/dsp/precise_slew.h @@ -0,0 +1,35 @@ +#include "daisysp.h" + +#ifndef PRECISE_SLEW_H_ +#define PRECISE_SLEW_H_ + +class PreciseSlew +{ + public: + PreciseSlew() {} + ~PreciseSlew() {} + void Init(float sample_rate, float htime) { + lastVal = 0; + prvhtim_ = -100.0; + htime_ = htime; + + sample_rate_ = sample_rate; + onedsr_ = 1.0 / sample_rate_; + } + float Process(float in) { + if(prvhtim_ != htime_) + { + c2_ = pow(0.5, onedsr_ / htime_); + c1_ = 1.0 - c2_; + prvhtim_ = htime_; + } + return lastVal = c1_ * in + c2_ * lastVal; + } + inline void SetHtime(float htime) { htime_ = htime; } + inline float GetHtime() { return htime_; } + float htime_; + float c1_, c2_, lastVal, prvhtim_; + float sample_rate_, onedsr_; +}; + +#endif // PRECISE_SLEW_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/read_head.h b/TimeMachine/dsp/read_head.h new file mode 100644 index 0000000..1278201 --- /dev/null +++ b/TimeMachine/dsp/read_head.h @@ -0,0 +1,59 @@ +#include "daisysp.h" +#include "daisy.h" +#include "util.h" +#include "loudness_detector.h" + +#ifndef READ_HEAD_H_ +#define READ_HEAD_H_ + +using namespace daisysp; +using namespace daisy; + +class ReadHead { +public: + LoudnessDetector loudness; + float* buffer; + int bufferSize; + float delayA = 0.0; + float delayB = 0.0; + float targetDelay = -1; + float ampA = 0.0; + float ampB = 0.0; + float targetAmp = -1; + float sampleRate; + float phase = 1.0; + float delta; + float blurAmount; + void Init(float sampleRate, float* buffer, int bufferSize) { + this->sampleRate = sampleRate; + this->delta = 5.0 / sampleRate; + this->buffer = buffer; + this->bufferSize = bufferSize; + this->blurAmount = 0.0; + } + void Set(float delay, float amp, float blur = 0) { + this->targetDelay = delay; + this->targetAmp = amp; + this->blurAmount = blur; + } + float Process(float writeHeadPosition) { + if(phase >= 1.0 && (targetDelay >= 0.0 || targetAmp > 0.0)) { + delayA = delayB; + delayB = targetDelay; + targetDelay = -1.0; + ampA = ampB; + ampB = targetAmp; + targetAmp = -1.0; + phase = 0.0; + delta = (5.0 + daisy::Random::GetFloat(-blurAmount, blurAmount)) / sampleRate; + } + float outputA = this->buffer[wrap_buffer_index(writeHeadPosition - seconds_to_samples(this->delayA, this->sampleRate), bufferSize)]; + float outputB = this->buffer[wrap_buffer_index(writeHeadPosition - seconds_to_samples(this->delayB, this->sampleRate), bufferSize)]; + float output = ((1 - phase) * outputA) + (phase * outputB); + float outputAmp = ((1 - phase) * ampA) + (phase * ampB); + phase = phase <= 1.0 ? phase + delta : 1.0; + return loudness.Process(output) * outputAmp; + } +}; + +#endif // READ_HEAD_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/slew.h b/TimeMachine/dsp/slew.h new file mode 100644 index 0000000..8a68777 --- /dev/null +++ b/TimeMachine/dsp/slew.h @@ -0,0 +1,46 @@ +#include "daisysp.h" + +#ifndef SLEW_H_ +#define SLEW_H_ + +class Slew { +public: + double lastVal = 0.0; + double coef = 0.001; + double noiseFloor = 0.0; + double noiseCoef = 0.0; + int settleSamples = 0; + int settleSamplesThreshold = 96; + + void Init(double coef = 0.001, double nf=0.0) { + this->coef = coef; + this->noiseFloor = nf; + } + float Process(float x) { + double c = coef; + double d = (x - lastVal); + // if we've set a noise floor + if(noiseFloor > 0.0) { + // if we're under the noise floor + if(abs(d) < noiseFloor) { + // if the input needs to settle + if(settleSamples < settleSamplesThreshold) { + // keep sampling + settleSamples++; + // if the input is done settling + } else { + // don't change the value + d = 0.0; + } + // if we're over the noise floor + } else { + // reset the settle wait + settleSamples = 0; + } + } + lastVal = lastVal + d * c; + return lastVal; + } +}; + +#endif // SLEW_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/stereo_time_machine.h b/TimeMachine/dsp/stereo_time_machine.h new file mode 100644 index 0000000..70fb306 --- /dev/null +++ b/TimeMachine/dsp/stereo_time_machine.h @@ -0,0 +1,33 @@ +#include "time_machine.h" +#include "daisysp.h" + +#ifndef STEREO_TIME_MACHINE_H_ +#define STEREO_TIME_MACHINE_H_ + +class StereoTimeMachine { + public: + TimeMachine timeMachineLeft; + TimeMachine timeMachineRight; + float outputs[2]; + void Init(float sampleRate, float maxDelay, float* bufferLeft, float* bufferRight) { + timeMachineLeft.Init(sampleRate, maxDelay, bufferLeft); + timeMachineRight.Init(sampleRate, maxDelay, bufferRight); + } + void Set(float dryAmp, float feedback, float blur=0.0) { + timeMachineLeft.Set(dryAmp, feedback, blur); + timeMachineRight.Set(dryAmp, feedback, blur); + } + float* Process(float inLeft, float inRight) { + outputs[0] = timeMachineLeft.Process(inLeft); + outputs[1] = timeMachineRight.Process(inRight); + return outputs; + } + + // 0 is DRY + // 1-8 is ReadHeads + float GetLoudness(int idx) { + float loudness = timeMachineLeft.GetLoudness(idx); + return std::max(loudness, timeMachineRight.GetLoudness(idx)); + } + }; +#endif // STEREO_TIME_MACHINE_H_ diff --git a/TimeMachine/dsp/time_machine.h b/TimeMachine/dsp/time_machine.h new file mode 100644 index 0000000..fed553f --- /dev/null +++ b/TimeMachine/dsp/time_machine.h @@ -0,0 +1,91 @@ +#include "read_head.h" +#include "loudness_detector.h" +#include "slew.h" +#include "ultra_slow_dc_blocker.h" +#include "limiter.h" + +#ifndef TIME_MACHINE_H_ +#define TIME_MACHINE_H_ + +class TimeMachine { +public: + ReadHead readHeads[8]; + LoudnessDetector loudness; + float sampleRate; + float* buffer; + int bufferSize; + int writeHeadPosition; + float dryAmp; + float feedback; + float blur; + Slew dryAmpSlew; + Slew feedbackSlew; + Slew ampCoefSlew; + Slew blurSlew; + oam::Limiter outputLimiter; + oam::Limiter feedbackLimiter; + UltraSlowDCBlocker dcblk; + daisysp::Compressor compressor; + float (*blurFunc)(float, float) = nullptr; + + void Init(float sampleRate, float maxDelay, float* buffer) { + this->sampleRate = sampleRate; + this->bufferSize = seconds_to_samples(maxDelay, sampleRate); + this->buffer = buffer; + for(int i=0; idryAmp = dryAmp; + this->feedback = feedback; + this->blur = blur; + } + void SetBlurFunc(float (*f)(float, float)) { + blurFunc = f; + } + float Process(float in) { + float out = 0; + + float ampCoef = 0.0; + for(int i=0; i<8; i++) ampCoef += readHeads[i].targetAmp; + ampCoef = ampCoefSlew.Process(1.0 / std::max(1.0f, ampCoef)); + + buffer[writeHeadPosition] = loudness.Process(in); + + for(int i=0; i<8; i++) out += readHeads[i].Process(writeHeadPosition); + out = dcblk.Process(out); + out = compressor.Process(out, buffer[writeHeadPosition] + out); + + buffer[writeHeadPosition] = -(feedbackLimiter.Process(buffer[writeHeadPosition] + (out * feedbackSlew.Process(feedback) * ampCoef))); + out = outputLimiter.Process(out + in * dryAmpSlew.Process(dryAmp)); + writeHeadPosition = wrap_buffer_index(writeHeadPosition + 1, bufferSize); + + return out; + } + + // 0 is DRY + // 1-8 is ReadHeads + float GetLoudness(int idx) { + if (idx == 0) { + return loudness.Get(); + } else { + return readHeads[idx - 1].loudness.Get(); + } + } +}; + +#endif // TIME_MACHINE_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/ultra_slow_dc_blocker.h b/TimeMachine/dsp/ultra_slow_dc_blocker.h new file mode 100644 index 0000000..c0abaa3 --- /dev/null +++ b/TimeMachine/dsp/ultra_slow_dc_blocker.h @@ -0,0 +1,18 @@ +#include "daisysp.h" +#include "slew.h" + +#ifndef ULTRA_SLOW_DC_BLOCKER_H_ +#define ULTRA_SLOW_DC_BLOCKER_H_ + +class UltraSlowDCBlocker { +public: + Slew slew; + void Init(float coef = 0.00001) { + slew.Init(coef); + } + float Process(float x) { + return x - slew.Process(x); + } +}; + +#endif // ULTRA_SLOW_DC_BLOCKER_H_ \ No newline at end of file diff --git a/TimeMachine/dsp/util.h b/TimeMachine/dsp/util.h new file mode 100644 index 0000000..b46f2e1 --- /dev/null +++ b/TimeMachine/dsp/util.h @@ -0,0 +1,87 @@ +#include "daisysp.h" + +#ifndef UTIL_H_ +#define UTIL_H_ + +int wrap_buffer_index(int x, int size) { + while(x >= size) x -= size; + while(x < 0) x += size; + return x; +}; + +int seconds_to_samples(float x, float sampleRate) { + return (int)(x * sampleRate); +} + +float mix(float x, float a, float b) { + return x*(1-x) + b*x; +} + +float clamp(float x, float a, float b) { + return std::max(a,std::min(b,x)); +} + +float fourPointWarp(float x, + float ai=0.0, + float av=0.0, + float bi=0.45, + float bv=0.5, + float ci=0.55, + float cv=0.5, + float di=1.0, + float dv=1.0) { + if(x < ai) { + return av; + } else if(x < bi) { + x = (x - ai) / (bi - ai); + return av * (1.0 - x) + bv * x; + } else if(x < ci) { + x = (x - bi) / (ci - bi); + return bv * (1.0 - x) + cv * x; + } else if(x < di) { + x = (x - ci) / (di - ci); + return cv * (1.0 - x) + dv * x; + } else { + return dv; + } +} + +float minMaxKnob(float in, float dz=0.002) { + in = in - dz * 0.5; + in = in * (1.0 + dz); + return std::min(1.0f, std::max(0.0f, in)); +} + +float minMaxSlider(float in, float dz=0.002) { + return minMaxKnob(in, dz); +} + +float softClip(float x, float kneeStart=0.9, float kneeCurve=5.0) { + float linPart = clamp(x, -kneeStart, kneeStart); + float clipPart = x - linPart; + clipPart = atan(clipPart * kneeCurve) / kneeCurve; + return linPart + clipPart; +} + +float spread(float x, float s, float e=2.5) { + s = clamp(s, 0.0, 1.0); + if(s > 0.5) { + s = (s-0.5)*2.0; + s = s*e+1.0; + return 1.0 - pow(1.0-x, s); + } else if(s < 0.5) { + s = 1.0-(s*2.0); + s = s*e+1.0; + return pow(x, s); + } else { + return x; + } +} + +float defaultBlurFunc(float x, float mix) { + return x; +} + + +#endif //UTIL_H_ + diff --git a/TimeMachine/time_machine_hardware.cpp b/TimeMachine/time_machine_hardware.cpp index 047e5d0..c96162a 100644 --- a/TimeMachine/time_machine_hardware.cpp +++ b/TimeMachine/time_machine_hardware.cpp @@ -503,6 +503,14 @@ namespace time_machine } } + void TimeMachineHardware::WriteNormalization(bool value) { + if (value) { + pimpl_->WriteCvOut(CV_OUT_1, 5.0); + } else { + pimpl_->WriteCvOut(CV_OUT_1, 0.0); + } + } + dsy_gpio_pin TimeMachineHardware::GetPin(const PinBank bank, const int idx) { if(idx <= 0 || idx > 10) diff --git a/TimeMachine/time_machine_hardware.h b/TimeMachine/time_machine_hardware.h index 7c66e82..fae87a0 100644 --- a/TimeMachine/time_machine_hardware.h +++ b/TimeMachine/time_machine_hardware.h @@ -12,6 +12,11 @@ using namespace patch_sm; #define TIME_CV CV_2 #define FEEDBACK_CV CV_3 +#define VCA_1_CV CV_4 +#define VCA_2_CV CV_5 +#define VCA_3_CV CV_6 +#define VCA_4_CV CV_7 + #define TIME_KNOB ADC_10 #define FEEDBACK_KNOB CV_8 #define SKEW_KNOB ADC_9 @@ -157,6 +162,8 @@ namespace time_machine float GetSliderValue(int idx); + void WriteNormalization(bool value); + /** Returns the STM32 port/pin combo for the desired pin (or an invalid pin for HW only pins) * * Macros at top of file can be used in place of separate arguments (i.e. GetPin(A4), etc.) diff --git a/TimeMachine/ui/calibration_data.h b/TimeMachine/ui/calibration_data.h new file mode 100644 index 0000000..63546ea --- /dev/null +++ b/TimeMachine/ui/calibration_data.h @@ -0,0 +1,33 @@ +#ifndef CALIBRATION_DATA_H_ +#define CALIBRATION_DATA_H_ + +//Setting Struct containing parameters we want to save to flash +struct CalibrationData { + float timeCvOffset = 0.0; + float skewCvOffset = 0.0; + float feedbackCvOffset = 0.0; + + float vca1CvOffset = 0.0; + float vca2CvOffset = 0.0; + float vca3CvOffset = 0.0; + float vca4CvOffset = 0.0; + + int calibrated = false; + + //Overloading the != operator + //This is necessary as this operator is used in the PersistentStorage source code + bool operator!=(const CalibrationData& a) const { + return !( + a.timeCvOffset==timeCvOffset && \ + a.skewCvOffset==skewCvOffset && \ + a.feedbackCvOffset==feedbackCvOffset && \ + a.vca1CvOffset==vca1CvOffset && \ + a.vca2CvOffset==vca2CvOffset && \ + a.vca3CvOffset==vca3CvOffset && \ + a.vca4CvOffset==vca4CvOffset && \ + a.calibrated==calibrated + ); + } +}; + +#endif // CALIBRATION_DATA_H_ \ No newline at end of file diff --git a/TimeMachine/ui/calibrator.h b/TimeMachine/ui/calibrator.h new file mode 100644 index 0000000..ac8807f --- /dev/null +++ b/TimeMachine/ui/calibrator.h @@ -0,0 +1,118 @@ +#include "../time_machine_hardware.h" +#include "calibration_data.h" + +#include "daisy.h" + +#include "leds.h" +#include "../dsp/util.h" + +#ifndef CALIBRATOR_H_ +#define CALIBRATOR_H_ + +class Calibrator { + public: + void Init(PersistentStorage cs) { + calibration_storage_ = &cs; + + // load calibration data, using sensible defaults + calibration_storage_->Init({0.0f, 0.0f, 0.0f, false}); + saved_calibration_data_ = &calibration_storage_->GetSettings(); + } + + void Calibrate( + oam::time_machine::TimeMachineHardware hw, + oam::time_machine::Leds leds) { + if(ShouldCalibrate(hw, leds)) { + + // perform calibration routine + int numSamples = 128; + for(int i = 0; i < numSamples; i++) { + + // // accumulate cv values TODO: properly inject new values + // saved_calibration_data_->timeCvOffset += timeCv; + // saved_calibration_data_->skewCvOffset += skewCv; + // saved_calibration_data_->feedbackCvOffset += feedbackCv; + + // saved_calibration_data_->vca1CvOffset += vca1Cv; + // saved_calibration_data_->vca2CvOffset += vca2Cv; + // saved_calibration_data_->vca3CvOffset += vca3Cv; + // saved_calibration_data_->vca4CvOffset += vca4Cv; + + // wait 10ms + System::Delay(10); + + // set LEDs + for(int ledIndex=0; ledIndex < 9; ledIndex++) { + leds.Set(ledIndex, i % 8 < 4 ? 1.0f : 0.0f); + } + } + + // divide CVs by number of samples taken to get average + saved_calibration_data_->timeCvOffset = saved_calibration_data_->timeCvOffset / ((float)numSamples); + saved_calibration_data_->skewCvOffset = saved_calibration_data_->skewCvOffset / ((float)numSamples); + saved_calibration_data_->feedbackCvOffset = saved_calibration_data_->feedbackCvOffset / ((float)numSamples); + + saved_calibration_data_->vca1CvOffset = saved_calibration_data_->vca1CvOffset / ((float)numSamples); + saved_calibration_data_->vca2CvOffset = saved_calibration_data_->vca2CvOffset / ((float)numSamples); + saved_calibration_data_->vca3CvOffset = saved_calibration_data_->vca3CvOffset / ((float)numSamples); + saved_calibration_data_->vca4CvOffset = saved_calibration_data_->vca4CvOffset / ((float)numSamples); + + // set calibrated value to true + saved_calibration_data_->calibrated = true; + + // save calibration data + calibration_storage_->Save(); + } + } + + private: + PersistentStorage* calibration_storage_; + + CalibrationData* saved_calibration_data_; + + bool ShouldCalibrate( + oam::time_machine::TimeMachineHardware hw, + oam::time_machine::Leds leds) { + + if (!CheckIfInCalibrationPositions(hw)) return false; + + bool calibrationReady = true; + // do reverse LED startup sequence while + // checking that we definitely want to calibrate + int delayBetweenSequencesMs = 100; + for(int i=0; i < (5000/delayBetweenSequencesMs); i++) { + + leds.InitCalibrationSequence(i); + + System::Delay(delayBetweenSequencesMs); + + calibrationReady &= CheckIfInCalibrationPositions(hw); + if(!calibrationReady) break; + } + + return calibrationReady; + } + + bool CheckIfInCalibrationPositions(oam::time_machine::TimeMachineHardware hw) { + bool shouldCalibrate = \ + (hw.GetAdcValue(SKEW_CV) < 0.01) && \ + (hw.GetAdcValue(TIME_CV) < 0.01) && \ + (hw.GetAdcValue(FEEDBACK_CV) < 0.01) && \ + (hw.GetAdcValue(VCA_1_CV) < 0.01) && \ + (hw.GetAdcValue(VCA_2_CV) < 0.01) && \ + (hw.GetAdcValue(VCA_3_CV) < 0.01) && \ + (hw.GetAdcValue(VCA_4_CV) < 0.01) && \ + hw.gate_in_2.State(); + + for(int i=0; i<9; i++) { + shouldCalibrate &= hw.GetSliderValue(i) < 0.01; + } + + shouldCalibrate &= minMaxKnob(1.0 - hw.GetAdcValue(TIME_KNOB)) > 0.95; + shouldCalibrate &= minMaxKnob(1.0 - hw.GetAdcValue(SKEW_KNOB)) > 0.95; + shouldCalibrate &= minMaxKnob(1.0 - hw.GetAdcValue(FEEDBACK_KNOB)) > 0.95; + return shouldCalibrate; + } +}; + +#endif // CALIBRATOR_H_ \ No newline at end of file diff --git a/TimeMachine/ui/leds.h b/TimeMachine/ui/leds.h new file mode 100644 index 0000000..00ff86e --- /dev/null +++ b/TimeMachine/ui/leds.h @@ -0,0 +1,82 @@ +#include "daisy.h" + +#ifndef LEDS_H_ +#define LEDS_H_ + +namespace oam { + namespace time_machine { + + class Leds { + public: + + void Set(int idx, float intensity) { + if (initialized_ && ui_started_) { + leds_[idx].Set(intensity); + leds_[idx].Update(); + } + } + + void Init( + daisy::Pin dryPin, + daisy::Pin pin1, + daisy::Pin pin2, + daisy::Pin pin3, + daisy::Pin pin4, + daisy::Pin pin5, + daisy::Pin pin6, + daisy::Pin pin7, + daisy::Pin pin8) { + + leds_[0].Init(dryPin, false); + leds_[1].Init(pin1, false); + leds_[2].Init(pin2, false); + leds_[3].Init(pin3, false); + leds_[4].Init(pin4, false); + leds_[5].Init(pin5, false); + leds_[6].Init(pin6, false); + leds_[7].Init(pin7, false); + leds_[8].Init(pin8, false); + + initialized_ = true; + + } + + + void InitStartupSequence() { + if(!initialized_) return; + + // LED startup sequence + for(int i=0; i<9; i++) { + for(int j=0; j<9; j++) { + leds_[j].Set(j == i ? 1.0 : 0.0); + leds_[j].Update(); + } + System::Delay(ledSeqDelayMs_); + }; + }; + + void InitCalibrationSequence(int idx) { + if (!initialized_) return; + + for(int j=0; j<9; j++) { + leds_[j].Set(j == (8 - (idx % 9)) ? 1.0 : 0.0); + leds_[j].Update(); + } + } + + void StartUi() { + ui_started_ = true; + } + private: + bool initialized_ = false; + bool ui_started_ = false; + int ledSeqDelayMs_ = 100; + + // LED at idx 0 is DRY + // read heads are at 1-8 + daisy::Led leds_[9]; + }; + } +} + +#endif // LEDS_H_ diff --git a/TimeMachine/ui/ui.h b/TimeMachine/ui/ui.h new file mode 100644 index 0000000..ade5339 --- /dev/null +++ b/TimeMachine/ui/ui.h @@ -0,0 +1,151 @@ +#include "daisy.h" +#include "../time_machine_hardware.h" +#include "../dsp/util.h" +#include "../dsp/slew.h" +#include "../dsp/continuous_schmidt.h" +#include "../dsp/clock_rate_detector.h" + + +#ifndef UI_H_ +#define UI_H_ + +class Ui { + public: + + ClockRateDetector clockRateDetector; + ContSchmidt timeKnobSchmidt; + ContSchmidt timeCvSchmidt; + + Slew timeKnobSlew; + Slew feedbackKnobSlew; + Slew distributionKnobSlew; + Slew timeCvSlew; + Slew feedbackCvSlew; + Slew distributionCvSlew; + + Slew vca1CvSlew; + Slew vca2CvSlew; + Slew vca3CvSlew; + Slew vca4CvSlew; + + // global storage for CV/knobs so we don't get them twice to print diagnostics + float timeCv = 0.0; + float feedbackCv = 0.0; + float skewCv = 0.0; + + float vca1Cv = 0.0; + float vca2Cv = 0.0; + float vca3Cv = 0.0; + float vca4Cv = 0.0; + + float timeKnob = 0.0; + float feedbackKnob = 0.0; + float skewKnob = 0.0; + float drySlider = 0.0; + float delaySliders = 0.0; + + //tmp offset values for compilation + float timeCvOffset = 0.0; + float feedbackCvOffset = 0.0; + float skewCvOffset = 0.0; + + float sliderAmpValues_[8] = { 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0 }; + + float distribution = 0.0; + float feedback = 0.0; + float time = 0.0; + + void Init(oam::time_machine::TimeMachineHardware hw) { + hw_ = &hw; + + // init slew limiter for time (we should tune this more delibrately) + timeKnobSlew.Init(0.5, 0.0005); + feedbackKnobSlew.Init(0.5, 0.0005); + distributionKnobSlew.Init(0.5, 0.0005); + + hw_->PrintLine("UI_KNOB_SLEW_INITIALIZED"); + + timeCvSlew.Init(0.5, 0.0005); + feedbackCvSlew.Init(0.5, 0.0005); + distributionCvSlew.Init(0.5, 0.0005); + + hw_->PrintLine("UI_CV_SLEW_INITIALIZED"); + + vca1CvSlew.Init(0.5, 0.0005); + vca2CvSlew.Init(0.5, 0.0005); + vca3CvSlew.Init(0.5, 0.0005); + vca4CvSlew.Init(0.5, 0.0005); + + hw_->PrintLine("UI_VCA_CV_SLEW_INITIALIZED"); + + clockRateDetector.Init(hw_->AudioSampleRate()); + + hw_->PrintLine("UI_CLOCK_RATE_DETECTOR_INITIALIZED"); + + ProcessAllControls(); + + hw_->PrintLine("UI_INIT_COMPLETE"); + } + + void ProcessClockRate(bool triggered) { + clockRateDetector.Process(triggered); + } + + void ProcessAllControls() { + // process controls + hw_->ProcessAllControls(); + + // populate/update global CV/knob vars (time is slewed to reduce noise at large time values) + timeKnob = minMaxKnob(1.0 - hw_->GetAdcValue(TIME_KNOB), 0.0008); + feedbackKnob = fourPointWarp(1.0 - minMaxKnob(hw_->GetAdcValue(FEEDBACK_KNOB), 0.028)); + skewKnob = fourPointWarp(1.0 - minMaxKnob(hw_->GetAdcValue(SKEW_KNOB), 0.0008)); + + timeCv = clamp(hw_->GetAdcValue(TIME_CV) - timeCvOffset, -1, 1); + feedbackCv = clamp(hw_->GetAdcValue(FEEDBACK_CV) - feedbackCvOffset, -1, 1); + skewCv = clamp(hw_->GetAdcValue(SKEW_CV) - skewCvOffset, -1, 1); + + // // read modulation / normalized channels + // for (int i = 0; i < kNumNormalizedChannels; i++) { + // modulation_values_[i] = + // clamp( + // hw_->GetAdcValue(normalized_channels_[i]) - normalized_offsets_[i], + // -1, + // 1); + // } + + // read slider values + drySlider = minMaxSlider(1.0 - hw_->GetAdcValue(DRY_SLIDER)); + for (int i = 1; i < 9; i++) { + sliderAmpValues_[i-1] = std::max(0.0f, minMaxSlider(1.0f - hw_->GetSliderValue(i))); + } + + // calculate time based on clock if present, otherwise simple time + float tmp_time = 0.0; + if(clockRateDetector.GetInterval() > 0.0) { + // 12 quantized steps for knob, 10 for CV (idk what these quanta should actually be) + // time doubles and halves with each step, they are additive/subtractive + float timeCoef = pow(2.0, (timeKnobSchmidt.Process((1.0-timeKnob)*12)) + (timeCvSchmidt.Process(timeCv*10))) / pow(2.0, 6.0); + tmp_time = clockRateDetector.GetInterval() / timeCoef; + // make sure time is a power of two less than the max time available in the buffer + while(tmp_time > 150) tmp_time *= 0.5; + } else { + // time linear with knob, scaled v/oct style with CV + tmp_time = pow(timeKnobSlew.Process(timeKnob), 2.0) * 8.0 / pow(2.0, timeCvSlew.Process(timeCv) * 5.0); + } + + // force time down to a max value (taking whichever is lesser, the max or the time) + time = std::min((float)150, tmp_time); + + // condition distribution knob value to have deadzone in the middle, add CV + distribution = fourPointWarp(distributionKnobSlew.Process(skewKnob)) + distributionCvSlew.Process(skewCv); + + // condition feedback knob to have deadzone in the middle, add CV + feedback = clamp(fourPointWarp(feedbackKnobSlew.Process(feedbackKnob)) * 2.0 + feedbackCvSlew.Process(feedbackCv), 0, 3); + } + + private: + oam::time_machine::TimeMachineHardware* hw_; +}; + + +#endif // UI_H_ \ No newline at end of file