From f7602ed50cd4ad569bdd82b6bc87cc5069a90553 Mon Sep 17 00:00:00 2001 From: Mitchell Hudson Date: Sun, 16 Mar 2025 16:06:59 -0700 Subject: [PATCH 1/7] created bytebeats --- patch/ByteShift/Makefile | 62 ++++++++++ patch/ByteShift/README.md | 47 +++++++ patch/ByteShift/bytebeat.cpp | 18 +++ patch/ByteShift/bytebeat_synth.cpp | 133 ++++++++++++++++++++ patch/ByteShift/bytebeat_synth.h | 43 +++++++ patch/ByteShift/byteshift.cpp | 22 ++++ patch/ByteShift/byteshift.h | 19 +++ patch/ByteShift/control_manager.cpp | 21 ++++ patch/ByteShift/control_manager.h | 26 ++++ patch/ByteShift/main.cpp | 55 +++++++++ patch/bytebeat/Makefile | 25 ++++ patch/bytebeat/README.md | 47 +++++++ patch/bytebeat/bytebeat.cpp | 18 +++ patch/bytebeat/bytebeat_synth.cpp | 183 ++++++++++++++++++++++++++++ patch/bytebeat/bytebeat_synth.h | 38 ++++++ 15 files changed, 757 insertions(+) create mode 100644 patch/ByteShift/Makefile create mode 100644 patch/ByteShift/README.md create mode 100644 patch/ByteShift/bytebeat.cpp create mode 100644 patch/ByteShift/bytebeat_synth.cpp create mode 100644 patch/ByteShift/bytebeat_synth.h create mode 100644 patch/ByteShift/byteshift.cpp create mode 100644 patch/ByteShift/byteshift.h create mode 100644 patch/ByteShift/control_manager.cpp create mode 100644 patch/ByteShift/control_manager.h create mode 100644 patch/ByteShift/main.cpp create mode 100644 patch/bytebeat/Makefile create mode 100644 patch/bytebeat/README.md create mode 100644 patch/bytebeat/bytebeat.cpp create mode 100644 patch/bytebeat/bytebeat_synth.cpp create mode 100644 patch/bytebeat/bytebeat_synth.h diff --git a/patch/ByteShift/Makefile b/patch/ByteShift/Makefile new file mode 100644 index 000000000..e43eaaa16 --- /dev/null +++ b/patch/ByteShift/Makefile @@ -0,0 +1,62 @@ +# Project Name +TARGET = byteshift + +# Library Locations +LIBDAISY_DIR = ../../libDaisy +DAISYSP_DIR = ../../DaisySP + +# Core location and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# Source files +CPP_SOURCES = main.cpp byteshift.cpp + +# Include the Daisy system Makefile +include $(SYSTEM_FILES_DIR)/Makefile + +# Define the compiled program binary +PROGRAM = byteshift.bin +FLASH_ADDR = 0x08000000 + +# Convert ELF to BIN +build/$(PROGRAM): build/$(TARGET).elf + arm-none-eabi-objcopy -O binary build/$(TARGET).elf build/$(PROGRAM) + +# Flash to Daisy Patch using ST-LINK V3 Mini +program: build/$(PROGRAM) + st-flash --reset write build/$(PROGRAM) $(FLASH_ADDR) + + + + + + +# # Project Name +# TARGET = byteshift + +# # Use LGPL version of DaisySP (optional, set to 1 if needed) +# USE_DAISYSP_LGPL=1 + +# # Library Locations +# LIBDAISY_DIR = ../../libDaisy +# DAISYSP_DIR = ../../DaisySP + +# # Core location and generic Makefile. +# SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# # Source files +# CPP_SOURCES = main.cpp byteshift.cpp bytebeat_synth.cpp + +# # Include the Daisy system Makefile +# include $(SYSTEM_FILES_DIR)/Makefile + +# # Ensure .bin file is created from .elf +# build/byteshift.bin: build/byteshift.elf +# arm-none-eabi-objcopy -O binary build/byteshift.elf build/byteshift.bin + +# # Flashing using STLINK-V3MINI +# PROGRAM = byteshift.bin # Ensure we're flashing the .bin, not .elf +# FLASH_ADDR = 0x08000000 + +# program: build/byteshift.bin # Make sure .bin exists before flashing +# st-flash --reset write build/byteshift.bin $(FLASH_ADDR) \ No newline at end of file diff --git a/patch/ByteShift/README.md b/patch/ByteShift/README.md new file mode 100644 index 000000000..7102f54f6 --- /dev/null +++ b/patch/ByteShift/README.md @@ -0,0 +1,47 @@ +# 🎡 BytebeatSynth + +BytebeatSynth is an experimental music synthesizer that generates sound using mathematical formulas known as bytebeat equations. It runs on the Daisy Patch platform, using control voltage (CV) and knob-based inputs to modify parameters in real time. + +## πŸ”₯ Features +- Bytebeat Audio Synthesis: Uses a selection of classic bytebeat formulas. +- Dynamic Formula Parameters: Modify parameters a, b, and c in real-time. +- Adjusts how fast the bytebeat algorithm runs, affecting pitch and timbre, with higher values creating higher tones and lower values slowing it down, sometimes to silence. +- Built-in Formula Switching: Cycle through multiple equations with the encoder. +- Analog & CV Input Controls: Adjust speed, parameters, and pitch using hardware knobs or external control voltages. + +## πŸŽ›οΈ Controls + +### Control Function +CTRL 1 (Speed/Pitch) Controls the pitch/speed of the bytebeat equation. Accepts 0-5V CV input. +CTRL 2 (Parameter A) Adjusts the first variable (a) in the bytebeat formula. +CTRL 3 (Parameter B) Adjusts the second variable (b) in the bytebeat formula. +CTRL 4 (Parameter C) Adjusts the third variable (c) in the bytebeat formula. +Encoder (Press) Cycles through different bytebeat formulas. +OLED Display Shows the current formula, pitch, and values of a, b, and c. + +## ⚑ How to Use +1. Turn CTRL 1 to change the pitch and speed of the bytebeat synthesis. +2. Adjust CTRL 2, 3, and 4 to manipulate different formula parameters. +3. Press the encoder to switch to a different bytebeat equation. +4. Use CV input on CTRL 1 to modulate pitch from an external sequencer or LFO. +5. Observe the OLED display to see current formula and parameter values. + +## πŸ› οΈ Installation & Compilation + +To build and flash BytebeatSynth onto your Daisy Patch: +1. Clone or copy the project files. +2. Run `make` to compile the firmware. +3. Flash the binary using: `st-flash --reset write build/bytebeat.bin 0x08000000` + +## 🎚️ Example Patching Ideas +- Sequenced Rhythms: Use an external CV sequencer on CTRL 1 for evolving patterns. +- Glitchy Drones: Set CTRL 1 low and tweak CTRL 2, 3, 4 for texture shifts. +- Noise Percussion: Use a gate signal on CTRL 1 to trigger bursts of sound. + +## 🎡 What is a Bytebeat? + +A bytebeat is a type of generative music where sound is created using simple mathematical formulas instead of traditional synthesis or sampling. These formulas operate on a time variable (t) to generate an audio waveform in real time, often resulting in glitchy, chiptune-like, and chaotic sounds. + +Bytebeats emerged in 2011 when Finnish developer viznut (aka Ville-Matias HeikkilΓ€) discovered that short arithmetic expressions could create rich and evolving sound patterns. This led to an explosion of interest in algorithmic music, with musicians and programmers experimenting with different formulas to produce everything from rhythmic patterns to bizarre noise textures. + +Since bytebeat synthesis is entirely formula-driven, modifying the equations can create new sonic landscapes, making it an ideal playground for experimentation in digital sound generation. diff --git a/patch/ByteShift/bytebeat.cpp b/patch/ByteShift/bytebeat.cpp new file mode 100644 index 000000000..76976f5fb --- /dev/null +++ b/patch/ByteShift/bytebeat.cpp @@ -0,0 +1,18 @@ +#include "bytebeat_synth.h" + +int main(void) { + // Create an instance of DaisyPatch assigned to local var patch + DaisyPatch patch; + // Creat an instance of BytebeatSynth assigned to local var synth + BytebeatSynth synth; + // Initialize synth pass patch. Is this a reference what is & ? + synth.Init(&patch); + + // Create a main program loop + while (1) { + // Update display + synth.UpdateDisplay(); + // Chill for 50ms + System::Delay(50); + } +} \ No newline at end of file diff --git a/patch/ByteShift/bytebeat_synth.cpp b/patch/ByteShift/bytebeat_synth.cpp new file mode 100644 index 000000000..2deee1e9c --- /dev/null +++ b/patch/ByteShift/bytebeat_synth.cpp @@ -0,0 +1,133 @@ +#include "bytebeat_synth.h" + +// Global instance pointer for static callback +static BytebeatSynth* synthInstance = nullptr; + +// Bytebeat Function Table +BytebeatSynth::BytebeatFunc BytebeatSynth::formulaTable[] = { + &BytebeatSynth::BytebeatFormula0, + &BytebeatSynth::BytebeatFormula1, + &BytebeatSynth::BytebeatFormula2, + &BytebeatSynth::BytebeatFormula3, + &BytebeatSynth::BytebeatFormula4, + &BytebeatSynth::BytebeatFormula5 +}; + +// Bytebeat Equation Definitions +uint8_t BytebeatSynth::BytebeatFormula0(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; + int b = synth->b; + return (t * (t >> a) & 42) | (t * (t >> b) & 84); +} + +uint8_t BytebeatSynth::BytebeatFormula1(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; + int b = synth->b; + int c = synth->c; + return (t >> a) | (t << b) | (t * (t >> c)); +} + +uint8_t BytebeatSynth::BytebeatFormula2(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + return (t * (t >> a) | (t * (t >> b) & 50)); +} + +uint8_t BytebeatSynth::BytebeatFormula3(uint32_t t, BytebeatSynth* synth) { + int b = synth->b; + int c = synth->c; + return ((t * 3) & (t >> b)) | (t * (t >> c)); +} + +uint8_t BytebeatSynth::BytebeatFormula4(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + return (t * 5 & (t >> a)) | (t * (t >> b) & 123); +} + +uint8_t BytebeatSynth::BytebeatFormula5(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + return (t >> a) ^ (t * (t >> b) & 32); +} + +// Initialize Synth +void BytebeatSynth::Init(DaisyPatch* p) { + patch = p; + t = 0; + formula_index = 0; + synthInstance = this; + + // Initialize Parameters + a = 4; + b = 6; + c = 8; + + patch->Init(); + patch->StartAdc(); +} + +// Update Controls (Knobs & Encoder) +void BytebeatSynth::UpdateControls() { + patch->ProcessAnalogControls(); + patch->ProcessDigitalControls(); + + float pitchCV = patch->GetKnobValue(DaisyPatch::CTRL_1) * 5.0f; + float frequency = 16.0f * powf(2.0f, pitchCV - 1.0f); + tScale = 48000.0f / frequency; + + a = (int)(patch->GetKnobValue(DaisyPatch::CTRL_2) * 16) + 1; + b = (int)(patch->GetKnobValue(DaisyPatch::CTRL_3) * 32) + 1; + c = (int)(patch->GetKnobValue(DaisyPatch::CTRL_4) * 64) + 1; + + if (patch->encoder.RisingEdge()) { + formula_index = (formula_index + 1) % formula_count; + } +} + +// Generate a single sample for external processing +float BytebeatSynth::GenerateSample() { + BytebeatFunc currentFormula = formulaTable[formula_index]; + + static float tAccumulator = 0; + tAccumulator += 1.0f; + if (tAccumulator >= tScale) { + t += (uint32_t)(tScale); + tAccumulator -= tScale; + } + + uint8_t sample = currentFormula(t, this); + return ((float)sample / 255.0f) * 2.0f - 1.0f; +} + +void BytebeatSynth::NextFormula() { + formula_index = (formula_index + 1) % formula_count; +} + +// OLED Display Update +void BytebeatSynth::UpdateDisplay() { + static int last_formula = -1; + static int last_a = -1, last_b = -1, last_c = -1; + + if (formula_index != last_formula || a != last_a || b != last_b || c != last_c) { + patch->display.Fill(false); + patch->display.SetCursor(0, 0); + patch->display.WriteString("Bytebeat Synth", Font_7x10, true); + + char buffer[32]; + snprintf(buffer, sizeof(buffer), "Formula: %d", formula_index); + patch->display.SetCursor(0, 12); + patch->display.WriteString(buffer, Font_7x10, true); + + snprintf(buffer, sizeof(buffer), "a: %d b: %d c: %d", a, b, c); + patch->display.SetCursor(0, 24); + patch->display.WriteString(buffer, Font_7x10, true); + + patch->display.Update(); + + last_formula = formula_index; + last_a = a; + last_b = b; + last_c = c; + } +} \ No newline at end of file diff --git a/patch/ByteShift/bytebeat_synth.h b/patch/ByteShift/bytebeat_synth.h new file mode 100644 index 000000000..5429bb62e --- /dev/null +++ b/patch/ByteShift/bytebeat_synth.h @@ -0,0 +1,43 @@ +#ifndef BYTEBEAT_SYNTH_H +#define BYTEBEAT_SYNTH_H + +#include "daisy_patch.h" +#include "daisysp.h" + +using namespace daisy; +using namespace daisysp; + +class BytebeatSynth { +public: + void Init(DaisyPatch* p); + void UpdateControls(); + float GenerateSample(); // Now returns a single sample for processing + void UpdateDisplay(); + float ProcessSample(); + void NextFormula(); + int GetFormulaIndex() const { return formula_index; } + +private: + DaisyPatch* patch; + uint32_t t; + int formula_index; + static const int formula_count = 6; + + // Dynamic formula parameters + int a, b, c; + + // Scaling factor for musically tuned timing + float tScale; + + using BytebeatFunc = uint8_t (*)(uint32_t, BytebeatSynth*); + static BytebeatFunc formulaTable[]; + + static uint8_t BytebeatFormula0(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula1(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula2(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula3(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula4(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula5(uint32_t t, BytebeatSynth* synth); +}; + +#endif // BYTEBEAT_SYNTH_H \ No newline at end of file diff --git a/patch/ByteShift/byteshift.cpp b/patch/ByteShift/byteshift.cpp new file mode 100644 index 000000000..9f9da9d2c --- /dev/null +++ b/patch/ByteShift/byteshift.cpp @@ -0,0 +1,22 @@ +#include "byteshift.h" + +void ByteShift::Init(daisy::DaisyPatch* p) { + patch = p; + speed = paramA = paramB = paramC = 0.0f; + encoderValue = 0; +} + +void ByteShift::SetControlValues(float c1, float c2, float c3, float c4, int enc) { + speed = c1; + paramA = c2; + paramB = c3; + paramC = c4; + encoderValue = enc; +} + +void ByteShift::ProcessAudio(float** out, size_t size) { + for (size_t i = 0; i < size; i++) { + out[0][i] = speed * 0.01f; + out[1][i] = paramA * 0.01f; + } +} \ No newline at end of file diff --git a/patch/ByteShift/byteshift.h b/patch/ByteShift/byteshift.h new file mode 100644 index 000000000..cb1a34473 --- /dev/null +++ b/patch/ByteShift/byteshift.h @@ -0,0 +1,19 @@ +#ifndef BYTESHIFT_H +#define BYTESHIFT_H + +#include "daisy_patch.h" +#include "control_manager.h" + +class ByteShift { +public: + void Init(daisy::DaisyPatch* p); + void ProcessAudio(float** out, size_t size); + void SetControlValues(float c1, float c2, float c3, float c4, int enc); + +private: + daisy::DaisyPatch* patch; + float speed, paramA, paramB, paramC; + int encoderValue; +}; + +#endif \ No newline at end of file diff --git a/patch/ByteShift/control_manager.cpp b/patch/ByteShift/control_manager.cpp new file mode 100644 index 000000000..3771244e9 --- /dev/null +++ b/patch/ByteShift/control_manager.cpp @@ -0,0 +1,21 @@ +#include "control_manager.h" + +void ControlManager::Init(daisy::DaisyPatch* p) { + patch = p; + ctrl1 = ctrl2 = ctrl3 = ctrl4 = 0.0f; + encoderCount = 0; + encoderPressed = false; +} + +void ControlManager::Update() { + patch->ProcessAnalogControls(); + patch->ProcessDigitalControls(); + + ctrl1 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_1); + ctrl2 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_2); + ctrl3 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_3); + ctrl4 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_4); + + encoderCount += patch->encoder.Increment(); + encoderPressed = patch->encoder.RisingEdge(); +} \ No newline at end of file diff --git a/patch/ByteShift/control_manager.h b/patch/ByteShift/control_manager.h new file mode 100644 index 000000000..aeeb60b95 --- /dev/null +++ b/patch/ByteShift/control_manager.h @@ -0,0 +1,26 @@ +#ifndef CONTROL_MANAGER_H +#define CONTROL_MANAGER_H + +#include "daisy_patch.h" + +class ControlManager { +public: + void Init(daisy::DaisyPatch* p); + void Update(); + + float GetCtrl1() const { return ctrl1; } + float GetCtrl2() const { return ctrl2; } + float GetCtrl3() const { return ctrl3; } + float GetCtrl4() const { return ctrl4; } + int GetEncoderCount() const { return encoderCount; } + bool IsEncoderPressed() const { return encoderPressed; } + +private: + daisy::DaisyPatch* patch; + + float ctrl1, ctrl2, ctrl3, ctrl4; + int encoderCount; + bool encoderPressed; +}; + +#endif \ No newline at end of file diff --git a/patch/ByteShift/main.cpp b/patch/ByteShift/main.cpp new file mode 100644 index 000000000..4b64ba3f6 --- /dev/null +++ b/patch/ByteShift/main.cpp @@ -0,0 +1,55 @@ +#include "daisy_patch.h" +#include "control_manager.h" +#include "byteshift.h" + +daisy::DaisyPatch patch; +ControlManager controlManager; +ByteShift synth; + +static ByteShift* synthInstance = nullptr; // Pointer to ByteShift instance + +void AudioCallbackWrapper(daisy::AudioHandle::InputBuffer in, + daisy::AudioHandle::OutputBuffer out, + size_t size) { + if (synthInstance) { + synthInstance->ProcessAudio(out, size); + } +} + +void AudioCallbackWrapper(daisy::AudioHandle::InputBuffer in, + daisy::AudioHandle::OutputBuffer out, + size_t size); + +int main(void) { + patch.Init(); + controlManager.Init(&patch); + synth.Init(&patch); + + synthInstance = &synth; // Assign instance before calling StartAudio + patch.StartAudio(AudioCallbackWrapper); + + while (1) { + controlManager.Update(); + synth.SetControlValues(controlManager.GetCtrl1(), + controlManager.GetCtrl2(), + controlManager.GetCtrl3(), + controlManager.GetCtrl4(), + controlManager.GetEncoderCount()); + + patch.display.Fill(false); + patch.display.SetCursor(0, 0); + patch.display.WriteString("ByteShift v0.1", Font_7x10, true); + + char buffer[32]; + snprintf(buffer, sizeof(buffer), "C1: %d", (int)(controlManager.GetCtrl1() * 100)); + patch.display.SetCursor(0, 12); + patch.display.WriteString(buffer, Font_7x10, true); + + snprintf(buffer, sizeof(buffer), "Enc: %d", controlManager.GetEncoderCount()); + patch.display.SetCursor(0, 60); + patch.display.WriteString(buffer, Font_7x10, true); + + patch.display.Update(); + daisy::System::Delay(5); + } +} diff --git a/patch/bytebeat/Makefile b/patch/bytebeat/Makefile new file mode 100644 index 000000000..028332aef --- /dev/null +++ b/patch/bytebeat/Makefile @@ -0,0 +1,25 @@ +# Project Name +TARGET = bytebeat + +# Use LGPL version of DaisySP (optional, set to 1 if needed) +USE_DAISYSP_LGPL=1 + +# Library Locations +LIBDAISY_DIR = ../../libDaisy +DAISYSP_DIR = ../../DaisySP + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# Source files +# CPP_SOURCES = bytebeat.cpp +CPP_SOURCES = bytebeat.cpp bytebeat_synth.cpp + +# Include the Daisy system Makefile +include $(SYSTEM_FILES_DIR)/Makefile + + + + + + diff --git a/patch/bytebeat/README.md b/patch/bytebeat/README.md new file mode 100644 index 000000000..7102f54f6 --- /dev/null +++ b/patch/bytebeat/README.md @@ -0,0 +1,47 @@ +# 🎡 BytebeatSynth + +BytebeatSynth is an experimental music synthesizer that generates sound using mathematical formulas known as bytebeat equations. It runs on the Daisy Patch platform, using control voltage (CV) and knob-based inputs to modify parameters in real time. + +## πŸ”₯ Features +- Bytebeat Audio Synthesis: Uses a selection of classic bytebeat formulas. +- Dynamic Formula Parameters: Modify parameters a, b, and c in real-time. +- Adjusts how fast the bytebeat algorithm runs, affecting pitch and timbre, with higher values creating higher tones and lower values slowing it down, sometimes to silence. +- Built-in Formula Switching: Cycle through multiple equations with the encoder. +- Analog & CV Input Controls: Adjust speed, parameters, and pitch using hardware knobs or external control voltages. + +## πŸŽ›οΈ Controls + +### Control Function +CTRL 1 (Speed/Pitch) Controls the pitch/speed of the bytebeat equation. Accepts 0-5V CV input. +CTRL 2 (Parameter A) Adjusts the first variable (a) in the bytebeat formula. +CTRL 3 (Parameter B) Adjusts the second variable (b) in the bytebeat formula. +CTRL 4 (Parameter C) Adjusts the third variable (c) in the bytebeat formula. +Encoder (Press) Cycles through different bytebeat formulas. +OLED Display Shows the current formula, pitch, and values of a, b, and c. + +## ⚑ How to Use +1. Turn CTRL 1 to change the pitch and speed of the bytebeat synthesis. +2. Adjust CTRL 2, 3, and 4 to manipulate different formula parameters. +3. Press the encoder to switch to a different bytebeat equation. +4. Use CV input on CTRL 1 to modulate pitch from an external sequencer or LFO. +5. Observe the OLED display to see current formula and parameter values. + +## πŸ› οΈ Installation & Compilation + +To build and flash BytebeatSynth onto your Daisy Patch: +1. Clone or copy the project files. +2. Run `make` to compile the firmware. +3. Flash the binary using: `st-flash --reset write build/bytebeat.bin 0x08000000` + +## 🎚️ Example Patching Ideas +- Sequenced Rhythms: Use an external CV sequencer on CTRL 1 for evolving patterns. +- Glitchy Drones: Set CTRL 1 low and tweak CTRL 2, 3, 4 for texture shifts. +- Noise Percussion: Use a gate signal on CTRL 1 to trigger bursts of sound. + +## 🎡 What is a Bytebeat? + +A bytebeat is a type of generative music where sound is created using simple mathematical formulas instead of traditional synthesis or sampling. These formulas operate on a time variable (t) to generate an audio waveform in real time, often resulting in glitchy, chiptune-like, and chaotic sounds. + +Bytebeats emerged in 2011 when Finnish developer viznut (aka Ville-Matias HeikkilΓ€) discovered that short arithmetic expressions could create rich and evolving sound patterns. This led to an explosion of interest in algorithmic music, with musicians and programmers experimenting with different formulas to produce everything from rhythmic patterns to bizarre noise textures. + +Since bytebeat synthesis is entirely formula-driven, modifying the equations can create new sonic landscapes, making it an ideal playground for experimentation in digital sound generation. diff --git a/patch/bytebeat/bytebeat.cpp b/patch/bytebeat/bytebeat.cpp new file mode 100644 index 000000000..76976f5fb --- /dev/null +++ b/patch/bytebeat/bytebeat.cpp @@ -0,0 +1,18 @@ +#include "bytebeat_synth.h" + +int main(void) { + // Create an instance of DaisyPatch assigned to local var patch + DaisyPatch patch; + // Creat an instance of BytebeatSynth assigned to local var synth + BytebeatSynth synth; + // Initialize synth pass patch. Is this a reference what is & ? + synth.Init(&patch); + + // Create a main program loop + while (1) { + // Update display + synth.UpdateDisplay(); + // Chill for 50ms + System::Delay(50); + } +} \ No newline at end of file diff --git a/patch/bytebeat/bytebeat_synth.cpp b/patch/bytebeat/bytebeat_synth.cpp new file mode 100644 index 000000000..67f4667b2 --- /dev/null +++ b/patch/bytebeat/bytebeat_synth.cpp @@ -0,0 +1,183 @@ +#include "bytebeat_synth.h" + +// Global instance pointer for static callback +static BytebeatSynth* synthInstance = nullptr; + +// Bytebeat Formulas (Function Pointer Table) +BytebeatSynth::BytebeatFunc BytebeatSynth::formulaTable[] = { + &BytebeatSynth::BytebeatFormula0, + &BytebeatSynth::BytebeatFormula1, + &BytebeatSynth::BytebeatFormula2, + &BytebeatSynth::BytebeatFormula3, + &BytebeatSynth::BytebeatFormula4, + &BytebeatSynth::BytebeatFormula5 +}; + +// Bytebeat Equation Definitions (Now use a, b, c) +uint8_t BytebeatSynth::BytebeatFormula0(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + + uint8_t result = (t * (t >> a) & 42) | (t * (t >> b) & 84); + return result; +} + +uint8_t BytebeatSynth::BytebeatFormula1(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + int c = synth->c; + return (t >> a) | (t << b) | (t * (t >> c)); +} + +uint8_t BytebeatSynth::BytebeatFormula2(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + return (t * (t >> a) | (t * (t >> b) & 50)); +} + +uint8_t BytebeatSynth::BytebeatFormula3(uint32_t t, BytebeatSynth* synth) { + int b = synth->b; + int c = synth->c; + return ((t * 3) & (t >> b)) | (t * (t >> c)); +} + +uint8_t BytebeatSynth::BytebeatFormula4(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + return (t * 5 & (t >> a)) | (t * (t >> b) & 123); +} + +uint8_t BytebeatSynth::BytebeatFormula5(uint32_t t, BytebeatSynth* synth) { + int a = synth->a; // Stored in a register (if optimized) + int b = synth->b; + return (t >> a) ^ (t * (t >> b) & 32); +} + +// Static Wrapper for Audio Callback +static void AudioCallbackWrapper(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) { + synthInstance->ProcessAudio(in, out, size); +} + + +// ************************************** +// +// Initialize Synth +// +// ************************************** +void BytebeatSynth::Init(DaisyPatch* p) { + patch = p; + speed = 0.1f; + t = 0; + tScale = 1.0f; // βœ… Initialize tScale + formula_index = 0; + synthInstance = this; + + // Initialize Parameters (scaled) + a = 4; b = 6; c = 8; + + patch->Init(); + patch->StartAdc(); + patch->StartAudio(AudioCallbackWrapper); +} + +// Update Controls (Knobs & Encoder) +void BytebeatSynth::UpdateControls() { + patch->ProcessAnalogControls(); + patch->ProcessDigitalControls(); + + // Read Speed (Knob 1) - scaled to 0.1 to 4.1 + speed = patch->GetKnobValue(DaisyPatch::CTRL_1) * 4.0f + 0.1f; + + // Read dynamic parameters (CTRL_2, CTRL_3, CTRL_4) + // Map Knobs 2, 3, and 4 to parameters a, b, and c + a = (int)(patch->GetKnobValue(DaisyPatch::CTRL_2) * 16) + 1; // Range: 1 - 16 + b = (int)(patch->GetKnobValue(DaisyPatch::CTRL_3) * 32) + 1; // Range: 1 - 32 + c = (int)(patch->GetKnobValue(DaisyPatch::CTRL_4) * 64) + 1; // Range: 1 - 64 + + // Check encoder button for formula switching + if (patch->encoder.RisingEdge()) { + formula_index = (formula_index + 1) % formula_count; + } +} + +// Audio Processing Callback +// void BytebeatSynth::ProcessAudio(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) { +// UpdateControls(); + +// BytebeatFunc currentFormula = formulaTable[formula_index]; +// int speed_int = static_cast(speed * 10); + +// for (size_t i = 0; i < size; i++) { +// uint8_t sample = currentFormula(t, synthInstance); +// float output = ((float)sample / 255.0f) * 2.0f - 1.0f; +// out[0][i] = output; +// out[1][i] = output; +// t += 1 + speed_int; +// } +// } + +// Constants for min/max frequency +constexpr float MIN_FREQUENCY = 24.0f; // 8.0 Lower limit (adjust as needed) +constexpr float MAX_FREQUENCY = 112.0f; // Upper limit (adjust as needed) + +void BytebeatSynth::ProcessAudio(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) { + UpdateControls(); + + BytebeatFunc currentFormula = formulaTable[formula_index]; + + // Read the pitch CV and scale it within the min/max frequency range + float pitchCV = (1.0f - patch->GetKnobValue(DaisyPatch::CTRL_1)) * 5.0f; // Map 0-5V + float frequency = MIN_FREQUENCY * powf(2.0f, pitchCV * log2f(MAX_FREQUENCY / MIN_FREQUENCY)); // Constrain range + + // Convert frequency to a time step + uint32_t tScale = static_cast(48000.0f / frequency); + + for (size_t i = 0; i < size; i++) { + t += tScale; // Step `t` according to the musical pitch scaling + + // Generate bytebeat sample at updated `t` + uint8_t sample = currentFormula(t, this); + float output = ((float)sample / 255.0f) * 2.0f - 1.0f; + + // Send output + out[0][i] = output; + out[1][i] = output; + } +} + +// OLED Display Update (Now Shows a, b, c) +void BytebeatSynth::UpdateDisplay() { + static int last_speed = -1; + static int last_formula = -1; + static int last_a = -1, last_b = -1, last_c = -1; + + int speed_int = static_cast(speed * 100); + + if (speed_int != last_speed || formula_index != last_formula || a != last_a || b != last_b || c != last_c) { + patch->display.Fill(false); + patch->display.SetCursor(0, 0); + patch->display.WriteString("Bytebeat Synth", Font_7x10, true); + + char buffer[32]; + snprintf(buffer, sizeof(buffer), "Speed: %d", speed_int); + patch->display.SetCursor(0, 12); + patch->display.WriteString(buffer, Font_7x10, true); + + snprintf(buffer, sizeof(buffer), "Formula: %d", formula_index); + patch->display.SetCursor(0, 24); + patch->display.WriteString(buffer, Font_7x10, true); + + // Display a, b, and c values + snprintf(buffer, sizeof(buffer), "a: %d b: %d c: %d", a, b, c); + patch->display.SetCursor(0, 36); + patch->display.WriteString(buffer, Font_7x10, true); + + patch->display.Update(); + + last_speed = speed_int; + last_formula = formula_index; + last_a = a; + last_b = b; + last_c = c; + } +} \ No newline at end of file diff --git a/patch/bytebeat/bytebeat_synth.h b/patch/bytebeat/bytebeat_synth.h new file mode 100644 index 000000000..452ddc6e9 --- /dev/null +++ b/patch/bytebeat/bytebeat_synth.h @@ -0,0 +1,38 @@ +#ifndef BYTEBEAT_SYNTH_H +#define BYTEBEAT_SYNTH_H + +#include "daisy_patch.h" +#include "daisysp.h" + +using namespace daisy; +using namespace daisysp; + +class BytebeatSynth { +public: + void Init(DaisyPatch* p); + void UpdateControls(); + void ProcessAudio(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size); + void UpdateDisplay(); + +private: + DaisyPatch* patch; + float speed; + uint32_t t; + float tScale; // βœ… Declare tScale + int formula_index; + static const int formula_count = 6; + + int a, b, c; // Bytebeat parameters + + using BytebeatFunc = uint8_t (*)(uint32_t, BytebeatSynth*); + static BytebeatFunc formulaTable[]; + + static uint8_t BytebeatFormula0(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula1(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula2(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula3(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula4(uint32_t t, BytebeatSynth* synth); + static uint8_t BytebeatFormula5(uint32_t t, BytebeatSynth* synth); +}; + +#endif // BYTEBEAT_SYNTH_H \ No newline at end of file From 5de146501d3c46cfac8fd877e1fdbf3465562eed Mon Sep 17 00:00:00 2001 From: Mitchell Hudson Date: Sun, 16 Mar 2025 16:28:34 -0700 Subject: [PATCH 2/7] test control manager --- patch/ByteShift/Makefile | 2 +- patch/ByteShift/control_manager.cpp | 2 ++ patch/ByteShift/main.cpp | 7 ++----- patch/bytebeat/bytebeat_synth.cpp | 13 +++++++++++++ 4 files changed, 18 insertions(+), 6 deletions(-) diff --git a/patch/ByteShift/Makefile b/patch/ByteShift/Makefile index e43eaaa16..b282f1a6f 100644 --- a/patch/ByteShift/Makefile +++ b/patch/ByteShift/Makefile @@ -9,7 +9,7 @@ DAISYSP_DIR = ../../DaisySP SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core # Source files -CPP_SOURCES = main.cpp byteshift.cpp +CPP_SOURCES = main.cpp byteshift.cpp control_manager.cpp # Include the Daisy system Makefile include $(SYSTEM_FILES_DIR)/Makefile diff --git a/patch/ByteShift/control_manager.cpp b/patch/ByteShift/control_manager.cpp index 3771244e9..e820b0744 100644 --- a/patch/ByteShift/control_manager.cpp +++ b/patch/ByteShift/control_manager.cpp @@ -2,6 +2,8 @@ void ControlManager::Init(daisy::DaisyPatch* p) { patch = p; + patch->StartAdc(); + ctrl1 = ctrl2 = ctrl3 = ctrl4 = 0.0f; encoderCount = 0; encoderPressed = false; diff --git a/patch/ByteShift/main.cpp b/patch/ByteShift/main.cpp index 4b64ba3f6..8574765a8 100644 --- a/patch/ByteShift/main.cpp +++ b/patch/ByteShift/main.cpp @@ -16,12 +16,9 @@ void AudioCallbackWrapper(daisy::AudioHandle::InputBuffer in, } } -void AudioCallbackWrapper(daisy::AudioHandle::InputBuffer in, - daisy::AudioHandle::OutputBuffer out, - size_t size); - int main(void) { patch.Init(); + controlManager.Init(&patch); synth.Init(&patch); @@ -46,7 +43,7 @@ int main(void) { patch.display.WriteString(buffer, Font_7x10, true); snprintf(buffer, sizeof(buffer), "Enc: %d", controlManager.GetEncoderCount()); - patch.display.SetCursor(0, 60); + patch.display.SetCursor(0, 24); patch.display.WriteString(buffer, Font_7x10, true); patch.display.Update(); diff --git a/patch/bytebeat/bytebeat_synth.cpp b/patch/bytebeat/bytebeat_synth.cpp index 67f4667b2..57133784c 100644 --- a/patch/bytebeat/bytebeat_synth.cpp +++ b/patch/bytebeat/bytebeat_synth.cpp @@ -116,6 +116,12 @@ void BytebeatSynth::UpdateControls() { // } // } +// ***************************************************** +// +// Process Audio +// +// ***************************************************** + // Constants for min/max frequency constexpr float MIN_FREQUENCY = 24.0f; // 8.0 Lower limit (adjust as needed) constexpr float MAX_FREQUENCY = 112.0f; // Upper limit (adjust as needed) @@ -145,6 +151,13 @@ void BytebeatSynth::ProcessAudio(AudioHandle::InputBuffer in, AudioHandle::Outpu } } + +// ************************************************* +// +// Update Display +// +// ************************************************* + // OLED Display Update (Now Shows a, b, c) void BytebeatSynth::UpdateDisplay() { static int last_speed = -1; From d50aaa48bf4a511b763f50178f107217834f343a Mon Sep 17 00:00:00 2001 From: Mitchell Hudson Date: Sun, 16 Mar 2025 16:33:11 -0700 Subject: [PATCH 3/7] fixed encoder --- patch/ByteShift/control_manager.cpp | 5 ++++- patch/ByteShift/main.cpp | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/patch/ByteShift/control_manager.cpp b/patch/ByteShift/control_manager.cpp index e820b0744..1d0bf7f1f 100644 --- a/patch/ByteShift/control_manager.cpp +++ b/patch/ByteShift/control_manager.cpp @@ -3,7 +3,7 @@ void ControlManager::Init(daisy::DaisyPatch* p) { patch = p; patch->StartAdc(); - + ctrl1 = ctrl2 = ctrl3 = ctrl4 = 0.0f; encoderCount = 0; encoderPressed = false; @@ -20,4 +20,7 @@ void ControlManager::Update() { encoderCount += patch->encoder.Increment(); encoderPressed = patch->encoder.RisingEdge(); + if (encoderPressed) { + encoderCount = 0; + } } \ No newline at end of file diff --git a/patch/ByteShift/main.cpp b/patch/ByteShift/main.cpp index 8574765a8..0383ebd16 100644 --- a/patch/ByteShift/main.cpp +++ b/patch/ByteShift/main.cpp @@ -38,14 +38,27 @@ int main(void) { patch.display.WriteString("ByteShift v0.1", Font_7x10, true); char buffer[32]; + snprintf(buffer, sizeof(buffer), "C1: %d", (int)(controlManager.GetCtrl1() * 100)); patch.display.SetCursor(0, 12); patch.display.WriteString(buffer, Font_7x10, true); - snprintf(buffer, sizeof(buffer), "Enc: %d", controlManager.GetEncoderCount()); + snprintf(buffer, sizeof(buffer), "C2: %d", (int)(controlManager.GetCtrl2() * 100)); patch.display.SetCursor(0, 24); patch.display.WriteString(buffer, Font_7x10, true); + snprintf(buffer, sizeof(buffer), "C3: %d", (int)(controlManager.GetCtrl3() * 100)); + patch.display.SetCursor(0, 36); + patch.display.WriteString(buffer, Font_7x10, true); + + snprintf(buffer, sizeof(buffer), "C4: %d", (int)(controlManager.GetCtrl4() * 100)); + patch.display.SetCursor(0, 48); + patch.display.WriteString(buffer, Font_7x10, true); + + snprintf(buffer, sizeof(buffer), "Enc: %d", controlManager.GetEncoderCount()); + patch.display.SetCursor(60, 12); + patch.display.WriteString(buffer, Font_7x10, true); + patch.display.Update(); daisy::System::Delay(5); } From 67c5aa237020fe676efe899fedec59f8cc081028 Mon Sep 17 00:00:00 2001 From: Mitchell Hudson Date: Mon, 17 Mar 2025 10:40:38 -0700 Subject: [PATCH 4/7] updated v1.0 --- patch/ByteShift/.vscode/settings.json | 5 + patch/ByteShift/Makefile | 2 +- patch/ByteShift/README.md | 42 ++++----- patch/ByteShift/bytebeat.cpp | 18 ---- patch/ByteShift/bytebeat_synth.cpp | 128 +++++++++----------------- patch/ByteShift/bytebeat_synth.h | 35 ++++--- patch/ByteShift/byteshift.cpp | 30 +++--- patch/ByteShift/byteshift.h | 19 ++-- patch/ByteShift/control_manager.cpp | 13 ++- patch/ByteShift/control_manager.h | 9 ++ patch/ByteShift/main.cpp | 83 ++++++++++------- patch/bytebeat/Makefile | 12 +++ patch/bytebeat/bytebeat.cpp | 8 +- patch/bytebeat/bytebeat_synth.h | 8 +- 14 files changed, 211 insertions(+), 201 deletions(-) create mode 100644 patch/ByteShift/.vscode/settings.json delete mode 100644 patch/ByteShift/bytebeat.cpp diff --git a/patch/ByteShift/.vscode/settings.json b/patch/ByteShift/.vscode/settings.json new file mode 100644 index 000000000..57c203072 --- /dev/null +++ b/patch/ByteShift/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "algorithm": "cpp" + } +} \ No newline at end of file diff --git a/patch/ByteShift/Makefile b/patch/ByteShift/Makefile index b282f1a6f..25768a012 100644 --- a/patch/ByteShift/Makefile +++ b/patch/ByteShift/Makefile @@ -9,7 +9,7 @@ DAISYSP_DIR = ../../DaisySP SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core # Source files -CPP_SOURCES = main.cpp byteshift.cpp control_manager.cpp +CPP_SOURCES = main.cpp byteshift.cpp control_manager.cpp bytebeat_synth.cpp # Include the Daisy system Makefile include $(SYSTEM_FILES_DIR)/Makefile diff --git a/patch/ByteShift/README.md b/patch/ByteShift/README.md index 7102f54f6..af25d92d4 100644 --- a/patch/ByteShift/README.md +++ b/patch/ByteShift/README.md @@ -1,42 +1,38 @@ -# 🎡 BytebeatSynth +# 🎡 ByteShift -BytebeatSynth is an experimental music synthesizer that generates sound using mathematical formulas known as bytebeat equations. It runs on the Daisy Patch platform, using control voltage (CV) and knob-based inputs to modify parameters in real time. +ByteShift is an experimental music synthesizer that generates sound using mathematical formulas known as bytebeat equations. It runs on the Daisy Patch platform, using control voltage (CV) and knob-based inputs to modify parameters in real time. ## πŸ”₯ Features - Bytebeat Audio Synthesis: Uses a selection of classic bytebeat formulas. -- Dynamic Formula Parameters: Modify parameters a, b, and c in real-time. -- Adjusts how fast the bytebeat algorithm runs, affecting pitch and timbre, with higher values creating higher tones and lower values slowing it down, sometimes to silence. -- Built-in Formula Switching: Cycle through multiple equations with the encoder. -- Analog & CV Input Controls: Adjust speed, parameters, and pitch using hardware knobs or external control voltages. +- Dynamic Formula Parameters: Modify parameters a, b, and c in real-time. Not all formulas use all all three controls, some use only two of the controls. +- Built-in Formula Switching: Cycle through 6 bytebeat equations. +- CV Input: Adjust parameters. +- Adjust Pitch in Semitones: ByteShift uses PitchShifter from the DaisySP Lib to change the pitch of output. This works positive and negative. PitcheShifter seems to have some limits when lowering pitch. At -12 semitones the sound disappears, and beyond that the picth rises. +- The OLED display shows the values for a, b, c, pitch, and current formula. ## πŸŽ›οΈ Controls ### Control Function -CTRL 1 (Speed/Pitch) Controls the pitch/speed of the bytebeat equation. Accepts 0-5V CV input. -CTRL 2 (Parameter A) Adjusts the first variable (a) in the bytebeat formula. -CTRL 3 (Parameter B) Adjusts the second variable (b) in the bytebeat formula. -CTRL 4 (Parameter C) Adjusts the third variable (c) in the bytebeat formula. -Encoder (Press) Cycles through different bytebeat formulas. -OLED Display Shows the current formula, pitch, and values of a, b, and c. + +| Control | Function | +| :------ | :------- | +| CTRL 1 | (Parameter A) Adjusts the first variable (a) in the bytebeat formula. | +| CTRL 2 | (Parameter B) Adjusts the second variable (b) in the bytebeat formula. | +| CTRL 3 | (Parameter C) Adjusts the third variable (c) in the bytebeat formula. | +| Encoder (Press) | Cycles through different bytebeat formulas. | +| Encoder (Rotate) | changes the pitch it semitones. | ## ⚑ How to Use -1. Turn CTRL 1 to change the pitch and speed of the bytebeat synthesis. -2. Adjust CTRL 2, 3, and 4 to manipulate different formula parameters. -3. Press the encoder to switch to a different bytebeat equation. -4. Use CV input on CTRL 1 to modulate pitch from an external sequencer or LFO. -5. Observe the OLED display to see current formula and parameter values. +1. Audio is sent to out1 and out 2. +2. Use CTRL1-3 to adjust parameters affecting bytebeat formula. +3. Rotate the encoder to change the pitch in semitones. +4. Press the encoder to change the bytebeat formula. ## πŸ› οΈ Installation & Compilation To build and flash BytebeatSynth onto your Daisy Patch: 1. Clone or copy the project files. 2. Run `make` to compile the firmware. -3. Flash the binary using: `st-flash --reset write build/bytebeat.bin 0x08000000` - -## 🎚️ Example Patching Ideas -- Sequenced Rhythms: Use an external CV sequencer on CTRL 1 for evolving patterns. -- Glitchy Drones: Set CTRL 1 low and tweak CTRL 2, 3, 4 for texture shifts. -- Noise Percussion: Use a gate signal on CTRL 1 to trigger bursts of sound. ## 🎡 What is a Bytebeat? diff --git a/patch/ByteShift/bytebeat.cpp b/patch/ByteShift/bytebeat.cpp deleted file mode 100644 index 76976f5fb..000000000 --- a/patch/ByteShift/bytebeat.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "bytebeat_synth.h" - -int main(void) { - // Create an instance of DaisyPatch assigned to local var patch - DaisyPatch patch; - // Creat an instance of BytebeatSynth assigned to local var synth - BytebeatSynth synth; - // Initialize synth pass patch. Is this a reference what is & ? - synth.Init(&patch); - - // Create a main program loop - while (1) { - // Update display - synth.UpdateDisplay(); - // Chill for 50ms - System::Delay(50); - } -} \ No newline at end of file diff --git a/patch/ByteShift/bytebeat_synth.cpp b/patch/ByteShift/bytebeat_synth.cpp index 2deee1e9c..e306552ab 100644 --- a/patch/ByteShift/bytebeat_synth.cpp +++ b/patch/ByteShift/bytebeat_synth.cpp @@ -1,9 +1,12 @@ #include "bytebeat_synth.h" -// Global instance pointer for static callback -static BytebeatSynth* synthInstance = nullptr; +// ******************************************************************* +// +// Bytebeat formulas +// +// ******************************************************************* -// Bytebeat Function Table +// Bytebeat Formulas (Function Pointer Table) BytebeatSynth::BytebeatFunc BytebeatSynth::formulaTable[] = { &BytebeatSynth::BytebeatFormula0, &BytebeatSynth::BytebeatFormula1, @@ -13,22 +16,25 @@ BytebeatSynth::BytebeatFunc BytebeatSynth::formulaTable[] = { &BytebeatSynth::BytebeatFormula5 }; -// Bytebeat Equation Definitions + +// Bytebeat Equation Definitions (Now use a, b, c) uint8_t BytebeatSynth::BytebeatFormula0(uint32_t t, BytebeatSynth* synth) { - int a = synth->a; + int a = synth->a; int b = synth->b; - return (t * (t >> a) & 42) | (t * (t >> b) & 84); + + uint8_t result = (t * (t >> a) & 42) | (t * (t >> b) & 84); + return result; } uint8_t BytebeatSynth::BytebeatFormula1(uint32_t t, BytebeatSynth* synth) { - int a = synth->a; + int a = synth->a; int b = synth->b; int c = synth->c; return (t >> a) | (t << b) | (t * (t >> c)); } uint8_t BytebeatSynth::BytebeatFormula2(uint32_t t, BytebeatSynth* synth) { - int a = synth->a; // Stored in a register (if optimized) + int a = synth->a; int b = synth->b; return (t * (t >> a) | (t * (t >> b) & 50)); } @@ -40,94 +46,50 @@ uint8_t BytebeatSynth::BytebeatFormula3(uint32_t t, BytebeatSynth* synth) { } uint8_t BytebeatSynth::BytebeatFormula4(uint32_t t, BytebeatSynth* synth) { - int a = synth->a; // Stored in a register (if optimized) + int a = synth->a; int b = synth->b; return (t * 5 & (t >> a)) | (t * (t >> b) & 123); } uint8_t BytebeatSynth::BytebeatFormula5(uint32_t t, BytebeatSynth* synth) { - int a = synth->a; // Stored in a register (if optimized) + int a = synth->a; int b = synth->b; return (t >> a) ^ (t * (t >> b) & 32); } -// Initialize Synth -void BytebeatSynth::Init(DaisyPatch* p) { - patch = p; - t = 0; - formula_index = 0; - synthInstance = this; - - // Initialize Parameters - a = 4; - b = 6; - c = 8; - - patch->Init(); - patch->StartAdc(); -} - -// Update Controls (Knobs & Encoder) -void BytebeatSynth::UpdateControls() { - patch->ProcessAnalogControls(); - patch->ProcessDigitalControls(); - - float pitchCV = patch->GetKnobValue(DaisyPatch::CTRL_1) * 5.0f; - float frequency = 16.0f * powf(2.0f, pitchCV - 1.0f); - tScale = 48000.0f / frequency; - - a = (int)(patch->GetKnobValue(DaisyPatch::CTRL_2) * 16) + 1; - b = (int)(patch->GetKnobValue(DaisyPatch::CTRL_3) * 32) + 1; - c = (int)(patch->GetKnobValue(DaisyPatch::CTRL_4) * 64) + 1; - if (patch->encoder.RisingEdge()) { - formula_index = (formula_index + 1) % formula_count; - } -} +// ******************************************************************* +// +// Bytebeat Synth +// +// ******************************************************************* -// Generate a single sample for external processing float BytebeatSynth::GenerateSample() { - BytebeatFunc currentFormula = formulaTable[formula_index]; - - static float tAccumulator = 0; - tAccumulator += 1.0f; - if (tAccumulator >= tScale) { - t += (uint32_t)(tScale); - tAccumulator -= tScale; - } - - uint8_t sample = currentFormula(t, this); - return ((float)sample / 255.0f) * 2.0f - 1.0f; -} - -void BytebeatSynth::NextFormula() { - formula_index = (formula_index + 1) % formula_count; + static uint32_t t = 0; + t++; // Increment counter each sample + + // Get the current formula + BytebeatFunc currentFormula = formulaTable[formulaIndex]; + // Calculate the formula + uint8_t output = currentFormula(t, this); + float sample = ((float)output / 255.0f) * 2.0f - 1.0f; + + // Basic Bytebeat formula + // Don't lose this formula it sounded good. + // float sample = ((t * (t >> a | t >> b) & c & t >> 8)) / 255.0f; + + return sample; } -// OLED Display Update -void BytebeatSynth::UpdateDisplay() { - static int last_formula = -1; - static int last_a = -1, last_b = -1, last_c = -1; +void BytebeatSynth::UpdateControls(ControlManager& controlManager) { + // Read control values + a = (int)(controlManager.GetCtrl1() * 16) + 1; + b = (int)(controlManager.GetCtrl2() * 32) + 1; + c = (int)(controlManager.GetCtrl3() * 64) + 1; - if (formula_index != last_formula || a != last_a || b != last_b || c != last_c) { - patch->display.Fill(false); - patch->display.SetCursor(0, 0); - patch->display.WriteString("Bytebeat Synth", Font_7x10, true); - - char buffer[32]; - snprintf(buffer, sizeof(buffer), "Formula: %d", formula_index); - patch->display.SetCursor(0, 12); - patch->display.WriteString(buffer, Font_7x10, true); - - snprintf(buffer, sizeof(buffer), "a: %d b: %d c: %d", a, b, c); - patch->display.SetCursor(0, 24); - patch->display.WriteString(buffer, Font_7x10, true); - - patch->display.Update(); - - last_formula = formula_index; - last_a = a; - last_b = b; - last_c = c; + // Advance the formula index if the encoder is pressed. + if (controlManager.IsEncoderPressed()) { + formulaIndex = (formulaIndex + 1) % FORMULA_COUNT; } -} \ No newline at end of file + +} diff --git a/patch/ByteShift/bytebeat_synth.h b/patch/ByteShift/bytebeat_synth.h index 5429bb62e..dee4d014e 100644 --- a/patch/ByteShift/bytebeat_synth.h +++ b/patch/ByteShift/bytebeat_synth.h @@ -2,33 +2,29 @@ #define BYTEBEAT_SYNTH_H #include "daisy_patch.h" -#include "daisysp.h" +#include "control_manager.h" -using namespace daisy; -using namespace daisysp; + +// ******************************************************************* +// +// Bytebeat Synth h +// +// ******************************************************************* class BytebeatSynth { public: - void Init(DaisyPatch* p); - void UpdateControls(); - float GenerateSample(); // Now returns a single sample for processing - void UpdateDisplay(); - float ProcessSample(); - void NextFormula(); - int GetFormulaIndex() const { return formula_index; } + void Init(daisy::DaisyPatch* p) { patch = p; } + float GenerateSample(); + void UpdateControls(ControlManager& controlManager); -private: - DaisyPatch* patch; - uint32_t t; - int formula_index; - static const int formula_count = 6; + int a, b, c, formulaIndex; - // Dynamic formula parameters - int a, b, c; + static const int FORMULA_COUNT = 6; - // Scaling factor for musically tuned timing - float tScale; +private: + daisy::DaisyPatch* patch; + // Bytebeat formulas using BytebeatFunc = uint8_t (*)(uint32_t, BytebeatSynth*); static BytebeatFunc formulaTable[]; @@ -38,6 +34,7 @@ class BytebeatSynth { static uint8_t BytebeatFormula3(uint32_t t, BytebeatSynth* synth); static uint8_t BytebeatFormula4(uint32_t t, BytebeatSynth* synth); static uint8_t BytebeatFormula5(uint32_t t, BytebeatSynth* synth); + }; #endif // BYTEBEAT_SYNTH_H \ No newline at end of file diff --git a/patch/ByteShift/byteshift.cpp b/patch/ByteShift/byteshift.cpp index 9f9da9d2c..b13bc230c 100644 --- a/patch/ByteShift/byteshift.cpp +++ b/patch/ByteShift/byteshift.cpp @@ -1,22 +1,22 @@ #include "byteshift.h" +#include -void ByteShift::Init(daisy::DaisyPatch* p) { - patch = p; - speed = paramA = paramB = paramC = 0.0f; - encoderValue = 0; +// ******************************************************************* +// +// Byte Shift +// +// ******************************************************************* + +void ByteShift::Init(float sampleRate) { + pitchShifter.Init(sampleRate); + pitchShifter.SetTransposition(0.0f); // Default: No pitch shift } -void ByteShift::SetControlValues(float c1, float c2, float c3, float c4, int enc) { - speed = c1; - paramA = c2; - paramB = c3; - paramC = c4; - encoderValue = enc; +void ByteShift::SetPitchShift(int encoderCount) { + float semitoneShift = static_cast(encoderCount); + pitchShifter.SetTransposition(semitoneShift); } -void ByteShift::ProcessAudio(float** out, size_t size) { - for (size_t i = 0; i < size; i++) { - out[0][i] = speed * 0.01f; - out[1][i] = paramA * 0.01f; - } +float ByteShift::ProcessSample(float inSample) { + return pitchShifter.Process(inSample); } \ No newline at end of file diff --git a/patch/ByteShift/byteshift.h b/patch/ByteShift/byteshift.h index cb1a34473..442038ce8 100644 --- a/patch/ByteShift/byteshift.h +++ b/patch/ByteShift/byteshift.h @@ -1,19 +1,22 @@ #ifndef BYTESHIFT_H #define BYTESHIFT_H -#include "daisy_patch.h" -#include "control_manager.h" +#include "daisysp.h" + +// ******************************************************************* +// +// Byte shift +// +// ******************************************************************* class ByteShift { public: - void Init(daisy::DaisyPatch* p); - void ProcessAudio(float** out, size_t size); - void SetControlValues(float c1, float c2, float c3, float c4, int enc); + void Init(float sampleRate); + float ProcessSample(float inSample); // Takes input sample from BytebeatSynth + void SetPitchShift(int encoderCount); private: - daisy::DaisyPatch* patch; - float speed, paramA, paramB, paramC; - int encoderValue; + daisysp::PitchShifter pitchShifter; }; #endif \ No newline at end of file diff --git a/patch/ByteShift/control_manager.cpp b/patch/ByteShift/control_manager.cpp index 1d0bf7f1f..2c7ed3dca 100644 --- a/patch/ByteShift/control_manager.cpp +++ b/patch/ByteShift/control_manager.cpp @@ -1,8 +1,14 @@ #include "control_manager.h" +// ******************************************************************* +// +// Control Manager +// +// ******************************************************************* + void ControlManager::Init(daisy::DaisyPatch* p) { patch = p; - patch->StartAdc(); + patch->StartAdc(); // Must start ADC before reading controls! ctrl1 = ctrl2 = ctrl3 = ctrl4 = 0.0f; encoderCount = 0; @@ -10,16 +16,21 @@ void ControlManager::Init(daisy::DaisyPatch* p) { } void ControlManager::Update() { + // Read control values patch->ProcessAnalogControls(); patch->ProcessDigitalControls(); + // Store control values ctrl1 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_1); ctrl2 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_2); ctrl3 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_3); ctrl4 = patch->GetKnobValue(daisy::DaisyPatch::CTRL_4); + // Read the encoder encoderCount += patch->encoder.Increment(); encoderPressed = patch->encoder.RisingEdge(); + + // Reset pitch shift when changing formula if (encoderPressed) { encoderCount = 0; } diff --git a/patch/ByteShift/control_manager.h b/patch/ByteShift/control_manager.h index aeeb60b95..24b48ea01 100644 --- a/patch/ByteShift/control_manager.h +++ b/patch/ByteShift/control_manager.h @@ -3,15 +3,24 @@ #include "daisy_patch.h" +// ******************************************************************* +// +// Control Manager h +// +// ******************************************************************* + class ControlManager { public: void Init(daisy::DaisyPatch* p); void Update(); + // Getters return raw control values float GetCtrl1() const { return ctrl1; } float GetCtrl2() const { return ctrl2; } float GetCtrl3() const { return ctrl3; } float GetCtrl4() const { return ctrl4; } + + // Getter returns the encoder count and isPressed int GetEncoderCount() const { return encoderCount; } bool IsEncoderPressed() const { return encoderPressed; } diff --git a/patch/ByteShift/main.cpp b/patch/ByteShift/main.cpp index 0383ebd16..e1c12906e 100644 --- a/patch/ByteShift/main.cpp +++ b/patch/ByteShift/main.cpp @@ -1,65 +1,86 @@ + +// An app for Daisy Patch that creates a Bytebeat synthesizer. + #include "daisy_patch.h" #include "control_manager.h" +#include "bytebeat_synth.h" #include "byteshift.h" -daisy::DaisyPatch patch; +using namespace daisy; + +DaisyPatch patch; ControlManager controlManager; -ByteShift synth; +BytebeatSynth bytebeat; +ByteShift byteshift; + +// ******************************************************************* +// +// ⚑ Audio callback function +// +// ******************************************************************* -static ByteShift* synthInstance = nullptr; // Pointer to ByteShift instance +void AudioCallback(const float* const* in, float** out, size_t size) { + for (size_t i = 0; i < size; i++) { + // TODO: pass the control values to Generate sample to decouple controlManager + float rawSample = bytebeat.GenerateSample(); // Bytebeat audio + float shiftedSample = byteshift.ProcessSample(rawSample); // Apply pitch shifting -void AudioCallbackWrapper(daisy::AudioHandle::InputBuffer in, - daisy::AudioHandle::OutputBuffer out, - size_t size) { - if (synthInstance) { - synthInstance->ProcessAudio(out, size); + out[0][i] = shiftedSample; // Left Channel + out[1][i] = shiftedSample; // Right Channel } } + +// ******************************************************************* +// +// Main +// +// ******************************************************************* + int main(void) { + // initialize classes patch.Init(); - controlManager.Init(&patch); - synth.Init(&patch); + bytebeat.Init(&patch); // TODO: Does this need patch? + byteshift.Init(patch.AudioSampleRate()); - synthInstance = &synth; // Assign instance before calling StartAudio - patch.StartAudio(AudioCallbackWrapper); + patch.StartAudio(AudioCallback); - while (1) { + while (true) { controlManager.Update(); - synth.SetControlValues(controlManager.GetCtrl1(), - controlManager.GetCtrl2(), - controlManager.GetCtrl3(), - controlManager.GetCtrl4(), - controlManager.GetEncoderCount()); + bytebeat.UpdateControls(controlManager); // Update Bytebeat with controls + byteshift.SetPitchShift(controlManager.GetEncoderCount()); + // Clear the display and show the app name patch.display.Fill(false); patch.display.SetCursor(0, 0); - patch.display.WriteString("ByteShift v0.1", Font_7x10, true); + patch.display.WriteString("ByteShift v1.0", Font_7x10, true); char buffer[32]; - - snprintf(buffer, sizeof(buffer), "C1: %d", (int)(controlManager.GetCtrl1() * 100)); + + // Display control values + snprintf(buffer, sizeof(buffer), "a: %d", bytebeat.a); patch.display.SetCursor(0, 12); patch.display.WriteString(buffer, Font_7x10, true); - snprintf(buffer, sizeof(buffer), "C2: %d", (int)(controlManager.GetCtrl2() * 100)); - patch.display.SetCursor(0, 24); + snprintf(buffer, sizeof(buffer), "b: %d", (int)(controlManager.GetCtrl2() * 100)); + patch.display.SetCursor(42, 12); patch.display.WriteString(buffer, Font_7x10, true); - snprintf(buffer, sizeof(buffer), "C3: %d", (int)(controlManager.GetCtrl3() * 100)); - patch.display.SetCursor(0, 36); + snprintf(buffer, sizeof(buffer), "c: %d", (int)(controlManager.GetCtrl3() * 100)); + patch.display.SetCursor(84, 12); patch.display.WriteString(buffer, Font_7x10, true); - snprintf(buffer, sizeof(buffer), "C4: %d", (int)(controlManager.GetCtrl4() * 100)); - patch.display.SetCursor(0, 48); + // Display pitche shift + snprintf(buffer, sizeof(buffer), "Pitch: %d", controlManager.GetEncoderCount()); + patch.display.SetCursor(0, 24); patch.display.WriteString(buffer, Font_7x10, true); - snprintf(buffer, sizeof(buffer), "Enc: %d", controlManager.GetEncoderCount()); - patch.display.SetCursor(60, 12); + // Display Bytebeat formula + snprintf(buffer, sizeof(buffer), "Formula: %d", bytebeat.formulaIndex); + patch.display.SetCursor(0, 36); patch.display.WriteString(buffer, Font_7x10, true); patch.display.Update(); - daisy::System::Delay(5); } -} +} \ No newline at end of file diff --git a/patch/bytebeat/Makefile b/patch/bytebeat/Makefile index 028332aef..61106b23b 100644 --- a/patch/bytebeat/Makefile +++ b/patch/bytebeat/Makefile @@ -18,6 +18,18 @@ CPP_SOURCES = bytebeat.cpp bytebeat_synth.cpp # Include the Daisy system Makefile include $(SYSTEM_FILES_DIR)/Makefile +# Define the compiled program binary +PROGRAM = bytebeat.bin +FLASH_ADDR = 0x08000000 + +# Convert ELF to BIN +build/$(PROGRAM): build/$(TARGET).elf + arm-none-eabi-objcopy -O binary build/$(TARGET).elf build/$(PROGRAM) + +# Flash to Daisy Patch using ST-LINK V3 Mini +program: build/$(PROGRAM) + st-flash --reset write build/$(PROGRAM) $(FLASH_ADDR) + diff --git a/patch/bytebeat/bytebeat.cpp b/patch/bytebeat/bytebeat.cpp index 76976f5fb..834ce3a34 100644 --- a/patch/bytebeat/bytebeat.cpp +++ b/patch/bytebeat/bytebeat.cpp @@ -1,5 +1,11 @@ #include "bytebeat_synth.h" +// ******************************************************************* +// +// Bytebeat Synth main +// +// ******************************************************************* + int main(void) { // Create an instance of DaisyPatch assigned to local var patch DaisyPatch patch; @@ -15,4 +21,4 @@ int main(void) { // Chill for 50ms System::Delay(50); } -} \ No newline at end of file +} diff --git a/patch/bytebeat/bytebeat_synth.h b/patch/bytebeat/bytebeat_synth.h index 452ddc6e9..d88e2e3f9 100644 --- a/patch/bytebeat/bytebeat_synth.h +++ b/patch/bytebeat/bytebeat_synth.h @@ -7,6 +7,12 @@ using namespace daisy; using namespace daisysp; +// ******************************************************************* +// +// Bytebeat Synth h +// +// ******************************************************************* + class BytebeatSynth { public: void Init(DaisyPatch* p); @@ -18,7 +24,7 @@ class BytebeatSynth { DaisyPatch* patch; float speed; uint32_t t; - float tScale; // βœ… Declare tScale + float tScale; int formula_index; static const int formula_count = 6; From ab438b9fd09d151a2ef057a788bba011287e5169 Mon Sep 17 00:00:00 2001 From: Mitchell Hudson Date: Mon, 17 Mar 2025 14:34:17 -0700 Subject: [PATCH 5/7] fixed --- patch/ByteShift/byteshift.cpp | 2 +- patch/ByteShift/byteshift.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/patch/ByteShift/byteshift.cpp b/patch/ByteShift/byteshift.cpp index b13bc230c..c6d47405a 100644 --- a/patch/ByteShift/byteshift.cpp +++ b/patch/ByteShift/byteshift.cpp @@ -19,4 +19,4 @@ void ByteShift::SetPitchShift(int encoderCount) { float ByteShift::ProcessSample(float inSample) { return pitchShifter.Process(inSample); -} \ No newline at end of file +} diff --git a/patch/ByteShift/byteshift.h b/patch/ByteShift/byteshift.h index 442038ce8..626216c2f 100644 --- a/patch/ByteShift/byteshift.h +++ b/patch/ByteShift/byteshift.h @@ -19,4 +19,4 @@ class ByteShift { daisysp::PitchShifter pitchShifter; }; -#endif \ No newline at end of file +#endif From a94d27c93eded2cc5066ba6850e3a556ec9aae99 Mon Sep 17 00:00:00 2001 From: Mitchell Hudson Date: Wed, 2 Apr 2025 15:55:29 -0700 Subject: [PATCH 6/7] added Harmonic OSC --- patch/HarmonicOscillator/Makefile | 62 ++++++ patch/HarmonicOscillator/MorphOscillator.cpp | 46 ++++ patch/HarmonicOscillator/MorphOscillator.h | 19 ++ patch/HarmonicOscillator/main-v1.cpp | 168 ++++++++++++++ patch/HarmonicOscillator/main.cpp | 127 +++++++++++ patch/HarmonicOscillator/notes/three_limit | Bin 0 -> 77800 bytes .../HarmonicOscillator/notes/three_limit.cpp | 54 +++++ patch/HarmonicOscillator/ratios.h | 51 +++++ patch/HarmonicOscillator/vox.h | 33 +++ patch/Parametric-eq/Makefile | 40 ++++ patch/Parametric-eq/main.cpp | 134 ++++++++++++ terrarrium/DoubleDelay/DelayUnit.h | 44 ++++ terrarrium/DoubleDelay/DelayUnitReverse.h | 51 +++++ terrarrium/DoubleDelay/ExtendedDelayLine.h | 92 ++++++++ terrarrium/DoubleDelay/Makefile | 37 ++++ terrarrium/DoubleDelay/delayline_reverse.h | 207 ++++++++++++++++++ terrarrium/DoubleDelay/main.1.0.3.cpp | 131 +++++++++++ terrarrium/DoubleDelay/main.cpp | 158 +++++++++++++ terrarrium/DoubleDelay/notes/controls.cpp | 56 +++++ terrarrium/DoubleDelay/notes/delay_module.h | 166 ++++++++++++++ terrarrium/DoubleDelay/notes/delayline.cpp | 125 +++++++++++ terrarrium/DoubleDelay/notes/main-v1.cpp | 91 ++++++++ terrarrium/DoubleDelay/terrarium.h | 96 ++++++++ 23 files changed, 1988 insertions(+) create mode 100644 patch/HarmonicOscillator/Makefile create mode 100644 patch/HarmonicOscillator/MorphOscillator.cpp create mode 100644 patch/HarmonicOscillator/MorphOscillator.h create mode 100644 patch/HarmonicOscillator/main-v1.cpp create mode 100644 patch/HarmonicOscillator/main.cpp create mode 100755 patch/HarmonicOscillator/notes/three_limit create mode 100644 patch/HarmonicOscillator/notes/three_limit.cpp create mode 100644 patch/HarmonicOscillator/ratios.h create mode 100644 patch/HarmonicOscillator/vox.h create mode 100644 patch/Parametric-eq/Makefile create mode 100644 patch/Parametric-eq/main.cpp create mode 100644 terrarrium/DoubleDelay/DelayUnit.h create mode 100644 terrarrium/DoubleDelay/DelayUnitReverse.h create mode 100644 terrarrium/DoubleDelay/ExtendedDelayLine.h create mode 100644 terrarrium/DoubleDelay/Makefile create mode 100644 terrarrium/DoubleDelay/delayline_reverse.h create mode 100644 terrarrium/DoubleDelay/main.1.0.3.cpp create mode 100644 terrarrium/DoubleDelay/main.cpp create mode 100644 terrarrium/DoubleDelay/notes/controls.cpp create mode 100644 terrarrium/DoubleDelay/notes/delay_module.h create mode 100644 terrarrium/DoubleDelay/notes/delayline.cpp create mode 100644 terrarrium/DoubleDelay/notes/main-v1.cpp create mode 100644 terrarrium/DoubleDelay/terrarium.h diff --git a/patch/HarmonicOscillator/Makefile b/patch/HarmonicOscillator/Makefile new file mode 100644 index 000000000..1fc4d3818 --- /dev/null +++ b/patch/HarmonicOscillator/Makefile @@ -0,0 +1,62 @@ +# Project Name +TARGET = harmonic_osc + +# Library Locations +LIBDAISY_DIR = ../../libDaisy +DAISYSP_DIR = ../../DaisySP + +# Core location and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# Source files +CPP_SOURCES = main.cpp MorphOscillator.cpp + +# Include the Daisy system Makefile +include $(SYSTEM_FILES_DIR)/Makefile + +# Define the compiled program binary +PROGRAM = harmonic_osc.bin +FLASH_ADDR = 0x08000000 + +# Convert ELF to BIN +build/$(PROGRAM): build/$(TARGET).elf + arm-none-eabi-objcopy -O binary build/$(TARGET).elf build/$(PROGRAM) + +# Flash to Daisy Patch using ST-LINK V3 Mini +program: build/$(PROGRAM) + st-flash --reset write build/$(PROGRAM) $(FLASH_ADDR) + + + + + + +# # Project Name +# TARGET = byteshift + +# # Use LGPL version of DaisySP (optional, set to 1 if needed) +# USE_DAISYSP_LGPL=1 + +# # Library Locations +# LIBDAISY_DIR = ../../libDaisy +# DAISYSP_DIR = ../../DaisySP + +# # Core location and generic Makefile. +# SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# # Source files +# CPP_SOURCES = main.cpp byteshift.cpp bytebeat_synth.cpp + +# # Include the Daisy system Makefile +# include $(SYSTEM_FILES_DIR)/Makefile + +# # Ensure .bin file is created from .elf +# build/byteshift.bin: build/byteshift.elf +# arm-none-eabi-objcopy -O binary build/byteshift.elf build/byteshift.bin + +# # Flashing using STLINK-V3MINI +# PROGRAM = byteshift.bin # Ensure we're flashing the .bin, not .elf +# FLASH_ADDR = 0x08000000 + +# program: build/byteshift.bin # Make sure .bin exists before flashing +# st-flash --reset write build/byteshift.bin $(FLASH_ADDR) \ No newline at end of file diff --git a/patch/HarmonicOscillator/MorphOscillator.cpp b/patch/HarmonicOscillator/MorphOscillator.cpp new file mode 100644 index 000000000..73f2f4cd9 --- /dev/null +++ b/patch/HarmonicOscillator/MorphOscillator.cpp @@ -0,0 +1,46 @@ +#include "MorphOscillator.h" + +using namespace daisysp; + +void MorphOscillator::Init(float sample_rate) { + for(int i = 0; i < 4; i++) { + osc[i].Init(sample_rate); + osc[i].SetAmp(1.0f); + } + + osc[0].SetWaveform(Oscillator::WAVE_SIN); + osc[1].SetWaveform(Oscillator::WAVE_TRI); + osc[2].SetWaveform(Oscillator::WAVE_SAW); + osc[3].SetWaveform(Oscillator::WAVE_SQUARE); + + freq = 440.0f; + shape = 0.0f; +} + +void MorphOscillator::SetFreq(float f) { + freq = f; + for(int i = 0; i < 4; i++) { + osc[i].SetFreq(freq); + } +} + +void MorphOscillator::SetShape(float s) { + shape = fminf(fmaxf(s, 0.0f), 1.0f); // Clamp between 0 and 1 +} + +void MorphOscillator::SetAmp(float a) { + for(int i = 0; i < 4; i++) { + osc[i].SetAmp(a); + } +} + +float MorphOscillator::Process() { + float morph = shape * 3.0f; + int index = (int)morph; + float frac = morph - index; + + float a = osc[index].Process(); + float b = osc[(index + 1 < 4) ? (index + 1) : 3].Process(); + + return a * (1.0f - frac) + b * frac; +} \ No newline at end of file diff --git a/patch/HarmonicOscillator/MorphOscillator.h b/patch/HarmonicOscillator/MorphOscillator.h new file mode 100644 index 000000000..f133c0b63 --- /dev/null +++ b/patch/HarmonicOscillator/MorphOscillator.h @@ -0,0 +1,19 @@ +#pragma once + +#include "daisysp.h" +using namespace daisysp; + +class MorphOscillator { + public: + void Init(float sample_rate); + + void SetFreq(float f); + void SetShape(float s); + void SetAmp(float a); + float Process(); + + private: + Oscillator osc[4]; + float freq; + float shape; +}; \ No newline at end of file diff --git a/patch/HarmonicOscillator/main-v1.cpp b/patch/HarmonicOscillator/main-v1.cpp new file mode 100644 index 000000000..dc0e32175 --- /dev/null +++ b/patch/HarmonicOscillator/main-v1.cpp @@ -0,0 +1,168 @@ +/* + Harmonic Oscillator Synth - v1.0 + --------------------------------- + A 4-voice harmonic oscillator using the Daisy Patch and DaisySP's VariableShapeOscillator. + + Features: + - Selectable harmonic ratio systems (Just Intonation, Harmonic Series, Subharmonics, etc.) + - Smooth morphing between waveforms + - CV pitch input with 1V/oct scaling + - Wide frequency range from LFOs to full pitch + + Controls: + - CTRL_1: Pitch offset over a 3-octave range (scaled by CTRL_4) + - CTRL_2: Harmonic ratio selection (morphs through ratio array) + - CTRL_3: Wave shape morph (sine β†’ triangle β†’ square) + - CTRL_4: Sets the maximum frequency/octave range (from ~0.2Hz to 220Hz) + - Encoder Press: Cycle through harmonic ratio systems + - CV_1: 1V/oct pitch input +*/ + +#include "daisy_patch.h" +#include "daisysp.h" +#include +#include + +#define MAX_FREQ 220.0f // Maximum frequency when CTRL_4 is at max +#define MIN_FREQ 0.1f // Minimum frequency when CTRL_4 is at min (LFO rate) + +using namespace daisy; +using namespace daisysp; + +float f = 0.0f; // TEST +float oscFreqs[4] = {0}; + +DaisyPatch patch; +VariableShapeOscillator oscillators[4]; // Using VariableShapeOscillator + +// Expanded just intonation ratio systems (at least 12 per set) +static const float kCommonChords[12] = { + 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f +}; + +static const float kHarmonicSeries[12] = { + 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f +}; + +static const float kSubharmonics[12] = { + 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.2f, 1.333f, 1.5f, 1.666f, 2.0f, 2.5f, 3.0f +}; + +static const float kFullRatios[12] = { + 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f +}; + +// 3-limit (Pythagorean tuning) - powers of 2 and 3 only +static const float kLimit3[12] = { + 1.0f, 3.0f/2.0f, 4.0f/3.0f, 9.0f/8.0f, 27.0f/16.0f, 16.0f/9.0f, 2.0f, 8.0f/9.0f, 32.0f/27.0f, 243.0f/128.0f, 81.0f/64.0f, 128.0f/81.0f +}; + +// 5-limit tuning - includes prime 5 intervals (pure major/minor thirds) +static const float kLimit5[12] = { + 1.0f, 5.0f/4.0f, 6.0f/5.0f, 4.0f/3.0f, 3.0f/2.0f, 5.0f/3.0f, 8.0f/5.0f, 9.0f/8.0f, 15.0f/8.0f, 10.0f/9.0f, 16.0f/15.0f, 2.0f +}; + +// Tides-style: unison center, harmonics above, subharmonics below +static const float kTidesStyle[12] = { + 1.0f, 3.0f/2.0f, 2.0f, 3.0f, 0.75f, 0.666f, 0.5f, 0.333f, 1.25f, 1.5f, 2.25f, 2.5f +}; + +int mode = 0; // Track encoder mode selection + +void InitOscillators() { + for (int i = 0; i < 4; i++) { + oscillators[i].Init(patch.AudioSampleRate()); + oscillators[i].SetPW(0.5f); + } +} + +void DisplayCurrentMode() { + patch.display.Fill(false); + std::string modeName; + switch (mode) { + case 0: modeName = "Common Chords"; break; + case 1: modeName = "Harmonic Series"; break; + case 2: modeName = "Subharmonics"; break; + case 3: modeName = "Full Ratios"; break; + case 4: modeName = "Limit 3"; break; + case 5: modeName = "Limit 5"; break; + default: modeName = "Tides Style"; break; + } + patch.display.SetCursor(0, 0); + patch.display.WriteString(modeName.c_str(), Font_7x10, true); + + char buffer[32]; + for (int i = 0; i < 4; i++) { + snprintf(buffer, sizeof(buffer), "Out %d: %d Hz", i + 1, (int)(oscFreqs[i])); + patch.display.SetCursor(0, 12 + (i * 10)); + patch.display.WriteString(buffer, Font_7x10, true); + } + + snprintf(buffer, sizeof(buffer), "Knob1: %.2f", patch.GetKnobValue(DaisyPatch::CTRL_1)); + patch.display.SetCursor(0, 54); + patch.display.WriteString(buffer, Font_7x10, true); + + patch.display.Update(); +} + +void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) { + patch.ProcessAllControls(); + + float octaveShift = patch.GetKnobValue(DaisyPatch::CTRL_4); + float maxFreq = MIN_FREQ * powf(2.0f, octaveShift * log2f(MAX_FREQ / MIN_FREQ)); + + float pitchOffset = patch.GetKnobValue(DaisyPatch::CTRL_1); + float baseFreq = maxFreq * powf(0.015625f, (1.0f - pitchOffset)); + + // TEMP: ignore CV to debug knob behavior + // float cv = patch.controls[4].Value() * 5.0f; + // float cvFactor = powf(2.0f, cv); + // baseFreq *= cvFactor; + + f = baseFreq; // display for debugging + + const float* ratioSet; + int numRatios; + switch (mode) { + case 0: ratioSet = kCommonChords; numRatios = 12; break; + case 1: ratioSet = kHarmonicSeries; numRatios = 12; break; + case 2: ratioSet = kSubharmonics; numRatios = 12; break; + case 3: ratioSet = kFullRatios; numRatios = 12; break; + case 4: ratioSet = kLimit3; numRatios = 12; break; + case 5: ratioSet = kLimit5; numRatios = 12; break; + default: ratioSet = kTidesStyle; numRatios = 12; break; + } + + int ratioIndex = powf(patch.GetKnobValue(DaisyPatch::CTRL_2), 2.0f) * (numRatios - 1); + ratioIndex = fminf(fmaxf(ratioIndex, 0), numRatios - 1); + + for (int i = 0; i < 4; i++) { + int rIdx = (ratioIndex + i) % numRatios; + float freq = baseFreq * ratioSet[rIdx]; + oscillators[i].SetSync(true); + oscillators[i].SetFreq(freq); + oscillators[i].SetWaveshape(patch.GetKnobValue(DaisyPatch::CTRL_3)); + oscFreqs[i] = freq; + } + + for (size_t i = 0; i < size; i++) { + for (int j = 0; j < 4; j++) { + out[j][i] = oscillators[j].Process(); + } + } +} + +int main(void) { + patch.Init(); + patch.StartAdc(); + InitOscillators(); + patch.StartAudio(AudioCallback); + + while (1) { + patch.ProcessDigitalControls(); + if (patch.encoder.RisingEdge()) { + mode = (mode + 1) % 7; + DisplayCurrentMode(); + } + } +} diff --git a/patch/HarmonicOscillator/main.cpp b/patch/HarmonicOscillator/main.cpp new file mode 100644 index 000000000..b66f4e736 --- /dev/null +++ b/patch/HarmonicOscillator/main.cpp @@ -0,0 +1,127 @@ +/* + Harmonic Oscillator Synth - v2.0 + -------------------------------- + A 4-voice oscillator engine with CV and knob control, using MorphOscillator for waveform blending. + + Features: + - 4 independent voices sent to Out 1–4 + - Smooth waveform morphing from sine β†’ triangle β†’ saw β†’ square + - Selectable harmonic ratio systems: just intonation, 3/5/7/11-limit, harmonic series, subharmonics, and Tides-style + - CTRL_1: Pitch sweep (over 6 octaves, ending at max frequency) + - CTRL_2: Ratio selector (offset through chosen harmonic set) + - CTRL_3: Waveform morph control + - CTRL_4: Sets the top of the pitch range (from LFO to ~440Hz) + - Encoder press: Cycles through harmonic ratio systems +*/ + +#include "daisy_patch.h" +#include "daisysp.h" +#include "ratios.h" +#include "MorphOscillator.h" +#include +#include + +#define MAX_FREQ 440.0f +#define MIN_FREQ 0.2f + +using namespace daisy; +using namespace daisysp; + +DaisyPatch patch; +MorphOscillator oscillators[4]; +float oscFreqs[4] = {0}; +int mode = 0; + +void InitOscillators() { + for (int i = 0; i < 4; i++) { + oscillators[i].Init(patch.AudioSampleRate()); + oscillators[i].SetAmp(0.5f); + } +} + +void DisplayCurrentMode() { + patch.display.Fill(false); + std::string modeName; + switch (mode) { + case 0: modeName = "Common Chords"; break; + case 1: modeName = "Harmonic Series"; break; + case 2: modeName = "Subharmonics"; break; + case 3: modeName = "Full Ratios"; break; + case 4: modeName = "Limit 3"; break; + case 5: modeName = "Limit 5"; break; + case 6: modeName = "Limit 7"; break; + case 7: modeName = "Limit 11"; break; + default: modeName = "Tides Style"; break; + } + patch.display.SetCursor(0, 0); + patch.display.WriteString(modeName.c_str(), Font_7x10, true); + + char buffer[32]; + for (int i = 0; i < 4; i++) { + snprintf(buffer, sizeof(buffer), "Out %d: %d Hz", i + 1, (int)(oscFreqs[i])); + patch.display.SetCursor(0, 12 + (i * 10)); + patch.display.WriteString(buffer, Font_7x10, true); + } + + snprintf(buffer, sizeof(buffer), "Knob1: %.2f", patch.GetKnobValue(DaisyPatch::CTRL_1)); + patch.display.SetCursor(0, 54); + patch.display.WriteString(buffer, Font_7x10, true); + + patch.display.Update(); +} + +void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) { + patch.ProcessAllControls(); + + float octaveShift = patch.GetKnobValue(DaisyPatch::CTRL_4); + float maxFreq = MIN_FREQ * powf(2.0f, octaveShift * log2f(MAX_FREQ / MIN_FREQ)); + + float pitchOffset = patch.GetKnobValue(DaisyPatch::CTRL_1); + float baseFreq = maxFreq * powf(0.015625f, (1.0f - pitchOffset)); + + const float* ratioSet; + int numRatios; + switch (mode) { + case 0: ratioSet = kCommonChords; numRatios = 12; break; + case 1: ratioSet = kHarmonicSeries; numRatios = 12; break; + case 2: ratioSet = kSubharmonics; numRatios = 12; break; + case 3: ratioSet = kFullRatios; numRatios = 12; break; + case 4: ratioSet = kLimit3; numRatios = 12; break; + case 5: ratioSet = kLimit5; numRatios = 12; break; + case 6: ratioSet = kLimit7; numRatios = 12; break; + case 7: ratioSet = kLimit11; numRatios = 12; break; + default: ratioSet = kTidesStyle; numRatios = 12; break; + } + + int ratioIndex = powf(patch.GetKnobValue(DaisyPatch::CTRL_2), 2.0f) * (numRatios - 1); + ratioIndex = fminf(fmaxf(ratioIndex, 0), numRatios - 1); + + for (int i = 0; i < 4; i++) { + int rIdx = (ratioIndex + i) % numRatios; + float freq = baseFreq * ratioSet[rIdx]; + oscillators[i].SetFreq(freq); + oscillators[i].SetShape(patch.GetKnobValue(DaisyPatch::CTRL_3)); + oscFreqs[i] = freq; + } + + for (size_t i = 0; i < size; i++) { + for (int j = 0; j < 4; j++) { + out[j][i] = oscillators[j].Process(); + } + } +} + +int main(void) { + patch.Init(); + patch.StartAdc(); + InitOscillators(); + patch.StartAudio(AudioCallback); + + while (1) { + patch.ProcessDigitalControls(); + if (patch.encoder.RisingEdge()) { + mode = (mode + 1) % 9; + DisplayCurrentMode(); + } + } +} diff --git a/patch/HarmonicOscillator/notes/three_limit b/patch/HarmonicOscillator/notes/three_limit new file mode 100755 index 0000000000000000000000000000000000000000..21a8b59bb7fc0dc606252fafa07129d090276b69 GIT binary patch literal 77800 zcmeHw3w%}8mG?fm0WJuF5S1d2AhceTYFf*rn2#eI zE}J{A^rq4!i+R`krz?AJB|yY;6a~ZKWu?oPc^~WgE#7-d;6b?U9xM?KH%B%#^UD1B z@jahyIy2`+^PDjI75mF{F`GN#a8vX8il*3OKfd9&8+?1DlZ0)2LBR(e>c4P!O=V>` zvZ*rC&>U_qukho0a*M&YT<{UL@x=&<;j3(FZmg+W6NA0{3*Kq)oh#1?+kbxC;qdyp zjWu;u;hMVD^(g44Z>->pN=3qc95#N+^?r?~WbU%L;o`;DEj7|@F?6XSJmg=C{$IIp z03Z*tVH6JVt=%@Ina?I*zU9~UxF?Cz?7Y~oC9uQ)Y0fTmrNv{ zpqe_dtDA89!Lu4?PNT0PCS_O`Cr z7aZ~9anWM6pKZqAd20V`@Rhs^;-CzOCwdX(;XD?-LhZ-af*n}1&(i!QPY0><$m0&Q z8IuF=K`svp9m`QChHH%Kz6EgSNY&jrrl|Y)$fE8dX*1-LamTx>3OkhPJLy;@<$AWv z-wT~-I~WN6yz`=>?q|+dJI=H!=#aCwrx^W&{;+&!Ky_n%e@ADasJmy&+`TBH>zoNV zC>z+@b4azdw@&JUes#6ChPzlVn2G*^wkYU|o};?IkpbGzDe4}oin_t)H=?s19v2dM zunow(>nPf1`CzVUV_ocP$a0_k2cEI7%{a@2{*N{aXBH%F@-#D7A3!9>74TdvuQ`UVeZw(ZWPDT+GEx%%fGT6|?m6Wif= zC}^AZ`BmWc(0k@OElY0C5}!d`o}k9O448fWm7G0zrH_0bq@BVJ!HZt|N_)rt7%e;6 z)=*r#J}Rh!Z_w`=URr&XcKYI_<|Fzw$-yy5bsq=M&w#&bu4+4RwQ9pA9PML`Uy9?$ z9+@4<?SiaekWh!0W3E3S-TbahZ_XGUUw(QD-Hs;auRAl8V3d z1+LDy`t~`EBd)7{nQEJWen0AOD+x`M{o^QiO4ASCqyHRR?{->VP$Pc)fQQy3V|7dp zgWx~JS96>^#W9#;G-UFuJ4Qn$KZE?W55#kotKOl1$DTOw+r&5jXN=+XZ{eR$p*(Sg z26@NmY1AQKUtBp*ZnQ@a-6AW>AjW68$9l1QhrBxG@}BlYo{}fdokJhIOSQa!xFSI( z0x5Kx@?+cQp-#4A#!up-zop)5xq@y!haG0Q_pk>up$%8>SU)%yI{$U!8~f^m#)&%E z2idPN13MkPJnV(vUxv&){PvY)-#_1vGMWB1MMfUE`EZjD^yLY4edUNFq09jASNLgb zW!yRlv6T~xTVuX(w`#i>I#P)`Yr=ZvK%6J`g}Ff2KGbz# zXOL?9B4F44sPj(}27Qb(cDJtT>O7>{{t@>uuj`_XcTOqlzRc|h?3n#{2Q+AU(BI=I zA4M76mno|Id#KB2T4(7ep!dXyy}UAR&O`hy@fG4j-Oo$VPn3C6>UJ_-WIxYVu7{G_ zw&oG&aeXWH$0m&B$;K&_nI-zxeRDZPq;TVU_{za_AuG;+%_R z7s5WK4gJXjy3Qq-S4*8Jo-x;e{oh#}L*}Dg5I&dj1?ONpE7g%U;ACvcayq7m5AE4f zf_bRS;iOOGQwY5Ck7+SqnPfa;Om-6OxVFQ%>;&jI2|j+Db1dNW#ASgLb_YB|b)RAX zv_Ie=&oEEw<{V@q=d!rxKrYntaez72@p&g?c>!RywKK4?YZvBbC*d1j!CdWC@aHwy z!rP#QHq3cSM_{6s3+rgR2Hw5EMLBAmlU3XKct(B_&IatjXO3bUxOa$jvd?(-S>Sd3 zQ&66>P7riyoX{cU7mN=>-E&c2SK;Bt;@8dst&YZ`8od;e(hMfOU z=RS>luD?27Y3B8e$=o@h8{gS-$?i#v5imd9uI3c9twG&nHqG|w2fl{qo>&j^^3TuQ zvg7?_ArE^FMj6h~WvqVt%FsSJhUNOoxpN5eq-Vs9C~Q<=KAYe--c{dif9%BXVm9UL8~fj% zaaXCg@ALPMb77Uy^1@$2AE-kdUl}90eVEL=LPJsa#W-J+Rn*;ra#~N&Kl%zij*L|M zH{!mo10Gz6>r}F*eNz5O=S?Yez^yZaG@`E|&=TAXUkG{9f2Y!;nC#FV=a7`$-@>j2 zE;}m@fvwO#?}9%#iE)&6OB*_cJnmlb@;Lm;8TjiH@E@vNwQ-(B-yL)iM-H)kFzDfY z8M>6kKGFVmqTF|#ub{!cuwEEH?XS9KSps3)6am*9lNNLIl$}A zpJ-P-TjFs~GFiD}FLlBlxAmOSuh*8$w46|${ox#87TRP#*shLg&==-EJ@WGDvbIz3 zZ4>Hx{8)-|f9Wr0(c|WM{CydGnz0&a0i9~HYKzayEqg8QWA2%x9r9KC0O&*ODDazd za?~49jMvpN_Hy3DGIw+ChcWl_7<*}Nsd%DL?(4us-j?aJseCdU_bKZyIb3|+ma z9R4@@>Yoqk+$#Ey@qY(-#Q6~K;k`-?Q^$;sSp4wuALCa{&b4U2H(8O6Tly%Y!-j;={gXE{S z58(Pp_d*U}(8V~E*Z%Q?vWkx#@q9Jbcf^-X z=-LgQ>_MLS5#*!&{Adpxo6{RVqKC8}Mx1&OJbZXgoG0;|?wLdMp@Wfsbz|H~+K29j zOkTlU>{Ts?Ux^$b56WR=?{YA@@)_i2B_{o|v@z(&>H72c_H4NTeHqk7-`X1=+XLSK zKYoehejGP~-#4P)%favET^+5?5&FpXz)N#FFQfkb*PPBvcm}^Gq5q$P{#{+vy%}W? zn|B|dhhr)9@doJQwa~}K(8prLs`EHDWE^S#V#bkAjU+zs??U4F`JB!%z;hY!T!DI1 zQ16nW?ybNVg&e3yo%z5yi+w)S+)_LM_yWtbZ&bZv$1CaMf(1*DDIO6WBfujR{)x@L|P!{VX>D+G&M_aP;}7thW0K)D zG6lxnJrh@?YTO60<@iEA4nVgg*!%K%riDGeRSgk@_{kBK3jeD ztkpoC95YB4WkT6xXsj4U%(UpeA0n@xW3|vCz6z-tT^}VS8o4M+&*2oF)mW*fecDj)SJnWc)*8U2;16R2>f2D1$%d|}p{n}=@ImtpeEG!)GiQ4X zzR8}~Uz&s`S}zcnC+L0>p0uJ358teQYdr!@luP>*b%Z*a5NlfHZ9cIt^yiQlz8w(D{jkm_0ei*)s~>UN6ZMAnPWkQy z{gl6+Phc$xYnD4qK2JTU$6V1b`h%0OgAn$dWWvwkn+j;x(zjNx4N%^!Pus~KmS_jI z53&7(o5NwhO^j;hz1w8|^9*>8HCR30xQzL=A;>-cAo~s4eB3^5x~zEBk{icw;Kbgj z35CHx%L~{K)D;|sv+SoJ&u!jd|IY&*vX4Uh6UZZYzFFIe4py_?fNZd~x_#l_Ph(DD z)Pl{A?*m`&QO=~Fcj@`iDy<_9?8xX&xGMwnACx>0b$KUbHw^dKk2iE3>J;*gSf3|7 z8K}D}fY<|aAAxcw{AEx!12)F80hHa1vU^bWJe2)KsToX)?a{s0fP3uE_cX3}9s@koYrwIx zz`=ff7v-OD%MMcS7~5LWwSYNh$RK(N?Ey478Sg}Y^zgX(LD}mC{xP3KeB{}wOHBU# zb?7g{kG`|~-`nNMV`&5B+dSplO!-Hw@}$MZ z%l#U$x-MNlTp1|CHo-VeBjXLb4)yAI0P*(})qMo(ktQXgYW?%StE_f4`miJ7Y%=R9I+W}z@rBr zV{TcVkp`}#Md1gdCyQp!&y+YB?Q_mRU3viJ>1z;M4U0akW=CU5nB+}Ap4^ae2``-$-#_nDu)54m79 z*wtNivn{Oq>h(pI;X&G5 zemt#XIcxqkKt5ystoh8jiRLwUPTZb8x$*(8t1))EePtcavB_hdJ3tIHoB4L}4_s5n zT-op#{nb3a2s}eMJzr%XQJ?rar>M4VMs=rQ-6ji+2c5WW#N zc&zz$k(Y0-KVaW};^&)fFR&HH-;@#OKJ>}4y*JQNjHQ|fdM<*t{uy-B*TR=%abCli z^#Jej8Zy-RM9|E8oIkkr{sC|lw&IP2ESu?trZYpZmJgi7&%FZw#kp=k=;D3QK->Nq z?zaoQ;M@L=LzDI)2bI+c-6QW_!gKm<$isv0N4Sr1#q3LkFQl&i6!+#!S*-(tkNc%H zEz;&blq<69qMd_QJ1!l_iDb14Zr1V8A-EpFeU}cG=5OO(GCD|;*;{JyupfSRpI&dZ z*AnCT5zbHBr(iwU=d+AoLjE7Rlmj_ZXUQY3d$_)Yy6TSSO`!Rp@Pu^Ix6#hxvR{Rm z4Eh2b)$H?`A)5CeLs`4LyY3Y8f3qGQHxx2C1s?*P*LulzU0l}#7smD_peyEM+_Co! z8B0Ar82FwQJi1-z7VVtlv*s^x;~we!w4qbC$!9K|Hf`HPC&|N|kTvVL}Rd7~o}F-8JruE5oiD{LK!<9ZIbw2t68+lgLLv_DZt zIOfLjd38j$tH+NRohVB>|IyGH(-HE;#xo7N3mS)YG+k8K(R6bmcy4qU{A6DCyQr_% zQ?;$c$A9W1=N4zC;6VJU>Ar;Huc49iW6nQS9_P_G8i%566u77Ln7v8t_Ya}# z+`~e>(|M99P8)N4yU~YNK*KBWL$894S24bR5470+`6R~3xu_8W9z2&s~8unV|rq2 z)j@sQIq~;ky;a)|%4wa!nCRNhn~)LL`!()BqK)Xd6gb`l4%e2G(Ehr+O&zK4@3xeU490$onw< zAg_6JygR87d%Y3Q(SNVf^KG*aB;3^ko*Z=ZDaG(p;Qcn?yB`PkJ?WT`aRBrN*B2QM!=!-M!@k%3LG`SL3)Ea zZ^AV>lb0a<^f{#aM$jF^dn|mf?@Yda&n|J^fVNzJ5(V!md-BuF*;jU54je^66Jv6& z@G|DGe*4Dzd-^>w#_r75YW*ozZU2d~d-2RaelzxW%du`O7J)y-ddCv@%J_T?w6MHA z9~%mqScdKBI929jY|F2&(fp$QLA(nXMgN17@h&6$Nr(6fcdp^)F5Gd6^Fmw4uy?F(Y zySZNWcc`P+JM|t{#8i5|%ejz$?Ni50z!$<80G@jH!v^AOot!82nu`Ek|EgsWpTq3= zBlIL7dQxfTEl=Q{J^y(SFg^n8nXl}YGA0KV?z#`~LDz1ln0WL7+#^5T!(JCTtNaIe zaF}y?oVn(~xnDEdXmRJn(B0!xRNEb-jb-qh^R%^q{e5h5%iuXnI5~q2kym5=IeF54}ZgPiG3vfrWLjF+6+KL6@FeYx==2mww3i9`cSnWSN*w0avX5kP+=ne0|$?tXF1>+<8yS z)51Zz*>|l6v`_SnYn-=;@0gEg+)thd9o6F%_@(Fk@QTc;h#ZPK*9W zW3jkdvvTv_82hvi>$nB+Ir9~y&DCeV`)1q0zjtxX+R794qT7@eb%Hvs@xczCmHA}* z6m{fDYy$rmlz!1Rqp)qw(=lcoMc|_I zWM4x&e9pG`oNa}egXTT@B(y=C{`#60l;JqTvbLQD;5!xy@A@q}^v(ENwuSjO+GSnh z9!fg}{T};78PYwU{tA0w!9z`}rW@C`{d;T{c!&$}S=(;xQHa?r?&&K6wKX?}=KAt7iJOI z$Bv=Qcpr3YIysJL-;|t2w&l`!0(|~7W#Y!iN%W58d3fFr?6Hso z+2cO?dmh@^kL#Eoy$bo-^6oA6>y6G}5<1_5uhB6)*GA8d#&^gU#6(^iTd9xU_a)7q zXUUUs?A3jTUSi%c6ue@d7s8*%ep$BS_v`mqA_`Hcxr z9=4cySo3WicW&YGfcw55(K%R-E5`3l)BQF3XLNoL{)hQl&J|<40xv@D-id(seznWH zR%?&L6!vFD*QqJgGvr&5kHmU1@>1TqF5mGz8svZRuAbw(a|(4?o_l0?4{MMb7xexe z-gj-tkL%e?ks)k`bTdzOCYHZ#*&m|?wlm+=NyzdzayGjlgT>HEV>?RcU!#8q+xa`O z9pry$JDl@|fIDV8sH^=h+9F+1l+(6^c27bsyvIBu=|Nwivxrw+Iw4a{C-edNTBB!^ zK&Q^Jf@h4^Y&s!-()n@F8N|LH{1(paXuZ%kN$5jc9{T9Z;5*&5LVSnC(1-SD-&Q}>HfBz3=ZtC`(@+00CAuk=vu`P`MhR=%c7@0B4>Sst|G4|7pbE1D}&+RAg zQ|`-?^%F9{c;V^iDU|p06ZGnSqMYuhn~M*?egj_L;^`;&gV?XF#qB3xjTe1#`$@Wa zc*e;?@x34Jd|QuuXp4N~K0@v@N_f8taRSThvCTR7QqaG5#d^*@n|vtOCC+(c8f1!n zNI!|q-&?(Tf#d8e>X47@Gwr7a^JP0n;N}N(`~J4AKG9dZdy*NSf=4D70{jEn$4r^P zKS+P|-l5UKY&AQ!N73p-l>3LLFh14xhy614nyr@u!}=k0gx7ac_VdJt&V!E|Zt`9%+lh6c;d-1!EQRrd zu?ll|?ze|V;~DhF_}}89{X;J>>u(C2=F<0YY@xs6Jzi@&!&pEY_mtz< zZMCQ8<`PfZH2i_|$HuCn{jb2*_`a3izYYJ`c?om?-$dfPgu1NTgFM<1J91B_LLZ7^ z@rk#8I}zhJc+NUpJLk8z_|7W7E5|X`hRMsjUA|&0Ie=@Me(uS8NS%e_+-?#=Xg4IhG&nUEnP;wP4%qk;WOxuOJ^DI z61Qm+eF3g#WgI&WI(EH#tdQ#nsa{IwC zN@6^_ZH$3Sp!@rWqWw@T$R zQQ`X+ab5KKQTj;w$2sH&2vI_cB|SAaQTrsTi-ugxbVE;aL#n@dJBf zY!b=C(uAa2#iUn z7aIrrqUTficH1n~2L0Cb=>CDeJn)Kk$<=JAFXwbIcd`G znUfczztAPp6Vo?~7QTbPn22MBZd=#4WYU_*SK^D)X~hF<3wSgSi@onUdG1;5Gp5n~ z039Pizq<}YnlKjWu?%hLcOm%RhC8;Ie)di$_DSyuu0VWq=a8kr396BEE9$#PkCXI| z^%QipuX4x*?a0Q_SGj~3CmElT(9y4Y$#5M?v_YMJf!wI)c3fd}CO(Gh_{{gch~K`E zeEdyCA91MjEqx=6$de#;-OoG;@=NgXL)ZHK57wGToAokZ&ai1xA$6pq67N4izy1O| zr~EJES>~10mz_<&Dx~b7BYI3}NBIW8phx^+JjZU2M=VEQp~oTk_Z(dN*MHd_olFNSul2;=2D8B<(2VyvE?op#daJ{BZuONxUjNBa z8E7+O($DqvVC#CAb$zaNJ=(e+XI)=xU4O#5zSO$D+`7)Qu4h`;S6bJfwyv+Su0La4 zFSM>dXI-quf`fsi4 zZ&}xmnd>uWoKfmtM1S`NHr*dkTQiHW=irPpQZ?ZBh#zv=Gt|8qWeWEm9N}b!hR;WQ zuKzSVHmk^n2UTTmbL568AB;ag;|%9Z8uK$7)S?pI#wgVSWGznivyN(a@J!d46I>C) z$vW9eWrL`xMeg(A4YB8-Y0&bZx6P`|JL1oCollGp9?ft<9U0LK=lKjYa(^K6*?@W| zP^i?4flP#gM+3 zv|g#QLu#7Z5UFggZ$x=EfNvvVNkFLrfk6Vc1n9p^{0K2Gm!ZEr>)S=2#|MP7m3k95 zR^Z@oh4YWF{l_x=-EaQJ@?o5}UxK|OIKK^gqBy^KsZw1yKZlnbgLmK>0q!2n$cS1J~;+%6E=)-w9g4HO_ z&*9vKGqef6*L=Hr&(Yn;D8cEufXWDA09`RiIn6j14OY%AUGgAQ_g8z7oDS2`#HdY^S#5B(>@&i z#aUtV9^$BseK^{&QRncv$~lU2^GFqV8$Ve+3J&S;DCN9~bM0uA@!V+iZ;T3rvXoN< zg{Z=LQWwy z2dApQQJf!|rUpGVO*zj`Qvv?4cJ-BNP|KA{J#i&i{3)fLoue|Jo1>hrIq1V8(VCyp2 z==I83aXnNX=VLe@`bwc=Z|L)n|xTpCM9xM$D$q5T`z$ zCufLN!-$de8KTtZ^W}{9fHe%!>hoARL&O?}X!ZFbIYY!6#`vz!c=uSJA##1rmNUeQ zzfmm^uRcS(`V6t^^T*}<2|2@L@i(dkCacfW;4Y0em4W?hb5fb@nMOFa6A`OJ^1yRo(J$ziDmeZ#J>$wJs;rwrE^rz6Zn|KQ5;#rRZkI) zG8})2gCCdJhl3xN7(GHc1vqZOaXUUR@hu$RHD4r9;qdi!8*A#S!b>7e>(@m9-*nxz zOPlk<;oRKZaClw)hDf-!zP@3>bxXs!v%LGvvS>(M1 z8>KVD;hMUd=F)X8m1mF2Pfxy~ryc}F%GV_)s5HM}eRHX&?S568vATABQ+14kn&^I2 zoY@qqYi`^O<}O*9Zv-!DH<0RQ)z#Xj($%60{ihd|YV1lgW8!ng3OQRcb#=aUR#gOy zuitE$$#dga=1Q*Hgma6@@>^#c904F50o@Ub5V^TOd+Z^QL| zdINiFcQ)p6@}WIVk!H|TRZ-I%E{7K-%9^T3W4JNWT-{i|(PJ2~iZd!}Bjt^yw^|HK z)STqP$b~R8M>arn;&NEpJR9D2T}@qib3{A9WeeZ|J${aj@osV_r=NZ%`(xK2)q55tw4%EMwOk#J;FWuyV)Kpo2KCl!%3HFaUQz{+a( z79`-kvAQvmq^9OA>#W(dDcpe3roIkjHE#}Y$Wh^j`i(Ge=rP=PY;<SYe*0rmV` ze4gmQ9r&;7bNFw2HU4`gj3<2?>X<&QZftU?r8J=lfy`|yNy^Rmh*p1e{2R_d758r) zbg~@%%k8%xP#5>ZfYfg1!zgW!qcZ*n1C4WyKT>yPsEoXvd67ECDv|s}HS21cb+pp7 zdbJvQo!Sx5pM=V|Nj;dk4iUqqNagzGNVqyu4l7sxoQZFY2=SSKy@Nc^lb*5&`DVV+}C;KTURI)u|`Yot4HI%iiVC>YaU{yiMvQB4CQ7AM&dqu%5 zcrbSArh>6^vNly!J(021>2OL4S2WKLg|aiJ0!!##CsZ`O#R*N#Dah_{vZ|(!UG{cx z?D&F$IVFpt8T&GJIEOO|_GBI&{8Z*cgU3!^wh!%xGRJ0Z&p7BjmDz!Kb$iC)j1Fgy zvo+)GLD4{A_Vj}Ba|(fP`kR4*@dq;t##T+8K6cKW(DX%f=47FFS<4DT=*d)EOdT5v zZgr*?7O1iKVAe#ZMP=c`QjD*c3S1=cHvpfD2hR)q2YEhfswqDcAKGI5B7vI(ZV>o8 z0(ap(<{zKe2s~=Kf$tZ%LEs+?yj|c6@c}i~KPd3$ z1da;aE^yW-4Zi;pxKQ8_KD@^ID+In%;0A$zAn^SHpNkK)vHSsnuNU}vf$tZ%;R@5< zD+2Ej_)>h>jrBVOZV`BFjw%0wz$F5Q@Buj8KY?!-xKrRC2)rZL)K~aWoTg9Us|8kh z=J}%nkIFaj34!@-Me_e5d|;0C9};-2!21NgPvBPs-Y;-LfvNvTfmaAT4U=c$YY@0X z;KKs%5V%X=9|=5ahQW6}K5)nS1p==Xc)q~j5}4nFB>g`V_$h(k7dR^LBz!23_@>V^ z<(CWGBJkG*zGaqq{+7TG3EYSe?6Lkafu9yQv(S_uh7a#)`~u%1aI?VQ7kG!jZwmaj zz!%|zeXKuqw!!xqfwv0$Wr3d*_=f_&DDW73=#TYJ3tS^`)|Cd|KM7nY@HO}#Aj>}~ z@Fsy@5%@`gPYe91Y*W7QQwHA`1YRWYPX#_KaLHs-J~YRa|8Ifs75IC|nXvtRSDEJ* zVjLj+%BKzdcLIm5HtNDe!psb(SAB*Whmuc#*(|1#S`eV)$j2e?s8p0`r@tj#BC?0^f3tf$7&- zew)D4;HNcx0^cPtzjey`-2yik8+ac4F3Ue8@K*&sDDcYyzb)`c_+6HtT4L%K0p@sI zBJeZv{7r!`gJ0!yRcgvV2$=Peo6+=K4nN4}+Xa3AFrRk_d{CaB7I?|!=K0ozrv662 ztluv1UU~kazzTko&x6;R@+|_F2poW43ZtoX?v2>)|I^zWF)>za?;R zv4L-Z-z5Eu$_)G+z%1V)a1Q(+;XO;t^S>83dcA>*;Me$k{Bi>y08D&kHyijj0^cF< zCip3qe_r5M1>Uy8l)oN+iuD^-8u%9i-!Je?_$5AnNZ_pkKPB)l0TchT0^bC`L-^<_ zgYQQI-yb$`7=DD$pDQ=;a{@P47`Os{fzP*98u*OB_g5Ks2mAn^ABz~cTi~s$4IG9a zAibeA27U@K%TKR1aPd{<`S`U4ZWlOnoqU1b$E8!)|@p zJ?pFW2H)2N4heif;AH|2FE!=c1#TDkn7{>}G0(?tF!=sW;OPQSn`fRc7kIJ2TLo?t z_jmcbz6rMq z%>ZwkEWG6TOW@TdX< z&qtw=)DMa89~YS4FlYVK0`uGEgl|NHEWaJ&65)q6JPKC^{s#@?8{1P2ydN;i;eYe@ zV*|6Q{=>K5`8)57IEsnu}-^TeJ z97Ax8-$qiu!!>@_QH{XC@tZa@3deakM&lTRgFi}jJ`Vm!=2#rbXz2zX((^|g1EDDs z_y*zVjkaNUehv=&c3r>dya44~TCg_w}%}xyzYoB@Yvq3t* z)Ox&$ek8N|lX@Qz!hvS#q($o~G!Y&(Z z(-VbBwlT*x<^?5nlxtx)ToqYezP`3u?`R7*m#@JN9_*o!O+(?z+VZ+J*yd4-EhO%y z70bj1N`cjrep2ANiaZ^qdHNcT)JE319bQzEQ@W@Ep1|f7+Q3#Q@0OsHh`cOw_k=B~ z$pzkAFJA77Ov*cpHHopIO_@gpaIG~vIYFKddeHQ(NhKEl z2UwLf%I>(Z*#c&(6E+4G49xW=;So(`Be+7-8bCD@&K*}geV!ziz-t8d0m z74WS#(wxfv`@hvEH>WD%?$EL*(_0g<>re0Pl7Y2v0!be?wOWXb6YO^v3oFYT$}6#x zD*=UZyM5C@Q>14!iiNF=2^;%LxnIySs$A4)t`C=2Rnh0g8erKQ+u=iO16XoiW? zXn!k-byBdN)Y)v$ogEI>)>KqBG@wgKL{+w7on1Vy02_&$!q~UnsCVkYhp(=0I{)k_OM_$h`hyOHm!vOK0n`uaCQ<6EuCn z!i0^8MCK=?pO}(V9pC_joy^$wY3&^KIGVE^8fIW~W5kMM1!=!630h+|QznKaZdeCV z4Pd07vAU+QDJ4NEyD70lY$#39ul|~MFWr*B>?7%w!S(0r6m~ZkyF0I!HI~;kl~;0~ zY{Fn@n{7WlVREb~=Llzw(G!DkT>NP0v`5!I_Qa>djtlyyUQM-9UqxJOoEg8!>6c*7|T|O=IQyTD(GH z8Z#3pKLu!v<94Y%?->Z0v2<3y85GO&4%9p4Lt;mD1Ne_AdqY= zLX8C*Fp-eX%m^=?5lc0=gvN{6aqNP1-~gtM%fdJ$IUzMH&6BxCOhR$acxCkw4R%66 zN{{I^i-d_#ucJa95&;qv_!+NfQN1v~Pd$rg8@z8zczkclLbi*F7h>MROVfgN{gAq= zTm$ecc?o`{7jdR=O>xD*%oa*$Pu1OtGGkCT^kh7Zd5j$jFw2vudQffLx2j@DgI8`s zV!*s8C>RmqHJcaXkO7+2P+wDrtN`r-$v1l>8Hg+E(}MfYPRbbFbNLD0Ds?e2fTo=p~0dET-n`0bxvR5PHY_-uC6of~Bxs^OhrC@Li%asCd2C%`1ziOHx;~+2ehI zN6*$}h)LF@RO>AzTEJM!>QXETPTv)&E3d$e7df)!<>gI{dAXQD>sOy+uL#!QIbK&T zEuDck-_}Q(8gmQXGJGR?jYhBl=H`~b`;rBG2U2IT+>W~gd!O$+($~dOJsyMb%-st} zq}8bu5^;TmCJ!NlUe|!Dw~`ka#8N~>FU4~_Z zYdi#_$@O(Q@6WeAUD|yTKy1tOOKp95m0AC==_H9s*lNZ~KkepjZ`8x6=7u@BwtL{R zyjZim#O?vvBR#-GbIm}N2h_`(`*>e}Cb$Gu@`yASA{G36|KG=%UV1{lV63s~hHKU} z)Y7DM3cl2IvSe9hZBrw3I98T$ep7Q5U$m~Qg>`B(h5S5p&0bf&HlkNE`cINJmspRz z1U5!iBQaH3zL~8VL%$9TS-_TU`9NH=jivNG(4<`Z?1i9|YlD5WAP)fo6uYL1iGR%t zs9&TpQdeo)Z(j^Sj?@#S#Vp96)YjG)Pq`Qts~5MVQYe!(m>3t)2QFHdJg7hD!9bWg2GpWPrgKX`Lzyf1)UKXG=iwnKzf+u6<#y4>f(cipMNuDRuK;Pcq zs^${}7WtkG+dwe&wPZ68T#R+~f?&VaZ3fWYz7bRGvpDr!gLM=A9f#)fy5Pm&v2+WzgjmS$N;H!Q>Kq4e$F-;p&h2a44i3vBd9R6NA%D+}>cDYpG9dlUJ~8 zF-ZmP)X^hOoRCz*0L7EEiPFAz@gP*0g-M8K(NBk-k&5+e!Wf6E>epc|#H)mWD9MPF zniQQq*}wqX(^f9rx9)0+cri&EGCS#4-)8vx)_V+=oQv#K%&}Vo0zEQ!DtLy|frnrD zU_oVdgx_vKT#iNAjgj)Ti1zUj1$-k(b0=c0pt&0``tCo6j3wUqgxQDsHF-6bQq}4} z#w!%?0p{LVPsyaxx+T36;I3_PRK#vEOmFmdG4gT-?Mp9THk;fQqvhNgRW)2I;J_W$ zlfw#oyujRCav_zqx5ueBGrrW6Tz4(Et5wz_#4-~-uVwZfUd+}Zg>QbmL^`LV{0)iY zG1r;Nx_s;}-dqt0SHOBItC`l0Q~n{@s%JIAR>N6~TWJfjm1JWLKYn-H`tn+A6sbct ze|0}NDgK}j`N7fZ%mCTw^|u&Z@RclnFDph!-|XyfqfoY3UZO&8V8xK{B_JQ}HP&+2 z$AZS1HPs&>5Bfn4H_CX;79ayb@3yWF=R&bgX!OiGLMCCAH=~G- zessxGE?t}Ss+BxN3T~!7s{-3-iUob^P9K^o{f&7VG()BJSfeyH<4@FBLVJB%f68o# zbjQw;^)u@so4w8Svu_IenT&|D*G@Y0jlQ5RvdOl@7#@?f_UOWg2;v2;-Y$_OV#mDA z9%uSFN$H=)B@8yck158cX!yvOy<+jv45&#OYpPk@j4gm0?cEa}L1S!*X!;ond(E6o zpq=weYkztsAYf7;inExrSY2Dcxe7ZmKXL(? zVPSwGvWSC$XsXu#X@oNXy|IYmnmOc%jZ(;yGOCe?!*bvKG*J#k+AZk0^^(nYBS^T% z8qsmXpQ05CU+?}x%(M=hz#8iNF|V?84?!3v#+%`5EYkBqx<3Ys`MC;g++N#{M*7J? zWOwm}TdJ8}KUl(`db4jejZ;YOaEtd*$7P;vTqzt;ysI=YbQ8!uV?)vfA9qr_1Ti8I z$8*Z@)W_K_>{yfcW_$8(s}2)0GQ+RHDnwi@OYKs9kCDB`Zz*N~+~uA$yI?nM7$Ur_ z@TTXNAg!>ZqWAQCyrz}-ThM#Wq-iHgJ$a%^XQN``vRLR#zN5QIH}$8+jEyquVRdFz z&#W_Y^HLlO#)n#LO4sX)m;>_#rX-6=*Xr>d2_Y!HWqZj{^+t^}VRaE-cJ{6?6v%EV zcLP;wbfvjD@s3z1y$G39!FRxssw;2A9%QTsHes6RTW30}SLez|y1nzq|4HY+sW0SX*u>n`7NA-u> z-B$zo)*||6_7X5{AA9ZCw=$MHI~khf9Q9<|_2QnN1F%Th>?=HqHH*)(m#?v$xp&X! zZ1X{Qd^tZwj|*1U)LL&#dTS4)4>MPn*VNi+ABl8)i!^l131Ne|&if+$nf;w1HFn=?k*0$lZ`bvODj_W+1*gq?L1y}|yPtnu3 z0{Hpy5%k0I>As&SfZO!1VbK+xRrjz`&=_ebr5Ck1rxPLY`w6d>VuMA}EvtVjnk+61 zl%|w>IzKG4I4UQ+sH)voGLBgq-5W*LK2iKY#YgeZYGvy8OzD>Ud|j2lli^u9_ZWGq z5g)b6kUWrGgtE$hR;tPP4KHOO7}tB06Kv2Osm=;HLJjOFtgWwGlYsX_(F_eB%|x7% z@DJ4h2xkLL)ts*P#hO2nRi;=K7lHJq3h{wOopCV<;YRXeB5&UZFLHAuw{fjm zFZo)iYdk+F5aVE|#AU!Pg|P6FW!w@61ZLlRGov zR$f8Et^EANTZy=4BsMrBH=*Lp840&$6(-!m?`kF9%gal0FR=r;g^5k%7A7l~*lu1j zhUDeq1AErr`!5eah_C$}#VvgGPXA@$9=;}-aad*GBaQgE4BV~4vD`dXW0dpBQP~-4 zxWIj!Zk!_p9K2S7-*(Zj(5XE`C%tp@$a}#r&%A!{S3mgJ^4-^b@aU8;Ouy*McSc@2 z_3E)B-~Ov7zPr5s-%hPQnE8t%N7iO6UOwo|9Z%hH`xjn%`kwiBPk(f6&K)_~hp)Zj z%w;ciove8CU+!AI>zBcnyO+MaJZJY0zc~En%rBjM=%$sct}8$E+Z(HzKlY{b5B$Sx zyIK~VzWPVQ?jGHC?Xx%i>UX(~*~6ooHXLi}81~e+a()&*@Y}Ui(huXZz0m__3)^-##qk z&W~rWKUVVPE#LU|uIl|y6+BY&;Ag)3+iT9KmMg#|MRE6 z9C7Ji?F|<0TzYutted}i-(`zVMz);4xar%CTi)%ww)33P^>=MJ`kybI&hO~ztN$PN z`G0I*JfY|5w|;chU+*O0I9^P9q`sc&HbK~c4y6RiseeKIjwhV7^s@BZD ze)he`CNvEHoBT^p&-mnu_0ulCaQud2Tc@p@d*h0t>TfoEd;RYoez5Z8(O-W5$D3}d z*>}-z{^hIVO8?{J4<8-TIOkWxmVPI@?CGygIx*>6i*heGdDr!~e&;W$eqTI3r@Z#Q zk&iq)H}KL|_y6+l%cp*K+hc`4JO9E7^L}!fdaL6L-35Dhz1X?nm+!oO!$02rhd1wf zckS<{=0>`ft^NIq^7F4$^%d`JnE#pCe^pv`=cNl@{LMdYS^dC}`- +#include +#include +#include + +// Structure to store a ratio +struct Ratio { + float value; + int numerator; + int denominator; +}; + +// Generate all 3-limit ratios within [minRatio, maxRatio] +std::vector Generate3LimitRatios(float minRatio = 0.5f, float maxRatio = 2.0f) { + std::vector results; + + // Search powers from -6 to +6 for both primes + for (int n = -6; n <= 6; ++n) { + for (int m = -6; m <= 6; ++m) { + float ratio = powf(2.0f, n) * powf(3.0f, m); + if (ratio >= minRatio && ratio <= maxRatio) { + // Reduce to a fraction approximation + int num = static_cast(roundf(ratio * 10000)); + int den = 10000; + + // Reduce fraction + int a = num, b = den; + while (b != 0) { + int t = b; + b = a % b; + a = t; + } + int gcd = a; + + results.push_back({ ratio, num / gcd, den / gcd }); + } + } + } + + // Sort by ratio value + std::sort(results.begin(), results.end(), [](const Ratio& a, const Ratio& b) { + return a.value < b.value; + }); + + return results; +} + +int main() { + auto ratios = Generate3LimitRatios(0.5f, 2.0f); + for (const auto& r : ratios) { + std::cout << r.numerator << ":" << r.denominator << " (" << r.value << ")" << std::endl; + } + return 0; +} diff --git a/patch/HarmonicOscillator/ratios.h b/patch/HarmonicOscillator/ratios.h new file mode 100644 index 000000000..b82120b5c --- /dev/null +++ b/patch/HarmonicOscillator/ratios.h @@ -0,0 +1,51 @@ +#pragma once + +// Common Chords (triads and simple intervals) +static const float kCommonChords[12] = { + 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f +}; + +// Harmonic Series (partials of a fundamental) +static const float kHarmonicSeries[12] = { + 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f +}; + +// Subharmonics (inverted harmonic series) +static const float kSubharmonics[12] = { + 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.2f, 1.333f, 1.5f, 1.666f, 2.0f, 2.5f, 3.0f +}; + +// Extended Just Intonation Ratios +static const float kFullRatios[12] = { + 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f +}; + +// Pythagorean / 3-limit Tuning +static const float kLimit3[12] = { + 1.0f, 3.0f/2.0f, 4.0f/3.0f, 9.0f/8.0f, 27.0f/16.0f, 16.0f/9.0f, + 2.0f, 8.0f/9.0f, 32.0f/27.0f, 243.0f/128.0f, 81.0f/64.0f, 128.0f/81.0f +}; + +// 5-limit Just Intonation (includes pure thirds) +static const float kLimit5[12] = { + 1.0f, 5.0f/4.0f, 6.0f/5.0f, 4.0f/3.0f, 3.0f/2.0f, 5.0f/3.0f, + 8.0f/5.0f, 9.0f/8.0f, 15.0f/8.0f, 10.0f/9.0f, 16.0f/15.0f, 2.0f +}; + +// 7-limit Just Intonation (includes septimal intervals) +static const float kLimit7[12] = { + 1.0f, 7.0f/6.0f, 8.0f/7.0f, 7.0f/5.0f, 10.0f/7.0f, 7.0f/4.0f, + 9.0f/7.0f, 14.0f/9.0f, 7.0f/3.0f, 21.0f/16.0f, 28.0f/27.0f, 2.0f +}; + +// 11-limit Just Intonation (includes more complex prime ratios) +static const float kLimit11[12] = { + 1.0f, 11.0f/10.0f, 11.0f/8.0f, 9.0f/11.0f, 11.0f/6.0f, 22.0f/15.0f, + 33.0f/22.0f, 11.0f/9.0f, 16.0f/11.0f, 11.0f/7.0f, 11.0f/12.0f, 2.0f +}; + +// Tides-style: harmonics above and subharmonics below +static const float kTidesStyle[12] = { + 1.0f, 3.0f/2.0f, 2.0f, 3.0f, 0.75f, 0.666f, + 0.5f, 0.333f, 1.25f, 1.5f, 2.25f, 2.5f +}; diff --git a/patch/HarmonicOscillator/vox.h b/patch/HarmonicOscillator/vox.h new file mode 100644 index 000000000..a5f72b255 --- /dev/null +++ b/patch/HarmonicOscillator/vox.h @@ -0,0 +1,33 @@ + +#include "daisy_patch.h" +#include "daisysp.h" + +using namespace daisy; +using namespace daisysp; + +class vox { +private: + Oscillator _osc; + +public: + void Init(float sampleRate, DaisyPatch &patch) { + _osc.Init(sampleRate); + _osc.SetWaveForm(Oscillator::WAVE_SAW); + } + + void Read(int pitchPin, int freqPin) { + // fmap + oscFrqKnob = (1023.0 - ) + } + // vox(/* args */); + // ~vox(); + +}; + +// vox::vox(/* args */) +// { +// } + +// vox::~vox() +// { +// } diff --git a/patch/Parametric-eq/Makefile b/patch/Parametric-eq/Makefile new file mode 100644 index 000000000..14c1a488b --- /dev/null +++ b/patch/Parametric-eq/Makefile @@ -0,0 +1,40 @@ +# Project Name +TARGET = parametric_eq + +# Use LGPL version of DaisySP (optional, set to 1 if needed) +USE_DAISYSP_LGPL=1 + +# Library Locations +LIBDAISY_DIR = ../../libDaisy +DAISYSP_DIR = ../../DaisySP + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# Source files +# CPP_SOURCES = bytebeat.cpp +CPP_SOURCES = main.cpp + +# Include the Daisy system Makefile +include $(SYSTEM_FILES_DIR)/Makefile + +# Define the compiled program binary +PROGRAM = parametric_eq.bin +FLASH_ADDR = 0x08000000 + +clean: + rm -rf build + +# Convert ELF to BIN +build/$(PROGRAM): build/$(TARGET).elf + arm-none-eabi-objcopy -O binary $< $@ + +# Flash to Daisy Patch using ST-LINK V3 Mini +program: build/$(PROGRAM) + st-flash --reset write build/$(PROGRAM) $(FLASH_ADDR) + + + + + + diff --git a/patch/Parametric-eq/main.cpp b/patch/Parametric-eq/main.cpp new file mode 100644 index 000000000..14a6dfc81 --- /dev/null +++ b/patch/Parametric-eq/main.cpp @@ -0,0 +1,134 @@ +#include "daisy_patch.h" +#include "daisysp.h" +#include + +using namespace daisy; +using namespace daisysp; + +DaisyPatch patch; +float sample_rate; + +// ------------------------- +// Custom Parametric EQ +// ------------------------- +struct ParametricEQ +{ + float a0, a1, a2, b0, b1, b2; + float z1 = 0.0f, z2 = 0.0f; + float sample_rate = 48000.0f; + + void SetSampleRate(float sr) + { + sample_rate = sr; + } + + void SetParams(float freq, float q, float gain_db) + { + float A = powf(10.0f, gain_db / 40.0f); + float omega = 2.0f * M_PI * freq / sample_rate; + float alpha = sinf(omega) / (2.0f * q); + float cosw = cosf(omega); + + float b0 = 1 + alpha * A; + float b1 = -2 * cosw; + float b2 = 1 - alpha * A; + float a0 = 1 + alpha / A; + float a1 = -2 * cosw; + float a2 = 1 - alpha / A; + + // Normalize + this->b0 = b0 / a0; + this->b1 = b1 / a0; + this->b2 = b2 / a0; + this->a1 = a1 / a0; + this->a2 = a2 / a0; + } + + + float Process(float in) + { + float out = b0 * in + b1 * z1 + b2 * z2 - a1 * z1 - a2 * z2; + z2 = z1; + z1 = out; + return out; + } +}; + +ParametricEQ eq_left, eq_right; + +// ------------------------- +// EQ Initialization & Update +// ------------------------- + +void InitEQ(ParametricEQ& eq) +{ + eq.SetSampleRate(sample_rate); + eq.SetParams(1000.0f, 1.0f, 0.0f); // center, Q, gain +} + +void UpdateEQParams() +{ + patch.ProcessAnalogControls(); + + float freq = patch.controls[DaisyPatch::CTRL_1].Process(); + float gain = patch.controls[DaisyPatch::CTRL_2].Process(); + float q = patch.controls[DaisyPatch::CTRL_3].Process(); + + // float mappedFreq = 20.0f * powf(10.0f, freq * 2.7f); // 20Hz–10kHz + // float mappedGain = (gain * 24.0f) - 12.0f; // -12dB to +12dB + // float mappedQ = 0.1f + q * 9.9f; // 0.1 to 10.0 + + float mappedFreq = 100.0f + freq * 9900.0f; // 100Hz–10kHz (linear) + float mappedGain = (gain * 40.0f) - 20.0f; // -20 to +20 dB + float mappedQ = 0.1f + q * 9.9f; + + eq_left.SetParams(mappedFreq, mappedQ, mappedGain); + eq_right.SetParams(mappedFreq, mappedQ, mappedGain); + + // eq_left.SetParams(1000.0f, 0.5f, 12.0f); // 1kHz, narrow, +12dB + // eq_right.SetParams(1000.0f, 0.5f, 12.0f); +} + +// ------------------------- +// Audio Callback +// ------------------------- + +void AudioCallback(AudioHandle::InputBuffer in, + AudioHandle::OutputBuffer out, + size_t size) +{ + patch.ProcessAllControls(); + UpdateEQParams(); + + for (size_t i = 0; i < size; i++) + { + float left = in[0][i]; + float right = in[1][i]; + + // out[0][i] = eq_left.Process(left); + // out[1][i] = eq_right.Process(right); + out[0][i] = in[0][i]; // bypass + out[1][i] = in[1][i]; + } +} + +// ------------------------- +// Main Entry Point +// ------------------------- + +int main(void) +{ + patch.Init(); + sample_rate = patch.AudioSampleRate(); + + InitEQ(eq_left); + InitEQ(eq_right); + + patch.StartAdc(); + patch.StartAudio(AudioCallback); + + while (1) + { + patch.DelayMs(10); + } +} diff --git a/terrarrium/DoubleDelay/DelayUnit.h b/terrarrium/DoubleDelay/DelayUnit.h new file mode 100644 index 000000000..752f8a0a5 --- /dev/null +++ b/terrarrium/DoubleDelay/DelayUnit.h @@ -0,0 +1,44 @@ +#pragma once + +#include "daisysp.h" +#include "ExtendedDelayLine.h" // Include our wrapper + +using namespace daisy; +using namespace daisysp; + +constexpr size_t MAX_DELAY = 48000 * 5; // 5 seconds max delay + +struct DelayUnit { + ExtendedDelayLine* buffer; // Use ExtendedDelayLine now + float currentDelay = 0.5f; + float delayTarget = 0.5f; + float feedback = 0.5f; + Tone lp_filter; + bool reverse = false; // Flag for reverse playback + + void Init(float samplerate, ExtendedDelayLine& buf) + { + buffer = &buf; + buffer->Reset(); + lp_filter.Init(samplerate); + lp_filter.SetFreq(4000.0f); + } + + float Process(float in, float /*delay_samples*/) + { + // Smooth delay time changes. + fonepole(currentDelay, delayTarget, 0.0002f); + buffer->SetDelay(currentDelay); + + // Choose forward or reverse read. + float delayed = reverse ? buffer->ReverseRead() : buffer->Read(); + + // Mix input with delayed signal using feedback. + float input = in + delayed * feedback; + float filtered = lp_filter.Process(input); + filtered = fminf(fmaxf(filtered, -1.0f), 1.0f); + buffer->Write(filtered); + + return delayed; + } +}; diff --git a/terrarrium/DoubleDelay/DelayUnitReverse.h b/terrarrium/DoubleDelay/DelayUnitReverse.h new file mode 100644 index 000000000..95025f926 --- /dev/null +++ b/terrarrium/DoubleDelay/DelayUnitReverse.h @@ -0,0 +1,51 @@ +#pragma once + +#include "delayline_reverse.h" +#include "daisysp.h" + +using namespace daisy; +using namespace daisysp; + +constexpr size_t MAX_DELAY = 48000 * 5; // 5 seconds max delay + +struct DelayUnitReverse { + daisysp::DelayLineReverse* buffer; + float currentDelay = 0.5f; // in samples (after smoothing) + float delayTarget = 0.5f; + float feedback = 0.5f; + Tone lp_filter; + bool reverse = false; + + // For this reverse unit, we always use reverse playback. + // You could add a flag here if you want to toggle between forward and reverse. + + void Init(float samplerate, daisysp::DelayLineReverse& buf) + { + buffer = &buf; + buffer->Reset(); + lp_filter.Init(samplerate); + lp_filter.SetFreq(4000.0f); + } + + // Process the audio sample using reverse delay. + float Process(float in) + { + // Smooth the delay time changes. + fonepole(currentDelay, delayTarget, 0.002f); + // Use SetDelay1 (as defined in DelayLineReverse) to set the delay time. + buffer->SetDelay1(currentDelay); + + // Choose reading method based on the reverse flag. + float delayed = reverse ? buffer->ReadRev() : buffer->ReadFwd(); + + // Mix input with delayed sample using feedback. + float input = in + delayed * feedback; + float filtered = lp_filter.Process(input); + filtered = fminf(fmaxf(filtered, -1.0f), 1.0f); + + // Write the processed sample into the buffer. + buffer->Write(filtered); + + return delayed; + } +}; diff --git a/terrarrium/DoubleDelay/ExtendedDelayLine.h b/terrarrium/DoubleDelay/ExtendedDelayLine.h new file mode 100644 index 000000000..0ec43dd5f --- /dev/null +++ b/terrarrium/DoubleDelay/ExtendedDelayLine.h @@ -0,0 +1,92 @@ +#pragma once +#ifndef EXTENDED_DELAY_LINE_H +#define EXTENDED_DELAY_LINE_H + +#include +#include +#include + +namespace daisysp +{ + +/** ExtendedDelayLine implements a simple circular buffer delay line, + similar to DaisySP's DelayLine, but with added support for reverse + reading. It holds an internal buffer of type T with maximum size max_size. +*/ +template +class ExtendedDelayLine +{ + public: + ExtendedDelayLine() { Reset(); } + ~ExtendedDelayLine() {} + + /** Resets the delay line by clearing the buffer and setting the write + pointer to 0, and delay to 1 sample. + */ + void Reset() + { + for (size_t i = 0; i < max_size; i++) + { + buffer_[i] = T(0); + } + write_ptr_ = 0; + delay_ = 1; + frac_ = 0.0f; + } + + /** Sets the delay time in samples. + If a float is passed, the fractional part is stored for interpolation. + */ + inline void SetDelay(float delay) + { + int32_t int_delay = static_cast(delay); + frac_ = delay - static_cast(int_delay); + delay_ = (int_delay < (int32_t)max_size) ? int_delay : max_size - 1; + } + + /** Writes a sample into the delay line and advances the write pointer. + (This implementation writes forward, unlike the original DaisySP version.) + */ + inline void Write(const T sample) + { + buffer_[write_ptr_] = sample; + write_ptr_ = (write_ptr_ + 1) % max_size; + } + + /** Forward read: + Reads the delayed sample using linear interpolation. + */ + inline T Read() const + { + // Calculate read pointer for forward mode: + size_t index1 = (write_ptr_ + max_size - delay_) % max_size; + size_t index2 = (index1 + 1) % max_size; + T a = buffer_[index1]; + T b = buffer_[index2]; + return a + (b - a) * frac_; + } + + /** Reverse read: + Reads the delayed sample "backwards" from the write pointer. + The effective read pointer is calculated as (write_ptr_ + delay_) modulo max_size. + Linear interpolation is used between two adjacent samples. + */ + inline T ReverseRead() const + { + size_t index1 = (write_ptr_ + delay_) % max_size; + size_t index2 = (index1 + max_size - 1) % max_size; + T a = buffer_[index1]; + T b = buffer_[index2]; + return a + (b - a) * frac_; + } + + private: + float frac_; // fractional part of the delay time for interpolation + size_t write_ptr_; // current write pointer index + size_t delay_; // integer delay in samples + T buffer_[max_size]; +}; + +} // namespace daisysp + +#endif // EXTENDED_DELAY_LINE_H diff --git a/terrarrium/DoubleDelay/Makefile b/terrarrium/DoubleDelay/Makefile new file mode 100644 index 000000000..17a6cb0ff --- /dev/null +++ b/terrarrium/DoubleDelay/Makefile @@ -0,0 +1,37 @@ +# Project Name +TARGET = dbl_delay + +# Use LGPL version of DaisySP (optional, set to 1 if needed) +USE_DAISYSP_LGPL=1 + +# Library Locations +LIBDAISY_DIR = ../../libDaisy +DAISYSP_DIR = ../../DaisySP + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# Source files +# CPP_SOURCES = bytebeat.cpp +CPP_SOURCES = main.cpp + +# Include the Daisy system Makefile +include $(SYSTEM_FILES_DIR)/Makefile + +# Define the compiled program binary +PROGRAM = dbl_delay.bin +FLASH_ADDR = 0x08000000 + +# Convert ELF to BIN +build/$(PROGRAM): build/$(TARGET).elf + arm-none-eabi-objcopy -O binary build/$(TARGET).elf build/$(PROGRAM) + +# Flash to Daisy Patch using ST-LINK V3 Mini +program: build/$(PROGRAM) + st-flash --reset write build/$(PROGRAM) $(FLASH_ADDR) + + + + + + diff --git a/terrarrium/DoubleDelay/delayline_reverse.h b/terrarrium/DoubleDelay/delayline_reverse.h new file mode 100644 index 000000000..42ce856a6 --- /dev/null +++ b/terrarrium/DoubleDelay/delayline_reverse.h @@ -0,0 +1,207 @@ +// Copyright 2021 Adam Fulford +// +// 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 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. +// +// See http://creativecommons.org/licenses/MIT/ for more information. + +#pragma once +#ifndef DSY_DELAY_REVERSE_H +#define DSY_DELAY_REVERSE_H +#include +#include +#include + +namespace daisysp + +{ +/** Reverse Delay line +By: Adam Fulford +*/ +template + +class DelayLineReverse +{ + public: + DelayLineReverse() {} + ~DelayLineReverse() {} + /** initializes the delay line by clearing the values within, and setting delay to min time. + */ + void Init() + { + Reset(); + } + /** clears buffer, sets write ptr to 0, and delay to 1 sample. + */ + void Reset() + { + + delay1_ = 25000; // min Reverse delay time + fadetime = 24000; // in samples = 0.5 seconds + + for(size_t i = 0; i < max_size; i++) + { + line_[i] = T(0); + } + write_ptr_ = 0; + read_ptr1_ = 0; + read_ptr2_ = 0; + headDiff_ = 0; + playinghead_ = false; + fadepos_ = 0.0f; + fading_ = false; + } + + /** sets the delay time in samples + */ + inline void SetDelay1(size_t delay) + { + frac1_ = 0.0f; + delay1_ = delay < max_size ? delay : max_size - 1; + } + + /** sets the delay time in samples + If a float is passed in, a fractional component will be calculated for interpolating the delay line. + */ + inline void SetDelay1(float delay) + { + int32_t int_delay = static_cast(delay); + frac1_ = delay - static_cast(int_delay); + delay1_ = static_cast(int_delay) < max_size ? int_delay + : max_size - 1; + } + + /** writes the sample of type T to the delay line, and advances the write ptr + */ + inline void Write(const T sample) + { + line_[write_ptr_] = sample; + //advance write ptr in forward direction + write_ptr_ = (write_ptr_ + 1 + max_size) % max_size; //increment forwards + + //increment head difference + headDiff_ = (headDiff_ + 1 + delay1_) % delay1_; + + //advance read ptrs in reverse direction + read_ptr1_ = (read_ptr1_ - 1 + max_size) % max_size; + read_ptr2_ = (read_ptr2_ - 1 + max_size) % max_size; + + if (headDiff_ > (delay1_ - fadetime - 1)) //start cross fade region + { + if(!fading_) + { + fading_ = true; //start fading + + if(!playinghead_) + { + //jump ptr2 to fadetime beyond write position + read_ptr2_ = write_ptr_ - 1; + } + + else + { + //jump ptr1 to fadetime beyond write position + read_ptr1_ = write_ptr_ - 1; + } + } + + else + { + //continue fading + } + } + + if(fading_) + { + if(!playinghead_) //head1 playing + { + fadepos_ = fadepos_ + (1.0f / (fadetime) ); //increment up to 1. + if (fadepos_ > 1.0f) + { + fadepos_ = 1.0f; + fading_ = false; //stop fading + playinghead_ = true; + } + } + + else //head2 playing + { + fadepos_ = fadepos_ - (1.0f / (fadetime) ); //increment down to 0 + if (fadepos_ < 0.0f) + { + fadepos_ = 0.0f; + fading_ = false; //stop fading + playinghead_ = false; + } + } + } + else //not fading + { + + } + + } + + /** returns the next sample of type T in the delay line, interpolated if necessary. + */ + inline const T ReadRev() const + { + T a1 = line_[read_ptr1_]; + T a2 = line_[(read_ptr2_)]; + + float read1 = a1; + float read2 = a2; + + float scalar_1, scalar_2; + + //hann + //scalar_1 = sinf(fadepos_ * ((float)M_PI * 0.5f)); + //scalar_2 = sinf((1.0f - fadepos_) * ((float)M_PI * 0.5f)); + + //flattenned hann + scalar_1 = 2.0f*( (9.0f/16.0f)*sinf(float(M_PI)*(1.0f/2.0f)*fadepos_) + (1.0f/16.0f)*sinf(float(M_PI)*(3.0f/2.0f)*fadepos_) ); + scalar_2 = 2.0f*( (9.0f/16.0f)*sinf(float(M_PI)*(1.0f/2.0f)*(1.0f - fadepos_)) + (1.0f/16.0f)*sinf(float(M_PI)*(3.0f/2.0f)*(1.0f - fadepos_)) ); + + return (read2 * scalar_1) + (read1 * scalar_2); + } + + /** returns the next sample of type T in the delay line, interpolated if necessary. + */ + inline const T ReadFwd() const //read forward as feedback signal + { + T a = line_[(write_ptr_ + delay1_) % max_size]; + T b = line_[(write_ptr_ + delay1_ + 1) % max_size]; + return a + (b - a) * frac1_; + } + + private: + float frac1_; + size_t write_ptr_; + size_t read_ptr1_; + size_t read_ptr2_; + size_t delay1_; + size_t headDiff_; + T line_[max_size]; + size_t fadetime; + bool playinghead_; + float fadepos_; + bool fading_; + +}; +} // namespace daisysp +#endif \ No newline at end of file diff --git a/terrarrium/DoubleDelay/main.1.0.3.cpp b/terrarrium/DoubleDelay/main.1.0.3.cpp new file mode 100644 index 000000000..d338759b9 --- /dev/null +++ b/terrarrium/DoubleDelay/main.1.0.3.cpp @@ -0,0 +1,131 @@ +// Dual Delay Pedal - Version 1.0.3 + + + +#include "daisysp.h" +#include "daisy_petal.h" +#include "terrarium.h" + +using namespace daisy; +using namespace daisysp; +using namespace terrarium; + +DaisyPetal petal; +Parameter param_feedback, param_mix; +Led led1, led2; + +Tone lp_filter; + +float samplerate; +bool effect_enabled = true; + +constexpr size_t MAX_DELAY = 48000 * 5; // 5 seconds max delay + +DSY_SDRAM_BSS DelayLine delayBuffer; + +struct DelayUnit { + DelayLine* buffer; + float currentDelay = 0.5f; + float delayTarget = 0.5f; + float feedback = 0.5f; + Tone lp_filter; + + void Init(float samplerate, DelayLine& buf) + { + buffer = &buf; + buffer->Init(); + lp_filter.Init(samplerate); + lp_filter.SetFreq(4000.0f); + } + + float Process(float in, float delay_samples) + { + fonepole(currentDelay, delayTarget, 0.0002f); + buffer->SetDelay(currentDelay); + + float delayed = buffer->Read(); + float input = in + delayed * feedback; + float filtered = lp_filter.Process(input); + filtered = fminf(fmaxf(filtered, -1.0f), 1.0f); + buffer->Write(filtered); + + return delayed; + } +}; + +DelayUnit delay; + +void InitParams() +{ + param_feedback.Init(petal.knob[Terrarium::KNOB_2], 0.0f, 1.0f, Parameter::LINEAR); + param_mix.Init(petal.knob[Terrarium::KNOB_3], 0.0f, 1.0f, Parameter::LINEAR); +} + +void InitLeds() +{ + led1.Init(petal.seed.GetPin(Terrarium::LED_1), false); + led2.Init(petal.seed.GetPin(Terrarium::LED_2), false); +} + +void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) +{ + float knob = petal.knob[Terrarium::KNOB_1].Process(); + float delay_time = knob < 0.5f + ? fmap(knob * 2.0f, 0.01f, 1.0f) + : fmap((knob - 0.5f) * 2.0f, 1.0f, 5.0f); + + float mix = param_mix.Process(); + float delay_samples = delay_time * samplerate; + + delay.delayTarget = delay_samples; + delay.feedback = param_feedback.Process(); + + for (size_t i = 0; i < size; i++) + { + float dry = in[0][i]; + + float delayed = delay.Process(dry, delay_samples); + + float wet = delayed * 0.3f; + float mixed = (1.0f - mix) * dry + mix * wet; + + float output = effect_enabled ? mixed : dry; + out[0][i] = out[1][i] = output; + } +} + +int main(void) +{ + petal.Init(); + lp_filter.Init(petal.AudioSampleRate()); + lp_filter.SetFreq(4000.0f); + samplerate = petal.AudioSampleRate(); + + InitParams(); + InitLeds(); + + delay.Init(samplerate, delayBuffer); + petal.StartAdc(); + petal.StartAudio(AudioCallback); + + while (1) + { + petal.ProcessAnalogControls(); + petal.ProcessDigitalControls(); + + if (petal.switches[Terrarium::FOOTSWITCH_1].RisingEdge()) + { + effect_enabled = !effect_enabled; + } + + // LED1 indicates if effect is enabled + led1.Set(effect_enabled ? 1.0f : 0.0f); + led1.Update(); + + // LED2 shows mix level from KNOB_3 + led2.Set(param_mix.Process()); + led2.Update(); + + System::Delay(10); + } +} diff --git a/terrarrium/DoubleDelay/main.cpp b/terrarrium/DoubleDelay/main.cpp new file mode 100644 index 000000000..2f2a05364 --- /dev/null +++ b/terrarrium/DoubleDelay/main.cpp @@ -0,0 +1,158 @@ +// Dual Delay Pedal - Version 1.1.5 +// Rerverse delay is working, controls are not working +// Two 5 second delays with almost infinite feedback. + +#include "daisysp.h" +#include "daisy_petal.h" +#include "terrarium.h" +#include "DelayUnitReverse.h" + +using namespace daisy; +using namespace daisysp; +using namespace terrarium; + +DaisyPetal petal; +Parameter param_feedback_1, param_mix_1, param_feedback_2, param_mix_2; +Led led1, led2; +DelayUnitReverse delay1_rev, delay2_rev; +CrossFade cfade; + +DSY_SDRAM_BSS daisysp::DelayLineReverse reverseDelayBuffer1; +DSY_SDRAM_BSS daisysp::DelayLineReverse reverseDelayBuffer2; + +float samplerate; +bool bypassDelay1 = false; +bool bypassDelay2 = false; + +/////////////////////////////////////////////////////////////// +// Init Params + +void InitParams() +{ + // Initialize parameters for Delay1 ----------------------- + param_feedback_1.Init(petal.knob[Terrarium::KNOB_2], 0.0f, 1.0f, Parameter::LINEAR); + param_mix_1.Init(petal.knob[Terrarium::KNOB_3], 0.0f, 1.0f, Parameter::LINEAR); + // Initialize parameters for Delay2 ----------------------- + param_feedback_2.Init(petal.knob[Terrarium::KNOB_5], 0.0f, 1.0f, Parameter::LINEAR); + param_mix_2.Init(petal.knob[Terrarium::KNOB_6], 0.0f, 1.0f, Parameter::LINEAR); +} + +/////////////////////////////////////////////////////////////// +// Init LEDs + +void InitLeds() +{ + led1.Init(petal.seed.GetPin(Terrarium::LED_1), false); + led2.Init(petal.seed.GetPin(Terrarium::LED_2), false); +} + +/////////////////////////////////////////////////////////////// +// AudioCallback + +void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) +{ + // Delay 1 ------------------------------------------------ + float knob1 = 1.0f - petal.knob[Terrarium::KNOB_1].Process(); + // 0 - 0.5 -> 0 - 1s delay time, 0.5 - 1.0 -> 1.0 - 5.0s delay time + float delay1_time = knob1 < 0.5f ? fmap(knob1 * 2.0f, 0.1f, 1.0f) + : fmap((knob1 - 0.5f) * 2.0f, 1.0f, 5.0f); + + float delay1_samples = delay1_time * samplerate; + float delay1_mix = param_mix_1.Process(); + + delay1_rev.delayTarget = delay1_samples; + delay1_rev.feedback = param_feedback_1.Process(); + + // Delay 2 ------------------------------------------------ + float knob4 = 1.0f - petal.knob[Terrarium::KNOB_4].Process(); + // 0 - 0.5 -> 0 - 1s delay time, 0.5 - 1.0 -> 1.0 - 5.0s delay time + float delay2_time = knob4 < 0.5f ? fmap(knob4 * 2.0f, 0.1f, 1.0f) + : fmap((knob4 - 0.5f) * 2.0f, 1.0f, 5.0f); + + float delay2_samples = delay2_time * samplerate; + float delay2_mix = param_mix_2.Process(); + + delay2_rev.delayTarget = delay2_samples; + delay2_rev.feedback = param_feedback_2.Process(); + + // ffective wet signal from both delays: + float wetScale1 = delay1_mix; // or any mapping you prefer + float wetScale2 = delay2_mix; // or combine them differently + + // In the processing loop: + for (size_t i = 0; i < size; i++) + { + float dry = in[0][i]; + + float d1 = bypassDelay1 ? delay1_rev.Process(0.0f) : delay1_rev.Process(dry); + float d2 = bypassDelay2 ? delay2_rev.Process(0.0f) : delay2_rev.Process(dry); + + // Sum the wet outputs using the mix parameters: + float wet = d1 * wetScale1 + d2 * wetScale2; + + // Use the CrossFade object to blend dry and wet signals. + // This ensures constant power blending based on effectiveMix. + float finalMix = cfade.Process(dry, wet); + + out[0][i] = out[1][i] = finalMix; + } + + +} + +/////////////////////////////////////////////////////////////// +// Main + +int main(void) +{ + // Initialize patch --------------------------------------- + petal.Init(); + samplerate = petal.AudioSampleRate(); + + // Initialize Params -------------------------------------- + InitParams(); + InitLeds(); + + // Initialize CrossFade ----------------------------------- + cfade.Init(); + cfade.SetCurve(CROSSFADE_CPOW); + + // Initialize Delay units --------------------------------- + delay1_rev.Init(samplerate, reverseDelayBuffer1); + delay2_rev.Init(samplerate, reverseDelayBuffer2); + + // Start Adc and start AudioCallback ---------------------- + petal.StartAdc(); + petal.StartAudio(AudioCallback); + + while (1) + { + petal.ProcessAnalogControls(); + petal.ProcessDigitalControls(); + + // Use SWITCH_1 to control reverse mode for delay1: + delay1_rev.reverse = petal.switches[Terrarium::SWITCH_1].Pressed(); + // And similarly for delay2 if desired: + delay2_rev.reverse = petal.switches[Terrarium::SWITCH_2].Pressed(); + + // + if (petal.switches[Terrarium::FOOTSWITCH_1].RisingEdge()) + { + bypassDelay1 = !bypassDelay1; + } + + if (petal.switches[Terrarium::FOOTSWITCH_2].RisingEdge()) + { + bypassDelay2 = !bypassDelay2; + } + + // LED1 indicates if effect is enabled + led1.Set(!bypassDelay1 ? 1.0f : 0.0f); + led2.Set(!bypassDelay2 ? 1.0f : 0.0f); + + led1.Update(); + led2.Update(); + + System::Delay(10); + } +} diff --git a/terrarrium/DoubleDelay/notes/controls.cpp b/terrarrium/DoubleDelay/notes/controls.cpp new file mode 100644 index 000000000..b5e020621 --- /dev/null +++ b/terrarrium/DoubleDelay/notes/controls.cpp @@ -0,0 +1,56 @@ +#include "daisysp.h" +#include "daisy_petal.h" +#include "terrarium.h" + +using namespace daisy; +using namespace daisysp; +using namespace terrarium; + +DaisyPetal petal; + +Parameter param; + +Led led1, led2; + +void InitParam() +{ // Initialize param + // Curves: LINEAR, EXPONENTIAL, LOGARITHMIC, CUBE + param.Init(petal.knob[Terrarium::KNOB_1], 0.0, 1.0, Parameter::LINEAR); // swell is knob 6 +} + +void InitLeds(void) +{ + // Initialize the leds - these are using LED objects + led1.Init(petal.seed.GetPin(Terrarium::LED_1),false); + led2.Init(petal.seed.GetPin(Terrarium::LED_2),false); +} + +int main(void) +{ + petal.Init(); + + InitParam(); + InitLeds(); + + petal.StartAdc(); + + bool ledOn = false; + + while (1) + { + petal.ProcessAnalogControls(); + petal.ProcessDigitalControls(); + + float val = param.Process(); // swell_val = powf(swellParam.Process(),1/1.5); + led1.Set(val > 0.5f); + led1.Update(); + + if(petal.switches[Terrarium::FOOTSWITCH_2].RisingEdge()) { + ledOn = !ledOn; + } + + led2.Set(ledOn); + led2.Update(); + } +} + diff --git a/terrarrium/DoubleDelay/notes/delay_module.h b/terrarrium/DoubleDelay/notes/delay_module.h new file mode 100644 index 000000000..dc69dd820 --- /dev/null +++ b/terrarrium/DoubleDelay/notes/delay_module.h @@ -0,0 +1,166 @@ +#pragma once +#ifndef DELAY_MODULE_H +#define DELAY_MODULE_H + +#include "Delays/delayline_reverse.h" +#include "Delays/delayline_revoct.h" +#include "base_effect_module.h" +#include "daisysp.h" +#include +#ifdef __cplusplus + +/** @file _delay_module.h */ + +using namespace daisysp; + +// Delay Max Definitions (Assumes 48kHz samplerate) +constexpr size_t MAX_DELAY_NORM = + static_cast(48000.0f * 8.f); // 4 second max delay // Increased the max to 8 seconds, got horrible pop noise when set to 4 + // seconds, increasing buffer size fixes it for some reason. TODO figure out why? +constexpr size_t MAX_DELAY_REV = + static_cast(48000.0f * 8.f); // 8 second max delay (needs to be double for reverse, since read/write pointers are going + // opposite directions in the buffer) +constexpr size_t MAX_DELAY_SPREAD = static_cast(4800.0f); // 50 ms for Spread effect + +// This is the core delay struct, which actually includes two delays, +// one for forwared/octave, and one for reverse. This is required +// because the reverse delayline needs to be double the size of the +// normal delayline to have the same range of the Time control. Both +// delays are processed, with the main delay feeding into the reverse +// delay to allow for Reverse Octave. A lowpass filter is included in +// the feedback loop, which can tame the harsh high frequencies of the +// octave delay, or create a "fading into the distance" effect for the +// forward and reverse delays. A "level" param is included for modulation +// of the output volume, for stereo panning. +struct delayRevOct { + DelayLineRevOct *del; + DelayLineReverse *delreverse; + float currentDelay; + float delayTarget; + float feedback = 0.0; + float active = false; + bool reverseMode = false; + Tone toneOctLP; // Low Pass + float level = 1.0; // Level multiplier of output, added for stereo modulation + float level_reverse = 1.0; // Level multiplier of output, added for stereo modulation + bool dual_delay = false; + bool secondTapOn = false; + + float Process(float in) { + // set delay times + fonepole(currentDelay, delayTarget, .0002f); + del->SetDelay(currentDelay); + delreverse->SetDelay1(currentDelay); + + float del_read = del->Read(); + + float read_reverse = delreverse->ReadRev(); // REVERSE + + float read = + toneOctLP.Process(del_read); // LP filter, tames harsh high frequencies on octave, has fading effect for normal/reverse + + float secondTap = 0.0; + if (secondTapOn) { + secondTap = del->ReadSecondTap(); + } + // float read2 = delreverse->ReadFwd(); + if (active) { + del->Write((feedback * read) + in); + delreverse->Write((feedback * read) + + in); // Writing the read from fwd/oct delay line allows for combining oct and rev for reverse octave! + // delreverse->Write((feedback * read2) + in); + } else { + del->Write(feedback * read); // if not active, don't write any new sound to buffer + delreverse->Write(feedback * read); + // delreverse->Write((feedback * read2)); + } + + // TODO Figure out how to do dotted eighth with reverse + + if (dual_delay) { + return read_reverse * level_reverse * 0.5 + + (read + secondTap) * level * 0.5; // Half the volume to keep total level consistent + } else if (reverseMode) { + return read_reverse * level_reverse; + } else { + return (read + secondTap) * level; + } + } +}; + +// For stereo spread setting (delay the right channel signal from 0 to 50ms) +// A short, zero feedback (one repeat) delay for stereo spread + +struct delay_spread { + DelayLine *del; + float currentDelay; + float delayTarget; + float active = false; + + float Process(float in) { + // set delay times + fonepole(currentDelay, delayTarget, .0002f); + del->SetDelay(currentDelay); + + float read = del->Read(); + if (active) { + del->Write(in); + } + + return read; + } +}; + +namespace bkshepherd { + +class DelayModule : public BaseEffectModule { + public: + DelayModule(); + ~DelayModule(); + + void Init(float sample_rate) override; + void UpdateLEDRate(); + void CalculateDelayMix(); + void ParameterChanged(int parameter_id) override; + void ProcessModulation(); + void ProcessMono(float in) override; + void ProcessStereo(float inL, float inR) override; + void SetTempo(uint32_t bpm) override; + float GetBrightnessForLED(int led_id) const override; + + private: + float m_delaylpFreqMin; + float m_delaylpFreqMax; + float m_delaySamplesMin; + float m_delaySamplesMax; + float m_delaySpreadMin; + float m_delaySpreadMax; + float m_pdelRight_out; + float m_currentMod; + + Oscillator modOsc; + float m_modOscFreqMin; + float m_modOscFreqMax; + + // Delays + delayRevOct delayLeft; + delayRevOct delayRight; + delay_spread delaySpread; + + // Mix params + float delayWetMix = 0.5; + float delayDryMix = 0.5; + float WetMix = 0.5; + float DryMix = 0.5; + + float _level = 1.0; + + float effect_samplerate; + + // Oscillator for blinking tempo LED + Oscillator led_osc; + float m_LEDValue; +}; +} // namespace bkshepherd +#endif +#endif \ No newline at end of file diff --git a/terrarrium/DoubleDelay/notes/delayline.cpp b/terrarrium/DoubleDelay/notes/delayline.cpp new file mode 100644 index 000000000..2462ba5c5 --- /dev/null +++ b/terrarrium/DoubleDelay/notes/delayline.cpp @@ -0,0 +1,125 @@ +#include "daisysp.h" +#include "daisy_seed.h" + +// Interleaved audio definitions +#define LEFT (i) +#define RIGHT (i + 1) + +// Set delay time range in samples. +#define MIN_DELAY 24 +#define MAX_DELAY 63962 + +using namespace daisysp; +using namespace daisy; + +// Set up Daisy hardware seed +static DaisySeed hw; + +// Declare the button +Switch button1; + +// Declare DelayLines of MAX_DELAY number of floats. +static DelayLine(MAX_DELAY)> delL; +static DelayLine(MAX_DELAY)> delR; + +// Setup.. +const int num_adc_channels = 3; +const int blockSize = 1; + +// Scale the feedback signal +const float feedbackScalar=1.0; + + +// +// Audio Callback function (the audio loop) +// +static void AudioCallback(AudioHandle::InterleavingInputBuffer in, + AudioHandle::InterleavingOutputBuffer out, + size_t size) +{ + float feedbackL, feedbackR, delL_out, delR_out, sigL_out, sigR_out, del; + + for(size_t i = 0; i < size; i += 2) + { + + float knob = hw.adc.GetFloat(0); + float knob2 = hw.adc.GetFloat(1); + float knob3 = hw.adc.GetFloat(2); + + knob *= knob; + del = ((MAX_DELAY - MIN_DELAY) * knob)+(MIN_DELAY); + delL.SetDelay(del); + delR.SetDelay(del); + + button1.Debounce(); + float btn = button1.Pressed(); + + // Read from delay line + delL_out = delL.Read(); + delR_out = delR.Read(); + + // Calculate output mix + if(btn){ + if (knob3 <= .5){ + sigL_out = ((in[LEFT]) + (delL_out * (knob3*2.0f))); + sigR_out = ((in[RIGHT]) + (delR_out * (knob3*2.0f))); + }else{ + sigL_out = (((in[LEFT])*(1-((knob3-0.5f)*2.0f)))) + (delL_out); + sigR_out = (((in[RIGHT])*(1-((knob3-0.5f)*2.0f)))) + (delR_out); + } + }else{ + sigL_out = in[LEFT]; + sigR_out = in[RIGHT]; + } + + // Calculate feedback + feedbackL = (delL_out * knob2 * feedbackScalar) + in[LEFT]; + feedbackR = (delR_out * knob2 * feedbackScalar) + in[RIGHT]; + + // Write to the delay + delL.Write(feedbackL * btn); + delR.Write(feedbackR * btn); + + // Output + out[LEFT] = sigL_out; + out[RIGHT] = sigR_out; + } +} + +int main(void) +{ + // initialize seed hardware and daisysp modules + float sample_rate; + hw.Configure(); + hw.Init(); + hw.SetAudioBlockSize(blockSize); + sample_rate = hw.AudioSampleRate(); + delL.Init(); + delR.Init(); + + //This is our ADC configuration + AdcChannelConfig adcConfig[num_adc_channels]; + + //Configure pin 21 as an ADC input. This is where we'll read the knob. + adcConfig[0].InitSingle(hw.GetPin(21)); + adcConfig[1].InitSingle(hw.GetPin(22)); + adcConfig[2].InitSingle(hw.GetPin(23)); + + //Initialize the adc with the config we just made + hw.adc.Init(adcConfig, num_adc_channels); + + //Initialize the button + button1.Init(hw.GetPin(28),1000); + + // Set Default Delay time + delL.SetDelay(sample_rate * 0.75f); + delR.SetDelay(sample_rate * 0.75f); + + // Start reading ADC values + hw.adc.Start(); + + // Start audio loop + hw.StartAudio(AudioCallback); + + while(1) {} +} \ No newline at end of file diff --git a/terrarrium/DoubleDelay/notes/main-v1.cpp b/terrarrium/DoubleDelay/notes/main-v1.cpp new file mode 100644 index 000000000..00e7e6b1f --- /dev/null +++ b/terrarrium/DoubleDelay/notes/main-v1.cpp @@ -0,0 +1,91 @@ +// Dual Delay Pedal - Version 1.0.3 +#include "daisysp.h" +#include "daisy_petal.h" +#include "terrarium.h" +#include "DelayUnit.h" + +using namespace daisy; +using namespace daisysp; +using namespace terrarium; + +DaisyPetal petal; +Parameter param_feedback, param_mix; +Led led1, led2; +DelayUnit delay; + +Tone lp_filter; + +float samplerate; +bool effect_enabled = true; + +DSY_SDRAM_BSS DelayLine delayBuffer; + + +void InitParams() +{ + param_feedback.Init(petal.knob[Terrarium::KNOB_2], 0.0f, 1.0f, Parameter::LINEAR); + param_mix.Init(petal.knob[Terrarium::KNOB_3], 0.0f, 1.0f, Parameter::LINEAR); +} + +void InitLeds() +{ + led1.Init(petal.seed.GetPin(Terrarium::LED_1), false); + led2.Init(petal.seed.GetPin(Terrarium::LED_2), false); +} + +void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) +{ + float knob = petal.knob[Terrarium::KNOB_1].Process(); + float delay_time = knob < 0.5f + ? fmap(knob * 2.0f, 0.01f, 1.0f) + : fmap((knob - 0.5f) * 2.0f, 1.0f, 5.0f); + float mix = param_mix.Process(); + + float delay_samples = delay_time * samplerate; + delay.delayTarget = delay_samples; + delay.feedback = param_feedback.Process(); + + for (size_t i = 0; i < size; i++) + { + float dry = in[0][i]; + float delayed = delay.Process(dry, delay_samples); + + float wet = delayed * 0.3f; + float mixed = (1.0f - mix) * dry + mix * wet; + + float output = effect_enabled ? mixed : dry; + out[0][i] = out[1][i] = output; + } +} + +int main(void) +{ + petal.Init(); + lp_filter.Init(petal.AudioSampleRate()); + lp_filter.SetFreq(4000.0f); + samplerate = petal.AudioSampleRate(); + + InitParams(); + InitLeds(); + + delay.Init(samplerate, delayBuffer); + petal.StartAdc(); + petal.StartAudio(AudioCallback); + + while (1) + { + petal.ProcessAnalogControls(); + petal.ProcessDigitalControls(); + + if (petal.switches[Terrarium::FOOTSWITCH_1].RisingEdge()) + { + effect_enabled = !effect_enabled; + } + + // LED1 indicates if effect is enabled + led1.Set(effect_enabled ? 1.0f : 0.0f); + led1.Update(); + + System::Delay(10); + } +} diff --git a/terrarrium/DoubleDelay/terrarium.h b/terrarrium/DoubleDelay/terrarium.h new file mode 100644 index 000000000..dc6e3df0b --- /dev/null +++ b/terrarrium/DoubleDelay/terrarium.h @@ -0,0 +1,96 @@ +// PedalPCB Terrarium Header +// Copyright (C) 2020 PedalPCB.com +// http://www.pedalpcb.com + +namespace terrarium +{ + class Terrarium + { + public: + enum Sw + { + FOOTSWITCH_1 = 4, + FOOTSWITCH_2 = 5, + SWITCH_1 = 2, + SWITCH_2 = 1, + SWITCH_3 = 0, + SWITCH_4 = 6 + }; + + enum Knob + { + KNOB_1 = 0, + KNOB_2 = 2, + KNOB_3 = 4, + KNOB_4 = 1, + KNOB_5 = 3, + KNOB_6 = 5 + }; + enum LED + { + LED_1 = 22, + LED_2 = 23 + }; + }; +} + + + + +// #include "daisy_seed.h" +// #include "daisysp.h" + +// using namespace daisy; +// using namespace daisysp; + +// class Terrarium +// { +// public: +// DaisySeed seed; +// Led led_left, led_right; + +// void Init() +// { +// seed.Configure(); +// seed.Init(); + +// // Initialize LEDs as PWM outputs +// led_left.Init(seed.GetPin(22), false); // GPIO22 = Pin 22 +// led_right.Init(seed.GetPin(23), false); // GPIO23 = Pin 23 + +// // Initialize ADC for knobs (pots) +// AdcChannelConfig adc_cfg[6]; +// // for (int i = 0; i < 6; i++) +// for (int i = 1; i < 22; i++) +// { +// adc_cfg[i].InitSingle(seed.GetPin(15 + i)); // Pins 15–20 for KNOB_1 to KNOB_6 +// } +// seed.adc.Init(adc_cfg, 6); +// seed.adc.Start(); +// } + +// float GetKnob(int index) +// { +// return seed.adc.GetFloat(index); // 0.0 – 1.0 +// } + +// void SetLedLeftBrightness(float value) +// { +// led_left.Set(value); +// led_left.Update(); +// } + +// void SetLedRightBrightness(float value) +// { +// led_right.Set(value); +// led_right.Update(); +// } + +// void ToggleLeds() +// { +// static bool toggle_state = false; +// toggle_state = !toggle_state; +// SetLedLeftBrightness(toggle_state ? 1.0f : 0.0f); +// SetLedRightBrightness(toggle_state ? 1.0f : 0.0f); +// } +// }; From 950be13b0c52d17c9bd7a76f58e9de94f9e1ce50 Mon Sep 17 00:00:00 2001 From: Mitchell Hudson Date: Tue, 26 May 2026 10:31:14 -0700 Subject: [PATCH 7/7] update --- patch/HarmonicOscillator/MorphOscillator.cpp | 12 +- patch/HarmonicOscillator/MorphOscillator.h | 4 +- patch/HarmonicOscillator/main.cpp | 207 ++++++++++++++----- patch/HarmonicOscillator/ratios.h | 112 ++++++---- terrarrium/DoubleDelay/DelayUnit.h | 62 ++++-- terrarrium/DoubleDelay/DelayUnitReverse.h | 116 ++++++++--- terrarrium/DoubleDelay/Makefile | 7 - terrarrium/DoubleDelay/main.cpp | 169 +++++++-------- terrarrium/DoubleDelay/terrarium.h | 28 +-- terrarrium/ParametricDistortion/Makefile | 30 +++ terrarrium/ParametricDistortion/main.cpp | 151 ++++++++++++++ terrarrium/ParametricDistortion/readme.md | 4 + terrarrium/ParametricDistortion/terrarium.h | 96 +++++++++ 13 files changed, 735 insertions(+), 263 deletions(-) create mode 100644 terrarrium/ParametricDistortion/Makefile create mode 100644 terrarrium/ParametricDistortion/main.cpp create mode 100644 terrarrium/ParametricDistortion/readme.md create mode 100644 terrarrium/ParametricDistortion/terrarium.h diff --git a/patch/HarmonicOscillator/MorphOscillator.cpp b/patch/HarmonicOscillator/MorphOscillator.cpp index 73f2f4cd9..7726d3bd6 100644 --- a/patch/HarmonicOscillator/MorphOscillator.cpp +++ b/patch/HarmonicOscillator/MorphOscillator.cpp @@ -35,12 +35,12 @@ void MorphOscillator::SetAmp(float a) { } float MorphOscillator::Process() { - float morph = shape * 3.0f; - int index = (int)morph; - float frac = morph - index; + float morph = shape * 3.0f; // 0 - 3 selects 4 waveforms + int index = (int)morph; // Get the index of the starting wave soemthing like 1.23 + float frac = morph - index; // Get the get the fraction of the next wave form 0.23 - float a = osc[index].Process(); - float b = osc[(index + 1 < 4) ? (index + 1) : 3].Process(); + float a = osc[index].Process(); // Get the value of the current wave form + float b = osc[(index + 1 < 4) ? (index + 1) : 3].Process(); // get the next wave form - return a * (1.0f - frac) + b * frac; + return a * (1.0f - frac) + b * frac; // Mix the wave with a fraction of the next wave } \ No newline at end of file diff --git a/patch/HarmonicOscillator/MorphOscillator.h b/patch/HarmonicOscillator/MorphOscillator.h index f133c0b63..4177eda4d 100644 --- a/patch/HarmonicOscillator/MorphOscillator.h +++ b/patch/HarmonicOscillator/MorphOscillator.h @@ -8,12 +8,12 @@ class MorphOscillator { void Init(float sample_rate); void SetFreq(float f); - void SetShape(float s); void SetAmp(float a); + void SetShape(float s); float Process(); private: - Oscillator osc[4]; + Oscillator osc[4]; float freq; float shape; }; \ No newline at end of file diff --git a/patch/HarmonicOscillator/main.cpp b/patch/HarmonicOscillator/main.cpp index b66f4e736..642eb6ce3 100644 --- a/patch/HarmonicOscillator/main.cpp +++ b/patch/HarmonicOscillator/main.cpp @@ -1,19 +1,3 @@ -/* - Harmonic Oscillator Synth - v2.0 - -------------------------------- - A 4-voice oscillator engine with CV and knob control, using MorphOscillator for waveform blending. - - Features: - - 4 independent voices sent to Out 1–4 - - Smooth waveform morphing from sine β†’ triangle β†’ saw β†’ square - - Selectable harmonic ratio systems: just intonation, 3/5/7/11-limit, harmonic series, subharmonics, and Tides-style - - CTRL_1: Pitch sweep (over 6 octaves, ending at max frequency) - - CTRL_2: Ratio selector (offset through chosen harmonic set) - - CTRL_3: Waveform morph control - - CTRL_4: Sets the top of the pitch range (from LFO to ~440Hz) - - Encoder press: Cycles through harmonic ratio systems -*/ - #include "daisy_patch.h" #include "daisysp.h" #include "ratios.h" @@ -27,10 +11,12 @@ using namespace daisy; using namespace daisysp; -DaisyPatch patch; -MorphOscillator oscillators[4]; +DaisyPatch patch; // Make a patch +MorphOscillator oscillators[4]; // make four oscillators float oscFreqs[4] = {0}; int mode = 0; +int ratioIndex = 0; +const char* currentRatioLabel = ""; void InitOscillators() { for (int i = 0; i < 4; i++) { @@ -41,18 +27,7 @@ void InitOscillators() { void DisplayCurrentMode() { patch.display.Fill(false); - std::string modeName; - switch (mode) { - case 0: modeName = "Common Chords"; break; - case 1: modeName = "Harmonic Series"; break; - case 2: modeName = "Subharmonics"; break; - case 3: modeName = "Full Ratios"; break; - case 4: modeName = "Limit 3"; break; - case 5: modeName = "Limit 5"; break; - case 6: modeName = "Limit 7"; break; - case 7: modeName = "Limit 11"; break; - default: modeName = "Tides Style"; break; - } + std::string modeName = "Symmetric Limit 5"; patch.display.SetCursor(0, 0); patch.display.WriteString(modeName.c_str(), Font_7x10, true); @@ -63,10 +38,8 @@ void DisplayCurrentMode() { patch.display.WriteString(buffer, Font_7x10, true); } - snprintf(buffer, sizeof(buffer), "Knob1: %.2f", patch.GetKnobValue(DaisyPatch::CTRL_1)); patch.display.SetCursor(0, 54); - patch.display.WriteString(buffer, Font_7x10, true); - + patch.display.WriteString(currentRatioLabel, Font_7x10, true); patch.display.Update(); } @@ -75,30 +48,16 @@ void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, s float octaveShift = patch.GetKnobValue(DaisyPatch::CTRL_4); float maxFreq = MIN_FREQ * powf(2.0f, octaveShift * log2f(MAX_FREQ / MIN_FREQ)); - float pitchOffset = patch.GetKnobValue(DaisyPatch::CTRL_1); float baseFreq = maxFreq * powf(0.015625f, (1.0f - pitchOffset)); - const float* ratioSet; - int numRatios; - switch (mode) { - case 0: ratioSet = kCommonChords; numRatios = 12; break; - case 1: ratioSet = kHarmonicSeries; numRatios = 12; break; - case 2: ratioSet = kSubharmonics; numRatios = 12; break; - case 3: ratioSet = kFullRatios; numRatios = 12; break; - case 4: ratioSet = kLimit3; numRatios = 12; break; - case 5: ratioSet = kLimit5; numRatios = 12; break; - case 6: ratioSet = kLimit7; numRatios = 12; break; - case 7: ratioSet = kLimit11; numRatios = 12; break; - default: ratioSet = kTidesStyle; numRatios = 12; break; - } - - int ratioIndex = powf(patch.GetKnobValue(DaisyPatch::CTRL_2), 2.0f) * (numRatios - 1); - ratioIndex = fminf(fmaxf(ratioIndex, 0), numRatios - 1); + ratioIndex = powf(patch.GetKnobValue(DaisyPatch::CTRL_2), 2.0f) * (kLimit5SymmetricalSize - 1); + ratioIndex = fminf(fmaxf(ratioIndex, 0), kLimit5SymmetricalSize - 1); + currentRatioLabel = kLimit5Symmetrical[ratioIndex].label; for (int i = 0; i < 4; i++) { - int rIdx = (ratioIndex + i) % numRatios; - float freq = baseFreq * ratioSet[rIdx]; + int rIdx = (ratioIndex + i) % kLimit5SymmetricalSize; + float freq = baseFreq * kLimit5Symmetrical[rIdx].value; oscillators[i].SetFreq(freq); oscillators[i].SetShape(patch.GetKnobValue(DaisyPatch::CTRL_3)); oscFreqs[i] = freq; @@ -119,9 +78,145 @@ int main(void) { while (1) { patch.ProcessDigitalControls(); - if (patch.encoder.RisingEdge()) { - mode = (mode + 1) % 9; - DisplayCurrentMode(); - } + DisplayCurrentMode(); } } + + + + + + + + + + + + +// /* +// Harmonic Oscillator Synth - v2.0 +// -------------------------------- +// A 4-voice oscillator engine with CV and knob control, using MorphOscillator for waveform blending. + +// Features: +// - 4 independent voices sent to Out 1–4 +// - Smooth waveform morphing from sine β†’ triangle β†’ saw β†’ square +// - Selectable harmonic ratio systems: just intonation, 3/5/7/11-limit, harmonic series, subharmonics, and Tides-style +// - CTRL_1: Pitch sweep (over 6 octaves, ending at max frequency) +// - CTRL_2: Ratio selector (offset through chosen harmonic set) +// - CTRL_3: Waveform morph control +// - CTRL_4: Sets the top of the pitch range (from LFO to ~440Hz) +// - Encoder press: Cycles through harmonic ratio systems +// */ + +// #include "daisy_patch.h" +// #include "daisysp.h" +// #include "ratios.h" +// #include "MorphOscillator.h" +// #include +// #include + +// #define MAX_FREQ 440.0f +// #define MIN_FREQ 0.2f + +// using namespace daisy; +// using namespace daisysp; + +// DaisyPatch patch; +// MorphOscillator oscillators[4]; +// float oscFreqs[4] = {0}; +// int mode = 0; + +// void InitOscillators() { +// for (int i = 0; i < 4; i++) { +// oscillators[i].Init(patch.AudioSampleRate()); +// oscillators[i].SetAmp(0.5f); +// } +// } + +// void DisplayCurrentMode() { +// patch.display.Fill(false); +// std::string modeName; +// switch (mode) { +// case 0: modeName = "Common Chords"; break; +// case 1: modeName = "Harmonic Series"; break; +// case 2: modeName = "Subharmonics"; break; +// case 3: modeName = "Full Ratios"; break; +// case 4: modeName = "Limit 3"; break; +// case 5: modeName = "Limit 5"; break; +// case 6: modeName = "Limit 7"; break; +// case 7: modeName = "Limit 11"; break; +// default: modeName = "Tides Style"; break; +// } +// patch.display.SetCursor(0, 0); +// patch.display.WriteString(modeName.c_str(), Font_7x10, true); + +// char buffer[32]; +// for (int i = 0; i < 4; i++) { +// snprintf(buffer, sizeof(buffer), "Out %d: %d Hz", i + 1, (int)(oscFreqs[i])); +// patch.display.SetCursor(0, 12 + (i * 10)); +// patch.display.WriteString(buffer, Font_7x10, true); +// } + +// snprintf(buffer, sizeof(buffer), "Knob1: %.2f", patch.GetKnobValue(DaisyPatch::CTRL_1)); +// patch.display.SetCursor(0, 54); +// patch.display.WriteString(buffer, Font_7x10, true); + +// patch.display.Update(); +// } + +// void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) { +// patch.ProcessAllControls(); + +// float octaveShift = patch.GetKnobValue(DaisyPatch::CTRL_4); +// float maxFreq = MIN_FREQ * powf(2.0f, octaveShift * log2f(MAX_FREQ / MIN_FREQ)); + +// float pitchOffset = patch.GetKnobValue(DaisyPatch::CTRL_1); +// float baseFreq = maxFreq * powf(0.015625f, (1.0f - pitchOffset)); + +// const float* ratioSet; +// int numRatios; +// switch (mode) { +// case 0: ratioSet = kCommonChords; numRatios = 12; break; +// case 1: ratioSet = kHarmonicSeries; numRatios = 12; break; +// case 2: ratioSet = kSubharmonics; numRatios = 12; break; +// case 3: ratioSet = kFullRatios; numRatios = 12; break; +// case 4: ratioSet = kLimit3; numRatios = 12; break; +// case 5: ratioSet = kLimit5; numRatios = 12; break; +// case 6: ratioSet = kLimit7; numRatios = 12; break; +// case 7: ratioSet = kLimit11; numRatios = 12; break; +// default: ratioSet = kTidesStyle; numRatios = 12; break; +// } + +// int ratioIndex = powf(patch.GetKnobValue(DaisyPatch::CTRL_2), 2.0f) * (numRatios - 1); +// ratioIndex = fminf(fmaxf(ratioIndex, 0), numRatios - 1); + +// for (int i = 0; i < 4; i++) { +// int rIdx = (ratioIndex + i) % numRatios; +// float freq = baseFreq * ratioSet[rIdx]; +// oscillators[i].SetFreq(freq); +// oscillators[i].SetShape(patch.GetKnobValue(DaisyPatch::CTRL_3)); +// oscFreqs[i] = freq; +// } + +// for (size_t i = 0; i < size; i++) { +// for (int j = 0; j < 4; j++) { +// out[j][i] = oscillators[j].Process(); +// } +// } +// } + +// int main(void) { +// patch.Init(); +// patch.StartAdc(); +// InitOscillators(); +// patch.StartAudio(AudioCallback); + +// while (1) { +// patch.ProcessDigitalControls(); +// if (patch.encoder.RisingEdge()) { +// mode = (mode + 1) % 9; +// DisplayCurrentMode(); +// } +// } +// } diff --git a/patch/HarmonicOscillator/ratios.h b/patch/HarmonicOscillator/ratios.h index b82120b5c..14cbd33fd 100644 --- a/patch/HarmonicOscillator/ratios.h +++ b/patch/HarmonicOscillator/ratios.h @@ -1,51 +1,85 @@ #pragma once -// Common Chords (triads and simple intervals) -static const float kCommonChords[12] = { - 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f +struct Ratio { + float value; + const char* label; }; -// Harmonic Series (partials of a fundamental) -static const float kHarmonicSeries[12] = { - 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f +// Example: 5-limit Just Intonation with symmetric structure +static const Ratio kLimit5Symmetrical[] = { + { 0.5f, "1:2 (Octave Down)" }, + { 0.666f, "2:3 (Perfect Fifth Down)" }, + { 0.75f, "3:4 (Perfect Fourth Down)" }, + { 0.8f, "4:5 (Major Third Down)" }, + { 0.833f, "5:6 (Minor Third Down)" }, + { 1.0f, "1:1 (Unison)" }, + { 1.2f, "6:5 (Minor Third Up)" }, + { 1.25f, "5:4 (Major Third Up)" }, + { 1.333f, "4:3 (Perfect Fourth Up)" }, + { 1.5f, "3:2 (Perfect Fifth Up)" }, + { 1.666f, "5:3 (Major Sixth Up)" }, + { 2.0f, "2:1 (Octave Up)" } }; -// Subharmonics (inverted harmonic series) -static const float kSubharmonics[12] = { - 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.2f, 1.333f, 1.5f, 1.666f, 2.0f, 2.5f, 3.0f -}; +static const int kLimit5SymmetricalSize = sizeof(kLimit5Symmetrical) / sizeof(Ratio); -// Extended Just Intonation Ratios -static const float kFullRatios[12] = { - 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f -}; -// Pythagorean / 3-limit Tuning -static const float kLimit3[12] = { - 1.0f, 3.0f/2.0f, 4.0f/3.0f, 9.0f/8.0f, 27.0f/16.0f, 16.0f/9.0f, - 2.0f, 8.0f/9.0f, 32.0f/27.0f, 243.0f/128.0f, 81.0f/64.0f, 128.0f/81.0f -}; -// 5-limit Just Intonation (includes pure thirds) -static const float kLimit5[12] = { - 1.0f, 5.0f/4.0f, 6.0f/5.0f, 4.0f/3.0f, 3.0f/2.0f, 5.0f/3.0f, - 8.0f/5.0f, 9.0f/8.0f, 15.0f/8.0f, 10.0f/9.0f, 16.0f/15.0f, 2.0f -}; -// 7-limit Just Intonation (includes septimal intervals) -static const float kLimit7[12] = { - 1.0f, 7.0f/6.0f, 8.0f/7.0f, 7.0f/5.0f, 10.0f/7.0f, 7.0f/4.0f, - 9.0f/7.0f, 14.0f/9.0f, 7.0f/3.0f, 21.0f/16.0f, 28.0f/27.0f, 2.0f -}; -// 11-limit Just Intonation (includes more complex prime ratios) -static const float kLimit11[12] = { - 1.0f, 11.0f/10.0f, 11.0f/8.0f, 9.0f/11.0f, 11.0f/6.0f, 22.0f/15.0f, - 33.0f/22.0f, 11.0f/9.0f, 16.0f/11.0f, 11.0f/7.0f, 11.0f/12.0f, 2.0f -}; -// Tides-style: harmonics above and subharmonics below -static const float kTidesStyle[12] = { - 1.0f, 3.0f/2.0f, 2.0f, 3.0f, 0.75f, 0.666f, - 0.5f, 0.333f, 1.25f, 1.5f, 2.25f, 2.5f -}; + + + + +// #pragma once + +// // Common Chords (triads and simple intervals) +// static const float kCommonChords[12] = { +// 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f +// }; + +// // Harmonic Series (partials of a fundamental) +// static const float kHarmonicSeries[12] = { +// 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f, 8.0f, 9.0f, 10.0f, 11.0f, 12.0f +// }; + +// // Subharmonics (inverted harmonic series) +// static const float kSubharmonics[12] = { +// 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.2f, 1.333f, 1.5f, 1.666f, 2.0f, 2.5f, 3.0f +// }; + +// // Extended Just Intonation Ratios +// static const float kFullRatios[12] = { +// 0.5f, 0.666f, 0.75f, 0.833f, 1.0f, 1.125f, 1.25f, 1.333f, 1.5f, 1.667f, 1.875f, 2.0f +// }; + +// // Pythagorean / 3-limit Tuning +// static const float kLimit3[12] = { +// 1.0f, 3.0f/2.0f, 4.0f/3.0f, 9.0f/8.0f, 27.0f/16.0f, 16.0f/9.0f, +// 2.0f, 8.0f/9.0f, 32.0f/27.0f, 243.0f/128.0f, 81.0f/64.0f, 128.0f/81.0f +// }; + +// // 5-limit Just Intonation (includes pure thirds) +// static const float kLimit5[12] = { +// 1.0f, 5.0f/4.0f, 6.0f/5.0f, 4.0f/3.0f, 3.0f/2.0f, 5.0f/3.0f, +// 8.0f/5.0f, 9.0f/8.0f, 15.0f/8.0f, 10.0f/9.0f, 16.0f/15.0f, 2.0f +// }; + +// // 7-limit Just Intonation (includes septimal intervals) +// static const float kLimit7[12] = { +// 1.0f, 7.0f/6.0f, 8.0f/7.0f, 7.0f/5.0f, 10.0f/7.0f, 7.0f/4.0f, +// 9.0f/7.0f, 14.0f/9.0f, 7.0f/3.0f, 21.0f/16.0f, 28.0f/27.0f, 2.0f +// }; + +// // 11-limit Just Intonation (includes more complex prime ratios) +// static const float kLimit11[12] = { +// 1.0f, 11.0f/10.0f, 11.0f/8.0f, 9.0f/11.0f, 11.0f/6.0f, 22.0f/15.0f, +// 33.0f/22.0f, 11.0f/9.0f, 16.0f/11.0f, 11.0f/7.0f, 11.0f/12.0f, 2.0f +// }; + +// // Tides-style: harmonics above and subharmonics below +// static const float kTidesStyle[12] = { +// 1.0f, 3.0f/2.0f, 2.0f, 3.0f, 0.75f, 0.666f, +// 0.5f, 0.333f, 1.25f, 1.5f, 2.25f, 2.5f +// }; diff --git a/terrarrium/DoubleDelay/DelayUnit.h b/terrarrium/DoubleDelay/DelayUnit.h index 752f8a0a5..94b7dd278 100644 --- a/terrarrium/DoubleDelay/DelayUnit.h +++ b/terrarrium/DoubleDelay/DelayUnit.h @@ -1,40 +1,74 @@ +// Version 1.0.4 #pragma once #include "daisysp.h" -#include "ExtendedDelayLine.h" // Include our wrapper using namespace daisy; using namespace daisysp; constexpr size_t MAX_DELAY = 48000 * 5; // 5 seconds max delay +inline float LocalSoftClip(float x) +{ + // return x / (1.0f + fabsf(x)); + + // or + + if(x > 1.0f) + return (2.0f / 3.0f); + else if(x < -1.0f) + return (-2.0f / 3.0f); + else + return x - (x * x * x) / 3.0f; + +} + struct DelayUnit { - ExtendedDelayLine* buffer; // Use ExtendedDelayLine now + DelayLine* buffer; float currentDelay = 0.5f; - float delayTarget = 0.5f; - float feedback = 0.5f; + float delayTarget = 0.5f; + float feedback = 0.5f; Tone lp_filter; - bool reverse = false; // Flag for reverse playback + Svf hpf; - void Init(float samplerate, ExtendedDelayLine& buf) + void Init(float samplerate, DelayLine& buf) { buffer = &buf; - buffer->Reset(); + buffer->Init(); lp_filter.Init(samplerate); lp_filter.SetFreq(4000.0f); + hpf.Init(samplerate); + hpf.SetRes(0.7f); + hpf.SetFreq(120.0f); // Cut off sub-bass + hpf.SetDrive(0.0f); } - float Process(float in, float /*delay_samples*/) + float SnapToRatio(float base_delay_time, float ratio_knob) { - // Smooth delay time changes. + const float ratios[] = { + 1.0f, 15.0f / 16.0f, 7.0f / 8.0f, 13.0f / 16.0f, 3.0f / 4.0f, + 11.0f / 16.0f, 5.0f / 8.0f, 9.0f / 16.0f, 1.0f / 2.0f, + 7.0f / 16.0f, 3.0f / 8.0f, 5.0f / 16.0f, 1.0f / 4.0f, + 3.0f / 16.0f, 1.0f / 8.0f, 1.0f / 16.0f + }; + const int num_ratios = sizeof(ratios) / sizeof(ratios[0]); + int index = static_cast(ratio_knob * num_ratios); + if(index >= num_ratios) + index = num_ratios - 1; + + float delay_time = base_delay_time * ratios[index]; + return fminf(delay_time, MAX_DELAY); + } + + float Process(float in, float delay_time) + { + delayTarget = delay_time; fonepole(currentDelay, delayTarget, 0.0002f); buffer->SetDelay(currentDelay); - // Choose forward or reverse read. - float delayed = reverse ? buffer->ReverseRead() : buffer->Read(); - - // Mix input with delayed signal using feedback. - float input = in + delayed * feedback; + float delayed = buffer->Read(); + float input = (in + delayed * feedback) * 0.7f; + input = LocalSoftClip(input); float filtered = lp_filter.Process(input); filtered = fminf(fmaxf(filtered, -1.0f), 1.0f); buffer->Write(filtered); diff --git a/terrarrium/DoubleDelay/DelayUnitReverse.h b/terrarrium/DoubleDelay/DelayUnitReverse.h index 95025f926..8e3dc6a45 100644 --- a/terrarrium/DoubleDelay/DelayUnitReverse.h +++ b/terrarrium/DoubleDelay/DelayUnitReverse.h @@ -1,6 +1,5 @@ #pragma once -#include "delayline_reverse.h" #include "daisysp.h" using namespace daisy; @@ -8,44 +7,107 @@ using namespace daisysp; constexpr size_t MAX_DELAY = 48000 * 5; // 5 seconds max delay -struct DelayUnitReverse { - daisysp::DelayLineReverse* buffer; - float currentDelay = 0.5f; // in samples (after smoothing) - float delayTarget = 0.5f; - float feedback = 0.5f; +struct DelayUnit { + DelayLine* buffer; + float currentDelay = 0.5f; + float delayTarget = 0.5f; + float feedback = 0.5f; Tone lp_filter; - bool reverse = false; - - // For this reverse unit, we always use reverse playback. - // You could add a flag here if you want to toggle between forward and reverse. - - void Init(float samplerate, daisysp::DelayLineReverse& buf) + + void Init(float samplerate, DelayLine& buf) { buffer = &buf; - buffer->Reset(); + buffer->Init(); lp_filter.Init(samplerate); lp_filter.SetFreq(4000.0f); } - // Process the audio sample using reverse delay. - float Process(float in) + float Process(float in, float delay_time) +{ + delayTarget = delay_time; + + fonepole(currentDelay, delayTarget, 0.0002f); + buffer->SetDelay(currentDelay); + + float delayed = buffer->Read(); + float input = in + delayed * feedback; + float filtered = lp_filter.Process(input); + filtered = fminf(fmaxf(filtered, -1.0f), 1.0f); + buffer->Write(filtered); + + return delayed; +} + + float Process(float in, float delay1_time, float ratio_knob) { - // Smooth the delay time changes. - fonepole(currentDelay, delayTarget, 0.002f); - // Use SetDelay1 (as defined in DelayLineReverse) to set the delay time. - buffer->SetDelay1(currentDelay); - - // Choose reading method based on the reverse flag. - float delayed = reverse ? buffer->ReadRev() : buffer->ReadFwd(); - - // Mix input with delayed sample using feedback. + float max_ratio = MAX_DELAY / delay1_time; + float log_ratio = powf(2.0f, (ratio_knob - 0.5f) * 4.0f); // ~0.25x–4x + float ratio = fminf(log_ratio, max_ratio); + delayTarget = delay1_time * ratio; + + fonepole(currentDelay, delayTarget, 0.0002f); + buffer->SetDelay(currentDelay); + + float delayed = buffer->Read(); float input = in + delayed * feedback; float filtered = lp_filter.Process(input); filtered = fminf(fmaxf(filtered, -1.0f), 1.0f); - - // Write the processed sample into the buffer. buffer->Write(filtered); - + return delayed; } }; + + + +// #pragma once + +// #include "delayline_reverse.h" +// #include "daisysp.h" + +// using namespace daisy; +// using namespace daisysp; + +// constexpr size_t MAX_DELAY = 48000 * 5; // 5 seconds max delay + +// struct DelayUnitReverse { +// daisysp::DelayLineReverse* buffer; +// float currentDelay = 0.5f; // in samples (after smoothing) +// float delayTarget = 0.5f; +// float feedback = 0.5f; +// Tone lp_filter; +// bool reverse = false; + +// // For this reverse unit, we always use reverse playback. +// // You could add a flag here if you want to toggle between forward and reverse. + +// void Init(float samplerate, daisysp::DelayLineReverse& buf) +// { +// buffer = &buf; +// buffer->Reset(); +// lp_filter.Init(samplerate); +// lp_filter.SetFreq(4000.0f); +// } + +// // Process the audio sample using reverse delay. +// float Process(float in) +// { +// // Smooth the delay time changes. +// fonepole(currentDelay, delayTarget, 0.002f); +// // Use SetDelay1 (as defined in DelayLineReverse) to set the delay time. +// buffer->SetDelay1(currentDelay); + +// // Choose reading method based on the reverse flag. +// float delayed = reverse ? buffer->ReadRev() : buffer->ReadFwd(); + +// // Mix input with delayed sample using feedback. +// float input = in + delayed * feedback; +// float filtered = lp_filter.Process(input); +// filtered = fminf(fmaxf(filtered, -1.0f), 1.0f); + +// // Write the processed sample into the buffer. +// buffer->Write(filtered); + +// return delayed; +// } +// }; diff --git a/terrarrium/DoubleDelay/Makefile b/terrarrium/DoubleDelay/Makefile index 17a6cb0ff..e5f5596ad 100644 --- a/terrarrium/DoubleDelay/Makefile +++ b/terrarrium/DoubleDelay/Makefile @@ -12,7 +12,6 @@ DAISYSP_DIR = ../../DaisySP SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core # Source files -# CPP_SOURCES = bytebeat.cpp CPP_SOURCES = main.cpp # Include the Daisy system Makefile @@ -29,9 +28,3 @@ build/$(PROGRAM): build/$(TARGET).elf # Flash to Daisy Patch using ST-LINK V3 Mini program: build/$(PROGRAM) st-flash --reset write build/$(PROGRAM) $(FLASH_ADDR) - - - - - - diff --git a/terrarrium/DoubleDelay/main.cpp b/terrarrium/DoubleDelay/main.cpp index 2f2a05364..638cab303 100644 --- a/terrarrium/DoubleDelay/main.cpp +++ b/terrarrium/DoubleDelay/main.cpp @@ -1,127 +1,117 @@ -// Dual Delay Pedal - Version 1.1.5 -// Rerverse delay is working, controls are not working -// Two 5 second delays with almost infinite feedback. +// Version 1.1.0 -#include "daisysp.h" #include "daisy_petal.h" +#include "daisysp.h" #include "terrarium.h" -#include "DelayUnitReverse.h" +#include "DelayUnit.h" using namespace daisy; using namespace daisysp; using namespace terrarium; DaisyPetal petal; -Parameter param_feedback_1, param_mix_1, param_feedback_2, param_mix_2; -Led led1, led2; -DelayUnitReverse delay1_rev, delay2_rev; -CrossFade cfade; -DSY_SDRAM_BSS daisysp::DelayLineReverse reverseDelayBuffer1; -DSY_SDRAM_BSS daisysp::DelayLineReverse reverseDelayBuffer2; +DSY_SDRAM_BSS DelayLine delayBuffer1; +DSY_SDRAM_BSS DelayLine delayBuffer2; + +DelayUnit delay1; +DelayUnit delay2; + +Parameter param_feedback_1, param_feedback_2; +Parameter param_mix_1, param_mix_2; +Parameter param_time_1, param_time_2; -float samplerate; bool bypassDelay1 = false; bool bypassDelay2 = false; -/////////////////////////////////////////////////////////////// -// Init Params +Led led1, led2; + +float samplerate; +uint32_t last_flash = 0; +bool flash_state = false; + +Tone hpf; + -void InitParams() +void InitControls() { - // Initialize parameters for Delay1 ----------------------- + param_time_1.Init(petal.knob[Terrarium::KNOB_1], 0.01f, 5.0f, Parameter::EXPONENTIAL); param_feedback_1.Init(petal.knob[Terrarium::KNOB_2], 0.0f, 1.0f, Parameter::LINEAR); param_mix_1.Init(petal.knob[Terrarium::KNOB_3], 0.0f, 1.0f, Parameter::LINEAR); - // Initialize parameters for Delay2 ----------------------- + + param_time_2.Init(petal.knob[Terrarium::KNOB_4], 0.01f, 5.0f, Parameter::EXPONENTIAL); param_feedback_2.Init(petal.knob[Terrarium::KNOB_5], 0.0f, 1.0f, Parameter::LINEAR); param_mix_2.Init(petal.knob[Terrarium::KNOB_6], 0.0f, 1.0f, Parameter::LINEAR); -} - -/////////////////////////////////////////////////////////////// -// Init LEDs -void InitLeds() -{ - led1.Init(petal.seed.GetPin(Terrarium::LED_1), false); - led2.Init(petal.seed.GetPin(Terrarium::LED_2), false); + led1.Init(petal.seed.GetPin(terrarium::Terrarium::LED_1), false); + led2.Init(petal.seed.GetPin(terrarium::Terrarium::LED_2), false); } -/////////////////////////////////////////////////////////////// -// AudioCallback - void AudioCallback(AudioHandle::InputBuffer in, AudioHandle::OutputBuffer out, size_t size) { - // Delay 1 ------------------------------------------------ - float knob1 = 1.0f - petal.knob[Terrarium::KNOB_1].Process(); - // 0 - 0.5 -> 0 - 1s delay time, 0.5 - 1.0 -> 1.0 - 5.0s delay time - float delay1_time = knob1 < 0.5f ? fmap(knob1 * 2.0f, 0.1f, 1.0f) - : fmap((knob1 - 0.5f) * 2.0f, 1.0f, 5.0f); - - float delay1_samples = delay1_time * samplerate; - float delay1_mix = param_mix_1.Process(); - - delay1_rev.delayTarget = delay1_samples; - delay1_rev.feedback = param_feedback_1.Process(); - - // Delay 2 ------------------------------------------------ - float knob4 = 1.0f - petal.knob[Terrarium::KNOB_4].Process(); - // 0 - 0.5 -> 0 - 1s delay time, 0.5 - 1.0 -> 1.0 - 5.0s delay time - float delay2_time = knob4 < 0.5f ? fmap(knob4 * 2.0f, 0.1f, 1.0f) - : fmap((knob4 - 0.5f) * 2.0f, 1.0f, 5.0f); + float delay_time_1 = param_time_1.Process(); + float feedback_1 = param_feedback_1.Process(); + float mix_1 = param_mix_1.Process(); + + float ratio_knob = param_time_2.Process(); + float delay_time_2 = delay2.SnapToRatio(delay_time_1, ratio_knob); + float feedback_2 = param_feedback_2.Process(); + float mix_2 = param_mix_2.Process(); - float delay2_samples = delay2_time * samplerate; - float delay2_mix = param_mix_2.Process(); - - delay2_rev.delayTarget = delay2_samples; - delay2_rev.feedback = param_feedback_2.Process(); - // ffective wet signal from both delays: - float wetScale1 = delay1_mix; // or any mapping you prefer - float wetScale2 = delay2_mix; // or combine them differently + delay1.delayTarget = delay_time_1 * samplerate; + delay1.feedback = feedback_1; + + delay2.delayTarget = delay_time_2 * samplerate; + delay2.feedback = feedback_2; - // In the processing loop: for (size_t i = 0; i < size; i++) { float dry = in[0][i]; + float delayed1 = delay1.Process(dry, delay_time_1 * samplerate); + float delayed2 = delay2.Process(dry, delay_time_2 * samplerate); - float d1 = bypassDelay1 ? delay1_rev.Process(0.0f) : delay1_rev.Process(dry); - float d2 = bypassDelay2 ? delay2_rev.Process(0.0f) : delay2_rev.Process(dry); - - // Sum the wet outputs using the mix parameters: - float wet = d1 * wetScale1 + d2 * wetScale2; - - // Use the CrossFade object to blend dry and wet signals. - // This ensures constant power blending based on effectiveMix. - float finalMix = cfade.Process(dry, wet); - - out[0][i] = out[1][i] = finalMix; + float mixed1 = (1.0f - mix_1) * dry + mix_1 * delayed1; + float mixed2 = (1.0f - mix_2) * dry + mix_2 * delayed2; + + float output = dry; + if (!bypassDelay1) + output = mixed1; + if (!bypassDelay2) + output = (output + mixed2) * 0.5f; + + out[0][i] = out[1][i] = output; } + // LED Flash Timing based on Delay 1 Time + uint32_t now = System::GetNow(); + if (!bypassDelay1 && (now - last_flash) >= static_cast(delay_time_1 * 1000)) + { + flash_state = !flash_state; + led1.Set(flash_state ? 1.0f : 0.0f); + last_flash = now; + } + else if (bypassDelay1) + { + led1.Set(0.0f); + } + led1.Update(); + led2.Set(!bypassDelay2 ? 1.0f : 0.0f); + led2.Update(); } -/////////////////////////////////////////////////////////////// -// Main - int main(void) { - // Initialize patch --------------------------------------- petal.Init(); samplerate = petal.AudioSampleRate(); + hpf.Init(samplerate); + hpf.SetFreq(80.0f); // Cut off sub-bass - // Initialize Params -------------------------------------- - InitParams(); - InitLeds(); + delay1.Init(samplerate, delayBuffer1); + delay2.Init(samplerate, delayBuffer2); + InitControls(); - // Initialize CrossFade ----------------------------------- - cfade.Init(); - cfade.SetCurve(CROSSFADE_CPOW); - - // Initialize Delay units --------------------------------- - delay1_rev.Init(samplerate, reverseDelayBuffer1); - delay2_rev.Init(samplerate, reverseDelayBuffer2); - - // Start Adc and start AudioCallback ---------------------- petal.StartAdc(); petal.StartAudio(AudioCallback); @@ -130,28 +120,11 @@ int main(void) petal.ProcessAnalogControls(); petal.ProcessDigitalControls(); - // Use SWITCH_1 to control reverse mode for delay1: - delay1_rev.reverse = petal.switches[Terrarium::SWITCH_1].Pressed(); - // And similarly for delay2 if desired: - delay2_rev.reverse = petal.switches[Terrarium::SWITCH_2].Pressed(); - - // if (petal.switches[Terrarium::FOOTSWITCH_1].RisingEdge()) - { bypassDelay1 = !bypassDelay1; - } if (petal.switches[Terrarium::FOOTSWITCH_2].RisingEdge()) - { bypassDelay2 = !bypassDelay2; - } - - // LED1 indicates if effect is enabled - led1.Set(!bypassDelay1 ? 1.0f : 0.0f); - led2.Set(!bypassDelay2 ? 1.0f : 0.0f); - - led1.Update(); - led2.Update(); System::Delay(10); } diff --git a/terrarrium/DoubleDelay/terrarium.h b/terrarrium/DoubleDelay/terrarium.h index dc6e3df0b..9a5d27fab 100644 --- a/terrarrium/DoubleDelay/terrarium.h +++ b/terrarrium/DoubleDelay/terrarium.h @@ -9,27 +9,27 @@ namespace terrarium public: enum Sw { - FOOTSWITCH_1 = 4, - FOOTSWITCH_2 = 5, - SWITCH_1 = 2, - SWITCH_2 = 1, - SWITCH_3 = 0, - SWITCH_4 = 6 + FOOTSWITCH_1 = 4, + FOOTSWITCH_2 = 5, + SWITCH_1 = 2, + SWITCH_2 = 1, + SWITCH_3 = 0, + SWITCH_4 = 6 }; enum Knob { - KNOB_1 = 0, - KNOB_2 = 2, - KNOB_3 = 4, - KNOB_4 = 1, - KNOB_5 = 3, - KNOB_6 = 5 + KNOB_1 = 0, + KNOB_2 = 2, + KNOB_3 = 4, + KNOB_4 = 1, + KNOB_5 = 3, + KNOB_6 = 5 }; enum LED { - LED_1 = 22, - LED_2 = 23 + LED_1 = 22, + LED_2 = 23 }; }; } diff --git a/terrarrium/ParametricDistortion/Makefile b/terrarrium/ParametricDistortion/Makefile new file mode 100644 index 000000000..d6263a08a --- /dev/null +++ b/terrarrium/ParametricDistortion/Makefile @@ -0,0 +1,30 @@ +# Project Name +TARGET = parametric_distortion + +# Use LGPL version of DaisySP (optional, set to 1 if needed) +USE_DAISYSP_LGPL = 1 + +# Library Locations +LIBDAISY_DIR = ../../libDaisy +DAISYSP_DIR = ../../DaisySP + +# Core location, and generic Makefile. +SYSTEM_FILES_DIR = $(LIBDAISY_DIR)/core + +# Source files +CPP_SOURCES = main.cpp + +# Include the Daisy system Makefile (does all the hard work) +include $(SYSTEM_FILES_DIR)/Makefile + +# Define the compiled program binary +PROGRAM = parametric_distortion.bin +FLASH_ADDR = 0x08000000 + +# Convert ELF to BIN +build/$(PROGRAM): build/$(TARGET).elf + arm-none-eabi-objcopy -O binary build/$(TARGET).elf build/$(PROGRAM) + +# Flash to Daisy Seed using ST-LINK V3 Mini +program: build/$(PROGRAM) + st-flash --reset write build/$(PROGRAM) $(FLASH_ADDR) diff --git a/terrarrium/ParametricDistortion/main.cpp b/terrarrium/ParametricDistortion/main.cpp new file mode 100644 index 000000000..5761c7bd8 --- /dev/null +++ b/terrarrium/ParametricDistortion/main.cpp @@ -0,0 +1,151 @@ +#include "daisysp.h" +#include "daisy_petal.h" +#include "terrarium.h" +#include + +using namespace daisy; +using namespace daisysp; +using namespace terrarium; + +// Knob 1 = Frequency +// Knob 2 = Gain +// Knob 3 = Q + +DaisyPetal petal; +Parameter knob_1, knob_2, knob_3, knob_4, knob_5, knob_6; +Led led1, led2; + +float sample_rate; + +struct ParametricEQ +{ + float sample_rate = 48000.0f; + float a1, a2, b0, b1, b2; + float x1, x2, y1, y2; + + void SetSampleRate(float sr) { sample_rate = sr; } + + void SetParams(float freq, float q, float gain_db) + { + float A = powf(10.0f, gain_db / 40.0f); + float omega = 2.0f * M_PI * freq / sample_rate; + float alpha = sinf(omega) / (2.0f * q); + float cosw = cosf(omega); + + float a0 = 1.0f + alpha / A; + a1 = -2.0f * cosw; + a2 = 1.0f - alpha / A; + b0 = 1.0f + alpha * A; + b1 = -2.0f * cosw; + b2 = 1.0f - alpha * A; + + // Normalize + b0 /= a0; + b1 /= a0; + b2 /= a0; + a1 /= a0; + a2 /= a0; + } + + float Process(float in) + { + float out = b0 * in + b1 * x1 + b2 * x2 - a1 * y1 - a2 * y2; + x2 = x1; x1 = in; + y2 = y1; y1 = out; + return out; + } +}; + +ParametricEQ eq; + + + +// Initialize the LEDs +void InitLeds() +{ + led1.Init(petal.seed.GetPin(Terrarium::LED_1), false); + led2.Init(petal.seed.GetPin(Terrarium::LED_2), false); +} + +void InitParams() +{ + // Initialize parameters for Delay1 ----------------------- + knob_1.Init(petal.knob[Terrarium::KNOB_2], 0.0f, 1.0f, Parameter::LINEAR); + knob_2.Init(petal.knob[Terrarium::KNOB_3], 0.0f, 1.0f, Parameter::LINEAR); + knob_3.Init(petal.knob[Terrarium::KNOB_4], 0.0f, 1.0f, Parameter::LINEAR); + knob_4.Init(petal.knob[Terrarium::KNOB_5], 0.0f, 1.0f, Parameter::LINEAR); + knob_5.Init(petal.knob[Terrarium::KNOB_6], 0.0f, 1.0f, Parameter::LINEAR); + knob_6.Init(petal.knob[Terrarium::KNOB_1], 0.0f, 1.0f, Parameter::LINEAR); +} + +// //////////////////////////////////////////////////////////////////// +// Audio callback function + +void AudioCallback(AudioHandle::InputBuffer in, + AudioHandle::OutputBuffer out, + size_t size) +{ + petal.ProcessAnalogControls(); + + float freq_val = petal.knob[Terrarium::KNOB_1].Process(); + float gain_val = petal.knob[Terrarium::KNOB_2].Process(); + float q_val = petal.knob[Terrarium::KNOB_3].Process(); + + const float minGain = -0.0f; + const float maxGain = 24.0f; + + const float minFreq = 80.0f; + const float maxFreq = 3000.0f; + + const float minQ = 0.1f; + const float maxQ = 10.0f; + + float gain = (gain_val * (maxGain - minGain)) + minGain; + float freq = minFreq * powf(maxFreq / minFreq, freq_val); + float q = (q_val * (maxQ - minQ)) + minQ; + + eq.SetParams(freq, q, gain); + // eq.SetParams(1000.0f, 6.0f, 24.0f); // Default values for testing + + for (size_t i = 0; i < size; i++) + { + float input = in[0][i]; + float out_sample = eq.Process(input); + out[0][i] = out_sample; + out[1][i] = out_sample; + } +} + +int main(void) +{ + petal.Init(); + InitLeds(); + petal.StartAdc(); + InitParams(); + + led1.Set(1.0f); // Turn off LED1 + led2.Set(1.0f); // Turn off LED2 + led1.Update(); // Apply to LED pin + led2.Update(); // Apply to LED pin + + sample_rate = petal.AudioSampleRate(); + + eq.SetSampleRate(sample_rate); + eq.SetParams(1000.0f, 1.0f, 0.0f); // Default + + petal.StartAudio(AudioCallback); + + while (1) { + // petal.ProcessAnalogControls(); + + // float k1 = petal.knob[Terrarium::KNOB_1].Process(); + // led1.Set(k1 > 0.5f); // LED1 on if knob1 > 50% + // led1.Update(); + + // float k2 = petal.knob[Terrarium::KNOB_2].Process(); + // led2.Set(k2 < 0.5f); // LED2 on if knob2 > 50% + // led2.Update(); + + // petal.DelayMs(10); // allow time for hardware to respond + } +} diff --git a/terrarrium/ParametricDistortion/readme.md b/terrarrium/ParametricDistortion/readme.md new file mode 100644 index 000000000..77b88837e --- /dev/null +++ b/terrarrium/ParametricDistortion/readme.md @@ -0,0 +1,4 @@ +# Parametric EQ for Terrarium + +cd ../../libDaisy && make clean && make +cd ../../DaisySP && make clean && make diff --git a/terrarrium/ParametricDistortion/terrarium.h b/terrarrium/ParametricDistortion/terrarium.h new file mode 100644 index 000000000..dc6e3df0b --- /dev/null +++ b/terrarrium/ParametricDistortion/terrarium.h @@ -0,0 +1,96 @@ +// PedalPCB Terrarium Header +// Copyright (C) 2020 PedalPCB.com +// http://www.pedalpcb.com + +namespace terrarium +{ + class Terrarium + { + public: + enum Sw + { + FOOTSWITCH_1 = 4, + FOOTSWITCH_2 = 5, + SWITCH_1 = 2, + SWITCH_2 = 1, + SWITCH_3 = 0, + SWITCH_4 = 6 + }; + + enum Knob + { + KNOB_1 = 0, + KNOB_2 = 2, + KNOB_3 = 4, + KNOB_4 = 1, + KNOB_5 = 3, + KNOB_6 = 5 + }; + enum LED + { + LED_1 = 22, + LED_2 = 23 + }; + }; +} + + + + +// #include "daisy_seed.h" +// #include "daisysp.h" + +// using namespace daisy; +// using namespace daisysp; + +// class Terrarium +// { +// public: +// DaisySeed seed; +// Led led_left, led_right; + +// void Init() +// { +// seed.Configure(); +// seed.Init(); + +// // Initialize LEDs as PWM outputs +// led_left.Init(seed.GetPin(22), false); // GPIO22 = Pin 22 +// led_right.Init(seed.GetPin(23), false); // GPIO23 = Pin 23 + +// // Initialize ADC for knobs (pots) +// AdcChannelConfig adc_cfg[6]; +// // for (int i = 0; i < 6; i++) +// for (int i = 1; i < 22; i++) +// { +// adc_cfg[i].InitSingle(seed.GetPin(15 + i)); // Pins 15–20 for KNOB_1 to KNOB_6 +// } +// seed.adc.Init(adc_cfg, 6); +// seed.adc.Start(); +// } + +// float GetKnob(int index) +// { +// return seed.adc.GetFloat(index); // 0.0 – 1.0 +// } + +// void SetLedLeftBrightness(float value) +// { +// led_left.Set(value); +// led_left.Update(); +// } + +// void SetLedRightBrightness(float value) +// { +// led_right.Set(value); +// led_right.Update(); +// } + +// void ToggleLeds() +// { +// static bool toggle_state = false; +// toggle_state = !toggle_state; +// SetLedLeftBrightness(toggle_state ? 1.0f : 0.0f); +// SetLedRightBrightness(toggle_state ? 1.0f : 0.0f); +// } +// };