From 92a902c8751b49761d81f39d92ae88b01086303b Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:04:29 +0100 Subject: [PATCH 01/13] initial commit --- releases/93_Turing_Matrix/README.md | 87 ++ .../TuringMatrix_Code/CMakeLists.txt | 97 ++ .../TuringMatrix_Code/Clock.cpp | 248 +++++ .../TuringMatrix_Code/Clock.h | 60 ++ .../TuringMatrix_Code/ComputerCard.h | 998 ++++++++++++++++++ .../TuringMatrix_Code/Config.cpp | 95 ++ .../TuringMatrix_Code/Config.h | 66 ++ .../TuringMatrix_Code/MainApp.cpp | 969 +++++++++++++++++ .../TuringMatrix_Code/MainApp.h | 132 +++ .../TuringMatrix_Code/Turing.cpp | 153 +++ .../TuringMatrix_Code/Turing.h | 40 + .../93_Turing_Matrix/TuringMatrix_Code/UI.cpp | 263 +++++ .../93_Turing_Matrix/TuringMatrix_Code/UI.h | 55 + .../TuringMatrix_Code/main.cpp | 77 ++ .../TuringMatrix_Code/pico_sdk_import.cmake | 121 +++ .../TuringMatrix_Code/tusb_config.h | 113 ++ .../TuringMatrix_Code/usb_descriptors.c | 133 +++ releases/93_Turing_Matrix/info.yaml | 150 +++ releases/93_Turing_Matrix/web/app.js | 374 +++++++ releases/93_Turing_Matrix/web/index.html | 200 ++++ releases/93_Turing_Matrix/web/styles.css | 142 +++ 21 files changed, 4573 insertions(+) create mode 100644 releases/93_Turing_Matrix/README.md create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/CMakeLists.txt create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/Clock.cpp create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/Clock.h create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/ComputerCard.h create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/Config.h create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/Turing.cpp create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/Turing.h create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/UI.h create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/main.cpp create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/pico_sdk_import.cmake create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/tusb_config.h create mode 100644 releases/93_Turing_Matrix/TuringMatrix_Code/usb_descriptors.c create mode 100644 releases/93_Turing_Matrix/info.yaml create mode 100644 releases/93_Turing_Matrix/web/app.js create mode 100644 releases/93_Turing_Matrix/web/index.html create mode 100644 releases/93_Turing_Matrix/web/styles.css diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md new file mode 100644 index 000000000..a5cd03627 --- /dev/null +++ b/releases/93_Turing_Matrix/README.md @@ -0,0 +1,87 @@ +# Turing Matrix + +This is a draft Workshop Computer card built from the official +**03_Turing_Machine** firmware, reshaped into two switch-selected layers inspired by the +Music Thing Modular **Turing Machine + Vactrol Mix Expander** combination. + +The original Vactrol Mix Expander is a four-input, two-output vactrol matrix mixer +for the hardware Turing Machine. On the Workshop Computer we have two audio inputs, +two CV inputs, two audio/CV outputs, two CV outputs, and two pulse outputs, so this +card treats the idea as a two-input, two-output random matrix/mixer. + +## Basic idea + +- One shared background Turing engine keeps running in both layers. +- **Z middle** is the Turing control layer and is intended to feel like the original Turing card. +- **Z up** is the Vactrol Mix layer and uses the background Turing engine to animate a two-input, + two-output audio mixer. +- **Z down** remains tap tempo. +- **Pulse Out 1** and **Pulse Out 2** keep the same clock/Turing pulse behavior in both layers. +- **CV Out 1** and **CV Out 2** stay quantized pitch outputs in `Z middle`, and become mirrored + crossfaded CV outputs in `Z up`. + +## Controls + +- **Z middle** +- Main knob: Turing randomness / write amount. +- X knob: loop length. +- Y knob: channel 2 divide/multiply relationship. +- Audio/CV In 1 and 2 are unused. + +- **Z up** +- Main knob: vactrol lag / slew time. +- X knob: crossfade depth for Audio Out 1. +- Y knob: crossfade depth for Audio Out 2. +- Audio/CV In 1 and 2 become the two audio inputs to the mixer. + +- **Z down** +- Tap tempo when no external clock is patched. + +## Inputs + +- **Pulse In 1**: external clock for the main Turing channel. +- **Pulse In 2**: independent clock for channel 2. +- **CV In 1**: divide/multiply modulation for channel 2 in `Z middle`, CV mix input 1 in `Z up`. +- **CV In 2**: quantized pitch offset in `Z middle`, CV mix input 2 in `Z up`. +- **Audio/CV In 1**: mixer input 1 in the Vactrol Mix layer. +- **Audio/CV In 2**: mixer input 2 in the Vactrol Mix layer. + +## Outputs + +- **Pulse Out 1**: matrix gate 1. +- **Pulse Out 2**: matrix gate 2. +- **CV Out 1**: channel 1 quantized pitch CV in `Z middle`, crossfaded CV output 1 in `Z up`. +- **CV Out 2**: channel 2 quantized pitch CV in `Z middle`, crossfaded CV output 2 in `Z up`. +- **Audio Out 1**: DAC CV output in the Turing layer, mixed audio output 1 in the Vactrol Mix layer. +- **Audio Out 2**: DAC CV output in the Turing layer, mixed audio output 2 in the Vactrol Mix layer. + +## Vactrol Mix behavior + +In the Vactrol Mix layer, the current implementation uses the two Turing DAC lanes as smoothed +crossfade controls. Audio Out 1 crossfades Audio In 1 against Audio In 2, and Audio Out 2 +crossfades Audio In 2 against Audio In 1. + +The same crossfade is mirrored on the CV pair: CV Out 1 crossfades CV In 1 against CV In 2, and +CV Out 2 crossfades CV In 2 against CV In 1. + +That gives a practical Workshop Computer interpretation of the Vactrol Mix idea: +shared random movement, click-softened transitions, and stereo drift driven by the background +Turing machine. The audio inputs can therefore be used for audio or slow CV, while the CV inputs +provide a dedicated CV-only version of the same motion. + +## Status + +Draft implementation. The firmware source is copied from card **03_Turing_Machine** with: + +- the internal card number changed to 93 +- the original Audio In 1 reset behavior removed +- the original Audio In 2 switch override behavior removed +- a new `Z up` Vactrol Mix audio layer added on top of the shared Turing engine + +## Web editor + +This card now needs its own editor model because the switch no longer selects two Turing presets. +The local `web/` editor is for the shared Turing engine settings only: timing, scale/range, pulse +behavior, and related persistent options. It also stores a first batch of Vactrol-layer settings: +crossfade law, lane relation, rise/fall timing, and per-lane minimum/maximum windows. The panel +still handles the live lag and crossfade depth gestures in `Z up`. diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/CMakeLists.txt b/releases/93_Turing_Matrix/TuringMatrix_Code/CMakeLists.txt new file mode 100644 index 000000000..c7f3b24bf --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/CMakeLists.txt @@ -0,0 +1,97 @@ +# == DO NOT EDIT THE FOLLOWING LINES for the Raspberry Pi Pico VS Code Extension to work == +if(WIN32) + set(USERHOME $ENV{USERPROFILE}) +else() + set(USERHOME $ENV{HOME}) +endif() +set(sdkVersion 2.1.1) +set(toolchainVersion 14_2_Rel1) +set(picotoolVersion 2.1.1) +set(picoVscode ${USERHOME}/.pico-sdk/cmake/pico-vscode.cmake) +if (EXISTS ${picoVscode}) + include(${picoVscode}) +endif() + +# ==================================================================================== +set(PICO_BOARD pico CACHE STRING "Board type") + + +cmake_minimum_required(VERSION 3.13) + +# Pull in the Pico SDK +include(pico_sdk_import.cmake) + + +# Define the project name +project(TuringMatrix C CXX ASM) + +pico_sdk_init() + +set(PICO_BOOT_STAGE2 boot2_w25q080.S) + + +# Create the executable from your source files +add_executable(${PROJECT_NAME} + main.cpp + MainApp.cpp + Clock.cpp + Config.cpp + UI.cpp + Turing.cpp +) +pico_set_binary_type(${PROJECT_NAME} copy_to_ram) # This line in this spot actually works! + +# Add include directories +target_include_directories(${PROJECT_NAME} PRIVATE + ${CMAKE_CURRENT_LIST_DIR} # local .h files +) + +# Give oscillator more time to start - workshop system won't start if this isn't included +target_compile_definitions(${PROJECT_NAME} PRIVATE + PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64 + + PICO_STDIO_USB_CONNECT_WAIT_TIMEOUT_MS=1000 + CFG_TUSB_MCU=OPT_MCU_RP2040 + CFG_TUSB_RHPORT0_MODE=OPT_MODE_DEVICE + CFG_TUD_ENABLED=1 + CFG_TUD_MIDI=1 # <-- Enable MIDI device + +) + +set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME ${PROJECT_NAME}) +set_target_properties(${PROJECT_NAME} PROPERTIES SUFFIX ".elf") + + +target_sources(${PROJECT_NAME} PRIVATE + usb_descriptors.c +) + + +# Link against the libraries you’re using (as per ComputerCard dependencies) +target_link_libraries(${PROJECT_NAME} +pico_unique_id +pico_stdlib +hardware_dma +hardware_i2c +hardware_pwm +hardware_adc +hardware_spi +hardware_flash +hardware_sync +pico_stdio_usb +pico_platform +pico_multicore + +tinyusb_device +tinyusb_board + + +) + +pico_enable_stdio_usb(${PROJECT_NAME} 1) +pico_enable_stdio_uart(${PROJECT_NAME} 0) +pico_enable_stdio_semihosting(${PROJECT_NAME} 0) # Enable semihosting + + +# Generate UF2, bin, and other output formats +pico_add_extra_outputs(${PROJECT_NAME}) diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.cpp new file mode 100644 index 000000000..85ef655a2 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.cpp @@ -0,0 +1,248 @@ +#include "Clock.h" + +void Clock::Tick() + +{ + + const uint32_t prev = phase; // store old phase + phase += phase_increment; // increment phase + rising_edge = (prev > phase); // detect wraparound for main clock + + // uint32_t prev_prod = (uint64_t)prev * subclockMultiplier; // multiply old phase + // uint32_t curr_prod = (uint64_t)phase * subclockMultiplier; // multiply new phase + + uint32_t prev_prod = prev << subclockShift; + uint32_t curr_prod = phase << subclockShift; + + if (prev_prod > curr_prod) + ++subclockCount; + + totalTicks++; + + if (subclockCount >= subclockDividor) // subclockDividor is set by knob Y + { + rising_edge_mult = true; // This is the line that creates the output pulse + subclockCount = 0; + subclockSync = true; + } + else + { + rising_edge_mult = false; + } + + if (subclockSync && rising_edge) + { + subclockCount = 0; + subclockSync = false; + } +} + +void Clock::Reset() +{ + + if (isExternalClock1) + { + if (phase > PHASE_WRAP_THRESHOLD) + { + // If we're near the top already, don't reset to avoid double trigger + return; + } + } + phase = 0; + + rising_edge = true; + subclockSync = false; +} + +void Clock::SetPhaseIncrement(uint32_t increment) +{ + phase_increment = increment; +} + +uint32_t Clock::GetPhase() const +{ + return phase; +} + +bool Clock::IsRisingEdge() const +{ + return rising_edge; +} + +bool Clock::IsRisingEdgeMult() const +{ + return rising_edge_mult; +} + +uint32_t Clock::GetTicks() const +{ + return totalTicks; +} + +void Clock::SetPhaseIncrementFromTicks(uint32_t ticks_per_beat) +{ + if (ticks_per_beat == 0) + return; + phase_increment = (uint64_t(1) << 32) / ticks_per_beat; +} + +void Clock::SetPhaseIncrementFromBPM10(uint16_t BPM10) +{ + if (BPM10 == 0) + return; + + // BPM10 = BPM * 10 + // Beats per second = BPM10 / 600 + // ticks_per_beat = clockSpeed / (BPM10 / 600) + // Simplified to: ticks_per_beat = (clockSpeed * 600) / BPM10 + + uint32_t ticks_per_beat = (clockSpeed * 600UL) / BPM10; + phase_increment = (uint64_t(1) << 32) / ticks_per_beat; +} + +uint16_t Clock::GetBPM10FromPhaseIncrement() +{ + if (phase_increment == 0) + return 0; + + // Multiply first as 64-bit to avoid overflow + uint64_t temp = (uint64_t)phase_increment * clockSpeed * 600ULL; + uint32_t bpm10 = temp >> 32; // equivalent to dividing by 2^32 + + return (uint16_t)bpm10; +} + +// uint16_t Clock::TapTempo(uint32_t tapTime) +// { + +// uint16_t localBPM10 = 0; +// if (lastTapTime != 0) +// { +// uint32_t interval = tapTime - lastTapTime; + +// if (interval > minInterval && interval < maxInterval) +// { +// SetPhaseIncrementFromTicks(interval); +// localBPM10 = GetBPM10FromPhaseIncrement(); +// } +// else +// { +// lastTapTime = 0; // reset tap system +// return 0; +// } +// } +// lastTapTime = tapTime; +// Reset(); +// return localBPM10; +// } + +uint16_t Clock::TapTempo(uint32_t tapTime) +{ + if (lastTapTime == 0) + { + lastTapTime = tapTime; + return 0; // First tap: not enough data to calculate BPM + } + + uint32_t interval = tapTime - lastTapTime; + + if (interval < minInterval || interval > maxInterval) + { + lastTapTime = 0; // Reset on invalid tap + return 0; + } + + lastTapTime = tapTime; + SetPhaseIncrementFromTicks(interval); + Reset(); + return GetBPM10FromPhaseIncrement(); +} + +void Clock::UpdateDivide(uint8_t step) +{ + subclockDividor = subclockDivisions[step]; + subclockSync = true; + + if (isExternalClock1) + { + Reset(); + } +} + +void Clock::setExternalClock1(bool ext) +{ + isExternalClock1 = ext; +} + +void Clock::setExternalClock2(bool ext) +{ + isExternalClock2 = ext; +} + +bool Clock::getExternalClock1() +{ + return isExternalClock1; +} + +bool Clock::getExternalClock2() +{ + return isExternalClock2; +} + +void Clock::ExtPulse1() +{ + receivedExtPulse1 = true; +} + +void Clock::ExtPulse2() +{ + receivedExtPulse2 = true; +} + +bool Clock::ExtPulseReceived1() +{ // returns true if ext pulse 1 received, but only once + + bool temp = receivedExtPulse1; + receivedExtPulse1 = false; + return temp; +} + +bool Clock::ExtPulseReceived2() +{ // returns true if ext pulse 2 received, but only once + bool temp = receivedExtPulse2; + receivedExtPulse2 = false; + return temp; +} + +void Clock::setBPM10(uint16_t bpm10) +{ + // bypass tap tempo, set BPM directly + // BPM is always BPMx10 ie 120.0 bpm = 1200 + + SetPhaseIncrementFromBPM10(bpm10); +} + +uint16_t Clock::getBPM10() +{ + return GetBPM10FromPhaseIncrement(); +} + +uint32_t Clock::GetTicksPerBeat() +{ + if (phase_increment == 0) + return 0; + + return (uint64_t(1) << 32) / phase_increment; +} + +uint32_t Clock::GetTicksPerSubclockBeat() +{ + if (phase_increment == 0) + return 0; + + // One full beat in ticks + uint32_t ticks_per_beat = (uint64_t(1) << 32) / phase_increment; + + // Multiply by the subclockDividor to get the divided/multiplied beat length + return (ticks_per_beat / 16) * subclockDividor; +} diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.h b/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.h new file mode 100644 index 000000000..9824da6d3 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.h @@ -0,0 +1,60 @@ +#pragma once +#include + +class Clock +{ +public: + void Tick(); + void Reset(); + void SetPhaseIncrement(uint32_t increment); + uint32_t GetPhase() const; + bool IsRisingEdge() const; + bool IsRisingEdgeMult() const; + uint16_t TapTempo(uint32_t tapTime); // returns BPM10 when tempo is set, otherwise retuns 0 + uint32_t GetTicks() const; + void UpdateDivide(uint8_t step); + void setExternalClock1(bool ext); + void setExternalClock2(bool ext); + bool getExternalClock1(); + bool getExternalClock2(); + + void ExtPulse1(); + void ExtPulse2(); + bool ExtPulseReceived1(); + bool ExtPulseReceived2(); + void setBPM10(uint16_t bpm10); + uint16_t getBPM10(); + uint32_t GetTicksPerBeat(); + uint32_t GetTicksPerSubclockBeat(); + uint32_t TEST_subclock_phase = 0; + +private: + uint32_t minInterval = 480; // e.g., 10ms at 48kHz - to lock out double taps and noise + uint32_t maxInterval = 144000; // 3 seconds + uint32_t phase = 0; + uint32_t phase_increment = 0; + bool rising_edge = false; + bool rising_edge_mult = false; + uint32_t lastTapTime = 0; + uint32_t totalTicks = 0; + void SetPhaseIncrementFromTicks(uint32_t ticks_per_beat); + void SetPhaseIncrementFromBPM10(uint16_t BPM10); + uint16_t GetBPM10FromPhaseIncrement(); + + volatile uint32_t subclockDividor = 16; + const uint16_t subclockDivisions[9] = {512, 256, 128, 64, 32, 16, 8, 4, 2}; + // const uint16_t subclockDivisions[9] = {1024, 512, 256, 128, 64, 32, 16, 8, 4}; + const uint16_t subclockMultiplier = 16; // fastest clock multiplication, from which all other clocks are derived + const uint8_t subclockShift = 4; // since 2⁴ = 16 + + uint32_t subclockCount = 0; + bool subclockSync = false; + bool isExternalClock1 = false; + bool isExternalClock2 = false; + bool receivedExtPulse1 = false; + bool receivedExtPulse2 = false; + + uint32_t PHASE_WRAP_THRESHOLD = 0xF0000000; + + uint32_t clockSpeed = 48000; // just to calculate BPM, you can't change clock speed by changing this +}; diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/ComputerCard.h b/releases/93_Turing_Matrix/TuringMatrix_Code/ComputerCard.h new file mode 100644 index 000000000..f198fd770 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/ComputerCard.h @@ -0,0 +1,998 @@ +/* +ComputerCard - by Chris Johnson + +ComputerCard is a header-only C++ library, providing a class that +manages the hardware aspects of the Music Thing Modular Workshop +System Computer. + +It aims to present a very simple C++ interface for card programmers +to use the jacks, knobs, switch and LEDs, for programs running at +a fixed 48kHz audio sample rate. + +See examples/ directory +*/ + +#ifndef COMPUTERCARD_H +#define COMPUTERCARD_H + +#include "hardware/gpio.h" +#include "hardware/pwm.h" + +#define PULSE_1_RAW_OUT 8 +#define PULSE_2_RAW_OUT 9 + +#define CV_OUT_1 23 +#define CV_OUT_2 22 + +// The ADC (/DMA) run mode, used to stop DMA in a known state before writing to flash +#define RUN_ADC_MODE_RUNNING 0 +#define RUN_ADC_MODE_REQUEST_ADC_STOP 1 +#define RUN_ADC_MODE_ADC_STOPPED 2 +#define RUN_ADC_MODE_REQUEST_ADC_RESTART 3 + +// USB host status pin +#define USB_HOST_STATUS 20 + +class ComputerCard +{ + constexpr static int numLeds = 6; + constexpr static uint8_t leds[numLeds] = {10, 11, 12, 13, 14, 15}; + +public: + /// Knob index, used by KnobVal + enum Knob + { + Main, + X, + Y + }; + /// Switch position, used by SwitchVal + enum Switch + { + Down, + Middle, + Up + }; + /// Input jack socket, used by Connected and Disconnected + enum Input + { + Audio1, + Audio2, + CV1, + CV2, + Pulse1, + Pulse2 + }; + /// Hardware version + enum HardwareVersion_t + { + Proto1 = 0x2a, + Proto2_Rev1 = 0x30, + Rev1_1 = 0x0C, + Unknown = 0xFF + }; + /// USB Power state + enum USBPowerState_t + { + DFP, + UFP, + Unsupported + }; + + ComputerCard(); + + /** \brief Start audio processing. + + The Run method starts audio processing, calling ProcessSample using an interrupt. + Run is a blocking function (it never returns) + */ + void Run() + { + ComputerCard::thisptr = this; + AudioWorker(); + } + + /// Use before Run() to enable Connected/Disconnected detection + void EnableNormalisationProbe() { useNormProbe = true; } + +protected: + /// Callback, called once per sample at 48kHz + virtual void ProcessSample() = 0; + + /// Read knob position (returns 0-4095) + int32_t __not_in_flash_func(KnobVal)(Knob ind) { return knobs[ind]; } + + /// Read switch position + Switch __not_in_flash_func(SwitchVal)() { return switchVal; } + + /// Read switch position + bool __not_in_flash_func(SwitchChanged)() { return switchVal != lastSwitchVal; } + + /// Set Audio output (values -2048 to 2047) + void __not_in_flash_func(AudioOut)(int i, int16_t val) + { + dacOut[i] = val; + } + + /// Set Audio 1 output (values -2048 to 2047) + void __not_in_flash_func(AudioOut1)(int16_t val) + { + dacOut[0] = val; + } + + /// Set Audio 2 output (values -2048 to 2047) + void __not_in_flash_func(AudioOut2)(int16_t val) + { + dacOut[1] = val; + } + + /// Set CV output (values -2048 to 2047) + void __not_in_flash_func(CVOut)(int i, int16_t val) + { + pwm_set_gpio_level(CV_OUT_1 - i, (2047 - val) >> 1); + } + + /// Set CV 1 output (values -2048 to 2047) + void __not_in_flash_func(CVOut1)(int16_t val) + { + pwm_set_gpio_level(CV_OUT_1, (2047 - val) >> 1); + } + + /// Set CV 2 output (values -2048 to 2047) + void __not_in_flash_func(CVOut2)(int16_t val) + { + pwm_set_gpio_level(CV_OUT_2, (2047 - val) >> 1); + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOutMIDINote)(int i, uint8_t noteNum) + { + pwm_set_gpio_level(CV_OUT_1 - i, MIDIToDac(noteNum, 0) >> 8); + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOut1MIDINote)(uint8_t noteNum) + { + pwm_set_gpio_level(CV_OUT_1, MIDIToDac(noteNum, 0) >> 8); + } + + /// Set CV 2 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOut2MIDINote)(uint8_t noteNum) + { + pwm_set_gpio_level(CV_OUT_2, MIDIToDac(noteNum, 1) >> 8); + } + + /// Set Pulse output (true = on) + void __not_in_flash_func(PulseOut)(int i, bool val) + { + gpio_put(PULSE_1_RAW_OUT + i, !val); + } + + /// Set Pulse 1 output (true = on) + void __not_in_flash_func(PulseOut1)(bool val) + { + gpio_put(PULSE_1_RAW_OUT, !val); + } + + /// Set Pulse 2 output (true = on) + void __not_in_flash_func(PulseOut2)(bool val) + { + gpio_put(PULSE_2_RAW_OUT, !val); + } + + /// Return audio in (-2048 to 2047) + int16_t __not_in_flash_func(AudioIn)(int i) { return i ? adcInR : adcInL; } + + /// Return audio in 1 (-2048 to 2047) + int16_t __not_in_flash_func(AudioIn1)() { return adcInL; } + + /// Return audio in 1 (-2048 to 2047) + int16_t __not_in_flash_func(AudioIn2)() { return adcInR; } + + /// Return CV in (-2048 to 2047) + int16_t __not_in_flash_func(CVIn)(int i) { return cv[i]; } + + /// Return CV in 1 (-2048 to 2047) + int16_t __not_in_flash_func(CVIn1)() { return cv[0]; } + + /// Return CV in 2 (-2048 to 2047) + int16_t __not_in_flash_func(CVIn2)() { return cv[1]; } + + /// Read pulse in + bool __not_in_flash_func(PulseIn)(int i) { return pulse[i]; } + /// Return true for one sample on pulse rising edge + bool __not_in_flash_func(PulseInRisingEdge)(int i) { return pulse[i] && !last_pulse[i]; } + /// Return true for one sample on pulse falling edge + bool __not_in_flash_func(PulseInFallingEdge)(int i) { return !pulse[i] && last_pulse[i]; } + + /// Read pulse in 1 + bool __not_in_flash_func(PulseIn1)() { return pulse[0]; } + /// Return true for one sample on pulse 1 rising edge + bool __not_in_flash_func(PulseIn1RisingEdge)() { return pulse[0] && !last_pulse[0]; } + /// Return true for one sample on pulse 1 falling edge + bool __not_in_flash_func(PulseIn1FallingEdge)() { return !pulse[0] && last_pulse[0]; } + + /// Read pulse in 2 + bool __not_in_flash_func(PulseIn2)() { return pulse[1]; } + /// Return true for one sample on pulse 2 falling edge + bool __not_in_flash_func(PulseIn2FallingEdge)() { return !pulse[1] && last_pulse[1]; } + /// Return true for one sample on pulse 2 rising edge + bool __not_in_flash_func(PulseIn2RisingEdge)() { return pulse[1] && !last_pulse[1]; } + + /// Return true if jack connected to input + bool __not_in_flash_func(Connected)(Input i) { return connected[i]; } + /// Return true if no jack connected to input + bool __not_in_flash_func(Disconnected)(Input i) { return !connected[i]; } + + /// Set LED brightness, values 0-4095 + // Led numbers are: + // 0 1 + // 2 3 + // 4 5 + void __not_in_flash_func(LedBrightness)(uint32_t index, uint16_t value) + { + pwm_set_gpio_level(leds[index], (value * value) >> 8); + } + + /// Turn LED on/off + void __not_in_flash_func(LedOn)(uint32_t index, bool value = true) + { + pwm_set_gpio_level(leds[index], value ? 65535 : 0); + } + + /// Turn LED off + void __not_in_flash_func(LedOff)(uint32_t index) + { + pwm_set_gpio_level(leds[index], 0); + } + + // Return power state of USB port + USBPowerState_t USBPowerState() + { + if (HardwareVersion() != Rev1_1) + return Unsupported; + else if (gpio_get(USB_HOST_STATUS)) + return UFP; + else + return DFP; + } + + /// Return hardware version + HardwareVersion_t HardwareVersion() { return hw; } + + /// Return ID number unique to flash card + uint64_t UniqueCardID() { return uniqueID; } + + static ComputerCard *ThisPtr() { return thisptr; } + + void Abort(); + + void requestADCStop() + { + runADCMode = RUN_ADC_MODE_REQUEST_ADC_STOP; + } + + void requestADCRestart() + { + runADCMode = RUN_ADC_MODE_REQUEST_ADC_RESTART; + } + + bool isADCStopped() const + { + return runADCMode == RUN_ADC_MODE_ADC_STOPPED; + } + bool processSampleRunning = false; + +private: + typedef struct + { + float m, b; + int32_t mi, bi; + } CalCoeffs; + + typedef struct + { + int32_t dacSetting; + int8_t voltage; + } CalPoint; + + static constexpr int calMaxChannels = 2; + static constexpr int calMaxPoints = 10; + + uint8_t numCalibrationPoints[calMaxChannels]; + CalPoint calibrationTable[calMaxChannels][calMaxPoints]; + CalCoeffs calCoeffs[calMaxChannels]; + + uint64_t uniqueID; + + uint8_t ReadByteFromEEPROM(unsigned int eeAddress); + int ReadIntFromEEPROM(unsigned int eeAddress); + uint16_t CRCencode(const uint8_t *data, int length); + void CalcCalCoeffs(int channel); + int ReadEEPROM(); + uint32_t MIDIToDac(int midiNote, int channel); + + HardwareVersion_t hw; + HardwareVersion_t ProbeHardwareVersion(); + + int16_t dacOut[2]; + + volatile int32_t knobs[4] = {0, 0, 0, 0}; // 0-4095 + volatile bool pulse[2] = {0, 0}; + volatile bool last_pulse[2] = {0, 0}; + volatile int32_t cv[2] = {0, 0}; // -2047 - 2048 + volatile int16_t adcInL = 0x800, adcInR = 0x800; + + volatile uint8_t mxPos = 0; // external multiplexer value + + volatile int32_t plug_state[6] = {0, 0, 0, 0, 0, 0}; + volatile bool connected[6] = {0, 0, 0, 0, 0, 0}; + bool useNormProbe; + + Switch switchVal, lastSwitchVal; + + volatile uint8_t runADCMode; + + // Buffers that DMA reads into / out of + uint16_t ADC_Buffer[2][8]; + uint16_t SPI_Buffer[2][2]; + + uint8_t adc_dma, spi_dma; // DMA ids + + uint8_t dmaPhase = 0; + + // Convert signed int16 value into data string for DAC output + uint16_t __not_in_flash_func(dacval)(int16_t value, uint16_t dacChannel) + { + if (value < -2048) + value = -2048; + if (value > 2047) + value = 2047; + return (dacChannel | 0x3000) | (((uint16_t)((value & 0x0FFF) + 0x800)) & 0x0FFF); + } + uint32_t next_norm_probe(); + + void BufferFull(); + + void AudioWorker(); + + static void AudioCallback() + { + thisptr->BufferFull(); + } + static ComputerCard *thisptr; +}; + +#ifndef COMPUTERCARD_NOIMPL + +#include "hardware/adc.h" +#include "hardware/clocks.h" +#include "hardware/dma.h" +#include "hardware/flash.h" +#include "hardware/i2c.h" +#include "hardware/irq.h" +#include "hardware/spi.h" + +// Input normalisation probe pin +#define NORMALISATION_PROBE 4 + +// Mux pins +#define MX_A 24 +#define MX_B 25 + +// ADC input pins +#define AUDIO_L_IN_1 27 +#define AUDIO_R_IN_1 26 +#define MUX_IO_1 28 +#define MUX_IO_2 29 + +#define DAC_CHANNEL_A 0x0000 +#define DAC_CHANNEL_B 0x8000 + +#define DAC_CS 21 +#define DAC_SCK 18 +#define DAC_TX 19 + +#define EEPROM_SDA 16 +#define EEPROM_SCL 17 + +#define PULSE_1_INPUT 2 +#define PULSE_2_INPUT 3 + +#define DEBUG_1 0 +#define DEBUG_2 1 + +#define SPI_PORT spi0 +#define SPI_DREQ DREQ_SPI0_TX + +#define BOARD_ID_0 7 +#define BOARD_ID_1 6 +#define BOARD_ID_2 5 + +#define EEPROM_ADDR_ID 0 +#define EEPROM_ADDR_VERSION 2 +#define EEPROM_ADDR_CRC_L 87 +#define EEPROM_ADDR_CRC_H 86 +#define EEPROM_VAL_ID 2001 +#define EEPROM_NUM_BYTES 88 + +#define EEPROM_PAGE_ADDRESS 0x50 + +ComputerCard *ComputerCard::thisptr; + +// Return pseudo-random bit for normalisation probe +uint32_t __not_in_flash_func(ComputerCard::next_norm_probe)() +{ + static uint32_t lcg_seed = 1; + lcg_seed = 1664525 * lcg_seed + 1013904223; + return lcg_seed >> 31; +} + +// Main audio core function +void __not_in_flash_func(ComputerCard::AudioWorker)() +{ + + adc_select_input(0); + adc_set_round_robin(0b0001111U); + + // enabled, with DMA request when FIFO contains data, no erro flag, no byte shift + adc_fifo_setup(true, true, 1, false, false); + + // ADC clock runs at 48MHz + // 48MHz ÷ (124+1) = 384kHz ADC sample rate + // = 8×48kHz audio sample rate + adc_set_clkdiv(124); + + // claim and setup DMAs for reading to ADC, and writing to SPI DAC + adc_dma = dma_claim_unused_channel(true); + spi_dma = dma_claim_unused_channel(true); + + dma_channel_config adc_dmacfg, spi_dmacfg; + adc_dmacfg = dma_channel_get_default_config(adc_dma); + spi_dmacfg = dma_channel_get_default_config(spi_dma); + + // Reading from ADC into memory buffer, so increment on write, but no increment on read + channel_config_set_transfer_data_size(&adc_dmacfg, DMA_SIZE_16); + channel_config_set_read_increment(&adc_dmacfg, false); + channel_config_set_write_increment(&adc_dmacfg, true); + + // Synchronise ADC DMA the ADC samples + channel_config_set_dreq(&adc_dmacfg, DREQ_ADC); + + // Setup DMA for 8 ADC samples + dma_channel_configure(adc_dma, &adc_dmacfg, ADC_Buffer[dmaPhase], &adc_hw->fifo, 8, true); + + // Turn on IRQ for ADC DMA + dma_channel_set_irq0_enabled(adc_dma, true); + + // Call buffer_full ISR when ADC DMA finished + irq_set_enabled(DMA_IRQ_0, true); + irq_set_exclusive_handler(DMA_IRQ_0, ComputerCard::AudioCallback); + + // Set up DMA for SPI + spi_dmacfg = dma_channel_get_default_config(spi_dma); + channel_config_set_transfer_data_size(&spi_dmacfg, DMA_SIZE_16); + + // SPI DMA timed to SPI TX + channel_config_set_dreq(&spi_dmacfg, SPI_DREQ); + + // Set up DMA to transmit 2 samples to SPI + dma_channel_configure(spi_dma, &spi_dmacfg, &spi_get_hw(SPI_PORT)->dr, NULL, 2, false); + + adc_run(true); + + while (1) + { + // If ready to restart + if (runADCMode == RUN_ADC_MODE_REQUEST_ADC_RESTART) + { + runADCMode = RUN_ADC_MODE_RUNNING; + + dma_hw->ints0 = 1u << adc_dma; // reset adc interrupt flag + dma_channel_set_write_addr(adc_dma, ADC_Buffer[dmaPhase], true); // start writing into new buffer + dma_channel_set_read_addr(spi_dma, SPI_Buffer[dmaPhase], true); // start reading from new buffer + + adc_set_round_robin(0); + adc_select_input(0); + adc_set_round_robin(0b0001111U); + adc_run(true); + } + else if (runADCMode == RUN_ADC_MODE_ADC_STOPPED) + { + // break; + continue; // Wait until restart is requested + } + } +} + +void ComputerCard::Abort() +{ + runADCMode = RUN_ADC_MODE_REQUEST_ADC_STOP; +} + +// Per-audio-sample ISR, called when two sets of ADC samples have been collected from all four inputs +void __not_in_flash_func(ComputerCard::BufferFull)() +{ + + processSampleRunning = true; + static int startupCounter = 8; // Decreases by 1 each sample, can do startup things when nonzero. + static int mux_state = 0; + static int norm_probe_count = 0; + + // Internal variables for IIR filters on knobs/cv + static volatile int32_t knobssm[4] = {0, 0, 0, 0}; + static volatile int32_t cvsm[2] = {0, 0}; + __attribute__((unused)) static int np = 0, np1 = 0, np2 = 0; + + adc_select_input(0); + + // Advance external mux to next state + int next_mux_state = (mux_state + 1) & 0x3; + gpio_put(MX_A, next_mux_state & 1); + gpio_put(MX_B, next_mux_state & 2); + + // Set up new writes into next buffer + uint8_t cpuPhase = dmaPhase; + dmaPhase = 1 - dmaPhase; + + dma_hw->ints0 = 1u << adc_dma; // reset adc interrupt flag + dma_channel_set_write_addr(adc_dma, ADC_Buffer[dmaPhase], true); // start writing into new buffer + dma_channel_set_read_addr(spi_dma, SPI_Buffer[dmaPhase], true); // start reading from new buffer + + //////////////////////////////////////// + // Collect various inputs and put them in variables for the DSP + + // Set CV inputs, with ~240Hz LPF on CV input + int cvi = mux_state % 2; + + // Attempted compensation of ADC DNL errors. Not really tested. + uint16_t adc512 = ADC_Buffer[cpuPhase][3] + 512; + if (!(adc512 % 0x01FF)) + ADC_Buffer[cpuPhase][3] += 4; + ADC_Buffer[cpuPhase][3] += (adc512 >> 10) << 3; + + cvsm[cvi] = (15 * (cvsm[cvi]) + 16 * ADC_Buffer[cpuPhase][3]) >> 4; + cv[cvi] = 2048 - (cvsm[cvi] >> 4); + + // Set audio inputs, by averaging the two samples collected. + // Invert to counteract inverting op-amp input configuration + adcInR = -(((ADC_Buffer[cpuPhase][0] + ADC_Buffer[cpuPhase][4]) - 0x1000) >> 1); + + adcInL = -(((ADC_Buffer[cpuPhase][1] + ADC_Buffer[cpuPhase][5]) - 0x1000) >> 1); + + // Set pulse inputs + last_pulse[0] = pulse[0]; + last_pulse[1] = pulse[1]; + pulse[0] = !gpio_get(PULSE_1_INPUT); + pulse[1] = !gpio_get(PULSE_2_INPUT); + + // Set knobs, with ~60Hz LPF + int knob = mux_state; + knobssm[knob] = (127 * (knobssm[knob]) + 16 * ADC_Buffer[cpuPhase][6]) >> 7; + knobs[knob] = knobssm[knob] >> 4; + + // Set switch value + switchVal = static_cast((knobs[3] > 1000) + (knobs[3] > 3000)); + if (startupCounter) + { + // Don't detect switch changes in first few cycles + lastSwitchVal = switchVal; + // Should initialise knob and CV smoothing filters here too + } + + //////////////////////////// + // Normalisation probe + + if (useNormProbe) + { + // Set normalisation probe output value + // and update np to the expected history string + if (norm_probe_count == 0) + { + int32_t normprobe = next_norm_probe(); + gpio_put(NORMALISATION_PROBE, normprobe); + np = (np << 1) + (normprobe & 0x1); + } + + // CV sampled at 24kHz comes in over two successive samples + if (norm_probe_count == 14 || norm_probe_count == 15) + { + plug_state[2 + cvi] = (plug_state[2 + cvi] << 1) + (ADC_Buffer[cpuPhase][3] < 1800); + } + + // Audio and pulse measured every sample at 48kHz + if (norm_probe_count == 15) + { + plug_state[Input::Audio1] = (plug_state[Input::Audio1] << 1) + (ADC_Buffer[cpuPhase][5] < 1800); + plug_state[Input::Audio2] = (plug_state[Input::Audio2] << 1) + (ADC_Buffer[cpuPhase][4] < 1800); + plug_state[Input::Pulse1] = (plug_state[Input::Pulse1] << 1) + (pulse[0]); + plug_state[Input::Pulse2] = (plug_state[Input::Pulse2] << 1) + (pulse[1]); + + for (int i = 0; i < 6; i++) + { + connected[i] = (np != plug_state[i]); + } + } + + // Force disconnected values to zero, rather than the normalisation probe garbage + if (Disconnected(Input::Audio1)) + adcInL = 0; + if (Disconnected(Input::Audio2)) + adcInR = 0; + if (Disconnected(Input::CV1)) + cv[0] = 0; + if (Disconnected(Input::CV2)) + cv[1] = 0; + if (Disconnected(Input::Pulse1)) + pulse[0] = 0; + if (Disconnected(Input::Pulse2)) + pulse[1] = 0; + processSampleRunning = false; + } + + //////////////////////////////////////// + // Run the DSP + ProcessSample(); + + //////////////////////////////////////// + // Collect DSP outputs and put them in the DAC SPI buffer + // CV/Pulse outputs are done immediately in ProcessSample + + // Invert dacout to counteract inverting output configuration + SPI_Buffer[cpuPhase][0] = dacval(-dacOut[0], DAC_CHANNEL_A); + SPI_Buffer[cpuPhase][1] = dacval(-dacOut[1], DAC_CHANNEL_B); + + mux_state = next_mux_state; + + // If Abort called, stop ADC and DMA + if (runADCMode == RUN_ADC_MODE_REQUEST_ADC_STOP) + { + adc_run(false); + adc_set_round_robin(0); + adc_select_input(0); + + dma_hw->ints0 = 1u << adc_dma; // reset adc interrupt flag + dma_channel_cleanup(adc_dma); + dma_channel_cleanup(spi_dma); + irq_remove_handler(DMA_IRQ_0, ComputerCard::AudioCallback); + + runADCMode = RUN_ADC_MODE_ADC_STOPPED; + } + + norm_probe_count = (norm_probe_count + 1) & 0xF; + + lastSwitchVal = switchVal; + + if (startupCounter) + startupCounter--; +} + +ComputerCard::HardwareVersion_t ComputerCard::ProbeHardwareVersion() +{ + // Enable pull-downs, and measure + gpio_set_pulls(BOARD_ID_0, false, true); + gpio_set_pulls(BOARD_ID_1, false, true); + gpio_set_pulls(BOARD_ID_2, false, true); + sleep_us(1); + + // Pull-down state in bits 0, 2, 4 + uint8_t pd = gpio_get(BOARD_ID_0) | (gpio_get(BOARD_ID_1) << 2) | (gpio_get(BOARD_ID_2) << 4); + + // Enable pull-ups, and measure + gpio_set_pulls(BOARD_ID_0, true, false); + gpio_set_pulls(BOARD_ID_1, true, false); + gpio_set_pulls(BOARD_ID_2, true, false); + sleep_us(1); + + // Pull-up state in bits 1, 3, 5 + uint8_t pu = (gpio_get(BOARD_ID_0) << 1) | (gpio_get(BOARD_ID_1) << 3) | (gpio_get(BOARD_ID_2) << 5); + + // Combine to give 6-bit ID + uint8_t id = pd | pu; + + // Set pull-downs + gpio_set_pulls(BOARD_ID_0, false, true); + gpio_set_pulls(BOARD_ID_1, false, true); + gpio_set_pulls(BOARD_ID_2, false, true); + + switch (id) + { + case Proto1: + case Proto2_Rev1: + case Rev1_1: + return static_cast(id); + default: + return Unknown; + } +} + +ComputerCard::ComputerCard() +{ + + runADCMode = RUN_ADC_MODE_RUNNING; + + adc_run(false); + adc_select_input(0); + + useNormProbe = false; + for (int i = 0; i < 6; i++) + { + connected[i] = false; + } + + // Initialize PWM for LEDs, in pairs due pinout and PWM hardware + for (int i = 0; i < numLeds; i += 2) + { + gpio_set_function(leds[i], GPIO_FUNC_PWM); + gpio_set_function(leds[i] + 1, GPIO_FUNC_PWM); + + // now create PWM config struct + pwm_config config = pwm_get_default_config(); + pwm_config_set_wrap(&config, 65535); // 16-bit PWM + + // now set this PWM config to apply to the two outputs + pwm_init(pwm_gpio_to_slice_num(leds[i]), &config, true); + pwm_init(pwm_gpio_to_slice_num(leds[i] + 1), &config, true); + + // set initial level + pwm_set_gpio_level(leds[i], 0); + pwm_set_gpio_level(leds[i] + 1, 0); + } + + // Board version ID pins + gpio_init(BOARD_ID_0); + gpio_init(BOARD_ID_1); + gpio_init(BOARD_ID_2); + gpio_set_dir(BOARD_ID_0, GPIO_IN); + gpio_set_dir(BOARD_ID_1, GPIO_IN); + gpio_set_dir(BOARD_ID_2, GPIO_IN); + hw = ProbeHardwareVersion(); + + // USB host status pin + gpio_init(USB_HOST_STATUS); + gpio_disable_pulls(USB_HOST_STATUS); + + // Normalisation probe pin + gpio_init(NORMALISATION_PROBE); + gpio_set_dir(NORMALISATION_PROBE, GPIO_OUT); + gpio_put(NORMALISATION_PROBE, false); + + adc_init(); // Initialize the ADC + + // Set ADC pins + adc_gpio_init(AUDIO_L_IN_1); + adc_gpio_init(AUDIO_R_IN_1); + adc_gpio_init(MUX_IO_1); + adc_gpio_init(MUX_IO_2); + + // Initialize Mux Control pins + gpio_init(MX_A); + gpio_init(MX_B); + gpio_set_dir(MX_A, GPIO_OUT); + gpio_set_dir(MX_B, GPIO_OUT); + + // Initialize pulse out + gpio_init(PULSE_1_RAW_OUT); + gpio_set_dir(PULSE_1_RAW_OUT, GPIO_OUT); + gpio_put(PULSE_1_RAW_OUT, true); // set raw value high (output low) + + gpio_init(PULSE_2_RAW_OUT); + gpio_set_dir(PULSE_2_RAW_OUT, GPIO_OUT); + gpio_put(PULSE_2_RAW_OUT, true); // set raw value high (output low) + + // Initialize pulse in + gpio_init(PULSE_1_INPUT); + gpio_set_dir(PULSE_1_INPUT, GPIO_IN); + gpio_pull_up(PULSE_1_INPUT); // NB Needs pullup to activate transistor on inputs + + gpio_init(PULSE_2_INPUT); + gpio_set_dir(PULSE_2_INPUT, GPIO_IN); + gpio_pull_up(PULSE_2_INPUT); // NB: Needs pullup to activate transistor on inputs + + // Setup SPI for DAC output + spi_init(SPI_PORT, 15625000); + spi_set_format(SPI_PORT, 16, SPI_CPOL_0, SPI_CPHA_0, SPI_MSB_FIRST); + gpio_set_function(DAC_SCK, GPIO_FUNC_SPI); + gpio_set_function(DAC_TX, GPIO_FUNC_SPI); + gpio_set_function(DAC_CS, GPIO_FUNC_SPI); + + // Setup I2C for EEPROM + i2c_init(i2c0, 100 * 1000); + gpio_set_function(EEPROM_SDA, GPIO_FUNC_I2C); + gpio_set_function(EEPROM_SCL, GPIO_FUNC_I2C); + + // Setup CV PWM + // First, tell the CV pins that the PWM is in charge of the value. + gpio_set_function(CV_OUT_1, GPIO_FUNC_PWM); + gpio_set_function(CV_OUT_2, GPIO_FUNC_PWM); + + // now create PWM config struct + pwm_config config = pwm_get_default_config(); + pwm_config_set_wrap(&config, 2047); // 11-bit PWM + + // now set this PWM config to apply to the two outputs + // NB: CV_A and CV_B share the same PWM slice, which means that they share a PWM config + // They have separate 'gpio_level's (output compare unit) though, so they can have different PWM on-times + pwm_init(pwm_gpio_to_slice_num(CV_OUT_1), &config, true); // Slice 1, channel A + pwm_init(pwm_gpio_to_slice_num(CV_OUT_2), &config, true); // slice 1 channel B (redundant to set up again) + + // set initial level to half way (0V) + pwm_set_gpio_level(CV_OUT_1, 1024); + pwm_set_gpio_level(CV_OUT_2, 1024); + +// If not using UART pins for UART, instead use as debug lines +#ifndef ENABLE_UART_DEBUGGING + // Debug pins + gpio_init(DEBUG_1); + gpio_set_dir(DEBUG_1, GPIO_OUT); + + gpio_init(DEBUG_2); + gpio_set_dir(DEBUG_2, GPIO_OUT); +#endif + + // Read EEPROM calibration values + ReadEEPROM(); + + // Read unique card ID + flash_get_unique_id((uint8_t *)&uniqueID); + // Do some mixing up of the bits using full-cycle 64-bit LCG + // Should help ensure most bytes change even if many bits of + // the original flash unique ID are the same between flash chips. + for (int i = 0; i < 20; i++) + { + uniqueID = uniqueID * 6364136223846793005ULL + 1442695040888963407ULL; + } +} + +// Read a byte from EEPROM +uint8_t ComputerCard::ReadByteFromEEPROM(unsigned int eeAddress) +{ + uint8_t deviceAddress = EEPROM_PAGE_ADDRESS | ((eeAddress >> 8) & 0x0F); + uint8_t data = 0xFF; + + uint8_t addr_low_byte = eeAddress & 0xFF; + i2c_write_blocking(i2c0, deviceAddress, &addr_low_byte, 1, false); + + i2c_read_blocking(i2c0, deviceAddress, &data, 1, false); + return data; +} + +// Read a 16-bit integer from EEPROM +int ComputerCard::ReadIntFromEEPROM(unsigned int eeAddress) +{ + uint8_t highByte = ReadByteFromEEPROM(eeAddress); + uint8_t lowByte = ReadByteFromEEPROM(eeAddress + 1); + return (highByte << 8) | lowByte; +} + +uint16_t ComputerCard::CRCencode(const uint8_t *data, int length) +{ + uint16_t crc = 0xFFFF; // Initial CRC value + for (int i = 0; i < length; i++) + { + crc ^= ((uint16_t)data[i]) << 8; // Bring in the next byte + for (uint8_t bit = 0; bit < 8; bit++) + { + if (crc & 0x8000) + { + crc = (crc << 1) ^ 0x1021; // CRC-CCITT polynomial + } + else + { + crc = crc << 1; + } + } + } + return crc; +} + +int ComputerCard::ReadEEPROM() +{ + // Set up default values in the calibration table, + // to be used if EEPROM read fails + calibrationTable[0][0].voltage = -20; // -2V + calibrationTable[0][0].dacSetting = 347700; + calibrationTable[0][1].voltage = 0; // 0V + calibrationTable[0][1].dacSetting = 261200; + calibrationTable[0][2].voltage = 20; // +2V + calibrationTable[0][2].dacSetting = 174400; + + calibrationTable[1][0].voltage = -20; // -2V + calibrationTable[1][0].dacSetting = 347700; + calibrationTable[1][1].voltage = 0; // 0V + calibrationTable[1][1].dacSetting = 261200; + calibrationTable[1][2].voltage = 20; // +2V + calibrationTable[1][2].dacSetting = 174400; + + if (ReadIntFromEEPROM(EEPROM_ADDR_ID) != EEPROM_VAL_ID) + { + return 1; + } + uint8_t buf[EEPROM_NUM_BYTES]; + for (int i = 0; i < EEPROM_NUM_BYTES; i++) + { + buf[i] = ReadByteFromEEPROM(i); + } + + uint16_t calculatedCRC = CRCencode(buf, 86); + uint16_t foundCRC = ((uint16_t)buf[EEPROM_ADDR_CRC_H] << 8) | buf[EEPROM_ADDR_CRC_L]; + + if (calculatedCRC != foundCRC) + { + return 1; + } + + int bufferIndex = 4; + + for (uint8_t channel = 0; channel < calMaxChannels; channel++) + { + int channelOffset = bufferIndex + (41 * channel); // channel 0 = 4, channel 1 = 45 + numCalibrationPoints[channel] = buf[channelOffset++]; + for (uint8_t point = 0; point < numCalibrationPoints[channel]; point++) + { + // Unpack Pack targetVoltage (int8_t) from buf + int8_t targetVoltage = (int8_t)buf[channelOffset++]; + + // Unack dacSetting (uint32_t) from buf (4 bytes) + uint32_t dacSetting = 0; + dacSetting |= ((uint32_t)buf[channelOffset++]) << 24; // MSB + dacSetting |= ((uint32_t)buf[channelOffset++]) << 16; + dacSetting |= ((uint32_t)buf[channelOffset++]) << 8; + dacSetting |= ((uint32_t)buf[channelOffset++]); // LSB + + // Write settings into calibration table + calibrationTable[channel][point].voltage = targetVoltage; + calibrationTable[channel][point].dacSetting = dacSetting; + } + CalcCalCoeffs(channel); + } + + return 0; +} + +void ComputerCard::CalcCalCoeffs(int channel) +{ + float sumV = 0.0; + float sumDAC = 0.0; + float sumV2 = 0.0; + float sumVDAC = 0.0; + int N = numCalibrationPoints[channel]; + + for (int i = 0; i < N; i++) + { + float v = calibrationTable[channel][i].voltage * 0.1f; + float dac = calibrationTable[channel][i].dacSetting; + sumV += v; + sumDAC += dac; + sumV2 += v * v; + sumVDAC += v * dac; + } + + float denominator = N * sumV2 - sumV * sumV; + if (denominator != 0) + { + calCoeffs[channel].m = (N * sumVDAC - sumV * sumDAC) / denominator; + } + else + { + calCoeffs[channel].m = 0.0; + } + calCoeffs[channel].b = (sumDAC - calCoeffs[channel].m * sumV) / N; + + calCoeffs[channel].mi = int32_t(calCoeffs[channel].m * 1.333333333333333f + 0.5f); + calCoeffs[channel].bi = int32_t(calCoeffs[channel].b + 0.5f); +} + +uint32_t ComputerCard::MIDIToDac(int midiNote, int channel) +{ + int32_t dacValue = ((calCoeffs[channel].mi * (midiNote - 60)) >> 4) + calCoeffs[channel].bi; + if (dacValue > 524287) + dacValue = 524287; + if (dacValue < 0) + dacValue = 0; + return dacValue; +} + +#endif + +#endif diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp new file mode 100644 index 000000000..171ea4208 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp @@ -0,0 +1,95 @@ +#include "Config.h" +#include "hardware/flash.h" +#include "hardware/sync.h" +#include +#include "hardware/regs/addressmap.h" +#include "pico/multicore.h" + +uint32_t const Config::MAGIC = 0x434F4E46; +size_t const Config::FLASH_SIZE = 2 * 1024 * 1024; +size_t const Config::BLOCK_SIZE = 4096; +size_t const Config::OFFSET = Config::FLASH_SIZE - Config::BLOCK_SIZE; + +static const uint8_t *CONFIG_FLASH_PTR = reinterpret_cast(XIP_BASE + Config::OFFSET); + +// Config.cpp – put these lines near the top of the file +static uint8_t sector_buf[Config::BLOCK_SIZE] __attribute__((aligned(4))); +static uint8_t wr_buf[Config::BLOCK_SIZE] __attribute__((aligned(4))); + +void Config::load(bool forceReset) +{ + std::memcpy(&config, CONFIG_FLASH_PTR, sizeof(Data)); + + if (config.magic != Config::MAGIC || forceReset) + { + config = Data(); // Reset to defaults + save(); + } + + if (config.vactrol.law > 2) + config.vactrol.law = 0; + if (config.vactrol.relation > 2) + config.vactrol.relation = 0; + if (config.vactrol.min1 > config.vactrol.max1) + config.vactrol.max1 = config.vactrol.min1; + if (config.vactrol.min2 > config.vactrol.max2) + config.vactrol.max2 = config.vactrol.min2; +} + +/* +void Config::save() +{ + uint8_t flash_copy[BLOCK_SIZE]; + std::memcpy(flash_copy, CONFIG_FLASH_PTR, BLOCK_SIZE); + + if (std::memcmp(&config, flash_copy, sizeof(Data)) == 0) + return; // No change, skip + + uint8_t temp[BLOCK_SIZE] = {0}; + std::memcpy(temp, &config, sizeof(Data)); + + // // Stop all other execution paths + multicore_lockout_start_blocking(); + + // // Kill interrupts + uint32_t ints = save_and_disable_interrupts(); + + // // Do the write + // flash_range_erase(OFFSET, FLASH_SECTOR_SIZE); + // flash_range_program(OFFSET, temp, BLOCK_SIZE); + + // // Restore + + restore_interrupts(ints); + multicore_lockout_end_blocking(); +} +*/ + +void Config::save() +{ + /* ---------- 1. Compare ---------- */ + memcpy(sector_buf, CONFIG_FLASH_PTR, Config::BLOCK_SIZE); + if (memcmp(&config, sector_buf, sizeof config) == 0) + return; // no change + + /* ---------- 2. Prepare write buffer ---------- */ + memcpy(wr_buf, sector_buf, Config::BLOCK_SIZE); // old sector + memcpy(wr_buf, &config, sizeof config); // patch with new data + + /* ---------- 3. Critical section ---------- */ + uint32_t ints = save_and_disable_interrupts(); // 1) IRQs off + multicore_lockout_start_blocking(); // 2) park Core 1 + + // --- flash operations (uncomment when ready) --- + flash_range_erase(OFFSET, FLASH_SECTOR_SIZE); + flash_range_program(OFFSET, wr_buf, Config::BLOCK_SIZE); + + multicore_lockout_end_blocking(); // 3) release Core 1 + restore_interrupts(ints); // 4) IRQs on + +} + +Config::Data &Config::get() +{ + return config; +} diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.h b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.h new file mode 100644 index 000000000..522fdc80f --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.h @@ -0,0 +1,66 @@ +#pragma once + +#include +#include + +class Config +{ +public: + static const uint32_t MAGIC; + static const size_t FLASH_SIZE; + static const size_t BLOCK_SIZE; + static const size_t OFFSET; + + struct Preset + { + uint8_t scale; + uint8_t range; + uint8_t length; + uint8_t looplen; + uint8_t pulseMode1; + uint8_t pulseMode2; + uint8_t cvRange; + }; + + struct Vactrol + { + uint8_t law = 0; + uint8_t relation = 0; + uint8_t rise = 48; + uint8_t fall = 56; + uint8_t min1 = 0; + uint8_t max1 = 255; + uint8_t min2 = 0; + uint8_t max2 = 255; + }; + + struct Data + { + uint32_t magic = MAGIC; + + union + { + struct + { + uint8_t bpm_lo; + uint8_t bpm_hi; + }; + uint16_t bpm = 1605; + }; + uint8_t divide = 5; + uint8_t cvRange = 0; + + Preset preset[2] = { + {3, 2, 5, 1, 0, 0, 0}, // Preset 0 + {3, 1, 5, 1, 0, 1, 3} // Preset 1 + }; + Vactrol vactrol = {}; + }; + + void load(bool forceReset = false); + void save(); + Data &get(); + +private: + Data config; +}; diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp new file mode 100644 index 000000000..f4c73bcb7 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp @@ -0,0 +1,969 @@ +// MainApp.cpp +#include "MainApp.h" +#include +#include "pico/time.h" // temporary for testing +#include // temporary for testing +#include "tusb.h" + +// Config variables in HEX +const int CARD_NUMBER = 0x93; +const int MAJOR_VERSION = 0x01; +const int MINOR_VERSION = 0x05; +const int POINT_VERSION = 0x00; + +MainApp::MainApp() + + // Initialise the Turing machines with variations of the memory card ID, unique but not random + : turingDAC1(8, MemoryCardID()), + turingDAC2(8, MemoryCardID() * 2), + turingPWM1(8, MemoryCardID() * 3), + turingPWM2(8, MemoryCardID() * 4), + turingPulseLength1(8, MemoryCardID() * 5), + turingPulseLength2(8, MemoryCardID() * 6) + +{ + + ui.init(this, &clk); +} + +void MainApp::UpdateNotePools() +{ + // create note pools for PWM precision CV outputs + bool p = 0; + int base_note = 48; // C3 + int range = settings->preset[p].range; + int scale = settings->preset[p].scale; + turingPWM1.UpdateNotePool(base_note, range, scale); + turingPWM2.UpdateNotePool(base_note, range, scale); +} + +void MainApp::UpdatePulseLengths() +{ + + bool p = 0; + uint8_t lengthMode = settings->preset[p].length; + + switch (lengthMode) + { + case 0: + ui.SetPulseLength(1); + ui.SetPulseMod(0); + break; + case 1: + ui.SetPulseLength(25); + ui.SetPulseMod(0); + break; + case 2: + ui.SetPulseLength(50); + ui.SetPulseMod(0); + break; + case 3: + ui.SetPulseLength(75); + ui.SetPulseMod(0); + break; + case 4: + ui.SetPulseLength(99); + ui.SetPulseMod(0); + break; + case 5: + ui.SetPulseLength(15); + ui.SetPulseMod(12); + break; + case 6: + ui.SetPulseLength(50); + ui.SetPulseMod(30); + break; + default: + // Optional: Handle out-of-range values for `setting` + ui.SetPulseLength(1); + ui.SetPulseMod(0); + break; + } +} + +void MainApp::LoadSettings(bool reset) +{ + // Load or initialise config + cfg.load(reset); // 1 = force reset + settings = &cfg.get(); + CurrentBPM10 = settings->bpm; // load bpm from settings file NB bpm always 10x i.e 1200 = 120.0 bpm. + clk.setBPM10(CurrentBPM10); + + UpdateNotePools(); + UpdatePulseLengths(); + UpdateCh2Lengths(); + UpdateCVRange(); + UpdateVactrolTiming(); +} + +void __not_in_flash_func(MainApp::ProcessSample)() +{ + + // TEST_write_to_Pulse(0, true); // pulse to test on oscilloscope + + // Call tap before ui.tick and before clk.tick, so that reset triggered tap is tapped make it to ui. + if (tapReceived()) + { + uint32_t now = clk.GetTicks(); + uint16_t tempBPM = clk.TapTempo(now); + if (tempBPM > 0 && newBPM10 == 0) + { + newBPM10 = tempBPM; + } + } + if (extPulse1Received()) + { + uint32_t now = clk.GetTicks(); + clk.TapTempo(now); + clk.ExtPulse1(); + } + + if (extPulse2Received()) + { + + clk.ExtPulse2(); + } + + clk.Tick(); + + ui.Tick(); + ProcessVactrolMix(); + + // CVOut1((clk.GetPhase() >> 20) - 2048); // just for debugging, remove + // CVOut2((clk.TEST_subclock_phase >> 20) - 2048); + + // blink(1, 50); // show that Core 1 is alive + + // TEST_write_to_Pulse(0, false); // oscilloscope test +} + +void MainApp::Housekeeping() +{ + + static uint8_t packet[128]; + + while (tud_midi_available()) + { + size_t len = tud_midi_stream_read(packet, sizeof(packet)); + handleSysExMessage(packet, len); + } + + // LedOn(2, pendingSave); + uint64_t nowUs = time_us_64(); + + // BPM changed? + + if (newBPM10 > 0 && newBPM10 < 8000 && newBPM10 != CurrentBPM10) + { + settings->bpm = newBPM10; + CurrentBPM10 = newBPM10; + newBPM10 = 0; + + lastChangeTimeUs = nowUs; + pendingSave = true; + } + else if (newBPM10 > 0 && (newBPM10 >= 8000 || newBPM10 == CurrentBPM10)) + { + // Clear invalid or duplicate BPM + newBPM10 = 0; + } + + // Has 2 seconds passed since last change, and save is pending? + if (pendingSave && (nowUs - lastChangeTimeUs >= 2000000)) + { + cfg.save(); + pendingSave = false; + } + + // blink(0, 250); // show that Core 0 is alive + + if (!VactrolLayerActive()) + { + turingRandomness = KnobVal(Main); + } + + ui.SlowUI(); // call knob checking etc + + updateLedState(); + + ui.UpdatePulseMod(turingPulseLength1.DAC_8(), turingPulseLength2.DAC_8()); + + UpdatePulseLengths(); + + // Check if external clocks have been unplugged + if (clk.getExternalClock1() && !PulseInConnected1()) + { + + clk.setExternalClock1(false); + CurrentBPM10 = settings->bpm; + clk.setBPM10(CurrentBPM10); + } + + if (!PulseInConnected2() && clk.getExternalClock2()) + { + clk.setExternalClock2(false); + } + + if (!VactrolLayerActive() && Connected(CV2)) + { + midiOffset = CVtoMidiOffset(CVIn2()); + } + else + { + midiOffset = 0; + } + + if (sendViz && tud_midi_n_mounted(0)) + { + SendLiveStatus(); + sendViz = false; + } +} + +void MainApp::PulseLed1(bool status) +{ + pulseLed1_status = status; +} + +void MainApp::PulseLed2(bool status) +{ + + pulseLed2_status = status; +} + +bool MainApp::PulseOutput1(bool requested) +{ + bool emit = false; + bool isTuringMode = settings->preset[0].pulseMode1; + + if (isTuringMode && requested) + { + emit = (turingPWM1.DAC_8() & 0x01); + } + else + { + emit = requested; + } + + PulseOut1(emit); + sendViz = true; + return emit; +} + +bool MainApp::PulseOutput2(bool requested) +{ + bool emit = false; + bool isTuringMode = settings->preset[0].pulseMode2; + + if (isTuringMode && requested) + { + emit = (turingPWM2.DAC_8() & 0x01); + } + else + { + emit = requested; + } + + PulseOut2(emit); + sendViz = true; // for testing + + return emit; +} + +bool MainApp::PulseInConnected1() +{ + return Connected(Pulse1); +} + +bool MainApp::PulseInConnected2() +{ + return Connected(Pulse2); +} + +bool(MainApp::tapReceived)() +{ + if (PulseInConnected1()) + { + return false; + } + else + { + // clk.setExternalClock1(false); // Remove to check what happens + return (SwitchChanged() && SwitchVal() == Down); + } +} + +bool MainApp::extPulse1Received() +{ + if (PulseInConnected1() && PulseIn1RisingEdge()) + { + clk.setExternalClock1(true); + return true; + } + else + { + return false; + } +} + +bool MainApp::extPulse2Received() +{ + if (PulseInConnected2() && PulseIn2RisingEdge()) + { + + clk.setExternalClock2(true); + return true; + } + else + { + return false; + } +} + +bool MainApp::VactrolLayerActive() +{ + return SwitchVal() == Up; +} + +uint16_t MainApp::KnobMain() +{ + return KnobVal(Main); +} +uint16_t MainApp::KnobX() +{ + return KnobVal(X); +} +uint16_t MainApp::KnobY() +{ + return KnobVal(Y); +} + +bool MainApp::ModeSwitch() +{ // 1 = up 0 = middle (or down) + return VactrolLayerActive(); +} + +bool MainApp::SwitchDown() +{ + return SwitchVal() == Down; +} + +bool MainApp::switchChanged() +{ + // 1 = up 0 = middle (or down) + bool result = false; + bool newSwitch = ModeSwitch(); + if (newSwitch != oldSwitch) + { + result = true; + oldSwitch = newSwitch; + } + return result; +} + +void MainApp::divideKnobChanged(uint8_t step) +{ + clk.UpdateDivide(step); +}; + +void MainApp::lengthKnobChanged(uint8_t length) +{ + + bool p = 0; + + int lengthPlus = settings->preset[p].looplen - 1; // Because 1-1 = 0, 0-1 = -1 + + turingDAC1.updateLength(length); + turingDAC2.updateLength(length + lengthPlus); + turingPWM1.updateLength(length); + turingPWM2.updateLength(length + lengthPlus); + turingPulseLength1.updateLength(length); + turingPulseLength2.updateLength(length + lengthPlus); + + // This is where to place the LED animation for length changes + showLengthPattern(length); + UpdatePulseLengths(); +} + +void MainApp::UpdateCh2Lengths() +{ + bool p = 0; + int lengthPlus = settings->preset[p].looplen - 1; // Because 1-1 = 0, 0-1 = -1 + uint16_t length = turingPWM1.returnLength(); + turingDAC2.updateLength(length + lengthPlus); + turingPWM2.updateLength(length + lengthPlus); + turingPulseLength2.updateLength(length + lengthPlus); +} + +void MainApp::UpdateCVRange() +{ + bool p = 0; + int cvRange = settings->preset[p].cvRange; + cv_set_mode(cvRange); +} + +void MainApp::updateMainTuring() +{ + + // Update Turing Machines + turingDAC1.Update(turingRandomness, maxRange); + turingPWM1.Update(turingRandomness, maxRange); + turingPulseLength1.Update(turingRandomness, maxRange); + + // Scaled CV out on CV/Audio 1 + uint8_t dac8 = turingDAC1.DAC_8(); + uint16_t dac = cv_map_u8(dac8); + vactrolTargetBase1 = int32_t(dac8) << 4; + if (!VactrolLayerActive()) + { + AudioOut1(dac); + } + + int midi_note = turingPWM1.MidiNote() + midiOffset; + CVOut1MIDINote(midi_note); +} + +void MainApp::updateDivTuring() +{ + turingDAC2.Update(turingRandomness, maxRange); + turingPWM2.Update(turingRandomness, maxRange); + turingPulseLength2.Update(turingRandomness, maxRange); + + // Scaled CV out on CV/Audio 2 + uint8_t dac8 = turingDAC2.DAC_8(); + uint16_t dac = cv_map_u8(dac8); + vactrolTargetBase2 = int32_t(dac8) << 4; + if (!VactrolLayerActive()) + { + AudioOut2(dac); + } + + int midi_note = turingPWM2.MidiNote() + midiOffset; + CVOut2MIDINote(midi_note); +} + +uint32_t MainApp::MemoryCardID() +{ + return static_cast(UniqueCardID()); +} + +void MainApp::blink(uint core, uint32_t interval_ms) +{ + + // uint pin = get_core_num(); + uint pin = core; + static absolute_time_t next_toggle_time[32]; // indexed by GPIO + static bool led_state[32] = {false}; // indexed by GPIO + + if (absolute_time_diff_us(get_absolute_time(), next_toggle_time[pin]) < 0) + { + led_state[pin] = !led_state[pin]; + LedOn(pin, led_state[pin]); + + next_toggle_time[pin] = make_timeout_time_ms(interval_ms); + } +} + +void MainApp::showLengthPattern(int length) +{ + struct PatternEntry + { + int length; + uint8_t bitmask; + }; + + const PatternEntry patternTable[] = { + {2, 0b110000}, + {3, 0b111000}, + {4, 0b111100}, + {5, 0b111110}, + {6, 0b111111}, + {8, 0b001111}, + {12, 0b000011}, + {16, 0b110011}}; + + uint8_t mask = 0; + + ledMode = STATIC_PATTERN; + lengthChangeStart = time_us_64(); + + for (const auto &entry : patternTable) + { + if (entry.length == length) + { + mask = entry.bitmask; + break; + } + } + + for (int i = 0; i < 6; ++i) + { + if (mask & (1 << (5 - i))) + { + LedOn(i); + } + else + { + LedOff(i); + } + } +} + +void MainApp::updateLedState() +{ + + if (ledMode == DYNAMIC_PWM) + { + if (VactrolLayerActive()) + { + const uint16_t mix1 = CLAMP(vactrolLevel1, 0, 4095); + const uint16_t mix2 = CLAMP(vactrolLevel2, 0, 4095); + const uint16_t depth1 = CLAMP(vactrolDepth1, 0, 4095); + const uint16_t depth2 = CLAMP(vactrolDepth2, 0, 4095); + + LedBrightness(0, mix1 << 4); + LedBrightness(1, mix2 << 4); + LedBrightness(2, depth1 << 4); + LedBrightness(3, depth2 << 4); + } + else + { + LedBrightness(0, turingDAC1.DAC_8() << 4); + LedBrightness(1, turingDAC2.DAC_8() << 4); + LedBrightness(2, turingPWM1.DAC_8() << 4); + LedBrightness(3, turingPWM2.DAC_8() << 4); + } + LedOn(4, pulseLed1_status); + LedOn(5, pulseLed2_status); + } + else if (ledMode == STATIC_PATTERN) + { + + if (time_us_64() - lengthChangeStart > 1500000) + { // 1.5 seconds in µs + ledMode = DYNAMIC_PWM; + } + } +} + +void MainApp::TEST_write_to_Pulse(int i, bool val) +{ + PulseOut(i, val); +} + +void MainApp::sysexRespond() +{ + const uint8_t sysExStart = 0xF0; + const uint8_t sysExEnd = 0xF7; + const uint8_t manufacturerId = 0x7D; + const uint8_t messageType = 0x02; + + const uint8_t *raw = reinterpret_cast(settings); + const size_t rawLen = sizeof(Config::Data); + + const size_t maxLen = 7 + ((rawLen + 6) / 7) * 8 + 1; + uint8_t msg[maxLen]; + size_t out = 0; + + msg[out++] = sysExStart; + msg[out++] = manufacturerId; + msg[out++] = CARD_NUMBER; + msg[out++] = messageType; + msg[out++] = MAJOR_VERSION; + msg[out++] = MINOR_VERSION; + msg[out++] = POINT_VERSION; + + // Encodes the entire settings Data file from config.h + // Includes: + + // Encode in 7-byte chunks with MSB prefix + for (size_t i = 0; i < rawLen; i += 7) + { + uint8_t msb = 0; + uint8_t block[7] = {0}; + + for (size_t j = 0; j < 7; ++j) + { + size_t index = i + j; + if (index >= rawLen) + break; + + uint8_t byte = raw[index]; + if (byte & 0x80) + msb |= (1 << j); + + block[j] = byte & 0x7F; + } + + msg[out++] = msb; + for (size_t j = 0; j < 7 && (i + j) < rawLen; ++j) + msg[out++] = block[j]; + } + + msg[out++] = sysExEnd; + tud_midi_stream_write(0, msg, out); +} + +void MainApp::handleSysExMessage(const uint8_t *data, size_t len) +{ + if (len < 5 || data[0] != 0xF0 || data[len - 1] != 0xF7) + return; // not a sysex message + + const uint8_t manufacturerId = data[1]; + const uint8_t deviceId = data[2]; + const uint8_t command = data[3]; + const uint8_t *payload = &data[4]; + const size_t payloadLen = len - 5; + + if (manufacturerId != 0x7D || deviceId != CARD_NUMBER) + return; + + switch (command) + { + case 0x01: + sysexRespond(); + break; + + case 0x03: + { + uint8_t decoded[sizeof(Config::Data)] = {0}; + size_t in = 0, out = 0; + + while (in < payloadLen && out < sizeof(decoded)) + { + uint8_t msb = payload[in++]; + for (int j = 0; j < 7 && in < payloadLen && out < sizeof(decoded); ++j) + { + uint8_t b = payload[in++]; + if (msb & (1 << j)) + b |= 0x80; + decoded[out++] = b; + } + } + + if (out == sizeof(Config::Data)) + { + + memcpy(settings, decoded, sizeof(Config::Data)); + + // Before saving incoming config, overwrite BPM with corrrect local value + + settings->bpm = CurrentBPM10; + + cfg.save(); + LoadSettings(0); + } + break; + } + + default: + break; + } +} + +void MainApp::IdleLeds() +{ + static uint8_t tick = 0; + + // Use XOR to shuffle the pattern unpredictably + uint8_t scrambled = tick ^ (tick << 1); + uint8_t index = scrambled % 6; + + LedOn(index); + sleep_us(20000); // ~20ms flash + LedOff(index); + + tick++; +} + +// Returns the high 7-bit MIDI-safe byte (bit 7 of input) +uint8_t MainApp::midiHi(uint8_t input) +{ + return (input >> 7) & 0x01; +} + +// Returns the low 7-bit MIDI-safe byte (bits 0–6 of input) +uint8_t MainApp::midiLo(uint8_t input) +{ + return input & 0x7F; +} + +void MainApp::SendLiveStatus() +{ + // No need for encoding, all bytes <127 + + const uint8_t sysExStart = 0xF0; + const uint8_t sysExEnd = 0xF7; + const uint8_t manufacturerId = 0x7D; + const uint8_t deviceId = CARD_NUMBER; + const uint8_t messageType = 0x10; + + uint8_t msg[16]; // CHECK THIS! + size_t out = 0; + + msg[out++] = sysExStart; + msg[out++] = manufacturerId; + msg[out++] = deviceId; + msg[out++] = messageType; + + msg[out++] = midiHi(turingDAC1.DAC_8()); + msg[out++] = midiLo(turingDAC1.DAC_8()); + + msg[out++] = midiHi(turingDAC2.DAC_8()); + msg[out++] = midiLo(turingDAC2.DAC_8()); + + msg[out++] = midiHi(turingPWM1.DAC_8()); + msg[out++] = midiLo(turingPWM1.DAC_8()); + + msg[out++] = midiHi(turingPWM2.DAC_8()); + msg[out++] = midiLo(turingPWM2.DAC_8()); + + msg[out++] = turingRandomness >> 5; // 0-4095 down to 0-127 + msg[out++] = VactrolLayerActive(); + msg[out++] = turingPWM1.returnLength(); + + msg[out++] = sysExEnd; + + tud_midi_stream_write(0, msg, out); +} + +void MainApp::cv_map_build(int32_t low, int32_t high) +{ + const int32_t span = high - low; // can be negative + const int32_t lo = (low < high) ? low : high; // for safety clamp + const int32_t hi = (low < high) ? high : low; + + for (int x = 0; x < 256; ++x) + { + // Exact linear map on 0..255 without overflow; signed-safe. + // No rounding term so x=255 lands exactly on 'high'. + int32_t y = low + (span * x) / 255; + + // Optional safety clamp to the given endpoints (supports low>high too) + if (y < lo) + y = lo; + if (y > hi) + y = hi; + + cv_lut[x] = (int16_t)y; + } +} + +void MainApp::cv_set_mode(uint8_t mode) +{ + // Matches your 4 ranges + switch (mode) + { + case 0: /* ±6V */ + cv_map_build(-2048, 2047); + break; + case 1: /* ±3V */ + cv_map_build(-1024, 1024); + break; + case 2: /* 0..6V*/ + cv_map_build(0, 2047); + break; + case 3: /* 0..3V*/ + cv_map_build(0, 1024); + break; // 0..+2.5V + default: + cv_map_build(-2048, 2047); + break; // safe default + } +} + +int16_t MainApp::cv_map_u8(uint8_t x) +{ + return cv_lut[x]; // O(1) in the audio loop +} + +int16_t MainApp::readInputIfConnected(Input inputType) +{ + if (Connected(inputType)) + { + switch (inputType) + { + case Audio1: + return AudioIn1(); + case Audio2: + return AudioIn2(); + case CV1: + return CVIn1(); + case CV2: + return CVIn2(); + default: + return 0; + } + } + return 0; +} + +#include + +// Returns semitone offset above C3 (0..12). +// VERY CRUDE AND UNCALIBRATED, TREAT AS EXPERIMENTAL +int MainApp::CVtoMidiOffset(int16_t raw) +{ + if (raw == 0) + return 0; // disconnected -> offset 0 + + // Measured centers for C3..C4 + static constexpr int16_t NOTE_COUNTS[13] = { + -10, // C3 + 34, // C#3 + 58, // D3 + 85, // D#3 + 104, // E3 + 127, // F3 + 153, // F#3 + 175, // G3 + 202, // G#3 + 227, // A3 + 253, // A#3 + 278, // B3 + 303 // C4 + }; + + // Midpoint thresholds *2 between adjacent notes (integer-only compare) + static constexpr int16_t MID_2X[12] = { + (-10 + 34), // C3|C#3 + (34 + 58), // C#3|D3 + (58 + 85), // D3|D#3 + (85 + 104), // D#3|E3 + (104 + 127), // E3|F3 + (127 + 153), // F3|F#3 + (153 + 175), // F#3|G3 + (175 + 202), // G3|G#3 + (202 + 227), // G#3|A3 + (227 + 253), // A3|A#3 + (253 + 278), // A#3|B3 + (278 + 303) // B3|C4 + }; + + int c2 = int(raw) * 2; + int semi = 0; + while (semi < 12 && c2 >= MID_2X[semi]) + ++semi; + + // Clamp to 0..12 (handles e.g. -1500 -> 0, +1500 -> 12) + if (semi < 0) + semi = 0; + if (semi > 12) + semi = 12; + return semi; +} + +void MainApp::SetVactrolControls(uint16_t slew, uint16_t depth1, uint16_t depth2) +{ + if (!VactrolLayerActive()) + { + turingRandomness = KnobVal(Main); + return; + } + + vactrolSlew = slew; + vactrolDepth1 = depth1; + vactrolDepth2 = depth2; + UpdateVactrolTiming(); +} + +void MainApp::UpdateVactrolTiming() +{ + const int32_t riseTime = 1 + settings->vactrol.rise; + const int32_t fallTime = 1 + settings->vactrol.fall; + const int32_t knobLag = 8 + ((vactrolSlew * 120) >> 12); + + vactrolRiseStep = 1 + (8192 / (riseTime + knobLag)); + vactrolFallStep = 1 + (8192 / (fallTime + knobLag)); +} + +void MainApp::ProcessVactrolMix() +{ + if (!VactrolLayerActive()) + { + return; + } + + const int32_t in1 = AudioIn1(); + const int32_t in2 = AudioIn2(); + const int32_t cvIn1 = CVIn1(); + const int32_t cvIn2 = CVIn2(); + + int32_t lane1 = vactrolTargetBase1; + int32_t lane2 = vactrolTargetBase2; + + if (settings->vactrol.relation == 1) + { + lane2 = lane1; + } + else if (settings->vactrol.relation == 2) + { + lane2 = 4095 - lane1; + } + + const int32_t min1 = int32_t(settings->vactrol.min1) << 4; + const int32_t max1 = (int32_t(settings->vactrol.max1) << 4) | 0x0f; + const int32_t min2 = int32_t(settings->vactrol.min2) << 4; + const int32_t max2 = (int32_t(settings->vactrol.max2) << 4) | 0x0f; + + lane1 = min1 + ((lane1 * (max1 - min1)) >> 12); + lane2 = min2 + ((lane2 * (max2 - min2)) >> 12); + + int32_t shaped1 = 2048 + (((lane1 - 2048) * int32_t(vactrolDepth1)) >> 12); + int32_t shaped2 = 2048 + (((lane2 - 2048) * int32_t(vactrolDepth2)) >> 12); + + shaped1 = CLAMP(shaped1, 0, 4095); + shaped2 = CLAMP(shaped2, 0, 4095); + + switch (settings->vactrol.law) + { + case 1: + shaped1 = (shaped1 * shaped1 * (12288 - (shaped1 << 1))) >> 24; + shaped2 = (shaped2 * shaped2 * (12288 - (shaped2 << 1))) >> 24; + break; + case 2: + shaped1 = (shaped1 * shaped1) >> 12; + shaped2 = (shaped2 * shaped2) >> 12; + break; + default: + break; + } + + if (vactrolLevel1 < shaped1) + { + vactrolLevel1 += vactrolRiseStep; + if (vactrolLevel1 > shaped1) + vactrolLevel1 = shaped1; + } + else if (vactrolLevel1 > shaped1) + { + vactrolLevel1 -= vactrolFallStep; + if (vactrolLevel1 < shaped1) + vactrolLevel1 = shaped1; + } + + if (vactrolLevel2 < shaped2) + { + vactrolLevel2 += vactrolRiseStep; + if (vactrolLevel2 > shaped2) + vactrolLevel2 = shaped2; + } + else if (vactrolLevel2 > shaped2) + { + vactrolLevel2 -= vactrolFallStep; + if (vactrolLevel2 < shaped2) + vactrolLevel2 = shaped2; + } + + const int32_t g1 = CLAMP(vactrolLevel1, 0, 4095); + const int32_t g2 = CLAMP(vactrolLevel2, 0, 4095); + const int32_t out1 = ((in1 * g1) + (in2 * (4095 - g1))) >> 12; + const int32_t out2 = ((in2 * g2) + (in1 * (4095 - g2))) >> 12; + const int32_t cvOut1 = ((cvIn1 * g1) + (cvIn2 * (4095 - g1))) >> 12; + const int32_t cvOut2 = ((cvIn2 * g2) + (cvIn1 * (4095 - g2))) >> 12; + + AudioOut1(CLAMP(out1, -2048, 2047)); + AudioOut2(CLAMP(out2, -2048, 2047)); + CVOut1(CLAMP(cvOut1, -2048, 2047)); + CVOut2(CLAMP(cvOut2, -2048, 2047)); +} diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h new file mode 100644 index 000000000..da325c375 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h @@ -0,0 +1,132 @@ +// MainApp.h +#pragma once +#define COMPUTERCARD_NOIMPL +#include "ComputerCard.h" +#include "Clock.h" +#include "UI.h" +#include "Turing.h" +#include "Config.h" + +class MainApp : public ComputerCard +{ + Config::Data *settings = nullptr; + +public: + MainApp(); + void ProcessSample() override; + + void PulseLed1(bool status); + void PulseLed2(bool status); + bool PulseOutput1(bool status); + bool PulseOutput2(bool status); + bool PulseInConnected1(); + bool PulseInConnected2(); + bool tapReceived(); + bool extPulse1Received(); + bool extPulse2Received(); + bool VactrolLayerActive(); + + uint16_t KnobMain(); + uint16_t KnobX(); + uint16_t KnobY(); + bool ModeSwitch(); + bool SwitchDown(); + int16_t readInputIfConnected(Input inputType); + + void divideKnobChanged(uint8_t step); + void lengthKnobChanged(uint8_t length); + + void updateMainTuring(); + void updateDivTuring(); + + uint32_t MemoryCardID(); + + void Housekeeping(); + + void LoadSettings(bool reset); + + uint64_t processTime; // TESTING + uint64_t lastProcessTime; // TESTING + uint64_t processStepTime; // TESTING + + void blink(uint core, uint32_t interval_ms); // TESTING blinks LED related to core at given freq + + void updateLedState(); + void TEST_write_to_Pulse(int i, bool val); + void UpdateNotePools(); + void UpdatePulseLengths(); + bool switchChanged(); + void IdleLeds(); + void UpdateCh2Lengths(); + void UpdateCVRange(); + void SetVactrolControls(uint16_t slew, uint16_t depth1, uint16_t depth2); + void UpdateVactrolTiming(); + +private: + Clock clk; + UI ui; + Config cfg; + + Turing turingDAC1; + Turing turingDAC2; + Turing turingPWM1; + Turing turingPWM2; + Turing turingPulseLength1; + Turing turingPulseLength2; + uint16_t maxRange = 4095; // maximum pot value + + volatile uint16_t CurrentBPM10 = 1200; // 10x bpm default + volatile uint16_t newBPM10 = 0; // 10x bpm default + + uint32_t lastTap = 0; + uint32_t debounceTimeout = 480; // 10ms in 48khz clock ticks + uint64_t lastChangeTimeUs; + volatile bool pendingSave; + + bool pulseLed1_status = 0; + bool pulseLed2_status = 0; + + enum LedMode + { + STATIC_PATTERN, + DYNAMIC_PWM + }; + LedMode ledMode = DYNAMIC_PWM; + uint64_t lengthChangeStart = 0; + void showLengthPattern(int length); + + bool oldSwitch = 0; + + void sysexRespond(); + void handleSysExMessage(const uint8_t *data, size_t len); + + void SendLiveStatus(); + uint8_t midiHi(uint8_t input); + uint8_t midiLo(uint8_t input); + + bool sendViz = false; + + // To handle CV mapping + int16_t cv_lut[256]; + void cv_map_build(int32_t low, int32_t high); + void cv_set_mode(uint8_t mode); + int16_t cv_map_u8(uint8_t x); + + // Experimental - CV2 to note offset + int CVtoMidiOffset(int16_t raw); + uint8_t midiOffset = 0; + + void ProcessVactrolMix(); + uint16_t turingRandomness = 2048; + uint16_t vactrolSlew = 256; + uint16_t vactrolDepth1 = 4095; + uint16_t vactrolDepth2 = 4095; + int32_t vactrolLevel1 = 2048; + int32_t vactrolLevel2 = 2048; + int32_t vactrolTarget1 = 2048; + int32_t vactrolTarget2 = 2048; + int32_t vactrolTargetBase1 = 2048; + int32_t vactrolTargetBase2 = 2048; + int32_t vactrolRiseStep = 8; + int32_t vactrolFallStep = 6; +}; diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.cpp new file mode 100644 index 000000000..edd88fa55 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.cpp @@ -0,0 +1,153 @@ +#include "Turing.h" + +Turing::Turing(int length, uint32_t seed) +{ + _length = length; + randomSeed(seed); + _sequence = next() & 0xFFFF; + + // create default note pool when created + UpdateNotePool(48, 3, 0); +} + +// Call this each time the clock 'ticks' +// Pick a random number 0 to maxRange +// if the number is below the pot reading: bitRotateLflip, otherwise bitRotateL +// set _outputValue as +void Turing::Update(int pot, int maxRange) +{ + int safeZone = maxRange >> 5; + int sample = safeZone + random(maxRange - (safeZone * 2)); // add safe zones at top and bottom + + if (_count == 0) + { + _startValue = _sequence; + } + + if (++_count >= _length) + { + _count = 0; + } + + if (sample >= pot) + { + _sequence = bitRotateLflip(_sequence, _length); + } + + else + { + _sequence = bitRotateL(_sequence, _length); + } + + if (_count++ == 0) + { + _startValue = _sequence; + } +} + +// returns the full current sequence value as 16 bit number 0 to 65535 +uint16_t Turing::DAC_16() +{ + return _sequence; +} + +// returns the current sequence value as 8 bit number 0 to 255 = ignores the last 8 binary digits +uint8_t Turing::DAC_8() +{ + + // return _sequence >> 8 ; // left hand 8 bits + return _sequence & 0xFF; // right hand 8 bits +} + +void Turing::updateLength(int newLen) +{ + _length = newLen; +} + +uint16_t Turing::returnLength() +{ + return _length; +} + +uint32_t Turing::next() +{ + constexpr uint32_t a = 1103515245u; + constexpr uint32_t c = 12345u; + _seed = a * _seed + c; + return static_cast(_seed >> 1); // 31-bit positive +} + +void Turing::randomSeed(uint32_t seed) +{ + if (seed != 0) + _seed = seed; // ignore zero (matches Arduino) +} + +uint32_t Turing::random(uint32_t max) // [0, max) +{ + if (max <= 0) + return 0; + return next() % max; +} + +void Turing::reset() +{ + _sequence = _startValue; + _count = 0; +} + +uint8_t Turing::MidiNote() +{ + if (note_pool_size == 0) + return 0; // fallback, silence or base note + + uint8_t val = DAC_8(); // 0–255 from looping 8-bit register + int index = (val * note_pool_size) >> 8; // fast mapping + if (index >= note_pool_size) + index = note_pool_size - 1; // safety + + return note_pool[index]; +} + +void Turing::UpdateNotePool(int root_note, int octave_range, int scale_type) +{ + // Define the scale tables (can be moved to global/static later) + static const int chromatic[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; + static const int major[] = {0, 2, 4, 5, 7, 9, 11}; + static const int minor[] = {0, 2, 3, 5, 7, 8, 10}; + static const int minor_pent[] = {0, 3, 5, 7, 10}; + static const int dorian[] = {0, 2, 3, 5, 7, 9, 10}; + static const int pelog[] = {0, 1, 3, 7, 10}; + static const int wholetone[] = {0, 2, 4, 6, 8, 10}; + + static const int *const scale_tables[] = { + chromatic, major, minor, minor_pent, dorian, pelog, wholetone}; + static const int scale_sizes[] = { + 12, 7, 7, 5, 7, 5, 6}; + + // Bounds check + if (scale_type < 0 || scale_type >= (int)(sizeof(scale_tables) / sizeof(scale_tables[0]))) + { + scale_type = 0; + } + + const int *scale = scale_tables[scale_type]; + int scale_size = scale_sizes[scale_type]; + + note_pool_size = 0; + + for (int oct = 0; oct <= octave_range; ++oct) + { + int base = root_note + 12 * oct; + for (int i = 0; i < scale_size; ++i) + { + int note = base + scale[i]; + if (note >= 0 && note < 128) + { + note_pool[note_pool_size++] = note; + if (note_pool_size >= MAX_NOTES) + return; + } + } + } +} diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.h b/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.h new file mode 100644 index 000000000..fa1ce2980 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.h @@ -0,0 +1,40 @@ +#pragma once +#include + +class Turing + +{ + +#define bitRotateL(value, len) ((((value) >> ((len) - 1)) & 0x01) | ((value) << 1)) +#define bitRotateLflip(value, len) (((~((value) >> ((len) - 1)) & 0x01) | ((value) << 1))) + +public: + Turing(int length, uint32_t seed); + void Update(int pot, int maxRange); + void updateLength(int newLen); + uint16_t returnLength(); + uint16_t DAC_16(); + uint8_t DAC_8(); + void DAC_print8(); + void DAC_print16(); + void randomSeed(uint32_t seed); + // uint8_t MidiNote(int low_note, int high_note, int scale_type, int sieve_type); + void UpdateNotePool(int root_note, int octave_range, int scale_type); + uint8_t MidiNote(); + void reset(); // Experimental - resets turing to the value at start of the current cycle + +private: + uint16_t _sequence = 0; // randomise on initialisation + int _length = 16; + inline static uint32_t _seed = 1; + uint32_t next(); + uint32_t random(uint32_t max); + + static constexpr uint8_t MAX_NOTES = 128; + uint8_t note_pool[MAX_NOTES]; + int note_pool_size = 0; + + // Experimental: Reset system + uint16_t _startValue = 0; + uint8_t _count = 0; +}; diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp new file mode 100644 index 000000000..4a8334fe3 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp @@ -0,0 +1,263 @@ +#include "UI.h" +#include "Clock.h" +#define COMPUTERCARD_NOIMPL +#include "ComputerCard.h" +#include "MainApp.h" +#include + +void UI::init(MainApp *a, Clock *c) +{ + app = a; + clk = c; +} + +void UI::Tick() +{ + + // use simple round robin to call TriggerPulse1 and EndPulse1 in 50% of cycles, + // and TriggerPulse2 and EndPulse2 in 50% of cycles, in order to reduce the time to + // complete 48khz clocked events + // store requested actions in pending variables + + static bool toggle = false; + static bool trigger1Pending = false; + static bool trigger2Pending = false; + + // First set up all the pending conditions + + if (clk->IsRisingEdge() && !app->PulseInConnected1()) + { + trigger1Pending = true; + } + + if (clk->IsRisingEdgeMult() && !app->PulseInConnected2()) + { + trigger2Pending = true; + } + + if (clk->ExtPulseReceived1()) + { + trigger1Pending = true; + } + + if (clk->ExtPulseReceived2()) + { + trigger2Pending = true; + } + + // Then act on the relevant functions according to the toggle + + if (toggle && trigger1Pending) + { + + TriggerPulse1(); + + trigger1Pending = false; + } + if (!toggle && trigger2Pending) + { + + TriggerPulse2(); + + trigger2Pending = false; + } + + if (toggle) + { + EndPulse1(); // Countdown pulse timer and check if Pulse1 should be stopped + } + + if (!toggle) + { + EndPulse2(); + } + // Then flip the toggle + toggle = !toggle; +} + +void UI::SlowUI() +{ + if (app->VactrolLayerActive()) + { + uint16_t slew = app->KnobMain(); + uint16_t depth1 = app->KnobX(); + uint16_t depth2 = app->KnobY(); + + if (slew != lastSlew || depth1 != lastDepth1 || depth2 != lastDepth2) + { + app->SetVactrolControls(slew, depth1, depth2); + lastSlew = slew; + lastDepth1 = depth1; + lastDepth2 = depth2; + } + return; + } + + uint16_t minVal = 0; + uint16_t maxVal = 4095; + + // Check for divide knob changes + uint16_t knobTemp = app->KnobY(); + + // Add knob value + int16_t inputTemp = app->readInputIfConnected(ComputerCard::CV1); // returns zero if nothing connected + int16_t valueTemp = knobTemp + inputTemp; + CLAMP(valueTemp, minVal, maxVal); + + uint16_t step = QuantiseToStep(valueTemp, numDivideSteps, 4095); + if (step >= numDivideSteps) + step = numDivideSteps - 1; + if (step != lastDivideStep) + { + app->divideKnobChanged(step); + lastDivideStep = step; + } + + // Check for Length knob changes + knobTemp = app->KnobX(); + step = QuantiseToStep(knobTemp, numLengthSteps, 4095); + + int newlen = lengths[step]; + + if (newlen != lastLength) + { + app->lengthKnobChanged(newlen); + + lastLength = newlen; + } + + app->switchChanged(); +} + +// uint8_t UI::QuantiseToStep(uint32_t knobVal, uint8_t steps, uint32_t range) +// { +// uint16_t step_size = range / steps; +// return knobVal / step_size; +// } + +uint8_t UI::QuantiseToStep(uint32_t knobVal, uint8_t steps, uint32_t range) +{ + if (steps == 0) + return 0; // safety + + uint32_t step_size = range / steps; + if (step_size == 0) + return 0; // safety for very small ranges + + // Round to nearest step, not floor + uint32_t step = (knobVal + (step_size / 2)) / step_size; + + // Clamp to max valid index + if (step >= steps) + step = steps - 1; + + return static_cast(step); +} + +void UI::TriggerPulse1() +{ + + bool active = app->PulseOutput1(true); + + if (active) + { + + app->PulseLed1(true); + outputPulseTicksRemaining1 = outputPulseLength; + ledPulseTicksRemaining1 = ledPulseLength; + ledPulseActive1 = true; + outputPulseActive1 = true; + } + + app->updateMainTuring(); +} + +void UI::TriggerPulse2() +{ + bool active = app->PulseOutput2(true); + + app->PulseLed2(active); + outputPulseTicksRemaining2 = outputDivideLength; + ledPulseTicksRemaining2 = ledPulseLength; + ledPulseActive2 = true; // always prepare to end pulses no matter if they're turned on or not + outputPulseActive2 = true; + + app->updateDivTuring(); +} + +void UI::EndPulse1() +{ + if (outputPulseActive1 && --outputPulseTicksRemaining1 == 0) + { + outputPulseActive1 = false; + app->PulseOutput1(false); + } + + if (ledPulseActive1 && --ledPulseTicksRemaining1 == 0) + { + ledPulseActive1 = false; + app->PulseLed1(false); + } +} + +void UI::EndPulse2() +{ + + if (outputPulseActive2 && --outputPulseTicksRemaining2 == 0) + { + outputPulseActive2 = false; + app->PulseOutput2(false); + } + + if (ledPulseActive2 && --ledPulseTicksRemaining2 == 0) + { + ledPulseActive2 = false; + app->PulseLed2(false); + } +} + +void UI::SetPulseLength(uint8_t lenPercent) +{ + uint32_t mainPercent = lenPercent + outputPulseMod1; + uint32_t dividePercent = lenPercent + outputPulseMod2; + + if (mainPercent > 100) + mainPercent = 100; + + if (mainPercent < 0) + mainPercent = 0; + + if (dividePercent > 100) + dividePercent = 100; + + if (dividePercent < 0) + dividePercent = 0; + + uint32_t wholeStep = clk->GetTicksPerBeat(); + uint32_t newLen = (uint64_t(wholeStep) * mainPercent) / 200; // Not sure why, 50% = 100% length without this + if (newLen < 96) // Clamp minimum pulse at 96 = 2ms + newLen = 96; + + outputPulseLength = newLen; + + uint32_t divideStep = clk->GetTicksPerSubclockBeat(); + uint32_t newDivLen = (uint64_t(divideStep) * dividePercent) / 200; // Not sure why, 50% = 100% length without this + if (newDivLen < 96) // Clamp minimum pulse at 96 = 2ms + newDivLen = 96; + + outputDivideLength = newDivLen; +} + +void UI::SetPulseMod(uint8_t level) +{ + pulseModLevel = level; +} + +void UI::UpdatePulseMod(uint8_t turing1, uint8_t turing2) +{ + int bipolarModulation1 = (int)turing1 - 128; + outputPulseMod1 = (bipolarModulation1 * pulseModLevel) / 128; + + int bipolarModulation2 = (int)turing2 - 128; + outputPulseMod2 = (bipolarModulation2 * pulseModLevel) / 128; +} diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h new file mode 100644 index 000000000..3b28b6875 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h @@ -0,0 +1,55 @@ +#pragma once +#include + +class MainApp; +class Clock; +#define CLAMP(val, min, max) ((val) < (min) ? (min) : ((val) > (max) ? (max) : (val))) + +class UI +{ +public: + void Tick(); + void init(MainApp *app, Clock *clock); + void SlowUI(); + void SetPulseLength(uint8_t lenPercent); + void SetPulseMod(uint8_t lenPercent); + void UpdatePulseMod(uint8_t turing1, uint8_t turing2); + +private: + int threshold = 48; // how many ticks before calling slow UI = 1ms + MainApp *app = nullptr; + Clock *clk = nullptr; + bool led1Status = false; + bool led2Status = false; + bool pulse1Status = false; + bool pulse2Status = false; + int ledPulseLength = 480; // number of ticks at 48khz = 10ms + int outputPulseLength = 96; // 8ms = just for testing should be more like 2ms + int outputDivideLength = 96; + int ledPulseTicksRemaining1 = 0; + int ledPulseTicksRemaining2 = 0; + int outputPulseTicksRemaining1 = 0; + int outputPulseTicksRemaining2 = 0; + bool ledPulseActive1 = false; + bool ledPulseActive2 = false; + bool outputPulseActive1 = false; + bool outputPulseActive2 = false; + void TriggerPulse1(); + void EndPulse1(); + void TriggerPulse2(); + void EndPulse2(); + uint8_t pulseModLevel = 0; + int outputPulseMod1 = 0; // NB must be signed + int outputPulseMod2 = 0; + + uint8_t lastDivideStep = 0; + uint8_t numDivideSteps = 9; + uint8_t QuantiseToStep(uint32_t knobVal, uint8_t steps, uint32_t range); + + uint8_t lastLength = 0; + uint8_t const numLengthSteps = 8; + uint8_t const lengths[8] = {2, 3, 4, 5, 6, 8, 12, 16}; + uint16_t lastSlew = 0; + uint16_t lastDepth1 = 0; + uint16_t lastDepth2 = 0; +}; diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/main.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/main.cpp new file mode 100644 index 000000000..9d6e1b774 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/main.cpp @@ -0,0 +1,77 @@ +/************************************************************ + * Core-split bootstrap for ComputerCard on RP2040 + * + * • Core 0 – USB-CDC stdio + flash-save service + * • Core 1 – ComputerCard audio engine (48 kHz ISR) + * + * Requires: + * - CMakeLists.txt links pico_stdlib & pico_multicore + * - PICO_COPY_TO_RAM 1 (so flash stalls don’t hurt audio) + ***********************************************************/ + +#include "pico/version.h" +#pragma message("SDK = " PICO_SDK_VERSION_STRING) + +#include "ComputerCard.h" +#include "pico/multicore.h" +#include "hardware/clocks.h" +#include "Clock.h" +#include "UI.h" +#include "MainApp.h" +#include "pico/stdlib.h" +#include +#include "tusb.h" + +/* Global handle published by Core 1 after MainApp is constructed */ +static MainApp *volatile gApp = nullptr; + +static void core1_entry() +{ + multicore_lockout_victim_init(); + // prinft("Core 1 running on core %d\n", get_core_num()); + + static MainApp app; // all ComputerCard work lives here + gApp = &app; // publish pointer for Core 0 + multicore_fifo_push_blocking(reinterpret_cast(gApp)); + app.EnableNormalisationProbe(); + app.Run(); // never returns +} + +int main() +{ + set_sys_clock_khz(192000, true); + + stdio_usb_init(); // Initialize USB serial // Claims for Core 0 + sleep_ms(10); + tusb_init(); + // launch the audio engine on core 1 + multicore_launch_core1(core1_entry); + + // wait until Core 1 has published MainApp* (rarely more than 100 µs) + while (multicore_fifo_rvalid()) + { // do nothing + } + + uintptr_t ptr = multicore_fifo_pop_blocking(); + gApp = reinterpret_cast(ptr); + + absolute_time_t next = make_timeout_time_ms(1); + sleep_ms(10); // allow switch readings to settle + // Reload settings - initialised to defaults, then wait until the switch is released + gApp->LoadSettings(gApp->SwitchDown()); + while (gApp->SwitchDown()) + { + // do nothing + gApp->IdleLeds(); + } + + while (true) + { + gApp->Housekeeping(); + tud_task(); // Process USB MIDI on Core 0 + + // ------------- pace the loop --------------- + sleep_until(next); // keeps 1-ms period + next = delayed_by_ms(next, 1); + } +} diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/pico_sdk_import.cmake b/releases/93_Turing_Matrix/TuringMatrix_Code/pico_sdk_import.cmake new file mode 100644 index 000000000..d493cc23a --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/pico_sdk_import.cmake @@ -0,0 +1,121 @@ +# This is a copy of /external/pico_sdk_import.cmake + +# This can be dropped into an external project to help locate this SDK +# It should be include()ed prior to project() + +# Copyright 2020 (c) 2020 Raspberry Pi (Trading) Ltd. +# +# Redistribution and use in source and binary forms, with or without modification, are permitted provided that the +# following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following +# disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, +# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF +# THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +if (DEFINED ENV{PICO_SDK_PATH} AND (NOT PICO_SDK_PATH)) + set(PICO_SDK_PATH $ENV{PICO_SDK_PATH}) + message("Using PICO_SDK_PATH from environment ('${PICO_SDK_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT} AND (NOT PICO_SDK_FETCH_FROM_GIT)) + set(PICO_SDK_FETCH_FROM_GIT $ENV{PICO_SDK_FETCH_FROM_GIT}) + message("Using PICO_SDK_FETCH_FROM_GIT from environment ('${PICO_SDK_FETCH_FROM_GIT}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_PATH} AND (NOT PICO_SDK_FETCH_FROM_GIT_PATH)) + set(PICO_SDK_FETCH_FROM_GIT_PATH $ENV{PICO_SDK_FETCH_FROM_GIT_PATH}) + message("Using PICO_SDK_FETCH_FROM_GIT_PATH from environment ('${PICO_SDK_FETCH_FROM_GIT_PATH}')") +endif () + +if (DEFINED ENV{PICO_SDK_FETCH_FROM_GIT_TAG} AND (NOT PICO_SDK_FETCH_FROM_GIT_TAG)) + set(PICO_SDK_FETCH_FROM_GIT_TAG $ENV{PICO_SDK_FETCH_FROM_GIT_TAG}) + message("Using PICO_SDK_FETCH_FROM_GIT_TAG from environment ('${PICO_SDK_FETCH_FROM_GIT_TAG}')") +endif () + +if (PICO_SDK_FETCH_FROM_GIT AND NOT PICO_SDK_FETCH_FROM_GIT_TAG) + set(PICO_SDK_FETCH_FROM_GIT_TAG "master") + message("Using master as default value for PICO_SDK_FETCH_FROM_GIT_TAG") +endif() + +set(PICO_SDK_PATH "${PICO_SDK_PATH}" CACHE PATH "Path to the Raspberry Pi Pico SDK") +set(PICO_SDK_FETCH_FROM_GIT "${PICO_SDK_FETCH_FROM_GIT}" CACHE BOOL "Set to ON to fetch copy of SDK from git if not otherwise locatable") +set(PICO_SDK_FETCH_FROM_GIT_PATH "${PICO_SDK_FETCH_FROM_GIT_PATH}" CACHE FILEPATH "location to download SDK") +set(PICO_SDK_FETCH_FROM_GIT_TAG "${PICO_SDK_FETCH_FROM_GIT_TAG}" CACHE FILEPATH "release tag for SDK") + +if (NOT PICO_SDK_PATH) + if (PICO_SDK_FETCH_FROM_GIT) + include(FetchContent) + set(FETCHCONTENT_BASE_DIR_SAVE ${FETCHCONTENT_BASE_DIR}) + if (PICO_SDK_FETCH_FROM_GIT_PATH) + get_filename_component(FETCHCONTENT_BASE_DIR "${PICO_SDK_FETCH_FROM_GIT_PATH}" REALPATH BASE_DIR "${CMAKE_SOURCE_DIR}") + endif () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + ) + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + GIT_SUBMODULES_RECURSE FALSE + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + else () + FetchContent_Populate( + pico_sdk + QUIET + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + + SOURCE_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-src + BINARY_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-build + SUBBUILD_DIR ${FETCHCONTENT_BASE_DIR}/pico_sdk-subbuild + ) + endif () + + set(PICO_SDK_PATH ${pico_sdk_SOURCE_DIR}) + endif () + set(FETCHCONTENT_BASE_DIR ${FETCHCONTENT_BASE_DIR_SAVE}) + else () + message(FATAL_ERROR + "SDK location was not specified. Please set PICO_SDK_PATH or set PICO_SDK_FETCH_FROM_GIT to on to fetch from git." + ) + endif () +endif () + +get_filename_component(PICO_SDK_PATH "${PICO_SDK_PATH}" REALPATH BASE_DIR "${CMAKE_BINARY_DIR}") +if (NOT EXISTS ${PICO_SDK_PATH}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' not found") +endif () + +set(PICO_SDK_INIT_CMAKE_FILE ${PICO_SDK_PATH}/pico_sdk_init.cmake) +if (NOT EXISTS ${PICO_SDK_INIT_CMAKE_FILE}) + message(FATAL_ERROR "Directory '${PICO_SDK_PATH}' does not appear to contain the Raspberry Pi Pico SDK") +endif () + +set(PICO_SDK_PATH ${PICO_SDK_PATH} CACHE PATH "Path to the Raspberry Pi Pico SDK" FORCE) + +include(${PICO_SDK_INIT_CMAKE_FILE}) diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/tusb_config.h b/releases/93_Turing_Matrix/TuringMatrix_Code/tusb_config.h new file mode 100644 index 000000000..14aea9d48 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/tusb_config.h @@ -0,0 +1,113 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2019 Ha Thach (tinyusb.org) + * + * 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. + * + */ + +#ifndef _TUSB_CONFIG_H_ +#define _TUSB_CONFIG_H_ + +#ifdef __cplusplus +extern "C" +{ +#endif + +//-------------------------------------------------------------------- +// COMMON CONFIGURATION +//-------------------------------------------------------------------- + +// defined by board.mk +#ifndef CFG_TUSB_MCU +#error CFG_TUSB_MCU must be defined +#endif + +// RHPort number used for device can be defined by board.mk, default to port 0 +#ifndef BOARD_DEVICE_RHPORT_NUM +#define BOARD_DEVICE_RHPORT_NUM 0 +#endif + +// RHPort max operational speed can defined by board.mk +// Default to Highspeed for MCU with internal HighSpeed PHY (can be port specific), otherwise FullSpeed +#ifndef BOARD_DEVICE_RHPORT_SPEED +#if (CFG_TUSB_MCU == OPT_MCU_LPC18XX || CFG_TUSB_MCU == OPT_MCU_LPC43XX || CFG_TUSB_MCU == OPT_MCU_MIMXRT10XX || \ + CFG_TUSB_MCU == OPT_MCU_NUC505 || CFG_TUSB_MCU == OPT_MCU_CXD56 || CFG_TUSB_MCU == OPT_MCU_SAMX7X) +#define BOARD_DEVICE_RHPORT_SPEED OPT_MODE_HIGH_SPEED +#else +#define BOARD_DEVICE_RHPORT_SPEED OPT_MODE_FULL_SPEED +#endif +#endif + + // // Device mode with rhport and speed defined by board.mk + // #if BOARD_DEVICE_RHPORT_NUM == 0 + // #define CFG_TUSB_RHPORT0_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) + // #elif BOARD_DEVICE_RHPORT_NUM == 1 + // #define CFG_TUSB_RHPORT1_MODE (OPT_MODE_DEVICE | BOARD_DEVICE_RHPORT_SPEED) + // #else + // #error "Incorrect RHPort configuration" + // #endif + +#ifndef CFG_TUSB_OS +#define CFG_TUSB_OS OPT_OS_NONE +#endif + +// CFG_TUSB_DEBUG is defined by compiler in DEBUG build +// #define CFG_TUSB_DEBUG 0 + +/* USB DMA on some MCUs can only access a specific SRAM region with restriction on alignment. + * Tinyusb use follows macros to declare transferring memory so that they can be put + * into those specific section. + * e.g + * - CFG_TUSB_MEM SECTION : __attribute__ (( section(".usb_ram") )) + * - CFG_TUSB_MEM_ALIGN : __attribute__ ((aligned(4))) + */ +#ifndef CFG_TUSB_MEM_SECTION +#define CFG_TUSB_MEM_SECTION +#endif + +#ifndef CFG_TUSB_MEM_ALIGN +#define CFG_TUSB_MEM_ALIGN __attribute__((aligned(4))) +#endif + + //-------------------------------------------------------------------- + // DEVICE CONFIGURATION + //-------------------------------------------------------------------- + +#ifndef CFG_TUD_ENDPOINT0_SIZE +#define CFG_TUD_ENDPOINT0_SIZE 64 +#endif + +//------------- CLASS -------------// +#define CFG_TUD_HID 0 +#define CFG_TUD_CDC 0 +#define CFG_TUD_MSC 0 +#define CFG_TUD_MIDI 1 +#define CFG_TUD_VENDOR 0 + +// MIDI FIFO size of TX and RX +#define CFG_TUD_MIDI_RX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) +#define CFG_TUD_MIDI_TX_BUFSIZE (TUD_OPT_HIGH_SPEED ? 512 : 64) + +#ifdef __cplusplus +} +#endif + +#endif /* _TUSB_CONFIG_H_ */ \ No newline at end of file diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/usb_descriptors.c b/releases/93_Turing_Matrix/TuringMatrix_Code/usb_descriptors.c new file mode 100644 index 000000000..bf3675934 --- /dev/null +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/usb_descriptors.c @@ -0,0 +1,133 @@ +#include "tusb.h" + +#define USB_PID 0x10C1 // Music Thing Workshop System +#define USB_VID 0x2E8A // Raspberry Pi +#define USB_BCD 0x0200 + +// Device Descriptor +tusb_desc_device_t const desc_device = + { + .bLength = sizeof(tusb_desc_device_t), + .bDescriptorType = TUSB_DESC_DEVICE, + .bcdUSB = USB_BCD, + .bDeviceClass = 0x00, + .bDeviceSubClass = 0x00, + .bDeviceProtocol = 0x00, + .bMaxPacketSize0 = CFG_TUD_ENDPOINT0_SIZE, + + .idVendor = USB_VID, + .idProduct = USB_PID, + .bcdDevice = 0x0100, + + .iManufacturer = 0x01, + .iProduct = 0x02, + .iSerialNumber = 0x03, + + .bNumConfigurations = 0x01}; + +uint8_t const *tud_descriptor_device_cb(void) +{ + return (uint8_t const *)&desc_device; +} + +// Configuration descriptor +enum +{ + ITF_NUM_MIDI = 0, + ITF_NUM_MIDI_STREAMING, + ITF_NUM_TOTAL +}; + +#define CONFIG_TOTAL_LEN (TUD_CONFIG_DESC_LEN + TUD_MIDI_DESC_LEN) + +// Endpoint number +#define EPNUM_MIDI 0x01 + +uint8_t const desc_fs_configuration[] = + { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_MIDI_DESCRIPTOR(ITF_NUM_MIDI, 0, EPNUM_MIDI, 0x80 | EPNUM_MIDI, 64)}; + +#if TUD_OPT_HIGH_SPEED +uint8_t const desc_hs_configuration[] = + { + // Config number, interface count, string index, total length, attribute, power in mA + TUD_CONFIG_DESCRIPTOR(1, ITF_NUM_TOTAL, 0, CONFIG_TOTAL_LEN, 0x00, 100), + + // Interface number, string index, EP Out & EP In address, EP size + TUD_MIDI_DESCRIPTOR(ITF_NUM_MIDI, 0, EPNUM_MIDI, 0x80 | EPNUM_MIDI, 512)}; +#endif + +// Invoked when received GET CONFIGURATION DESCRIPTOR +// Application return pointer to descriptor +// Descriptor contents must exist long enough for transfer to complete +uint8_t const *tud_descriptor_configuration_cb(uint8_t index) +{ + (void)index; // for multiple configurations + +#if TUD_OPT_HIGH_SPEED + // Although we are highspeed, host may be fullspeed. + return (tud_speed_get() == TUSB_SPEED_HIGH) ? desc_hs_configuration : desc_fs_configuration; +#else + return desc_fs_configuration; +#endif +} + +//--------------------------------------------------------------------+ +// String Descriptors +//--------------------------------------------------------------------+ + +// array of pointer to string descriptors +char const *string_desc_arr[] = + { + (const char[]){0x09, 0x04}, // 0: is supported language is English (0x0409) + "Music Thing", // 1: Manufacturer + "MTMComputer", // 2: Product + "123456", // 3: Serials, should use chip ID +}; + +static uint16_t _desc_str[32]; + +// Invoked when received GET STRING DESCRIPTOR request +// Application return pointer to descriptor, whose contents must exist long enough for transfer to complete +uint16_t const *tud_descriptor_string_cb(uint8_t index, uint16_t langid) +{ + (void)langid; + + uint8_t chr_count; + + if (index == 0) + { + memcpy(&_desc_str[1], string_desc_arr[0], 2); + chr_count = 1; + } + else + { + // Note: the 0xEE index string is a Microsoft OS 1.0 Descriptors. + // https://docs.microsoft.com/en-us/windows-hardware/drivers/usbcon/microsoft-defined-usb-descriptors + + if (!(index < sizeof(string_desc_arr) / sizeof(string_desc_arr[0]))) + return NULL; + + const char *str = string_desc_arr[index]; + + // Cap at max char + chr_count = strlen(str); + if (chr_count > 31) + chr_count = 31; + + // Convert ASCII string into UTF-16 + for (uint8_t i = 0; i < chr_count; i++) + { + _desc_str[1 + i] = str[i]; + } + } + + // first byte is length (including header), second byte is string type + _desc_str[0] = (TUSB_DESC_STRING << 8) | (2 * chr_count + 2); + + return _desc_str; +} \ No newline at end of file diff --git a/releases/93_Turing_Matrix/info.yaml b/releases/93_Turing_Matrix/info.yaml new file mode 100644 index 000000000..fcd51c28c --- /dev/null +++ b/releases/93_Turing_Matrix/info.yaml @@ -0,0 +1,150 @@ +draft: true +Name: Turing Matrix +Description: Turing Machine sequencer plus a switchable Vactrol Mix-inspired audio layer driven by the same background Turing engine. +Language: C++ (ComputerCard) +Creator: Tom Whitwell / Music Thing Modular +Version: 0.1.0 +Status: Draft concept +License: MIT +Editor: web +Repository: https://github.com/TomWhitwell/Workshop_Computer + +tags: + - sequencer + - turing-machine + - random + - vactrol + - matrix-mixer + - modulation + - cv + +manual: | + Turing Matrix uses one shared background Turing engine with two front-panel layers. + + Z middle is the Turing control layer: the card behaves like the original Turing Machine card for + clock, randomness, loop length, divide/multiply, pulse outputs, quantized CV, and DAC/CV outputs. + Audio In 1 and Audio In 2 are unused in this layer. + + Z up is the Vactrol Mix layer: the same background Turing engine keeps running, but Audio In 1 and + Audio In 2 become the two audio mixer inputs and Audio Out 1 and Audio Out 2 become the stereo + mixer outputs. CV In 1 and CV In 2 are crossfaded in parallel to CV Out 1 and CV Out 2 using the + same Turing-driven control signals. The Turing DAC lanes are used as smoothed crossfade controls, + while Pulse Out 1 and Pulse Out 2 continue to follow the same shared clock behavior as the + Turing layer. Web editor settings define crossfade law, lane relation, rise/fall timing, and + minimum/maximum depth windows for each lane. + + Z down remains tap tempo when no external clock is patched. + +panel: + inputs: + - id: PulseIn1 + name: External Clock 1 + description: Replaces tap tempo and drives the main Turing channel + type: pulse + - id: PulseIn2 + name: External Clock 2 + description: Independently clocks channel 2 when patched + type: pulse + - id: CVIn1 + name: CV Mix Input 1 + description: Divide/multiply modulation in the Turing layer; CV mix input 1 in the Vactrol Mix layer + type: cv + - id: CVIn2 + name: CV Mix Input 2 + description: Pitch offset in the Turing layer; CV mix input 2 in the Vactrol Mix layer + type: cv + - id: AudioIn1 + name: Mixer Input 1 + description: Audio input for the Vactrol Mix layer; unused in the Turing control layer + type: audio + - id: AudioIn2 + name: Mixer Input 2 + description: Audio input for the Vactrol Mix layer; unused in the Turing control layer + type: audio + outputs: + - id: PulseOut1 + name: Matrix Gate 1 + description: Clock or Turing-bit pulse for the first shared lane in both layers + type: pulse + - id: PulseOut2 + name: Matrix Gate 2 + description: Clock or Turing-bit pulse for the second shared lane in both layers + type: pulse + - id: CVOut1 + name: Channel 1 Quantized CV / CV Mix Out 1 + description: Quantized pitch CV in the Turing layer; crossfaded CV output 1 in the Vactrol Mix layer + type: cv + - id: CVOut2 + name: Channel 2 Quantized CV / CV Mix Out 2 + description: Quantized pitch CV in the Turing layer; crossfaded CV output 2 in the Vactrol Mix layer + type: cv + - id: AudioOut1 + name: Mixer Output 1 / DAC CV 1 + description: DAC CV output in the Turing layer; mixed audio output 1 in the Vactrol Mix layer + type: audio + - id: AudioOut2 + name: Mixer Output 2 / DAC CV 2 + description: DAC CV output in the Turing layer; mixed audio output 2 in the Vactrol Mix layer + type: audio + +# certainty: medium — built from 03_Turing_Machine current source with a new switch-up Vactrol Mix layer +controls: + knobs: + - when: { z: middle } + main: + name: Randomness / Write + description: Sets how often the shared Turing pattern changes + x: + name: Loop Length + description: Sets channel 1 sequence length; channel 2 length can be offset in the editor + y: + name: Divide/Multiply + description: Sets channel 2 clock relationship exactly as on the Turing Machine card + - when: { z: up } + main: + name: Vactrol Lag + description: Sets how quickly the Turing-controlled mix opens and closes + x: + name: Crossfade Depth 1 + description: Sets how strongly Turing lane 1 crossfades Audio In 1 and Audio In 2 into Audio Out 1 + y: + name: Crossfade Depth 2 + description: Sets how strongly Turing lane 2 crossfades Audio In 2 and Audio In 1 into Audio Out 2 + - when: { z: down, gesture: momentary } + main: + name: Tap Tempo + description: Tap switch sets internal BPM when no external clock is present + leds: + - when: { z: middle } + display: list + items: + - id: LED0 + name: Channel 1 DAC / Mix Level + - id: LED1 + name: Channel 2 DAC / Mix Level + - id: LED2 + name: Channel 1 Turing Level + - id: LED3 + name: Channel 2 Turing Level + - id: LED4 + name: Matrix Gate 1 Activity + - id: LED5 + name: Matrix Gate 2 Activity + +host: + usb: + - name: Web MIDI editor + role: editor + description: Browser app in `web/` reads and writes shared Turing engine settings over USB MIDI SysEx; Chrome recommended + notes: | + The Web MIDI editor configures the shared Turing engine and timing behavior. The Vactrol Mix + layer has its own stored settings for crossfade law, lane relation, rise/fall timing, and + lane depth windows, while the panel still controls live lag and depth performance. + +source: + - releases/03_Turing_Machine/info.yaml + - releases/03_Turing_Machine/README.md + - https://www.musicthing.co.uk/Turing-Vactrol-Mix-Expander/ +source_file: releases/93_Turing_Matrix/info.yaml +source_url: https://github.com/TomWhitwell/Workshop_Computer/tree/main/releases/93_Turing_Matrix +readme_url: https://github.com/TomWhitwell/Workshop_Computer/blob/main/releases/93_Turing_Matrix/README.md diff --git a/releases/93_Turing_Matrix/web/app.js b/releases/93_Turing_Matrix/web/app.js new file mode 100644 index 000000000..7f65914f9 --- /dev/null +++ b/releases/93_Turing_Matrix/web/app.js @@ -0,0 +1,374 @@ +const SYSEX_START = 0xf0; +const SYSEX_END = 0xf7; +const MANUFACTURER = 0x7d; +const DEVICE = 0x93; +const CMD_GET = 0x01; +const CMD_GET_RESPONSE = 0x02; +const CMD_SET = 0x03; +const CONFIG_SIZE = 32; + +const DEFAULT_CONFIG = { + magic: 0x434f4e46, + bpm: 1605, + divide: 5, + cvRange: 0, + preset0: { + scale: 3, + range: 2, + length: 5, + looplen: 1, + pulseMode1: 0, + pulseMode2: 0, + cvRange: 0, + }, + preset1: { + scale: 3, + range: 1, + length: 5, + looplen: 1, + pulseMode1: 0, + pulseMode2: 1, + cvRange: 3, + }, + vactrol: { + law: 0, + relation: 0, + rise: 48, + fall: 56, + min1: 0, + max1: 255, + min2: 0, + max2: 255, + }, +}; + +const state = { + midiAccess: null, + input: null, + output: null, + config: structuredClone(DEFAULT_CONFIG), +}; + +function byId(id) { + return document.getElementById(id); +} + +function setStatus(text) { + byId("status").textContent = text; +} + +function bindRangePair(rangeId, numberId) { + const range = byId(rangeId); + const number = byId(numberId); + range.addEventListener("input", () => { + number.value = range.value; + }); + number.addEventListener("input", () => { + range.value = number.value; + }); +} + +function syncFormFromConfig(cfg) { + byId("bpm").value = cfg.bpm; + byId("bpm_num").value = cfg.bpm; + byId("divide").value = cfg.divide; + byId("divide_num").value = cfg.divide; + byId("global_cv_range").value = cfg.cvRange; + + const p = cfg.preset0; + byId("scale").value = p.scale; + byId("range").value = p.range; + byId("length").value = p.length; + byId("looplen").value = p.looplen; + byId("pulseMode1").value = p.pulseMode1; + byId("pulseMode2").value = p.pulseMode2; + byId("preset_cvRange").value = p.cvRange; + + byId("vactrol_law").value = cfg.vactrol.law; + byId("vactrol_relation").value = cfg.vactrol.relation; + byId("vactrol_rise").value = cfg.vactrol.rise; + byId("vactrol_rise_num").value = cfg.vactrol.rise; + byId("vactrol_fall").value = cfg.vactrol.fall; + byId("vactrol_fall_num").value = cfg.vactrol.fall; + byId("vactrol_min1").value = cfg.vactrol.min1; + byId("vactrol_min1_num").value = cfg.vactrol.min1; + byId("vactrol_max1").value = cfg.vactrol.max1; + byId("vactrol_max1_num").value = cfg.vactrol.max1; + byId("vactrol_min2").value = cfg.vactrol.min2; + byId("vactrol_min2_num").value = cfg.vactrol.min2; + byId("vactrol_max2").value = cfg.vactrol.max2; + byId("vactrol_max2_num").value = cfg.vactrol.max2; +} + +function readFormIntoConfig() { + const cfg = structuredClone(state.config); + cfg.bpm = Number(byId("bpm_num").value); + cfg.divide = Number(byId("divide_num").value); + cfg.cvRange = Number(byId("global_cv_range").value); + cfg.preset0.scale = Number(byId("scale").value); + cfg.preset0.range = Number(byId("range").value); + cfg.preset0.length = Number(byId("length").value); + cfg.preset0.looplen = Number(byId("looplen").value); + cfg.preset0.pulseMode1 = Number(byId("pulseMode1").value); + cfg.preset0.pulseMode2 = Number(byId("pulseMode2").value); + cfg.preset0.cvRange = Number(byId("preset_cvRange").value); + cfg.vactrol.law = Number(byId("vactrol_law").value); + cfg.vactrol.relation = Number(byId("vactrol_relation").value); + cfg.vactrol.rise = Number(byId("vactrol_rise_num").value); + cfg.vactrol.fall = Number(byId("vactrol_fall_num").value); + cfg.vactrol.min1 = Number(byId("vactrol_min1_num").value); + cfg.vactrol.max1 = Number(byId("vactrol_max1_num").value); + cfg.vactrol.min2 = Number(byId("vactrol_min2_num").value); + cfg.vactrol.max2 = Number(byId("vactrol_max2_num").value); + return cfg; +} + +function encodeConfig(cfg) { + const bytes = []; + const push8 = (value) => bytes.push(value & 0xff); + const push16 = (value) => { + push8(value & 0xff); + push8((value >> 8) & 0xff); + }; + const push32 = (value) => { + push8(value & 0xff); + push8((value >> 8) & 0xff); + push8((value >> 16) & 0xff); + push8((value >> 24) & 0xff); + }; + const pushPreset = (preset) => { + push8(preset.scale); + push8(preset.range); + push8(preset.length); + push8(preset.looplen); + push8(preset.pulseMode1); + push8(preset.pulseMode2); + push8(preset.cvRange); + }; + const pushVactrol = (vactrol) => { + push8(vactrol.law); + push8(vactrol.relation); + push8(vactrol.rise); + push8(vactrol.fall); + push8(vactrol.min1); + push8(vactrol.max1); + push8(vactrol.min2); + push8(vactrol.max2); + }; + + push32(cfg.magic >>> 0); + push16(cfg.bpm); + push8(cfg.divide); + push8(cfg.cvRange); + pushPreset(cfg.preset0); + pushPreset(cfg.preset1); + pushVactrol(cfg.vactrol); + while (bytes.length < CONFIG_SIZE) { + push8(0); + } + return bytes; +} + +function decodeConfig(bytes) { + let index = 0; + const read8 = () => bytes[index++] ?? 0; + const read16 = () => read8() | (read8() << 8); + const read32 = () => (read8() | (read8() << 8) | (read8() << 16) | (read8() << 24)) >>> 0; + const readPreset = () => ({ + scale: read8(), + range: read8(), + length: read8(), + looplen: read8(), + pulseMode1: read8(), + pulseMode2: read8(), + cvRange: read8(), + }); + const readVactrol = () => ({ + law: read8(), + relation: read8(), + rise: read8(), + fall: read8(), + min1: read8(), + max1: read8(), + min2: read8(), + max2: read8(), + }); + + return { + magic: read32(), + bpm: read16(), + divide: read8(), + cvRange: read8(), + preset0: readPreset(), + preset1: readPreset(), + vactrol: readVactrol(), + }; +} + +function encode7Bit(raw) { + const out = []; + for (let i = 0; i < raw.length; i += 7) { + const block = raw.slice(i, i + 7); + let msb = 0; + for (let j = 0; j < block.length; j += 1) { + if (block[j] & 0x80) { + msb |= 1 << j; + } + } + out.push(msb); + for (const value of block) { + out.push(value & 0x7f); + } + } + return out; +} + +function decode7Bit(payload) { + const out = []; + let index = 0; + while (index < payload.length) { + const msb = payload[index++]; + for (let bit = 0; bit < 7 && index < payload.length; bit += 1) { + let value = payload[index++]; + if (msb & (1 << bit)) { + value |= 0x80; + } + out.push(value); + } + } + return out; +} + +function sendSysEx(command, payload = []) { + if (!state.output) { + setStatus("No MIDI output selected."); + return; + } + const message = new Uint8Array(5 + payload.length); + message[0] = SYSEX_START; + message[1] = MANUFACTURER; + message[2] = DEVICE; + message[3] = command; + message.set(payload, 4); + message[message.length - 1] = SYSEX_END; + state.output.send(message); +} + +function handleMIDIMessage(event) { + const data = [...event.data]; + if (data.length < 5 || data[0] !== SYSEX_START || data.at(-1) !== SYSEX_END) { + return; + } + if (data[1] !== MANUFACTURER || data[2] !== DEVICE) { + return; + } + if (data[3] !== CMD_GET_RESPONSE) { + return; + } + + const payload = data.slice(7, -1); + const raw = decode7Bit(payload); + if (raw.length < CONFIG_SIZE) { + setStatus(`Config reply was too short (${raw.length} bytes).`); + return; + } + state.config = decodeConfig(raw); + syncFormFromConfig(state.config); + setStatus(`Config received from card (${raw.length} bytes).`); +} + +function updatePorts() { + const midiIn = byId("midiIn"); + const midiOut = byId("midiOut"); + midiIn.innerHTML = ""; + midiOut.innerHTML = ""; + + const inputs = [...state.midiAccess.inputs.values()]; + const outputs = [...state.midiAccess.outputs.values()]; + + for (const input of inputs) { + const option = document.createElement("option"); + option.value = input.id; + option.textContent = input.name || input.id; + midiIn.appendChild(option); + } + + for (const output of outputs) { + const option = document.createElement("option"); + option.value = output.id; + option.textContent = output.name || output.id; + midiOut.appendChild(option); + } + + state.input = inputs[0] || null; + state.output = outputs[0] || null; + midiIn.value = state.input?.id || ""; + midiOut.value = state.output?.id || ""; + + if (state.input) { + state.input.onmidimessage = handleMIDIMessage; + } +} + +async function connectMIDI() { + if (!navigator.requestMIDIAccess) { + setStatus("Web MIDI is not available in this browser."); + return; + } + try { + state.midiAccess = await navigator.requestMIDIAccess({ sysex: true }); + state.midiAccess.onstatechange = updatePorts; + updatePorts(); + setStatus("Web MIDI connected."); + } catch (error) { + setStatus(`Web MIDI connection failed: ${error.message}`); + } +} + +function init() { + bindRangePair("bpm", "bpm_num"); + bindRangePair("divide", "divide_num"); + bindRangePair("vactrol_rise", "vactrol_rise_num"); + bindRangePair("vactrol_fall", "vactrol_fall_num"); + bindRangePair("vactrol_min1", "vactrol_min1_num"); + bindRangePair("vactrol_max1", "vactrol_max1_num"); + bindRangePair("vactrol_min2", "vactrol_min2_num"); + bindRangePair("vactrol_max2", "vactrol_max2_num"); + syncFormFromConfig(state.config); + + byId("connectBtn").addEventListener("click", connectMIDI); + byId("midiIn").addEventListener("change", (event) => { + const input = state.midiAccess?.inputs.get(event.target.value) || null; + if (state.input) { + state.input.onmidimessage = null; + } + state.input = input; + if (state.input) { + state.input.onmidimessage = handleMIDIMessage; + } + }); + byId("midiOut").addEventListener("change", (event) => { + state.output = state.midiAccess?.outputs.get(event.target.value) || null; + }); + + byId("readBtn").addEventListener("click", () => { + sendSysEx(CMD_GET); + setStatus("Requested config from card."); + }); + + byId("sendBtn").addEventListener("click", () => { + state.config = readFormIntoConfig(); + const raw = encodeConfig(state.config); + const packed = encode7Bit(raw); + sendSysEx(CMD_SET, packed); + setStatus(`Sent shared Turing settings to card (${raw.length} bytes raw).`); + }); + + byId("defaultsBtn").addEventListener("click", () => { + state.config = structuredClone(DEFAULT_CONFIG); + syncFormFromConfig(state.config); + setStatus("Loaded editor defaults locally."); + }); +} + +init(); diff --git a/releases/93_Turing_Matrix/web/index.html b/releases/93_Turing_Matrix/web/index.html new file mode 100644 index 000000000..de4826640 --- /dev/null +++ b/releases/93_Turing_Matrix/web/index.html @@ -0,0 +1,200 @@ + + + + + + + Turing Matrix Web Config + + + + +
+
+

Program Card 93

+

Turing Matrix

+

+ Shared Turing engine editor for the Workshop Computer. The browser configures the persistent + sequencer settings; the Vactrol Mix layer remains mostly on the front panel. +

+
+ +
+
+ + + +
+
+ + + +
+

Not connected.

+
+ +
+
+

Shared Engine

+
+ + + +
+
+ +
+

Turing Layer Settings

+
+ + + + + + + +
+
+
+ +
+

Vactrol Layer Settings

+
+ + + + + + + + +
+
+ +
+

Vactrol Layer

+
+

Z up uses the same background Turing engine but changes the panel role.

+

Main sets lag, X sets audio/CV crossfade depth 1, Y sets audio/CV crossfade depth 2.

+

Audio In 1/2 are the audio pair. CV In 1/2 are the mirrored CV pair. These controls are not stored in flash.

+
+
+
+ + + + diff --git a/releases/93_Turing_Matrix/web/styles.css b/releases/93_Turing_Matrix/web/styles.css new file mode 100644 index 000000000..e67bfb0f6 --- /dev/null +++ b/releases/93_Turing_Matrix/web/styles.css @@ -0,0 +1,142 @@ +:root { + --bg: #0e1411; + --panel: #16201b; + --panel-2: #1d2a24; + --line: #31433a; + --text: #eef5ef; + --muted: #aebeb1; + --accent: #e6b94c; + --accent-2: #7fd1a7; +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Segoe UI", system-ui, sans-serif; + background: + radial-gradient(circle at top, rgba(230, 185, 76, 0.14), transparent 34%), + linear-gradient(180deg, #0c120f 0%, var(--bg) 100%); + color: var(--text); +} + +main { + max-width: 1080px; + margin: 0 auto; + padding: 28px 18px 36px; +} + +.hero { + padding: 10px 2px 20px; +} + +.eyebrow { + margin: 0 0 8px; + color: var(--accent); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +h1, +h2, +p { + margin-top: 0; +} + +h1 { + margin-bottom: 8px; + font-size: clamp(2rem, 5vw, 3.2rem); +} + +.lede { + max-width: 60rem; + color: var(--muted); + line-height: 1.5; +} + +.layout { + display: grid; + gap: 16px; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 340px), 1fr)); +} + +.panel { + margin-top: 16px; + padding: 16px; + border: 1px solid var(--line); + border-radius: 8px; + background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%); +} + +.toolbar, +.grid { + display: grid; + gap: 12px; +} + +.toolbar { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + align-items: end; +} + +.toolbar--actions { + margin-top: 14px; +} + +.grid { + grid-template-columns: repeat(auto-fit, minmax(min(100%, 250px), 1fr)); +} + +label { + display: grid; + gap: 6px; + color: var(--muted); + font-size: 0.92rem; +} + +.pair { + display: grid; + grid-template-columns: minmax(0, 1fr) 5.4rem; + gap: 10px; + align-items: center; +} + +input, +select, +button { + width: 100%; + min-width: 0; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: #0f1713; + color: var(--text); +} + +button { + cursor: pointer; + background: linear-gradient(180deg, #26332d 0%, #16201b 100%); +} + +button:hover { + border-color: var(--accent-2); +} + +input[type="range"] { + padding: 0; +} + +#status { + margin: 12px 0 0; + color: #c8e9d6; +} + +.notes { + color: var(--muted); + line-height: 1.55; +} From cc07720f2d98fcc4616b44cb8147638be263a4d5 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:10:44 +0100 Subject: [PATCH 02/13] Update index.html --- releases/93_Turing_Matrix/web/index.html | 65 +++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/releases/93_Turing_Matrix/web/index.html b/releases/93_Turing_Matrix/web/index.html index de4826640..9a5687263 100644 --- a/releases/93_Turing_Matrix/web/index.html +++ b/releases/93_Turing_Matrix/web/index.html @@ -15,7 +15,7 @@

Turing Matrix

Shared Turing engine editor for the Workshop Computer. The browser configures the persistent - sequencer settings; the Vactrol Mix layer remains mostly on the front panel. + sequencer settings; the mixer layer remains mostly on the front panel.

@@ -127,6 +127,7 @@

Turing Layer Settings

+<<<<<<< Updated upstream
@@ -192,6 +193,68 @@

Vactrol Layer

Main sets lag, X sets audio/CV crossfade depth 1, Y sets audio/CV crossfade depth 2.

Audio In 1/2 are the audio pair. CV In 1/2 are the mirrored CV pair. These controls are not stored in flash.

+======= +
+

Mixer Layer Settings

+
+ + + + + + + + +
+
+

Z up uses the same background Turing engine but changes the panel role.

+

Main sets lag, X sets audio/CV crossfade depth 1, Y sets audio/CV crossfade depth 2.

+

Audio In 1/2 are the audio pair. CV In 1/2 are the mirrored CV pair. These controls are not stored in flash.

+
+
+>>>>>>> Stashed changes
From bfc0aefc42e6cca10d71094b0416f5a138f0af64 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 17:07:23 +0100 Subject: [PATCH 03/13] Update README.md --- releases/93_Turing_Matrix/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md index a5cd03627..98bcb42a7 100644 --- a/releases/93_Turing_Matrix/README.md +++ b/releases/93_Turing_Matrix/README.md @@ -34,6 +34,13 @@ card treats the idea as a two-input, two-output random matrix/mixer. - Y knob: crossfade depth for Audio Out 2. - Audio/CV In 1 and 2 become the two audio inputs to the mixer. +## LED feedback + +- **Z middle** keeps the inherited Turing-style LED view: + DAC lane 1, DAC lane 2, PWM lane 1, PWM lane 2, then pulse activity on LEDs 5 and 6. +- **Z up** switches the four brightness LEDs to mixer feedback: + mix position 1, mix position 2, depth 1, depth 2, with pulse activity still shown on LEDs 5 and 6. + - **Z down** - Tap tempo when no external clock is patched. From 1c4ed5dc5fb6671e725c980d6c4a56e7fd8a729c Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 21:02:29 +0100 Subject: [PATCH 04/13] various bug fixes --- releases/93_Turing_Matrix/README.md | 50 ++--- .../TuringMatrix_Code/Config.cpp | 1 + .../TuringMatrix_Code/MainApp.cpp | 89 +++++---- .../TuringMatrix_Code/MainApp.h | 7 + .../93_Turing_Matrix/TuringMatrix_Code/UI.cpp | 120 +++++++++++- .../93_Turing_Matrix/TuringMatrix_Code/UI.h | 35 +++- .../93_Turing_Matrix/uf2/TuringMatrix.uf2 | Bin 0 -> 211456 bytes releases/93_Turing_Matrix/web/app.js | 174 ++++++++++++++---- releases/93_Turing_Matrix/web/index.html | 46 ++--- releases/93_Turing_Matrix/web/styles.css | 36 ++++ 10 files changed, 430 insertions(+), 128 deletions(-) create mode 100644 releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md index 98bcb42a7..ea5e1c2e3 100644 --- a/releases/93_Turing_Matrix/README.md +++ b/releases/93_Turing_Matrix/README.md @@ -11,28 +11,31 @@ card treats the idea as a two-input, two-output random matrix/mixer. ## Basic idea -- One shared background Turing engine keeps running in both layers. - **Z middle** is the Turing control layer and is intended to feel like the original Turing card. -- **Z up** is the Vactrol Mix layer and uses the background Turing engine to animate a two-input, - two-output audio mixer. +- **Z up** is the Vactrol Mix layer and uses the card's Turing-style control signals to animate a + two-input, two-output audio/CV mixer. - **Z down** remains tap tempo. - **Pulse Out 1** and **Pulse Out 2** keep the same clock/Turing pulse behavior in both layers. - **CV Out 1** and **CV Out 2** stay quantized pitch outputs in `Z middle`, and become mirrored crossfaded CV outputs in `Z up`. +- On startup the active layer reads the physical knob positions directly; pickup only applies after + switching between layers so the controls do not jump when you return to a layer. ## Controls -- **Z middle** +**Z middle** - Main knob: Turing randomness / write amount. - X knob: loop length. - Y knob: channel 2 divide/multiply relationship. -- Audio/CV In 1 and 2 are unused. +- Audio/CV In 1 is ignored in normal use, and Audio/CV In 2 can still be patched as a CV offset + source in the current implementation. -- **Z up** +**Z up** - Main knob: vactrol lag / slew time. - X knob: crossfade depth for Audio Out 1. - Y knob: crossfade depth for Audio Out 2. -- Audio/CV In 1 and 2 become the two audio inputs to the mixer. +- Audio/CV In 1 and 2 are the two mixer inputs. The audio inputs can also be used as slow CV + sources, but the card is not trying to detect or re-scale them differently. ## LED feedback @@ -40,9 +43,7 @@ card treats the idea as a two-input, two-output random matrix/mixer. DAC lane 1, DAC lane 2, PWM lane 1, PWM lane 2, then pulse activity on LEDs 5 and 6. - **Z up** switches the four brightness LEDs to mixer feedback: mix position 1, mix position 2, depth 1, depth 2, with pulse activity still shown on LEDs 5 and 6. - -- **Z down** -- Tap tempo when no external clock is patched. +- **Z down** is tap tempo when no external clock is patched. ## Inputs @@ -53,28 +54,30 @@ card treats the idea as a two-input, two-output random matrix/mixer. - **Audio/CV In 1**: mixer input 1 in the Vactrol Mix layer. - **Audio/CV In 2**: mixer input 2 in the Vactrol Mix layer. +The CV inputs are still available in the mixer layer, and the current implementation mirrors the +audio crossfade behavior there so they can be used as CV or audio sources depending on the patch. + ## Outputs - **Pulse Out 1**: matrix gate 1. - **Pulse Out 2**: matrix gate 2. - **CV Out 1**: channel 1 quantized pitch CV in `Z middle`, crossfaded CV output 1 in `Z up`. - **CV Out 2**: channel 2 quantized pitch CV in `Z middle`, crossfaded CV output 2 in `Z up`. -- **Audio Out 1**: DAC CV output in the Turing layer, mixed audio output 1 in the Vactrol Mix layer. -- **Audio Out 2**: DAC CV output in the Turing layer, mixed audio output 2 in the Vactrol Mix layer. +- **Audio Out 1**: direct pass-through of Audio In 1 in `Z middle`, mixed audio output 1 in `Z up`. +- **Audio Out 2**: direct pass-through of Audio In 2 in `Z middle`, mixed audio output 2 in `Z up`. ## Vactrol Mix behavior -In the Vactrol Mix layer, the current implementation uses the two Turing DAC lanes as smoothed -crossfade controls. Audio Out 1 crossfades Audio In 1 against Audio In 2, and Audio Out 2 -crossfades Audio In 2 against Audio In 1. +In the Vactrol Mix layer, the card uses the Turing-style control signal as a smoothed crossfade +driver. Audio Out 1 crossfades Audio In 1 against Audio In 2, and Audio Out 2 crossfades Audio In 2 +against Audio In 1. The same crossfade is mirrored on the CV pair: CV Out 1 crossfades CV In 1 against CV In 2, and CV Out 2 crossfades CV In 2 against CV In 1. That gives a practical Workshop Computer interpretation of the Vactrol Mix idea: -shared random movement, click-softened transitions, and stereo drift driven by the background -Turing machine. The audio inputs can therefore be used for audio or slow CV, while the CV inputs -provide a dedicated CV-only version of the same motion. +click-softened transitions, mirrored movement, and a layer that can behave like audio mixing or CV +crossfading depending on what is patched in. ## Status @@ -83,12 +86,13 @@ Draft implementation. The firmware source is copied from card **03_Turing_Machin - the internal card number changed to 93 - the original Audio In 1 reset behavior removed - the original Audio In 2 switch override behavior removed -- a new `Z up` Vactrol Mix audio layer added on top of the shared Turing engine +- a new `Z up` Vactrol Mix layer added alongside the Turing control layer +- startup knob values applied directly to the active layer, with pickup only applied after layer changes ## Web editor This card now needs its own editor model because the switch no longer selects two Turing presets. -The local `web/` editor is for the shared Turing engine settings only: timing, scale/range, pulse -behavior, and related persistent options. It also stores a first batch of Vactrol-layer settings: -crossfade law, lane relation, rise/fall timing, and per-lane minimum/maximum windows. The panel -still handles the live lag and crossfade depth gestures in `Z up`. +The local `web/` editor handles the Turing settings plus the Vactrol-layer settings: timing, +scale/range, pulse behavior, crossfade law, lane relation, rise/fall timing, and per-lane +minimum/maximum windows. The panel still handles the live lag and crossfade depth gestures in +`Z up`. diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp index 171ea4208..2a1f8fdb8 100644 --- a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp @@ -26,6 +26,7 @@ void Config::load(bool forceReset) save(); } + config.divide = 5; if (config.vactrol.law > 2) config.vactrol.law = 0; if (config.vactrol.relation > 2) diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp index f4c73bcb7..3ac020d86 100644 --- a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp @@ -6,7 +6,7 @@ #include "tusb.h" // Config variables in HEX -const int CARD_NUMBER = 0x93; +const int CARD_NUMBER = 93; const int MAJOR_VERSION = 0x01; const int MINOR_VERSION = 0x05; const int POINT_VERSION = 0x00; @@ -88,6 +88,8 @@ void MainApp::LoadSettings(bool reset) settings = &cfg.get(); CurrentBPM10 = settings->bpm; // load bpm from settings file NB bpm always 10x i.e 1200 = 120.0 bpm. clk.setBPM10(CurrentBPM10); + settings->divide = 5; + clk.UpdateDivide(5); UpdateNotePools(); UpdatePulseLengths(); @@ -140,13 +142,7 @@ void __not_in_flash_func(MainApp::ProcessSample)() void MainApp::Housekeeping() { - static uint8_t packet[128]; - - while (tud_midi_available()) - { - size_t len = tud_midi_stream_read(packet, sizeof(packet)); - handleSysExMessage(packet, len); - } + pollSysEx(); // LedOn(2, pendingSave); uint64_t nowUs = time_us_64(); @@ -171,17 +167,13 @@ void MainApp::Housekeeping() // Has 2 seconds passed since last change, and save is pending? if (pendingSave && (nowUs - lastChangeTimeUs >= 2000000)) { + settings->divide = 5; cfg.save(); pendingSave = false; } // blink(0, 250); // show that Core 0 is alive - if (!VactrolLayerActive()) - { - turingRandomness = KnobVal(Main); - } - ui.SlowUI(); // call knob checking etc updateLedState(); @@ -213,10 +205,49 @@ void MainApp::Housekeeping() midiOffset = 0; } - if (sendViz && tud_midi_n_mounted(0)) + sendViz = false; +} + +void MainApp::pollSysEx() +{ + uint8_t packet[64]; + + while (tud_midi_available()) { - SendLiveStatus(); - sendViz = false; + size_t len = tud_midi_stream_read(packet, sizeof(packet)); + for (size_t i = 0; i < len; ++i) + { + const uint8_t byte = packet[i]; + + if (!sysexActive) + { + if (byte == 0xF0) + { + sysexActive = true; + sysexLength = 0; + sysexBuffer[sysexLength++] = byte; + } + continue; + } + + if (sysexLength < kSysExBufferSize) + { + sysexBuffer[sysexLength++] = byte; + } + else + { + sysexActive = false; + sysexLength = 0; + continue; + } + + if (byte == 0xF7) + { + handleSysExMessage(sysexBuffer, sysexLength); + sysexActive = false; + sysexLength = 0; + } + } } } @@ -246,7 +277,6 @@ bool MainApp::PulseOutput1(bool requested) } PulseOut1(emit); - sendViz = true; return emit; } @@ -265,8 +295,6 @@ bool MainApp::PulseOutput2(bool requested) } PulseOut2(emit); - sendViz = true; // for testing - return emit; } @@ -361,6 +389,11 @@ bool MainApp::switchChanged() return result; } +void MainApp::SetTuringRandomness(uint16_t value) +{ + turingRandomness = value; +} + void MainApp::divideKnobChanged(uint8_t step) { clk.UpdateDivide(step); @@ -412,12 +445,7 @@ void MainApp::updateMainTuring() // Scaled CV out on CV/Audio 1 uint8_t dac8 = turingDAC1.DAC_8(); - uint16_t dac = cv_map_u8(dac8); vactrolTargetBase1 = int32_t(dac8) << 4; - if (!VactrolLayerActive()) - { - AudioOut1(dac); - } int midi_note = turingPWM1.MidiNote() + midiOffset; CVOut1MIDINote(midi_note); @@ -431,12 +459,7 @@ void MainApp::updateDivTuring() // Scaled CV out on CV/Audio 2 uint8_t dac8 = turingDAC2.DAC_8(); - uint16_t dac = cv_map_u8(dac8); vactrolTargetBase2 = int32_t(dac8) << 4; - if (!VactrolLayerActive()) - { - AudioOut2(dac); - } int midi_note = turingPWM2.MidiNote() + midiOffset; CVOut2MIDINote(midi_note); @@ -643,12 +666,10 @@ void MainApp::handleSysExMessage(const uint8_t *data, size_t len) if (out == sizeof(Config::Data)) { - memcpy(settings, decoded, sizeof(Config::Data)); - - // Before saving incoming config, overwrite BPM with corrrect local value - + // BPM and divide stay under panel/live control rather than web config ownership. settings->bpm = CurrentBPM10; + settings->divide = 5; cfg.save(); LoadSettings(0); @@ -881,6 +902,8 @@ void MainApp::ProcessVactrolMix() { if (!VactrolLayerActive()) { + AudioOut1(AudioIn1()); + AudioOut2(AudioIn2()); return; } diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h index da325c375..af3fbc7ed 100644 --- a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h @@ -57,6 +57,7 @@ class MainApp : public ComputerCard void UpdatePulseLengths(); bool switchChanged(); void IdleLeds(); + void SetTuringRandomness(uint16_t value); void UpdateCh2Lengths(); void UpdateCVRange(); void SetVactrolControls(uint16_t slew, uint16_t depth1, uint16_t depth2); @@ -99,6 +100,7 @@ class MainApp : public ComputerCard void sysexRespond(); void handleSysExMessage(const uint8_t *data, size_t len); + void pollSysEx(); void SendLiveStatus(); uint8_t midiHi(uint8_t input); @@ -129,4 +131,9 @@ class MainApp : public ComputerCard int32_t vactrolTargetBase2 = 2048; int32_t vactrolRiseStep = 8; int32_t vactrolFallStep = 6; + + static constexpr size_t kSysExBufferSize = 512; + uint8_t sysexBuffer[kSysExBufferSize] = {}; + size_t sysexLength = 0; + bool sysexActive = false; }; diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp index 4a8334fe3..0262c2ca4 100644 --- a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp @@ -11,6 +11,55 @@ void UI::init(MainApp *a, Clock *c) clk = c; } +void UI::ResetPickup(KnobPickup &pickup, uint16_t raw) +{ + pickup.entry = raw; + pickup.pickedUp = false; +} + +bool UI::ApplyPickup(KnobPickup &pickup, uint16_t raw, uint16_t target) +{ + if (pickup.pickedUp) + { + return true; + } + + int32_t moved = int32_t(raw) - int32_t(pickup.entry); + if (moved < 0) + moved = -moved; + + int32_t distance = int32_t(raw) - int32_t(target); + if (distance < 0) + distance = -distance; + + if (moved >= int32_t(pickupMoveThreshold) && distance <= int32_t(pickupCatchThreshold)) + { + pickup.pickedUp = true; + return true; + } + + return false; +} + +uint16_t UI::StepToKnobValue(uint8_t step, uint8_t steps, uint32_t range) +{ + if (steps <= 1) + return 0; + + if (step >= steps) + step = steps - 1; + + uint32_t stepSize = range / steps; + if (stepSize == 0) + return 0; + + uint32_t centre = (uint32_t(step) * stepSize) + (stepSize / 2); + if (step == steps - 1 || centre > range) + centre = range; + + return static_cast(centre); +} + void UI::Tick() { @@ -77,19 +126,55 @@ void UI::Tick() void UI::SlowUI() { - if (app->VactrolLayerActive()) + const bool upper = app->VactrolLayerActive(); + if (!pickupInitialised) + { + ResetPickup(middleMain, app->KnobMain()); + ResetPickup(middleX, app->KnobX()); + ResetPickup(middleY, app->KnobY()); + ResetPickup(upperMain, app->KnobMain()); + ResetPickup(upperX, app->KnobX()); + ResetPickup(upperY, app->KnobY()); + lastLayerUpper = upper; + pickupInitialised = true; + pickupArmed = false; + } + else if (upper != lastLayerUpper) + { + ResetPickup(middleMain, app->KnobMain()); + ResetPickup(middleX, app->KnobX()); + ResetPickup(middleY, app->KnobY()); + ResetPickup(upperMain, app->KnobMain()); + ResetPickup(upperX, app->KnobX()); + ResetPickup(upperY, app->KnobY()); + lastLayerUpper = upper; + pickupArmed = true; + } + + if (upper) { uint16_t slew = app->KnobMain(); uint16_t depth1 = app->KnobX(); uint16_t depth2 = app->KnobY(); - if (slew != lastSlew || depth1 != lastDepth1 || depth2 != lastDepth2) + if (pickupArmed) { - app->SetVactrolControls(slew, depth1, depth2); - lastSlew = slew; - lastDepth1 = depth1; - lastDepth2 = depth2; + slew = currentUpperMainValue; + depth1 = currentUpperXValue; + depth2 = currentUpperYValue; + + if (ApplyPickup(upperMain, app->KnobMain(), currentUpperMainValue)) + slew = app->KnobMain(); + if (ApplyPickup(upperX, app->KnobX(), currentUpperXValue)) + depth1 = app->KnobX(); + if (ApplyPickup(upperY, app->KnobY(), currentUpperYValue)) + depth2 = app->KnobY(); } + + currentUpperMainValue = slew; + currentUpperXValue = depth1; + currentUpperYValue = depth2; + app->SetVactrolControls(slew, depth1, depth2); return; } @@ -98,6 +183,12 @@ void UI::SlowUI() // Check for divide knob changes uint16_t knobTemp = app->KnobY(); + if (pickupArmed) + { + knobTemp = currentMiddleYValue; + if (ApplyPickup(middleY, app->KnobY(), currentMiddleYValue)) + knobTemp = app->KnobY(); + } // Add knob value int16_t inputTemp = app->readInputIfConnected(ComputerCard::CV1); // returns zero if nothing connected @@ -112,9 +203,16 @@ void UI::SlowUI() app->divideKnobChanged(step); lastDivideStep = step; } + currentMiddleYValue = StepToKnobValue(lastDivideStep, numDivideSteps, 4095); // Check for Length knob changes knobTemp = app->KnobX(); + if (pickupArmed) + { + knobTemp = currentMiddleXValue; + if (ApplyPickup(middleX, app->KnobX(), currentMiddleXValue)) + knobTemp = app->KnobX(); + } step = QuantiseToStep(knobTemp, numLengthSteps, 4095); int newlen = lengths[step]; @@ -125,8 +223,18 @@ void UI::SlowUI() lastLength = newlen; } + currentMiddleXValue = StepToKnobValue(step, numLengthSteps, 4095); app->switchChanged(); + uint16_t mainValue = app->KnobMain(); + if (pickupArmed) + { + mainValue = currentMiddleMainValue; + if (ApplyPickup(middleMain, app->KnobMain(), currentMiddleMainValue)) + mainValue = app->KnobMain(); + } + currentMiddleMainValue = mainValue; + app->SetTuringRandomness(mainValue); } // uint8_t UI::QuantiseToStep(uint32_t knobVal, uint8_t steps, uint32_t range) diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h index 3b28b6875..dc330efb2 100644 --- a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h @@ -42,14 +42,41 @@ class UI int outputPulseMod1 = 0; // NB must be signed int outputPulseMod2 = 0; - uint8_t lastDivideStep = 0; + uint8_t lastDivideStep = 5; uint8_t numDivideSteps = 9; uint8_t QuantiseToStep(uint32_t knobVal, uint8_t steps, uint32_t range); uint8_t lastLength = 0; uint8_t const numLengthSteps = 8; uint8_t const lengths[8] = {2, 3, 4, 5, 6, 8, 12, 16}; - uint16_t lastSlew = 0; - uint16_t lastDepth1 = 0; - uint16_t lastDepth2 = 0; + + struct KnobPickup + { + uint16_t entry = 0; + bool pickedUp = false; + }; + + KnobPickup middleMain; + KnobPickup middleX; + KnobPickup middleY; + KnobPickup upperMain; + KnobPickup upperX; + KnobPickup upperY; + + bool pickupInitialised = false; + bool pickupArmed = false; + bool lastLayerUpper = false; + uint16_t pickupDeadband = 48; + uint16_t pickupMoveThreshold = 64; + uint16_t pickupCatchThreshold = 96; + uint16_t currentMiddleMainValue = 2048; + uint16_t currentMiddleXValue = 2925; + uint16_t currentMiddleYValue = 2559; + uint16_t currentUpperMainValue = 256; + uint16_t currentUpperXValue = 4095; + uint16_t currentUpperYValue = 4095; + + void ResetPickup(KnobPickup &pickup, uint16_t raw); + bool ApplyPickup(KnobPickup &pickup, uint16_t raw, uint16_t target); + uint16_t StepToKnobValue(uint8_t step, uint8_t steps, uint32_t range); }; diff --git a/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 b/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..5639307f7462fb2ff1b30a9a78126c2bc96b30aa GIT binary patch literal 211456 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%0A19qLhT5YzYEx-4h@g4NHE8622 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 value.toString(16).padStart(2, "0")).join(" "); +} + +function scorePort(port) { + const text = `${port?.name || ""} ${port?.manufacturer || ""}`.toLowerCase(); + let score = 0; + if (text.includes("mtmcomputer")) score += 100; + if (text.includes("music thing")) score += 50; + if (text.includes("workshop")) score += 25; + return score; +} + +function selectPreferredPort(ports, previousId) { + if (previousId) { + const exact = ports.find((port) => port.id === previousId); + if (exact) { + return exact; + } + } + return [...ports].sort((a, b) => scorePort(b) - scorePort(a))[0] || null; +} + function bindRangePair(rangeId, numberId) { const range = byId(rangeId); const number = byId(numberId); @@ -69,12 +122,6 @@ function bindRangePair(rangeId, numberId) { } function syncFormFromConfig(cfg) { - byId("bpm").value = cfg.bpm; - byId("bpm_num").value = cfg.bpm; - byId("divide").value = cfg.divide; - byId("divide_num").value = cfg.divide; - byId("global_cv_range").value = cfg.cvRange; - const p = cfg.preset0; byId("scale").value = p.scale; byId("range").value = p.range; @@ -102,9 +149,8 @@ function syncFormFromConfig(cfg) { function readFormIntoConfig() { const cfg = structuredClone(state.config); - cfg.bpm = Number(byId("bpm_num").value); - cfg.divide = Number(byId("divide_num").value); - cfg.cvRange = Number(byId("global_cv_range").value); + cfg.divide = 5; + cfg.cvRange = cfg.preset0.cvRange; cfg.preset0.scale = Number(byId("scale").value); cfg.preset0.range = Number(byId("range").value); cfg.preset0.length = Number(byId("length").value); @@ -112,6 +158,7 @@ function readFormIntoConfig() { cfg.preset0.pulseMode1 = Number(byId("pulseMode1").value); cfg.preset0.pulseMode2 = Number(byId("pulseMode2").value); cfg.preset0.cvRange = Number(byId("preset_cvRange").value); + cfg.cvRange = cfg.preset0.cvRange; cfg.vactrol.law = Number(byId("vactrol_law").value); cfg.vactrol.relation = Number(byId("vactrol_relation").value); cfg.vactrol.rise = Number(byId("vactrol_rise_num").value); @@ -252,34 +299,94 @@ function sendSysEx(command, payload = []) { message.set(payload, 4); message[message.length - 1] = SYSEX_END; state.output.send(message); + state.lastTxAt = Date.now(); + setTransport(`TX cmd 0x${command.toString(16)} (${message.length} bytes): ${formatBytes(message)}`); } -function handleMIDIMessage(event) { - const data = [...event.data]; - if (data.length < 5 || data[0] !== SYSEX_START || data.at(-1) !== SYSEX_END) { - return; - } - if (data[1] !== MANUFACTURER || data[2] !== DEVICE) { - return; - } - if (data[3] !== CMD_GET_RESPONSE) { +function handleLiveStatus(payload) { + if (payload.length < 11) { + setTransport(`RX live status too short (${payload.length} bytes).`); return; } + const lane1 = ((payload[0] & 0x01) << 7) | payload[1]; + const lane2 = ((payload[2] & 0x01) << 7) | payload[3]; + const pwm1 = ((payload[4] & 0x01) << 7) | payload[5]; + const pwm2 = ((payload[6] & 0x01) << 7) | payload[7]; + const randomness = payload[8]; + const layer = payload[9] ? "Vactrol" : "Turing"; + const length = payload[10]; + setTransport( + `RX live status: layer=${layer}, len=${length}, rand=${randomness}, dac1=${lane1}, dac2=${lane2}, pwm1=${pwm1}, pwm2=${pwm2}` + ); +} - const payload = data.slice(7, -1); - const raw = decode7Bit(payload); - if (raw.length < CONFIG_SIZE) { - setStatus(`Config reply was too short (${raw.length} bytes).`); - return; +function handleMIDIMessage(event) { + for (const byte of event.data) { + if (byte === SYSEX_START) { + state.sysexBuffer = [byte]; + continue; + } + + if (state.sysexBuffer.length === 0) { + continue; + } + + state.sysexBuffer.push(byte); + + if (byte !== SYSEX_END) { + continue; + } + + const data = state.sysexBuffer; + state.sysexBuffer = []; + state.lastRxAt = Date.now(); + + if (data.length === 3 && data[0] === SYSEX_START && data[1] === MANUFACTURER && data[2] === SYSEX_END) { + state.ignoredShortCount += 1; + if (state.ignoredShortCount === 1 || state.ignoredShortCount % 25 === 0) { + setTransport(`Ignored ${state.ignoredShortCount} short MIDI noise packets (f0 7d f7).`); + } + continue; + } + + setTransport(`RX event (${data.length} bytes): ${formatBytes(data)}`); + + if (data.length < 5) { + setTransport(`RX short message (${data.length} bytes): ${formatBytes(data)}`); + continue; + } + if (data[1] !== MANUFACTURER || data[2] !== DEVICE) { + setTransport(`RX ignored for other device: ${formatBytes(data)}`); + continue; + } + if (data[3] === CMD_LIVE_STATUS) { + handleLiveStatus(data.slice(4, -1)); + continue; + } + if (data[3] !== CMD_GET_RESPONSE) { + setTransport(`RX unknown cmd 0x${data[3].toString(16)}: ${formatBytes(data)}`); + continue; + } + + const payload = data.slice(7, -1); + const raw = decode7Bit(payload); + if (raw.length < CONFIG_SIZE) { + setStatus(`Config reply was too short (${raw.length} bytes).`); + setTransport(`RX config too short (${raw.length} bytes raw): ${formatBytes(data)}`); + continue; + } + state.config = decodeConfig(raw); + syncFormFromConfig(state.config); + setStatus(`Config received from card (${raw.length} bytes).`); + setTransport(`RX config ok (${raw.length} raw bytes).`); } - state.config = decodeConfig(raw); - syncFormFromConfig(state.config); - setStatus(`Config received from card (${raw.length} bytes).`); } function updatePorts() { const midiIn = byId("midiIn"); const midiOut = byId("midiOut"); + const previousInputId = state.input?.id || ""; + const previousOutputId = state.output?.id || ""; midiIn.innerHTML = ""; midiOut.innerHTML = ""; @@ -300,14 +407,15 @@ function updatePorts() { midiOut.appendChild(option); } - state.input = inputs[0] || null; - state.output = outputs[0] || null; + state.input = selectPreferredPort(inputs, previousInputId); + state.output = selectPreferredPort(outputs, previousOutputId); midiIn.value = state.input?.id || ""; midiOut.value = state.output?.id || ""; if (state.input) { state.input.onmidimessage = handleMIDIMessage; } + setPortsText(); } async function connectMIDI() { @@ -320,14 +428,13 @@ async function connectMIDI() { state.midiAccess.onstatechange = updatePorts; updatePorts(); setStatus("Web MIDI connected."); + setTransport("Connected. Press Read From Card to test SysEx."); } catch (error) { setStatus(`Web MIDI connection failed: ${error.message}`); } } function init() { - bindRangePair("bpm", "bpm_num"); - bindRangePair("divide", "divide_num"); bindRangePair("vactrol_rise", "vactrol_rise_num"); bindRangePair("vactrol_fall", "vactrol_fall_num"); bindRangePair("vactrol_min1", "vactrol_min1_num"); @@ -346,9 +453,11 @@ function init() { if (state.input) { state.input.onmidimessage = handleMIDIMessage; } + setPortsText(); }); byId("midiOut").addEventListener("change", (event) => { state.output = state.midiAccess?.outputs.get(event.target.value) || null; + setPortsText(); }); byId("readBtn").addEventListener("click", () => { @@ -369,6 +478,9 @@ function init() { syncFormFromConfig(state.config); setStatus("Loaded editor defaults locally."); }); + byId("clearLogBtn").addEventListener("click", clearTransportLog); + + setPortsText(); } init(); diff --git a/releases/93_Turing_Matrix/web/index.html b/releases/93_Turing_Matrix/web/index.html index 9a5687263..c9a872756 100644 --- a/releases/93_Turing_Matrix/web/index.html +++ b/releases/93_Turing_Matrix/web/index.html @@ -37,35 +37,19 @@

Turing Matrix

Not connected.

+
+

No MIDI ports selected.

+
+
+ Transport Log + +
+
No SysEx traffic yet.
+
+
-
-

Shared Engine

-
- - - -
-
-

Turing Layer Settings

@@ -117,7 +101,7 @@

Turing Layer Settings

-
+<<<<<<< HEAD >>>>>>> Stashed changes +======= +>>>>>>> f2253689 (various bug fixes)
diff --git a/releases/93_Turing_Matrix/web/styles.css b/releases/93_Turing_Matrix/web/styles.css index e67bfb0f6..f93ce53fd 100644 --- a/releases/93_Turing_Matrix/web/styles.css +++ b/releases/93_Turing_Matrix/web/styles.css @@ -140,3 +140,39 @@ input[type="range"] { color: var(--muted); line-height: 1.55; } + +.transport-log-wrap { + margin-top: 10px; +} + +.transport-log-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; + color: var(--text); +} + +.transport-log-header button { + width: auto; + padding: 6px 10px; + font-size: 0.85rem; +} + +.transport-log { + margin: 0; + min-height: 9rem; + max-height: 16rem; + overflow: auto; + padding: 10px 12px; + border: 1px solid var(--line); + border-radius: 8px; + background: #0b120e; + color: #c8e9d6; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 0.83rem; + line-height: 1.45; + white-space: pre-wrap; + word-break: break-word; +} From e4aacd5da1a1b443b1d914045348f886ffe3e516 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:40:36 +0100 Subject: [PATCH 05/13] fine tuning --- .../TuringMatrix_Code/Config.cpp | 2 +- .../TuringMatrix_Code/MainApp.cpp | 14 +-- releases/93_Turing_Matrix/web/index.html | 96 +++---------------- 3 files changed, 20 insertions(+), 92 deletions(-) diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp index 2a1f8fdb8..b70e738b4 100644 --- a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp @@ -27,7 +27,7 @@ void Config::load(bool forceReset) } config.divide = 5; - if (config.vactrol.law > 2) + if (config.vactrol.law > 1) config.vactrol.law = 0; if (config.vactrol.relation > 2) config.vactrol.relation = 0; diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp index 3ac020d86..67026558d 100644 --- a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp +++ b/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp @@ -890,12 +890,12 @@ void MainApp::SetVactrolControls(uint16_t slew, uint16_t depth1, uint16_t depth2 void MainApp::UpdateVactrolTiming() { - const int32_t riseTime = 1 + settings->vactrol.rise; - const int32_t fallTime = 1 + settings->vactrol.fall; - const int32_t knobLag = 8 + ((vactrolSlew * 120) >> 12); + const int32_t riseTime = 32 + (int32_t(settings->vactrol.rise) * 24); + const int32_t fallTime = 32 + (int32_t(settings->vactrol.fall) * 24); + const int32_t knobLag = 64 + (int32_t(vactrolSlew) >> 1); - vactrolRiseStep = 1 + (8192 / (riseTime + knobLag)); - vactrolFallStep = 1 + (8192 / (fallTime + knobLag)); + vactrolRiseStep = 1 + (4096 / (riseTime + knobLag)); + vactrolFallStep = 1 + (4096 / (fallTime + knobLag)); } void MainApp::ProcessVactrolMix() @@ -941,10 +941,6 @@ void MainApp::ProcessVactrolMix() switch (settings->vactrol.law) { case 1: - shaped1 = (shaped1 * shaped1 * (12288 - (shaped1 << 1))) >> 24; - shaped2 = (shaped2 * shaped2 * (12288 - (shaped2 << 1))) >> 24; - break; - case 2: shaped1 = (shaped1 * shaped1) >> 12; shaped2 = (shaped2 * shaped2) >> 12; break; diff --git a/releases/93_Turing_Matrix/web/index.html b/releases/93_Turing_Matrix/web/index.html index c9a872756..0ee600b3c 100644 --- a/releases/93_Turing_Matrix/web/index.html +++ b/releases/93_Turing_Matrix/web/index.html @@ -111,118 +111,54 @@

Turing Layer Settings

- - -
-

Vactrol Layer Settings

-
- - - - - - - - -
-
-
-

Vactrol Layer

-
-

Z up uses the same background Turing engine but changes the panel role.

-

Main sets lag, X sets audio/CV crossfade depth 1, Y sets audio/CV crossfade depth 2.

-

Audio In 1/2 are the audio pair. CV In 1/2 are the mirrored CV pair. These controls are not stored in flash.

-
-
+
+

Mixer Layer Settings

-
-<<<<<<< HEAD ->>>>>>> Stashed changes -======= ->>>>>>> f2253689 (various bug fixes) From a7771eca816faa387314fbb45c8bd0cc259a8386 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 22:47:46 +0100 Subject: [PATCH 06/13] Update TuringMatrix.uf2 --- .../93_Turing_Matrix/uf2/TuringMatrix.uf2 | Bin 211456 -> 211456 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 b/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 index 5639307f7462fb2ff1b30a9a78126c2bc96b30aa..a63ecaf53465960b54dd491b64e3f1b2b931c3fe 100644 GIT binary patch delta 29191 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 Date: Fri, 26 Jun 2026 22:54:47 +0100 Subject: [PATCH 07/13] moved files to main folder and adapted info.yaml --- .../{TuringMatrix_Code => }/CMakeLists.txt | 0 .../{TuringMatrix_Code => }/Clock.cpp | 0 .../{TuringMatrix_Code => }/Clock.h | 0 .../{TuringMatrix_Code => }/ComputerCard.h | 0 .../{TuringMatrix_Code => }/Config.cpp | 0 .../{TuringMatrix_Code => }/Config.h | 0 .../{TuringMatrix_Code => }/MainApp.cpp | 0 .../{TuringMatrix_Code => }/MainApp.h | 0 releases/93_Turing_Matrix/README.md | 59 +++++++---------- .../{TuringMatrix_Code => }/Turing.cpp | 0 .../{TuringMatrix_Code => }/Turing.h | 0 .../{TuringMatrix_Code => }/UI.cpp | 0 .../{TuringMatrix_Code => }/UI.h | 0 releases/93_Turing_Matrix/info.yaml | 63 +++++++++---------- .../{TuringMatrix_Code => }/main.cpp | 0 .../pico_sdk_import.cmake | 0 .../{TuringMatrix_Code => }/tusb_config.h | 0 .../{TuringMatrix_Code => }/usb_descriptors.c | 0 18 files changed, 53 insertions(+), 69 deletions(-) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/CMakeLists.txt (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/Clock.cpp (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/Clock.h (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/ComputerCard.h (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/Config.cpp (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/Config.h (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/MainApp.cpp (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/MainApp.h (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/Turing.cpp (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/Turing.h (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/UI.cpp (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/UI.h (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/main.cpp (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/pico_sdk_import.cmake (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/tusb_config.h (100%) rename releases/93_Turing_Matrix/{TuringMatrix_Code => }/usb_descriptors.c (100%) diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/CMakeLists.txt b/releases/93_Turing_Matrix/CMakeLists.txt similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/CMakeLists.txt rename to releases/93_Turing_Matrix/CMakeLists.txt diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.cpp b/releases/93_Turing_Matrix/Clock.cpp similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/Clock.cpp rename to releases/93_Turing_Matrix/Clock.cpp diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Clock.h b/releases/93_Turing_Matrix/Clock.h similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/Clock.h rename to releases/93_Turing_Matrix/Clock.h diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/ComputerCard.h b/releases/93_Turing_Matrix/ComputerCard.h similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/ComputerCard.h rename to releases/93_Turing_Matrix/ComputerCard.h diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp b/releases/93_Turing_Matrix/Config.cpp similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/Config.cpp rename to releases/93_Turing_Matrix/Config.cpp diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Config.h b/releases/93_Turing_Matrix/Config.h similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/Config.h rename to releases/93_Turing_Matrix/Config.h diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp b/releases/93_Turing_Matrix/MainApp.cpp similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.cpp rename to releases/93_Turing_Matrix/MainApp.cpp diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h b/releases/93_Turing_Matrix/MainApp.h similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/MainApp.h rename to releases/93_Turing_Matrix/MainApp.h diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md index ea5e1c2e3..3eda69e67 100644 --- a/releases/93_Turing_Matrix/README.md +++ b/releases/93_Turing_Matrix/README.md @@ -1,6 +1,6 @@ # Turing Matrix -This is a draft Workshop Computer card built from the official +This is a beta Workshop Computer card built from the official **03_Turing_Machine** firmware, reshaped into two switch-selected layers inspired by the Music Thing Modular **Turing Machine + Vactrol Mix Expander** combination. @@ -12,7 +12,7 @@ card treats the idea as a two-input, two-output random matrix/mixer. ## Basic idea - **Z middle** is the Turing control layer and is intended to feel like the original Turing card. -- **Z up** is the Vactrol Mix layer and uses the card's Turing-style control signals to animate a +- **Z up** is the mixer layer and uses the card's Turing-style control signals to animate a two-input, two-output audio/CV mixer. - **Z down** remains tap tempo. - **Pulse Out 1** and **Pulse Out 2** keep the same clock/Turing pulse behavior in both layers. @@ -27,35 +27,27 @@ card treats the idea as a two-input, two-output random matrix/mixer. - Main knob: Turing randomness / write amount. - X knob: loop length. - Y knob: channel 2 divide/multiply relationship. -- Audio/CV In 1 is ignored in normal use, and Audio/CV In 2 can still be patched as a CV offset - source in the current implementation. **Z up** -- Main knob: vactrol lag / slew time. -- X knob: crossfade depth for Audio Out 1. -- Y knob: crossfade depth for Audio Out 2. -- Audio/CV In 1 and 2 are the two mixer inputs. The audio inputs can also be used as slow CV - sources, but the card is not trying to detect or re-scale them differently. +- Main knob: mixer lag / slew time. +- X knob: mix depth 1. +- Y knob: mix depth 2. +- Audio/CV In 1 and 2 are the mixer inputs. The audio inputs can also be used as slow CV sources. ## LED feedback -- **Z middle** keeps the inherited Turing-style LED view: - DAC lane 1, DAC lane 2, PWM lane 1, PWM lane 2, then pulse activity on LEDs 5 and 6. -- **Z up** switches the four brightness LEDs to mixer feedback: - mix position 1, mix position 2, depth 1, depth 2, with pulse activity still shown on LEDs 5 and 6. +- **Z middle** keeps the inherited Turing-style LED view. +- **Z up** switches the brightness LEDs to mixer feedback. - **Z down** is tap tempo when no external clock is patched. ## Inputs - **Pulse In 1**: external clock for the main Turing channel. - **Pulse In 2**: independent clock for channel 2. -- **CV In 1**: divide/multiply modulation for channel 2 in `Z middle`, CV mix input 1 in `Z up`. -- **CV In 2**: quantized pitch offset in `Z middle`, CV mix input 2 in `Z up`. -- **Audio/CV In 1**: mixer input 1 in the Vactrol Mix layer. -- **Audio/CV In 2**: mixer input 2 in the Vactrol Mix layer. - -The CV inputs are still available in the mixer layer, and the current implementation mirrors the -audio crossfade behavior there so they can be used as CV or audio sources depending on the patch. +- **CV In 1**: divide/multiply modulation for channel 2 in `Z middle`, mix input 1 in `Z up`. +- **CV In 2**: quantized pitch offset in `Z middle`, mix input 2 in `Z up`. +- **Audio/CV In 1**: mixer input 1 in the mixer layer. +- **Audio/CV In 2**: mixer input 2 in the mixer layer. ## Outputs @@ -66,33 +58,28 @@ audio crossfade behavior there so they can be used as CV or audio sources depend - **Audio Out 1**: direct pass-through of Audio In 1 in `Z middle`, mixed audio output 1 in `Z up`. - **Audio Out 2**: direct pass-through of Audio In 2 in `Z middle`, mixed audio output 2 in `Z up`. -## Vactrol Mix behavior - -In the Vactrol Mix layer, the card uses the Turing-style control signal as a smoothed crossfade -driver. Audio Out 1 crossfades Audio In 1 against Audio In 2, and Audio Out 2 crossfades Audio In 2 -against Audio In 1. +## Mixer behavior -The same crossfade is mirrored on the CV pair: CV Out 1 crossfades CV In 1 against CV In 2, and -CV Out 2 crossfades CV In 2 against CV In 1. +In the mixer layer, the card uses the Turing-style control signal as a crossfade driver. Audio Out +1 crossfades Audio In 1 against Audio In 2, and Audio Out 2 crossfades Audio In 2 against Audio In +1. -That gives a practical Workshop Computer interpretation of the Vactrol Mix idea: -click-softened transitions, mirrored movement, and a layer that can behave like audio mixing or CV -crossfading depending on what is patched in. +The same crossfade is mirrored on the CV pair: CV Out 1 crossfades CV In 1 against CV In 2, and CV +Out 2 crossfades CV In 2 against CV In 1. ## Status -Draft implementation. The firmware source is copied from card **03_Turing_Machine** with: +Beta implementation. The firmware source is copied from card **03_Turing_Machine** with: - the internal card number changed to 93 - the original Audio In 1 reset behavior removed - the original Audio In 2 switch override behavior removed -- a new `Z up` Vactrol Mix layer added alongside the Turing control layer +- a new `Z up` mixer layer added alongside the Turing control layer - startup knob values applied directly to the active layer, with pickup only applied after layer changes ## Web editor This card now needs its own editor model because the switch no longer selects two Turing presets. -The local `web/` editor handles the Turing settings plus the Vactrol-layer settings: timing, -scale/range, pulse behavior, crossfade law, lane relation, rise/fall timing, and per-lane -minimum/maximum windows. The panel still handles the live lag and crossfade depth gestures in -`Z up`. +The local `web/` editor handles the Turing settings plus the mixer-layer settings: timing, +scale/range, pulse behavior, mix curve, lane link, rise/fall timing, and per-lane minimum/maximum +windows. The panel still handles the live lag and mix depth gestures in `Z up`. diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.cpp b/releases/93_Turing_Matrix/Turing.cpp similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/Turing.cpp rename to releases/93_Turing_Matrix/Turing.cpp diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/Turing.h b/releases/93_Turing_Matrix/Turing.h similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/Turing.h rename to releases/93_Turing_Matrix/Turing.h diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp b/releases/93_Turing_Matrix/UI.cpp similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/UI.cpp rename to releases/93_Turing_Matrix/UI.cpp diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/UI.h b/releases/93_Turing_Matrix/UI.h similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/UI.h rename to releases/93_Turing_Matrix/UI.h diff --git a/releases/93_Turing_Matrix/info.yaml b/releases/93_Turing_Matrix/info.yaml index fcd51c28c..4ab4a1b0f 100644 --- a/releases/93_Turing_Matrix/info.yaml +++ b/releases/93_Turing_Matrix/info.yaml @@ -1,10 +1,10 @@ -draft: true +draft: false Name: Turing Matrix -Description: Turing Machine sequencer plus a switchable Vactrol Mix-inspired audio layer driven by the same background Turing engine. +Description: Turing Machine sequencer plus a switchable mixer layer inspired by the Music Thing Modular Turing Machine and Vactrol Mix combination. Language: C++ (ComputerCard) Creator: Tom Whitwell / Music Thing Modular -Version: 0.1.0 -Status: Draft concept +Version: 0.1.0-beta +Status: Beta release candidate License: MIT Editor: web Repository: https://github.com/TomWhitwell/Workshop_Computer @@ -19,19 +19,16 @@ tags: - cv manual: | - Turing Matrix uses one shared background Turing engine with two front-panel layers. + Turing Matrix uses two switch-selected front-panel layers. Z middle is the Turing control layer: the card behaves like the original Turing Machine card for clock, randomness, loop length, divide/multiply, pulse outputs, quantized CV, and DAC/CV outputs. - Audio In 1 and Audio In 2 are unused in this layer. - Z up is the Vactrol Mix layer: the same background Turing engine keeps running, but Audio In 1 and - Audio In 2 become the two audio mixer inputs and Audio Out 1 and Audio Out 2 become the stereo - mixer outputs. CV In 1 and CV In 2 are crossfaded in parallel to CV Out 1 and CV Out 2 using the - same Turing-driven control signals. The Turing DAC lanes are used as smoothed crossfade controls, - while Pulse Out 1 and Pulse Out 2 continue to follow the same shared clock behavior as the - Turing layer. Web editor settings define crossfade law, lane relation, rise/fall timing, and - minimum/maximum depth windows for each lane. + Z up is the mixer layer: Audio In 1 and Audio In 2 become the mixer inputs and Audio Out 1 and + Audio Out 2 become the mixer outputs. CV In 1 and CV In 2 are crossfaded in parallel to CV Out 1 + and CV Out 2 using the same control path. The front-panel knobs set mixer lag and mix depth, and + the web editor stores the persistent mixer behavior settings: mix curve, lane link, rise/fall + timing, and per-lane minimum/maximum windows. Z down remains tap tempo when no external clock is patched. @@ -47,19 +44,19 @@ panel: type: pulse - id: CVIn1 name: CV Mix Input 1 - description: Divide/multiply modulation in the Turing layer; CV mix input 1 in the Vactrol Mix layer + description: Divide/multiply modulation in the Turing layer; mix input 1 in the mixer layer type: cv - id: CVIn2 name: CV Mix Input 2 - description: Pitch offset in the Turing layer; CV mix input 2 in the Vactrol Mix layer + description: Pitch offset in the Turing layer; mix input 2 in the mixer layer type: cv - id: AudioIn1 name: Mixer Input 1 - description: Audio input for the Vactrol Mix layer; unused in the Turing control layer + description: Mixer input 1 in the mixer layer; unused in the Turing control layer type: audio - id: AudioIn2 name: Mixer Input 2 - description: Audio input for the Vactrol Mix layer; unused in the Turing control layer + description: Mixer input 2 in the mixer layer; unused in the Turing control layer type: audio outputs: - id: PulseOut1 @@ -72,22 +69,22 @@ panel: type: pulse - id: CVOut1 name: Channel 1 Quantized CV / CV Mix Out 1 - description: Quantized pitch CV in the Turing layer; crossfaded CV output 1 in the Vactrol Mix layer + description: Quantized pitch CV in the Turing layer; crossfaded CV output 1 in the mixer layer type: cv - id: CVOut2 name: Channel 2 Quantized CV / CV Mix Out 2 - description: Quantized pitch CV in the Turing layer; crossfaded CV output 2 in the Vactrol Mix layer + description: Quantized pitch CV in the Turing layer; crossfaded CV output 2 in the mixer layer type: cv - id: AudioOut1 name: Mixer Output 1 / DAC CV 1 - description: DAC CV output in the Turing layer; mixed audio output 1 in the Vactrol Mix layer + description: DAC CV output in the Turing layer; mixed audio output 1 in the mixer layer type: audio - id: AudioOut2 name: Mixer Output 2 / DAC CV 2 - description: DAC CV output in the Turing layer; mixed audio output 2 in the Vactrol Mix layer + description: DAC CV output in the Turing layer; mixed audio output 2 in the mixer layer type: audio -# certainty: medium — built from 03_Turing_Machine current source with a new switch-up Vactrol Mix layer +# certainty: high — beta card built from 03_Turing_Machine current source with a new switch-up mixer layer controls: knobs: - when: { z: middle } @@ -102,14 +99,14 @@ controls: description: Sets channel 2 clock relationship exactly as on the Turing Machine card - when: { z: up } main: - name: Vactrol Lag + name: Mixer Lag description: Sets how quickly the Turing-controlled mix opens and closes x: - name: Crossfade Depth 1 - description: Sets how strongly Turing lane 1 crossfades Audio In 1 and Audio In 2 into Audio Out 1 + name: Mix Depth 1 + description: Sets how strongly lane 1 crossfades Audio In 1 and Audio In 2 into Audio Out 1 y: - name: Crossfade Depth 2 - description: Sets how strongly Turing lane 2 crossfades Audio In 2 and Audio In 1 into Audio Out 2 + name: Mix Depth 2 + description: Sets how strongly lane 2 crossfades Audio In 2 and Audio In 1 into Audio Out 2 - when: { z: down, gesture: momentary } main: name: Tap Tempo @@ -119,9 +116,9 @@ controls: display: list items: - id: LED0 - name: Channel 1 DAC / Mix Level + name: Channel 1 DAC / Mix - id: LED1 - name: Channel 2 DAC / Mix Level + name: Channel 2 DAC / Mix - id: LED2 name: Channel 1 Turing Level - id: LED3 @@ -135,11 +132,11 @@ host: usb: - name: Web MIDI editor role: editor - description: Browser app in `web/` reads and writes shared Turing engine settings over USB MIDI SysEx; Chrome recommended + description: Browser app in `web/` reads and writes Turing and mixer settings over USB MIDI SysEx; Chrome recommended notes: | - The Web MIDI editor configures the shared Turing engine and timing behavior. The Vactrol Mix - layer has its own stored settings for crossfade law, lane relation, rise/fall timing, and - lane depth windows, while the panel still controls live lag and depth performance. + The Web MIDI editor configures the persistent Turing and mixer behavior. The mixer layer has + stored settings for mix curve, lane link, rise/fall timing, and lane depth windows, while the + panel still controls live lag and depth performance. source: - releases/03_Turing_Machine/info.yaml diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/main.cpp b/releases/93_Turing_Matrix/main.cpp similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/main.cpp rename to releases/93_Turing_Matrix/main.cpp diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/pico_sdk_import.cmake b/releases/93_Turing_Matrix/pico_sdk_import.cmake similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/pico_sdk_import.cmake rename to releases/93_Turing_Matrix/pico_sdk_import.cmake diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/tusb_config.h b/releases/93_Turing_Matrix/tusb_config.h similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/tusb_config.h rename to releases/93_Turing_Matrix/tusb_config.h diff --git a/releases/93_Turing_Matrix/TuringMatrix_Code/usb_descriptors.c b/releases/93_Turing_Matrix/usb_descriptors.c similarity index 100% rename from releases/93_Turing_Matrix/TuringMatrix_Code/usb_descriptors.c rename to releases/93_Turing_Matrix/usb_descriptors.c From 8c26b0954fb32bee2d57295ce76481fb14248649 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:09:19 +0100 Subject: [PATCH 08/13] updated readme and info.yaml --- releases/93_Turing_Matrix/README.md | 8 +- releases/93_Turing_Matrix/info.yaml | 261 ++++++++++++++-------------- 2 files changed, 140 insertions(+), 129 deletions(-) diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md index 3eda69e67..240b27e9d 100644 --- a/releases/93_Turing_Matrix/README.md +++ b/releases/93_Turing_Matrix/README.md @@ -4,6 +4,10 @@ This is a beta Workshop Computer card built from the official **03_Turing_Machine** firmware, reshaped into two switch-selected layers inspired by the Music Thing Modular **Turing Machine + Vactrol Mix Expander** combination. +The card builds on ideas and code originally developed by Tom Whitwell and Chris Johnson. +Their work on the Turing Machine family and mixer concepts is the foundation this beta card is +based on. + The original Vactrol Mix Expander is a four-input, two-output vactrol matrix mixer for the hardware Turing Machine. On the Workshop Computer we have two audio inputs, two CV inputs, two audio/CV outputs, two CV outputs, and two pulse outputs, so this @@ -55,8 +59,8 @@ card treats the idea as a two-input, two-output random matrix/mixer. - **Pulse Out 2**: matrix gate 2. - **CV Out 1**: channel 1 quantized pitch CV in `Z middle`, crossfaded CV output 1 in `Z up`. - **CV Out 2**: channel 2 quantized pitch CV in `Z middle`, crossfaded CV output 2 in `Z up`. -- **Audio Out 1**: direct pass-through of Audio In 1 in `Z middle`, mixed audio output 1 in `Z up`. -- **Audio Out 2**: direct pass-through of Audio In 2 in `Z middle`, mixed audio output 2 in `Z up`. +- **Audio Out 1**: direct audio pass-through in `Z middle`, mixed audio output 1 in `Z up`. +- **Audio Out 2**: direct audio pass-through in `Z middle`, mixed audio output 2 in `Z up`. ## Mixer behavior diff --git a/releases/93_Turing_Matrix/info.yaml b/releases/93_Turing_Matrix/info.yaml index 4ab4a1b0f..af98a4bbd 100644 --- a/releases/93_Turing_Matrix/info.yaml +++ b/releases/93_Turing_Matrix/info.yaml @@ -1,147 +1,154 @@ +id: 93_Turing_Matrix +title: Turing Matrix draft: false -Name: Turing Matrix -Description: Turing Machine sequencer plus a switchable mixer layer inspired by the Music Thing Modular Turing Machine and Vactrol Mix combination. -Language: C++ (ComputerCard) -Creator: Tom Whitwell / Music Thing Modular -Version: 0.1.0-beta -Status: Beta release candidate -License: MIT -Editor: web -Repository: https://github.com/TomWhitwell/Workshop_Computer - -tags: - - sequencer - - turing-machine - - random - - vactrol - - matrix-mixer - - modulation - - cv - -manual: | - Turing Matrix uses two switch-selected front-panel layers. - - Z middle is the Turing control layer: the card behaves like the original Turing Machine card for - clock, randomness, loop length, divide/multiply, pulse outputs, quantized CV, and DAC/CV outputs. - - Z up is the mixer layer: Audio In 1 and Audio In 2 become the mixer inputs and Audio Out 1 and - Audio Out 2 become the mixer outputs. CV In 1 and CV In 2 are crossfaded in parallel to CV Out 1 - and CV Out 2 using the same control path. The front-panel knobs set mixer lag and mix depth, and - the web editor stores the persistent mixer behavior settings: mix curve, lane link, rise/fall - timing, and per-lane minimum/maximum windows. - - Z down remains tap tempo when no external clock is patched. - +release: 93 / 0.1.0-beta +summary: Turing Machine sequencer with a switchable mixer layer inspired by the Music Thing Modular Turing Machine and Vactrol Mix combination +description: Turing Machine sequencer with a switchable mixer layer inspired by the Music Thing Modular Turing Machine and Vactrol Mix combination panel: + controls: + main: + label: |- + Random / Write + Mixer Lag + description: Drives the main Turing machine write behavior in middle mode and sets mixer lag in up mode + source: info.yaml + x: + label: |- + Loop Length + Mixer Depth 1 + description: Sets channel 1 sequence length in middle mode and mixer depth 1 in up mode + source: info.yaml + 'y': + label: |- + Div / Mult + Mixer Depth 2 + description: Sets channel 2 divide/multiply clock relationship in middle mode and mixer depth 2 in up mode + source: info.yaml + z: + label: Tap Tempo / Mode + description: Tap switch sets internal BPM when no external clock is present; switch position selects the active layer + source: info.yaml inputs: - - id: PulseIn1 - name: External Clock 1 - description: Replaces tap tempo and drives the main Turing channel + pulse_1: + label: Ext Clock 1 + description: Replaces tap tempo and drives the main Turing clock + source: info.yaml type: pulse - - id: PulseIn2 - name: External Clock 2 + pulse_2: + label: Ext Clock 2 description: Independently clocks channel 2 when patched + source: info.yaml type: pulse - - id: CVIn1 - name: CV Mix Input 1 - description: Divide/multiply modulation in the Turing layer; mix input 1 in the mixer layer + cv_1: + label: CV Input 1 + description: Divide/multiply modulation in middle mode; mix input 1 in up mode + source: info.yaml type: cv - - id: CVIn2 - name: CV Mix Input 2 - description: Pitch offset in the Turing layer; mix input 2 in the mixer layer + cv_2: + label: CV Input 2 + description: Pitch offset in middle mode; mix input 2 in up mode + source: info.yaml type: cv - - id: AudioIn1 - name: Mixer Input 1 - description: Mixer input 1 in the mixer layer; unused in the Turing control layer + audio_l: + label: Audio Out 1 + description: Audio pass-through in middle mode; mixed audio output 1 in up mode + source: info.yaml type: audio - - id: AudioIn2 - name: Mixer Input 2 - description: Mixer input 2 in the mixer layer; unused in the Turing control layer + audio_r: + label: Audio Out 2 + description: Audio pass-through in middle mode; mixed audio output 2 in up mode + source: info.yaml type: audio outputs: - - id: PulseOut1 - name: Matrix Gate 1 - description: Clock or Turing-bit pulse for the first shared lane in both layers + pulse_out_1: + label: Chan 1 Pulse + description: Clock or Turing-bit pulse behavior depending on pulse mode configuration + source: info.yaml type: pulse - - id: PulseOut2 - name: Matrix Gate 2 - description: Clock or Turing-bit pulse for the second shared lane in both layers + pulse_out_2: + label: Chan 2 Pulse + description: Clock or Turing-bit pulse behavior depending on pulse mode configuration + source: info.yaml type: pulse - - id: CVOut1 - name: Channel 1 Quantized CV / CV Mix Out 1 - description: Quantized pitch CV in the Turing layer; crossfaded CV output 1 in the mixer layer + cv_out_1: + label: |- + Chan 1 Quant + CV + description: Quantized pitch CV in middle mode; crossfaded CV output 1 in up mode + source: info.yaml type: cv - - id: CVOut2 - name: Channel 2 Quantized CV / CV Mix Out 2 - description: Quantized pitch CV in the Turing layer; crossfaded CV output 2 in the mixer layer + cv_out_2: + label: |- + Chan 2 Quant + CV + description: Quantized pitch CV in middle mode; crossfaded CV output 2 in up mode + source: info.yaml type: cv - - id: AudioOut1 - name: Mixer Output 1 / DAC CV 1 - description: DAC CV output in the Turing layer; mixed audio output 1 in the mixer layer + audio_out_l: + label: |- + Audio Out 1 + Pass-through + description: Audio pass-through in middle mode; mixed audio output 1 in up mode + source: info.yaml type: audio - - id: AudioOut2 - name: Mixer Output 2 / DAC CV 2 - description: DAC CV output in the Turing layer; mixed audio output 2 in the mixer layer + audio_out_r: + label: |- + Audio Out 2 + Pass-through + description: Audio pass-through in middle mode; mixed audio output 2 in up mode + source: info.yaml type: audio - -# certainty: high — beta card built from 03_Turing_Machine current source with a new switch-up mixer layer -controls: - knobs: - - when: { z: middle } - main: - name: Randomness / Write - description: Sets how often the shared Turing pattern changes - x: - name: Loop Length - description: Sets channel 1 sequence length; channel 2 length can be offset in the editor - y: - name: Divide/Multiply - description: Sets channel 2 clock relationship exactly as on the Turing Machine card - - when: { z: up } - main: - name: Mixer Lag - description: Sets how quickly the Turing-controlled mix opens and closes - x: - name: Mix Depth 1 - description: Sets how strongly lane 1 crossfades Audio In 1 and Audio In 2 into Audio Out 1 - y: - name: Mix Depth 2 - description: Sets how strongly lane 2 crossfades Audio In 2 and Audio In 1 into Audio Out 2 - - when: { z: down, gesture: momentary } - main: - name: Tap Tempo - description: Tap switch sets internal BPM when no external clock is present - leds: - - when: { z: middle } - display: list - items: - - id: LED0 - name: Channel 1 DAC / Mix - - id: LED1 - name: Channel 2 DAC / Mix - - id: LED2 - name: Channel 1 Turing Level - - id: LED3 - name: Channel 2 Turing Level - - id: LED4 - name: Matrix Gate 1 Activity - - id: LED5 - name: Matrix Gate 2 Activity - -host: - usb: - - name: Web MIDI editor - role: editor - description: Browser app in `web/` reads and writes Turing and mixer settings over USB MIDI SysEx; Chrome recommended - notes: | - The Web MIDI editor configures the persistent Turing and mixer behavior. The mixer layer has - stored settings for mix curve, lane link, rise/fall timing, and lane depth windows, while the - panel still controls live lag and depth performance. - +switch_modes: + up: 'Z up: MAIN: Mixer Lag; X: Mix Depth 1; Y: Mix Depth 2. Mixer mode active.' + middle: 'Z middle: MAIN: Randomness / Write; X: Loop Length; Y: Div / Mult. Turing mode active.' + down: 'Z down, momentary: MAIN: Tap Tempo.' +leds: + - >- + Z middle: Channel 1 DAC Level; Channel 2 DAC Level; Channel 1 Turing Level; Channel + 2 Turing Level; Pulse 1 Activity; Pulse 2 Activity. + - >- + Z up: Mixer position 1; Mixer position 2; Mix depth 1; Mix depth 2; Pulse 1 Activity; + Pulse 2 Activity. +notes: + - >- + Web editor via MIDI SysEx: Browser editor reads and writes Turing and mixer settings over USB + MIDI SysEx (Chrome recommended) source: - - releases/03_Turing_Machine/info.yaml - - releases/03_Turing_Machine/README.md - - https://www.musicthing.co.uk/Turing-Vactrol-Mix-Expander/ + - releases/93_Turing_Matrix/info.yaml + - releases/93_Turing_Matrix/README.md +slug: 93-turing-matrix +url: /workshopsystem/program-cards/93-turing-matrix/ +tags: + - sequencer + - turing-machine + - random + - clock + - cv + - mixer source_file: releases/93_Turing_Matrix/info.yaml source_url: https://github.com/TomWhitwell/Workshop_Computer/tree/main/releases/93_Turing_Matrix -readme_url: https://github.com/TomWhitwell/Workshop_Computer/blob/main/releases/93_Turing_Matrix/README.md +readme_url: >- + https://github.com/TomWhitwell/Workshop_Computer/blob/main/releases/93_Turing_Matrix/README.md +download_url: >- + https://raw.githubusercontent.com/TomWhitwell/Workshop_Computer/main/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 +metadata: + creator: Adrian Vos with initial code by Tom Whitwell / Music Thing Modular / Chris Johnson + language: C++ (ComputerCard) + version: 0.1.0-beta + status: Beta release candidate + editor_url: https://www.musicthing.co.uk/web_config/turing.html + editor_note: Configure this card in your browser +documentation: + intro: > + Dual-layer Turing-style random sequencer with a switchable mixer layer and tap tempo or external + clocking. + + The beta card builds on ideas and code from Tom Whitwell and Chris Johnson, with the Workshop + Computer port adapting those concepts into a two-layer panel workflow. + + Pulse In 1 overrides tap tempo as the main clock; Pulse In 2 can clock channel 2 independently. + + CV In 1 controls divide/multiply on channel 2 in middle mode and mix input 1 in up mode. CV In 2 + applies quantized pitch offset in middle mode and mix input 2 in up mode. + + Audio In 1 and Audio In 2 pass through in middle mode and become the mixer inputs in up mode. From d1fb202e90ea25e0dc8745be07e128091b5cdfa2 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:33:45 +0100 Subject: [PATCH 09/13] added stand alone editor link --- releases/93_Turing_Matrix/editorreadme.md | 55 +++++++++++++++++++++++ releases/93_Turing_Matrix/info.yaml | 4 +- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 releases/93_Turing_Matrix/editorreadme.md diff --git a/releases/93_Turing_Matrix/editorreadme.md b/releases/93_Turing_Matrix/editorreadme.md new file mode 100644 index 000000000..cbea907e9 --- /dev/null +++ b/releases/93_Turing_Matrix/editorreadme.md @@ -0,0 +1,55 @@ +# Turing Matrix Editor + +Standalone Web MIDI editor for the **Turing Matrix** Workshop Computer card. + +This editor configures the card's saved settings in the browser. It does not replace the +front-panel controls. + +## What it edits + +- Turing layer settings + - scale + - octave range + - pulse length mode + - channel 2 loop offset + - pulse output mode + - CV output range +- Mixer layer settings + - mix curve + - lane link + - rise + - fall + - lane 1 low/high + - lane 2 low/high + +## Using it + +1. Open the editor in a browser with Web MIDI support. +2. Connect the card. +3. Read the current settings from the card. +4. Change the settings in the form. +5. Send the settings back to the card. + +## Notes + +- Chrome is recommended. +- `Z middle` is the Turing layer. +- `Z up` is the mixer layer. +- `Z down` remains tap tempo. + +## Attribution + +The Turing Matrix editor and card build on ideas and code from **Tom Whitwell** and +**Chris Johnson**. + +## Hosting + +The GitHub Pages editor is here: + +`https://soveda.github.io/Turing_Matrix_Editor/web` + +## Files + +- `web/index.html` +- `web/app.js` +- `web/styles.css` diff --git a/releases/93_Turing_Matrix/info.yaml b/releases/93_Turing_Matrix/info.yaml index af98a4bbd..c116e55c1 100644 --- a/releases/93_Turing_Matrix/info.yaml +++ b/releases/93_Turing_Matrix/info.yaml @@ -132,11 +132,11 @@ readme_url: >- download_url: >- https://raw.githubusercontent.com/TomWhitwell/Workshop_Computer/main/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 metadata: - creator: Adrian Vos with initial code by Tom Whitwell / Music Thing Modular / Chris Johnson + creator: Adrian Vos from initial code by Tom Whitwell / Music Thing Modular / Chris Johnson language: C++ (ComputerCard) version: 0.1.0-beta status: Beta release candidate - editor_url: https://www.musicthing.co.uk/web_config/turing.html + editor_url: https://soveda.github.io/Turing_Matrix_Editor/web editor_note: Configure this card in your browser documentation: intro: > From 1966d404681e5458107c15c193c2931b29c90c60 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:35:10 +0100 Subject: [PATCH 10/13] Update README.md --- releases/93_Turing_Matrix/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md index 240b27e9d..fd0619728 100644 --- a/releases/93_Turing_Matrix/README.md +++ b/releases/93_Turing_Matrix/README.md @@ -87,3 +87,7 @@ This card now needs its own editor model because the switch no longer selects tw The local `web/` editor handles the Turing settings plus the mixer-layer settings: timing, scale/range, pulse behavior, mix curve, lane link, rise/fall timing, and per-lane minimum/maximum windows. The panel still handles the live lag and mix depth gestures in `Z up`. + +The hosted editor is here: + +`https://soveda.github.io/Turing_Matrix_Editor/web` From 5b2d0d5b85eace70ab7a1cc9059904ed5543fbb5 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:43:37 +0100 Subject: [PATCH 11/13] Create card quickstart.txt --- releases/93_Turing_Matrix/card quickstart.txt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 releases/93_Turing_Matrix/card quickstart.txt diff --git a/releases/93_Turing_Matrix/card quickstart.txt b/releases/93_Turing_Matrix/card quickstart.txt new file mode 100644 index 000000000..dbb6d856a --- /dev/null +++ b/releases/93_Turing_Matrix/card quickstart.txt @@ -0,0 +1,17 @@ +Turing Matrix Card Quickstart + +1. Power up the card. +2. Use Z middle for the Turing layer. +3. Use Z up for the mixer layer. +4. Use Z down for tap tempo. +5. Patch Pulse In 1 for external clock if needed. +6. Patch Audio In 1 and Audio In 2 for mixer mode. +7. Open the web editor in Chrome if you want to change saved settings. + +Web editor: +https://soveda.github.io/Turing_Matrix_Editor/web + +Notes: +- Z middle: Random / Write, Loop Length, Div / Mult +- Z up: Mixer Lag, Mix Depth 1, Mix Depth 2 +- Pickup is active when switching between layers. From 98763519213cdf6f304df914a30828adcdf46970 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Fri, 26 Jun 2026 23:45:57 +0100 Subject: [PATCH 12/13] Update card quickstart.txt --- releases/93_Turing_Matrix/card quickstart.txt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/releases/93_Turing_Matrix/card quickstart.txt b/releases/93_Turing_Matrix/card quickstart.txt index dbb6d856a..4df47f998 100644 --- a/releases/93_Turing_Matrix/card quickstart.txt +++ b/releases/93_Turing_Matrix/card quickstart.txt @@ -15,3 +15,19 @@ Notes: - Z middle: Random / Write, Loop Length, Div / Mult - Z up: Mixer Lag, Mix Depth 1, Mix Depth 2 - Pickup is active when switching between layers. + +Inputs: +- Pulse In 1: external clock for the main Turing channel +- Pulse In 2: independent clock for channel 2 +- CV In 1: divide/multiply modulation in Z middle, mix input 1 in Z up +- CV In 2: pitch offset in Z middle, mix input 2 in Z up +- Audio In 1: mixer input 1 in Z up, direct pass-through in Z middle +- Audio In 2: mixer input 2 in Z up, direct pass-through in Z middle + +Outputs: +- Pulse Out 1: channel 1 pulse +- Pulse Out 2: channel 2 pulse +- CV Out 1: channel 1 quantized CV in Z middle, crossfaded CV output 1 in Z up +- CV Out 2: channel 2 quantized CV in Z middle, crossfaded CV output 2 in Z up +- Audio Out 1: direct pass-through in Z middle, mixed audio output 1 in Z up +- Audio Out 2: direct pass-through in Z middle, mixed audio output 2 in Z up From a2d7aac66bb22ba14b5b3f4f43fdba6b278c8821 Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Sat, 27 Jun 2026 22:59:38 +0100 Subject: [PATCH 13/13] added initial patch suggestion --- releases/93_Turing_Matrix/README.md | 14 ++++++++++++++ releases/93_Turing_Matrix/card quickstart.txt | 11 +++++++++++ releases/93_Turing_Matrix/editorreadme.md | 15 +++++++-------- 3 files changed, 32 insertions(+), 8 deletions(-) diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md index fd0619728..6eb1fdb67 100644 --- a/releases/93_Turing_Matrix/README.md +++ b/releases/93_Turing_Matrix/README.md @@ -62,6 +62,20 @@ card treats the idea as a two-input, two-output random matrix/mixer. - **Audio Out 1**: direct audio pass-through in `Z middle`, mixed audio output 1 in `Z up`. - **Audio Out 2**: direct audio pass-through in `Z middle`, mixed audio output 2 in `Z up`. +## Quickstart + +Try this as a first patch: + +1. Tune oscillator 1 and oscillator 2 to a fifth interval. +2. Patch the two oscillators into `Audio In 1` and `Audio In 2`. +3. Patch `Audio Out 1` and `Audio Out 2` to two mixer channels and pan them hard left and right. +4. Switch to `Z up`. +5. Turn `X` and `Y` to maximum. +6. Listen first without any CV patched so you can hear the basic stereo mix movement. +7. Patch Slopes output to `CV In 1` and `CV In 2` and set Slopes to a slow loop. +8. Patch `CV Out 1` and `CV Out 2` to the pitch inputs of oscillator 1 and oscillator 2. +9. Listen on headphones and adjust the Slopes speed to hear the changes more clearly. + ## Mixer behavior In the mixer layer, the card uses the Turing-style control signal as a crossfade driver. Audio Out diff --git a/releases/93_Turing_Matrix/card quickstart.txt b/releases/93_Turing_Matrix/card quickstart.txt index 4df47f998..0579d6a1d 100644 --- a/releases/93_Turing_Matrix/card quickstart.txt +++ b/releases/93_Turing_Matrix/card quickstart.txt @@ -31,3 +31,14 @@ Outputs: - CV Out 2: channel 2 quantized CV in Z middle, crossfaded CV output 2 in Z up - Audio Out 1: direct pass-through in Z middle, mixed audio output 1 in Z up - Audio Out 2: direct pass-through in Z middle, mixed audio output 2 in Z up + +Quickstart patch: +1. Tune oscillator 1 and oscillator 2 to a fifth interval. +2. Patch the two oscillators into Audio In 1 and Audio In 2. +3. Patch Audio Out 1 and Audio Out 2 to two mixer channels and pan them hard left and right. +4. Switch to Z up. +5. Turn X and Y to maximum. +6. Listen first to the two oscillators without any CV patched so you can hear the basic stereo mix movement. +7. Patch Slopes output to CV In 1 and CV In 2 and set Slopes to a slow loop. +8. Patch CV Out 1 and CV Out 2 to the pitch inputs of oscillator 1 and oscillator 2. +9. Listen on headphones and adjust the Slopes speed to hear the changes more clearly. diff --git a/releases/93_Turing_Matrix/editorreadme.md b/releases/93_Turing_Matrix/editorreadme.md index cbea907e9..a18b02423 100644 --- a/releases/93_Turing_Matrix/editorreadme.md +++ b/releases/93_Turing_Matrix/editorreadme.md @@ -2,19 +2,19 @@ Standalone Web MIDI editor for the **Turing Matrix** Workshop Computer card. -This editor configures the card's saved settings in the browser. It does not replace the +This browser app changes the card's saved settings over USB MIDI. It does not replace the front-panel controls. ## What it edits -- Turing layer settings +- Turing layer - scale - octave range - pulse length mode - channel 2 loop offset - pulse output mode - CV output range -- Mixer layer settings +- Mixer layer - mix curve - lane link - rise @@ -24,7 +24,7 @@ front-panel controls. ## Using it -1. Open the editor in a browser with Web MIDI support. +1. Open the editor in Chrome or another Web MIDI browser. 2. Connect the card. 3. Read the current settings from the card. 4. Change the settings in the form. @@ -33,14 +33,13 @@ front-panel controls. ## Notes - Chrome is recommended. -- `Z middle` is the Turing layer. -- `Z up` is the mixer layer. +- `Z middle` is Turing mode. +- `Z up` is mixer mode. - `Z down` remains tap tempo. ## Attribution -The Turing Matrix editor and card build on ideas and code from **Tom Whitwell** and -**Chris Johnson**. +The Turing Matrix editor and card build on ideas and code from **Tom Whitwell** and **Chris Johnson**. ## Hosting