diff --git a/releases/93_Turing_Matrix/CMakeLists.txt b/releases/93_Turing_Matrix/CMakeLists.txt new file mode 100644 index 00000000..c7f3b24b --- /dev/null +++ b/releases/93_Turing_Matrix/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/Clock.cpp b/releases/93_Turing_Matrix/Clock.cpp new file mode 100644 index 00000000..85ef655a --- /dev/null +++ b/releases/93_Turing_Matrix/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/Clock.h b/releases/93_Turing_Matrix/Clock.h new file mode 100644 index 00000000..9824da6d --- /dev/null +++ b/releases/93_Turing_Matrix/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/ComputerCard.h b/releases/93_Turing_Matrix/ComputerCard.h new file mode 100644 index 00000000..f198fd77 --- /dev/null +++ b/releases/93_Turing_Matrix/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/Config.cpp b/releases/93_Turing_Matrix/Config.cpp new file mode 100644 index 00000000..b70e738b --- /dev/null +++ b/releases/93_Turing_Matrix/Config.cpp @@ -0,0 +1,96 @@ +#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(); + } + + config.divide = 5; + if (config.vactrol.law > 1) + 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/Config.h b/releases/93_Turing_Matrix/Config.h new file mode 100644 index 00000000..522fdc80 --- /dev/null +++ b/releases/93_Turing_Matrix/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/MainApp.cpp b/releases/93_Turing_Matrix/MainApp.cpp new file mode 100644 index 00000000..67026558 --- /dev/null +++ b/releases/93_Turing_Matrix/MainApp.cpp @@ -0,0 +1,988 @@ +// 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 = 93; +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); + settings->divide = 5; + clk.UpdateDivide(5); + + 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() +{ + + pollSysEx(); + + // 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)) + { + settings->divide = 5; + cfg.save(); + pendingSave = false; + } + + // blink(0, 250); // show that Core 0 is alive + + 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; + } + + sendViz = false; +} + +void MainApp::pollSysEx() +{ + uint8_t packet[64]; + + while (tud_midi_available()) + { + 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; + } + } + } +} + +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); + 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); + 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::SetTuringRandomness(uint16_t value) +{ + turingRandomness = value; +} + +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(); + vactrolTargetBase1 = int32_t(dac8) << 4; + + 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(); + vactrolTargetBase2 = int32_t(dac8) << 4; + + 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)); + // BPM and divide stay under panel/live control rather than web config ownership. + settings->bpm = CurrentBPM10; + settings->divide = 5; + + 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 = 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 + (4096 / (riseTime + knobLag)); + vactrolFallStep = 1 + (4096 / (fallTime + knobLag)); +} + +void MainApp::ProcessVactrolMix() +{ + if (!VactrolLayerActive()) + { + AudioOut1(AudioIn1()); + AudioOut2(AudioIn2()); + 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) >> 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/MainApp.h b/releases/93_Turing_Matrix/MainApp.h new file mode 100644 index 00000000..af3fbc7e --- /dev/null +++ b/releases/93_Turing_Matrix/MainApp.h @@ -0,0 +1,139 @@ +// 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 SetTuringRandomness(uint16_t value); + 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 pollSysEx(); + + 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; + + static constexpr size_t kSysExBufferSize = 512; + uint8_t sysexBuffer[kSysExBufferSize] = {}; + size_t sysexLength = 0; + bool sysexActive = false; +}; diff --git a/releases/93_Turing_Matrix/README.md b/releases/93_Turing_Matrix/README.md new file mode 100644 index 00000000..6eb1fdb6 --- /dev/null +++ b/releases/93_Turing_Matrix/README.md @@ -0,0 +1,107 @@ +# Turing Matrix + +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 +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 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. +- **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** +- Main knob: Turing randomness / write amount. +- X knob: loop length. +- Y knob: channel 2 divide/multiply relationship. + +**Z up** +- 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. +- **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`, 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 + +- **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**: 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 +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. + +## Status + +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` 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 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` diff --git a/releases/93_Turing_Matrix/Turing.cpp b/releases/93_Turing_Matrix/Turing.cpp new file mode 100644 index 00000000..edd88fa5 --- /dev/null +++ b/releases/93_Turing_Matrix/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/Turing.h b/releases/93_Turing_Matrix/Turing.h new file mode 100644 index 00000000..fa1ce298 --- /dev/null +++ b/releases/93_Turing_Matrix/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/UI.cpp b/releases/93_Turing_Matrix/UI.cpp new file mode 100644 index 00000000..0262c2ca --- /dev/null +++ b/releases/93_Turing_Matrix/UI.cpp @@ -0,0 +1,371 @@ +#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::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() +{ + + // 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() +{ + 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 (pickupArmed) + { + 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; + } + + uint16_t minVal = 0; + uint16_t maxVal = 4095; + + // 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 + 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; + } + 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]; + + if (newlen != lastLength) + { + app->lengthKnobChanged(newlen); + + 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) +// { +// 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/UI.h b/releases/93_Turing_Matrix/UI.h new file mode 100644 index 00000000..dc330efb --- /dev/null +++ b/releases/93_Turing_Matrix/UI.h @@ -0,0 +1,82 @@ +#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 = 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}; + + 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/card quickstart.txt b/releases/93_Turing_Matrix/card quickstart.txt new file mode 100644 index 00000000..0579d6a1 --- /dev/null +++ b/releases/93_Turing_Matrix/card quickstart.txt @@ -0,0 +1,44 @@ +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. + +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 + +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 new file mode 100644 index 00000000..a18b0242 --- /dev/null +++ b/releases/93_Turing_Matrix/editorreadme.md @@ -0,0 +1,54 @@ +# Turing Matrix Editor + +Standalone Web MIDI editor for the **Turing Matrix** Workshop Computer card. + +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 + - scale + - octave range + - pulse length mode + - channel 2 loop offset + - pulse output mode + - CV output range +- Mixer layer + - mix curve + - lane link + - rise + - fall + - lane 1 low/high + - lane 2 low/high + +## Using it + +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. +5. Send the settings back to the card. + +## Notes + +- Chrome is recommended. +- `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**. + +## 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 new file mode 100644 index 00000000..c116e55c --- /dev/null +++ b/releases/93_Turing_Matrix/info.yaml @@ -0,0 +1,154 @@ +id: 93_Turing_Matrix +title: Turing Matrix +draft: false +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: + pulse_1: + label: Ext Clock 1 + description: Replaces tap tempo and drives the main Turing clock + source: info.yaml + type: pulse + pulse_2: + label: Ext Clock 2 + description: Independently clocks channel 2 when patched + source: info.yaml + type: pulse + cv_1: + label: CV Input 1 + description: Divide/multiply modulation in middle mode; mix input 1 in up mode + source: info.yaml + type: cv + cv_2: + label: CV Input 2 + description: Pitch offset in middle mode; mix input 2 in up mode + source: info.yaml + type: cv + 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 + 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: + pulse_out_1: + label: Chan 1 Pulse + description: Clock or Turing-bit pulse behavior depending on pulse mode configuration + source: info.yaml + type: pulse + pulse_out_2: + label: Chan 2 Pulse + description: Clock or Turing-bit pulse behavior depending on pulse mode configuration + source: info.yaml + type: pulse + 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 + 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 + 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 + 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 +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/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 +download_url: >- + https://raw.githubusercontent.com/TomWhitwell/Workshop_Computer/main/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 +metadata: + 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://soveda.github.io/Turing_Matrix_Editor/web + 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. diff --git a/releases/93_Turing_Matrix/main.cpp b/releases/93_Turing_Matrix/main.cpp new file mode 100644 index 00000000..9d6e1b77 --- /dev/null +++ b/releases/93_Turing_Matrix/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/pico_sdk_import.cmake b/releases/93_Turing_Matrix/pico_sdk_import.cmake new file mode 100644 index 00000000..d493cc23 --- /dev/null +++ b/releases/93_Turing_Matrix/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/tusb_config.h b/releases/93_Turing_Matrix/tusb_config.h new file mode 100644 index 00000000..14aea9d4 --- /dev/null +++ b/releases/93_Turing_Matrix/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/uf2/TuringMatrix.uf2 b/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 new file mode 100644 index 00000000..a63ecaf5 Binary files /dev/null and b/releases/93_Turing_Matrix/uf2/TuringMatrix.uf2 differ diff --git a/releases/93_Turing_Matrix/usb_descriptors.c b/releases/93_Turing_Matrix/usb_descriptors.c new file mode 100644 index 00000000..bf367593 --- /dev/null +++ b/releases/93_Turing_Matrix/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/web/app.js b/releases/93_Turing_Matrix/web/app.js new file mode 100644 index 00000000..b3b9cafb --- /dev/null +++ b/releases/93_Turing_Matrix/web/app.js @@ -0,0 +1,486 @@ +const SYSEX_START = 0xf0; +const SYSEX_END = 0xf7; +const MANUFACTURER = 0x7d; +const DEVICE = 93; +const CMD_GET = 0x01; +const CMD_GET_RESPONSE = 0x02; +const CMD_SET = 0x03; +const CMD_LIVE_STATUS = 0x10; +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), + sysexBuffer: [], + lastTxAt: null, + lastRxAt: null, + transportLog: ["No SysEx traffic yet."], + ignoredShortCount: 0, +}; + +function byId(id) { + return document.getElementById(id); +} + +function setStatus(text) { + byId("status").textContent = text; +} + +function setTransport(text) { + const stamp = new Date().toLocaleTimeString(); + if (state.transportLog.length === 1 && state.transportLog[0] === "No SysEx traffic yet.") { + state.transportLog = []; + } + state.transportLog.push(`[${stamp}] ${text}`); + state.transportLog = state.transportLog.slice(-16); + const logEl = byId("transportLog"); + logEl.textContent = state.transportLog.join("\n"); + logEl.scrollTop = logEl.scrollHeight; +} + +function clearTransportLog() { + state.transportLog = ["No SysEx traffic yet."]; + state.ignoredShortCount = 0; + byId("transportLog").textContent = state.transportLog[0]; +} + +function setPortsText() { + const inputName = state.input?.name || "none"; + const outputName = state.output?.name || "none"; + byId("ports").textContent = `Input: ${inputName} | Output: ${outputName}`; +} + +function formatBytes(data) { + return [...data].map((value) => 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); + range.addEventListener("input", () => { + number.value = range.value; + }); + number.addEventListener("input", () => { + range.value = number.value; + }); +} + +function syncFormFromConfig(cfg) { + 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.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); + 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.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); + 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); + state.lastTxAt = Date.now(); + setTransport(`TX cmd 0x${command.toString(16)} (${message.length} bytes): ${formatBytes(message)}`); +} + +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}` + ); +} + +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).`); + } +} + +function updatePorts() { + const midiIn = byId("midiIn"); + const midiOut = byId("midiOut"); + const previousInputId = state.input?.id || ""; + const previousOutputId = state.output?.id || ""; + 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 = 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() { + 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."); + setTransport("Connected. Press Read From Card to test SysEx."); + } catch (error) { + setStatus(`Web MIDI connection failed: ${error.message}`); + } +} + +function init() { + 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; + } + setPortsText(); + }); + byId("midiOut").addEventListener("change", (event) => { + state.output = state.midiAccess?.outputs.get(event.target.value) || null; + setPortsText(); + }); + + 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."); + }); + 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 new file mode 100644 index 00000000..0ee600b3 --- /dev/null +++ b/releases/93_Turing_Matrix/web/index.html @@ -0,0 +1,179 @@ + + + + + + + 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 mixer layer remains mostly on the front panel. +

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

Not connected.

+
+

No MIDI ports selected.

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

Turing Layer Settings

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

Mixer Layer Settings

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

Z up is the mixer layer.

+

Main sets lag, X sets mix depth 1, Y sets mix depth 2.

+

Audio In 1/2 are the inputs. CV In 1/2 mirror the same crossfade path.

+
+
+
+
+ + + + diff --git a/releases/93_Turing_Matrix/web/styles.css b/releases/93_Turing_Matrix/web/styles.css new file mode 100644 index 00000000..f93ce53f --- /dev/null +++ b/releases/93_Turing_Matrix/web/styles.css @@ -0,0 +1,178 @@ +: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; +} + +.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; +}