From 9adbee68e7417a2e32d609c8e7667173a29fab7d Mon Sep 17 00:00:00 2001 From: soveda <161259864+soveda@users.noreply.github.com> Date: Sat, 22 Aug 2026 11:42:38 +0100 Subject: [PATCH 1/2] Add Dub Warning beta card --- releases/999_Dub_Warning/.gitignore | 10 + releases/999_Dub_Warning/CMakeLists.txt | 49 + releases/999_Dub_Warning/ComputerCard.h | 1182 +++++++++++++++++ releases/999_Dub_Warning/FUTURE_NOTES.md | 14 + releases/999_Dub_Warning/README.md | 84 ++ releases/999_Dub_Warning/TEST_PROCESS.md | 59 + releases/999_Dub_Warning/UF2/DubWarning.uf2 | Bin 0 -> 39936 bytes releases/999_Dub_Warning/info.yaml | 125 ++ releases/999_Dub_Warning/main.cpp | 275 ++++ .../999_Dub_Warning/pico_sdk_import.cmake | 84 ++ .../test_uf2/DubWarning_PT2399_TEST.uf2 | Bin 0 -> 39936 bytes 11 files changed, 1882 insertions(+) create mode 100644 releases/999_Dub_Warning/.gitignore create mode 100644 releases/999_Dub_Warning/CMakeLists.txt create mode 100644 releases/999_Dub_Warning/ComputerCard.h create mode 100644 releases/999_Dub_Warning/FUTURE_NOTES.md create mode 100644 releases/999_Dub_Warning/README.md create mode 100644 releases/999_Dub_Warning/TEST_PROCESS.md create mode 100644 releases/999_Dub_Warning/UF2/DubWarning.uf2 create mode 100644 releases/999_Dub_Warning/info.yaml create mode 100644 releases/999_Dub_Warning/main.cpp create mode 100644 releases/999_Dub_Warning/pico_sdk_import.cmake create mode 100644 releases/999_Dub_Warning/test_uf2/DubWarning_PT2399_TEST.uf2 diff --git a/releases/999_Dub_Warning/.gitignore b/releases/999_Dub_Warning/.gitignore new file mode 100644 index 000000000..6124a32d3 --- /dev/null +++ b/releases/999_Dub_Warning/.gitignore @@ -0,0 +1,10 @@ +build/ +cmake-build-*/ +*.elf +*.bin +*.hex +*.dis +*.map +compile_commands.json +.ninja_deps +.ninja_log diff --git a/releases/999_Dub_Warning/CMakeLists.txt b/releases/999_Dub_Warning/CMakeLists.txt new file mode 100644 index 000000000..916ba6237 --- /dev/null +++ b/releases/999_Dub_Warning/CMakeLists.txt @@ -0,0 +1,49 @@ +cmake_minimum_required(VERSION 3.13) + +include(pico_sdk_import.cmake) + +project(DubWarning C CXX ASM) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_CXX_STANDARD 17) + +pico_sdk_init() + +add_executable(DubWarning + main.cpp +) + +target_include_directories(DubWarning PRIVATE + . +) + +target_compile_options(DubWarning PRIVATE + -Wdouble-promotion + -Wfloat-conversion + -Wall + -Wextra +) + +target_compile_definitions(DubWarning PRIVATE + PICO_XOSC_STARTUP_DELAY_MULTIPLIER=64 +) + +target_link_options(DubWarning PRIVATE + -Wl,--print-memory-usage +) + +target_link_libraries(DubWarning + pico_stdlib + pico_unique_id + hardware_dma + hardware_i2c + hardware_pwm + hardware_adc + hardware_spi + hardware_clocks +) + +pico_enable_stdio_usb(DubWarning 0) +pico_enable_stdio_uart(DubWarning 0) +pico_set_binary_type(DubWarning copy_to_ram) +pico_add_extra_outputs(DubWarning) diff --git a/releases/999_Dub_Warning/ComputerCard.h b/releases/999_Dub_Warning/ComputerCard.h new file mode 100644 index 000000000..1da6de33e --- /dev/null +++ b/releases/999_Dub_Warning/ComputerCard.h @@ -0,0 +1,1182 @@ +/* +ComputerCard - by Chris Johnson + +version 0.3.0 - 12 May 2026 + +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 + +// 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;} + + static ComputerCard *ThisPtr() {return thisptr;} + +protected: + + class NotchFilter + { + public: + NotchFilter() + { + mix1 = mix2 = mixf1 = mixf2 = 0; + } + int32_t operator()(int32_t val) + { + int32_t mixf = (ooa0 * (val + mix2) - a2oa0 * mixf2) >> 14; + mix2 = mix1; + mix1 = val; + mixf2 = mixf1; + mixf1 = mixf; + return mixf; + } + private: + // 12kHz notch filter, to remove interference from mux lines + int32_t mix1, mix2, mixf1, mixf2; + static constexpr int32_t ooa0 = 16302, a2oa0 = 16221; // Q = 100, very narrow notch + + }; + + NotchFilter notchLeft, notchRight; + + /// 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) + { + if (val<-2048) val = -2048; + if (val > 2047) val = 2047; + cvValue[i] = (2047-val)*125; + } + + /// Set CV 1 output (values -2048 to 2047) + void __not_in_flash_func(CVOut1)(int16_t val) + { + if (val<-2048) val = -2048; + if (val > 2047) val = 2047; + cvValue[0] = (2047-val)*125; + } + + /// Set CV 2 output (values -2048 to 2047) + void __not_in_flash_func(CVOut2)(int16_t val) + { + if (val<-2048) val = -2048; + if (val > 2047) val = 2047; + cvValue[1] = (2047-val)*125; + } + + + /// Set CV output (values -262144 to 262143) + void __not_in_flash_func(CVOutPrecise)(int i, int32_t val) + { + if (val<-262144) val = -262144; + if (val > 262143) val = 262143; + cvValue[i] = ((262143-val)*125)>>7; + } + + /// Set CV 1 output (values -262144 to 262143) + void __not_in_flash_func(CVOut1Precise)(int32_t val) + { + if (val<-262144) val = -262144; + if (val > 262143) val = 262143; + cvValue[0] = ((262143-val)*125)>>7; + } + + /// Set CV 2 output (values -262144 to 262143) + void __not_in_flash_func(CVOut2Precise)(int32_t val) + { + if (val<-262144) val = -262144; + if (val > 262143) val = 262143; + cvValue[1] = ((262143-val)*125)>>7; + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOutMIDINote)(int i, uint8_t noteNum) + { + cvValue[i] = MIDIToDAC(noteNum, i); + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOut1MIDINote)(uint8_t noteNum) + { + cvValue[0] = MIDIToDAC(noteNum, 0); + } + + /// Set CV 2 output from calibrated MIDI note number (values 0 to 127) + void __not_in_flash_func(CVOut2MIDINote)(uint8_t noteNum) + { + cvValue[1] = MIDIToDAC(noteNum, 1); + } + + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + bool __not_in_flash_func(CVOutMillivolts)(int i, int32_t millivolts) + { + bool limited = false; + cvValue[i] = MillivoltsToDAC(millivolts, i, limited); + return limited; + } + + /// Set CV 1 output from calibrated MIDI note number (values 0 to 127) + bool __not_in_flash_func(CVOut1Millivolts)(int32_t millivolts) + { + bool limited = false; + cvValue[0] = MillivoltsToDAC(millivolts, 0, limited); + return limited; + } + + /// Set CV 2 output from calibrated MIDI note number (values 0 to 127) + bool __not_in_flash_func(CVOut2Millivolts)(int32_t millivolts) + { + bool limited = false; + cvValue[1] = MillivoltsToDAC(millivolts, 1, limited); + return limited; + } + + + /// 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() const + { + return hw; + } + + /// Return ID number unique to flash card + uint64_t UniqueCardID() const + { + return uniqueID; + } + + /// Return true iff CV outputs are calibrated. + /// Returns false if using default calibration values. + bool CVOutsCalibrated() const + { + return cvOutsCalibrated; + } + + + void Abort(); + + uint16_t CRCencode(const uint8_t *data, int length); + +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; + + static volatile uint32_t cvValue[2]; + + uint8_t numCalibrationPoints[calMaxChannels]; + CalPoint calibrationTable[calMaxChannels][calMaxPoints]; + CalCoeffs calCoeffs[calMaxChannels]; + + uint64_t uniqueID; + + uint8_t ReadByteFromEEPROM(unsigned int eeAddress, bool &failed); + int ReadIntFromEEPROM(unsigned int eeAddress, bool &failed); + void CalcCalCoeffs(int channel); + int ReadEEPROM(); + uint32_t MIDIToDAC(int midiNote, int channel); + uint32_t MillivoltsToDAC(int millivolts, int channel, bool &limited); + + 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; + + bool cvOutsCalibrated; + +// 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 CorrectADCDNL(uint16_t &value) const; + + void BufferFull(); + + void AudioWorker(); + + static void AudioCallback() + { + thisptr->BufferFull(); + } + static ComputerCard *thisptr; + + // 19-bit CV outputs + static void OnCVPWMWrap() + { + static int32_t error1 = 0, error2 = 0; + + pwm_clear_irq(pwm_gpio_to_slice_num(CV_OUT_1)); // clear the interrupt flag + uint32_t truncated_cv1_val = (cvValue[0]-error1) & 0xFFFFFF00; + error1 += truncated_cv1_val - cvValue[0]; + pwm_set_gpio_level(CV_OUT_1, (truncated_cv1_val>>8)); + uint32_t truncated_cv2_val = (cvValue[1]-error2) & 0xFFFFFF00; + error2 += truncated_cv2_val - cvValue[1]; + pwm_set_gpio_level(CV_OUT_2, (truncated_cv2_val>>8)); + } + +}; + + +#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 + +// 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 + + +#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 + + +// Initialise CV output delta-sigma target to half-way (near 0V) +volatile uint32_t ComputerCard::cvValue[2] = {262144,262144}; + + +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); + + + // Turn on IRQ for CV output PWM + uint slice_num = pwm_gpio_to_slice_num(CV_OUT_1); + pwm_clear_irq(slice_num); + pwm_set_irq_enabled(slice_num, true); + + irq_set_exclusive_handler(PWM_IRQ_WRAP, ComputerCard::OnCVPWMWrap); + irq_set_priority(PWM_IRQ_WRAP, 255); + irq_set_enabled(PWM_IRQ_WRAP, true); + + + // 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) + { + // We can't remove the PWM IRQ from within the ADC IRQ callback, so we do it here instead. + irq_set_enabled(PWM_IRQ_WRAP, false); + pwm_clear_irq(pwm_gpio_to_slice_num(CV_OUT_1)); // reset CV PWM interrupt flag + irq_remove_handler(PWM_IRQ_WRAP, ComputerCard::OnCVPWMWrap); + break; + } + + + } +} + +void ComputerCard::Abort() +{ + runADCMode = RUN_ADC_MODE_REQUEST_ADC_STOP; +} + +void __not_in_flash_func(ComputerCard::CorrectADCDNL)(uint16_t &value) const +{ + uint16_t adc512 = value + 512; + value += ((value & 0x3FF) == 0x1FF) << 2; + value += (adc512 >> 10) << 3; + value = uint32_t(value * 520349) >> 19; // Multiply by factor that maps 0-4095 input into 0-4095 output +} + +// 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)() +{ + 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; + + // Compensation of ADC DNL errors. + CorrectADCDNL(ADC_Buffer[cpuPhase][7]); // CV inputs + CorrectADCDNL(ADC_Buffer[cpuPhase][0]); // Audio inputs + CorrectADCDNL(ADC_Buffer[cpuPhase][4]); + CorrectADCDNL(ADC_Buffer[cpuPhase][1]); + CorrectADCDNL(ADC_Buffer[cpuPhase][5]); + + cvsm[cvi] = (15 * (cvsm[cvi]) + 16 * ADC_Buffer[cpuPhase][7]) >> 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); + + // 12kHz notch filters + adcInR = notchRight(adcInR); + adcInL = notchLeft(adcInL); + + // 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][7]<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; + } + + //////////////////////////////////////// + // 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_set_enabled(DMA_IRQ_0, false); + 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; + } + + + //////////////////////////////////////// + // Initialise LEDs (PWM, set up 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); + } + + + //////////////////////////////////////// + // Initialise knobs / audio in / CV in (ADC + Mux) + + 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); + + + //////////////////////////////////////// + + 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) + + + //////////////////////////////////////// + // Initialise pulse inputs + 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 + + + //////////////////////////////////////// + // Initialise audio outputs (SPI for external DAC) + 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); + + + //////////////////////////////////////// + // Initialise CV outputs + // We set up the PWM here, and add the IRQ for sigma-delta later one Run() is called + + // 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, 1999); // less than 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, 1000); + pwm_set_gpio_level(CV_OUT_2, 1000); + + + //////////////////////////////////////// + // Miscellaneous pins + + // Initialise 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); + + // Initialise USB host status pin + gpio_init(USB_HOST_STATUS); + gpio_disable_pulls(USB_HOST_STATUS); + + // Initialise normalisation probe pin + gpio_init(NORMALISATION_PROBE); + gpio_set_dir(NORMALISATION_PROBE, GPIO_OUT); + gpio_put(NORMALISATION_PROBE, false); + + // Initialise EEPROM (I2C) + i2c_init(i2c0, 100 * 1000); + gpio_set_function(EEPROM_SDA, GPIO_FUNC_I2C); + gpio_set_function(EEPROM_SCL, GPIO_FUNC_I2C); + + + // 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 hardware version + hw = ProbeHardwareVersion(); + + // Read EEPROM calibration values + cvOutsCalibrated = (ReadEEPROM() == 0); + + // 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, bool &failed) +{ + uint8_t deviceAddress = EEPROM_PAGE_ADDRESS | ((eeAddress >> 8) & 0x0F); + uint8_t data = 0xFF; + + uint8_t addr_low_byte = eeAddress & 0xFF; + + if (i2c_write_timeout_us(i2c0, deviceAddress, &addr_low_byte, 1, false, 10000) <= 0) + { + failed = true; + return 0; + } + + if (i2c_read_timeout_us(i2c0, deviceAddress, &data, 1, false, 10000) <= 0) + { + failed = true; + return 0; + } + + return data; +} + +// Read a 16-bit integer from EEPROM +int ComputerCard::ReadIntFromEEPROM(unsigned int eeAddress, bool &failed) +{ + uint8_t highByte = ReadByteFromEEPROM(eeAddress, failed); + uint8_t lowByte = ReadByteFromEEPROM(eeAddress + 1, failed); + + 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 we can't read valid calibration from EEPROM + for (unsigned channel = 0; channel < calMaxChannels; channel++) + { + numCalibrationPoints[channel] = 3; + calibrationTable[channel][0].voltage = -20; // -2V + calibrationTable[channel][0].dacSetting = 347700; + calibrationTable[channel][1].voltage = 0; // 0V + calibrationTable[channel][1].dacSetting = 261200; + calibrationTable[channel][2].voltage = 20; // +2V + calibrationTable[channel][2].dacSetting = 174400; + CalcCalCoeffs(channel); // calculate the coefficients + } + + // Read magic number + // Failure here could occur if I2C failed, or if incorrect/no magic number stored in EEPROM + bool i2cFailed = false; + if (ReadIntFromEEPROM(EEPROM_ADDR_ID, i2cFailed) != EEPROM_VAL_ID) + { + return 1; + } + + // Read the EEPROM into RAM + uint8_t buf[EEPROM_NUM_BYTES]; + for (int i = 0; i < EEPROM_NUM_BYTES; i++) + { + buf[i] = ReadByteFromEEPROM(i, i2cFailed); + } + + // Check CRC and fail if incorrect + 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; + } + + // CRC passed, so now read the calibration information + for (uint8_t channel = 0; channel < calMaxChannels; channel++) + { + int channelOffset = 4 + (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++]; + + // Unpack 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; + } + + // Now calculate the calibration coeffs that are actually used + // by the calibrated CVOut functions + 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*125)>>7; +} + +/// Converts voltage in millivolts to corresponding 19-bit sigma-delta PWM DAC value +/// Returns true if requested voltage is outside of full range of DAC values +/// millivolts should be in range -6000 to 6000. +/// Accuracy is dependent, of course, on the calibration coefficients +uint32_t ComputerCard::MillivoltsToDAC(int millivolts, int channel, bool &limited) +{ + limited = false; + int32_t dacValue = ((((calCoeffs[channel].mi * millivolts) >> 9) * 1573) >> 12) + calCoeffs[channel].bi; + if (dacValue > 524287) + { + dacValue = 524287; + limited = true; + } + if (dacValue < 0) + { + dacValue = 0; + limited = true; + } + return (dacValue*125)>>7; +} + +#endif + +#endif diff --git a/releases/999_Dub_Warning/FUTURE_NOTES.md b/releases/999_Dub_Warning/FUTURE_NOTES.md new file mode 100644 index 000000000..ada669417 --- /dev/null +++ b/releases/999_Dub_Warning/FUTURE_NOTES.md @@ -0,0 +1,14 @@ +# Dub Warning Future Notes + +## Current direction + +The shippable version is now a single PT2399-style dub siren. Separate reverb +experiments were tested and removed because the PT2399 repeats already smear +into a useful reverb-like wash at high settings. + +## Possible future changes + +- Tune the Wheel Up one-shot phrase after more listening on a sound system. +- Add a simple pre-delay high-pass or tone-button color before the delay. +- Consider using Audio In 2 as an external delay feedback/send input. +- Consider a smoother sine-like oscillator color at the low end of CV In 2. diff --git a/releases/999_Dub_Warning/README.md b/releases/999_Dub_Warning/README.md new file mode 100644 index 000000000..a2e22540c --- /dev/null +++ b/releases/999_Dub_Warning/README.md @@ -0,0 +1,84 @@ +# Dub Warning + +Beta status: passed single-device hardware testing. + +Dub Warning is a playable siren oscillator for the Music Thing Modular Workshop +Computer. It makes a bright triangle-to-square tone, bends it with an internal +LFO, adds a short noisy attack on triggers, and feeds it into a PT2399-style +delay that can smear into a reverb-like dub wash at high repeat settings. +Z Down fires a compact Wheel Up one-shot gesture for punctuation before drops, +while Pulse In 1 fires the normal gated siren. +Pulse In 2 is a reset pluck: at fast or audio-rate triggers it becomes a useful +rhythmic/noisy exciter, especially while sweeping Main pitch. + +The card is meant to be immediate: patch either audio output, grab the knobs, and +hit the switch or a pulse input when you want the siren to shout. + +## Controls + +| Control | Function | +| --- | --- | +| Main | Base pitch, unless Audio In 2 is patched | +| X | Siren sweep rate and pitch bend depth | +| Y | PT2399-style delay character: time, modulation, repeats, dirt, and wet level | +| Z Up | Continuous siren | +| Z Middle | Gated mode. Pulse In 1 fires the siren, then it fades away | +| Z Down | Fire Wheel Up one-shot and reset phase | + +## Inputs + +| Input | Function | +| --- | --- | +| Audio In 1 | Extra pitch modulation | +| Audio In 2 | Main pitch CV override. When patched, it defeats the Main knob | +| CV In 1 | Extra pitch modulation | +| CV In 2 | Siren color and drive modulation | +| Pulse In 1 | Fire gated siren | +| Pulse In 2 | Reset oscillator/LFO phase and create a short pluck | + +## Outputs + +| Output | Function | +| --- | --- | +| Audio Out 1 | Main siren through the wet/dry effect mix | +| Audio Out 2 | Alternate direct-plus-echo output | +| CV Out 1 | Internal LFO | +| CV Out 2 | Gate / hit envelope | +| Pulse Out 1 | Gate while the siren or Wheel Up gesture is active | +| Pulse Out 2 | Short hit pulse on trigger/reset/Wheel Up | + +## LEDs + +| LED | Meaning | +| --- | --- | +| LED0 | Siren level / active gate | +| LED1 | Active Main pitch source: Main knob or Audio In 2 override | +| LED2 | X sweep knob position | +| LED3 | Y delay/effect knob position | +| LED4 | Internal siren LFO | +| LED5 | Wheel Up / hit envelope while active, otherwise echo return level | + +## Test + +Use `test_uf2/DubWarning_PT2399_TEST.uf2` for hardware testing. The full input +and output test checklist is in `TEST_PROCESS.md`. + +## Development notes + +The firmware uses the `ComputerCard` framework and keeps the audio callback +integer-only. `CV In 2` handles oscillator color so the Y knob can stay focused +on the effect. `DubWarning.uf2` is the shippable PT2399-style delay version: +darker repeats, slight clock dirt, smeared feedback, and reverb-like wash at +longer settings. The Wheel Up gesture is fired from the switch and temporarily +overrides the free siren sweep with a short trilled pitch phrase and stronger +delay send. Audio In 2 uses jack detection, so the Main knob is only defeated +when a cable is patched. + +Build in the repository dev container from this directory: + +```sh +make +``` + +The dev container will configure CMake, build the firmware, and stage the UF2 in +`UF2/`. diff --git a/releases/999_Dub_Warning/TEST_PROCESS.md b/releases/999_Dub_Warning/TEST_PROCESS.md new file mode 100644 index 000000000..f83bbb64b --- /dev/null +++ b/releases/999_Dub_Warning/TEST_PROCESS.md @@ -0,0 +1,59 @@ +# Dub Warning Test Process + +Current beta result: passed single-device hardware testing. + +Use `test_uf2/DubWarning_PT2399_TEST.uf2`. + +## Setup + +- Flash the UF2 to the Workshop Computer. +- Patch `Audio Out 1` to a mixer or monitoring chain. +- Start with `Main`, `X`, and `Y` at noon. +- Leave `Audio In 1`, `Audio In 2`, `CV In 1`, and `CV In 2` unpatched for the baseline test. + +## Baseline controls + +1. Set `Z Up`. +2. Confirm a continuous siren is audible from `Audio Out 1`. +3. Turn `Main` across its travel and confirm smooth pitch change. `LED1` should follow. +4. Turn `X` across its travel and confirm smooth siren sweep changes. The top quarter should stay musical rather than extreme. `LED2` should follow. +5. Turn `Y` across its travel and confirm delay moves from subtle repeats to dub wash/self-oscillation territory without an excessive noise floor. `LED3` should follow. +6. Confirm `LED4` pulses with the internal LFO. + +## Switch + +1. Set `Z Up` and confirm continuous siren. +2. Set `Z Middle` and confirm the siren is silent until triggered. +3. Tap `Z Down` from either `Z Up` or `Z Middle`. +4. Confirm `Z Down` fires the Wheel Up one-shot: a compact trilled siren phrase with stronger echo send. +5. Confirm `LED5` follows the Wheel Up/hit envelope during the gesture. + +## Inputs + +1. `Audio In 1`: patch an audio-rate or CV signal. Confirm it adds pitch modulation on top of the current Main pitch source. +2. `Audio In 2`: patch a slow CV or offset. Confirm it overrides the Main knob as the base pitch source. Move the Main knob while Audio In 2 is patched and confirm the knob no longer changes the base pitch. `LED1` should follow Audio In 2. +3. `CV In 1`: patch a slow CV or sequencer. Confirm it modulates siren pitch. +4. `CV In 2`: patch a slow CV. Confirm it changes oscillator color/drive without changing the basic delay setting. +5. `Pulse In 1`: with `Z Middle`, send a gate or trigger. Confirm it fires the normal gated siren envelope, not the Wheel Up gesture. +6. `Pulse In 1`: with `Z Up`, send a gate or trigger. Confirm it retriggers the siren envelope/hit while the continuous siren remains active. +7. `Pulse In 2`: send a trigger. Confirm oscillator/LFO phase reset and a short pluck/noise event, but no Wheel Up phrase. +8. `Pulse In 2`: send fast triggers or audio-rate pulses. Confirm it creates interesting reset-pluck textures. Sweep `Main` while triggering, or patch a slow CV into `Audio In 2`, and confirm the texture changes musically. + +## Outputs + +1. `Audio Out 1`: confirm main wet/dry siren mix. +2. `Audio Out 2`: confirm alternate direct-plus-echo output. +3. `CV Out 1`: patch to a CV input or scope. Confirm it outputs the internal siren LFO. +4. `CV Out 2`: patch to a CV input or scope. Confirm it follows the gate/hit envelope. +5. `Pulse Out 1`: patch to an envelope, trigger input, or scope. Confirm it goes high while the gated siren or Wheel Up gesture is active. +6. `Pulse Out 2`: patch to a trigger input or scope. Confirm it emits a short pulse on `Pulse In 1`, `Pulse In 2`, and Wheel Up. + +## Pass Criteria + +- `Z Up` is playable as a continuous siren. +- `Z Middle` plus `Pulse In 1` gives a usable gated siren around 500 ms. +- `Z Down` gives a distinct Wheel Up gesture. +- `Pulse In 2` resets/plucks without firing Wheel Up, and fast triggers create useful textures. +- `Audio In 2` defeats the Main knob when patched and acts as the base pitch control. +- `Y` remains musical over its travel, with the last quarter intense but usable. +- No significant hiss remains after a gated siren has decayed. diff --git a/releases/999_Dub_Warning/UF2/DubWarning.uf2 b/releases/999_Dub_Warning/UF2/DubWarning.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..8b4536abfbb911bfb66ad17e891ab497395a70de GIT binary patch literal 39936 zcmeHwd01P=x%YdH7Q|-6Vp~WUOFCd1@PffkoY+YYk{FN#j#!+qILQ&hw!jms+akEsilU^I_RCrTw`=wFhw1AD#_l^z_<&t~9 z=lkn>9{e3WqnR_?Ip_V&yfgF8J2PuYUd)|kcfAN&pqCPuunnuxx+Apb0`%rtb=oSS z!^!Gej2*%$L1nM9FSdfNQ($$~c19P}ciM8ysRDyO%e4?R4YT@vh7_+{m%1K;%Z5|8 zFw~`0tykmyLcFIx8OB^P%;*$`5g*MxB17DbVyx3f{IWeiun4E_)MB*)lZ)q2(X(9l;Q)fUwj_80)m z<7cS=q5vfKfKCtaHQwvBZ~(tssrVfH7(RL3-?RGR$(w2D$ldyjm^G33BMm72vNZn(0-}{?Q>|m@acQebf7terW?)MXx>LN+}lUw z#Zl&H^&aK%gySk@uURSaO0AFs-i{*WF`6GW^cga%vKMkN)GRB;TK`UH=v+v7tdGzz z0iQ|V`w*wl(fjFoO~GFx;xEAuo~Zv#y`hXGOLDQ#mTY01$1Iq-Wce`}9fZ?lARnQRvIGxefBO`?xlxhS^V&!C_+BcXKXXE&O|p zV&!c;zpaX9erSM!!P^JTX!j?8OQ-3HX7-@p8S5D{+WoN*Qnhf?6?^(!Q}CCH_)8=3 zr!=F~()3W;d}{r4oHna;p{0hk`8O4;eU?C$-20Ty3-p(?1kXlbPlq?yXjnCh9dJ_^99CW>oZRnldZ4hU9P?G3Zky^B~(vNlO4KCwUU{2vn)Z8G;RWh>cbvJhDt=znf*Ck0h(D`dr<-K})# z+KHLjZD0ltTiOZh{%J{BI#X6a%*L*g=pv;7_B-Rf3iA?46Y_L)X@Ti+%YNlP^0>uh+DEpoUT3^P^JvMVw@a%ZwQMjJp`T0tXnYbwLm#pj z@qe4qoT_*sI(eOOK>1o?y~=2E81FM{lCiX(J%8?A^L2&4T*O}wN`1g`IL&*L~xXzNFvqou#fyk zUutZFqe-u-6fY?94wG`zOo(-_Eon5)OfF5WHA|~Sq)#B15V2mTIV#;8*RVXayf9U& zKYF);9M!a@#TOh+EnRK1JXN-%YK!_`$_G_X+l-bwjl89e@RmDG-?v#T@5Hwe#(k%$ zJn2qj9bvLrU$py9<88?gC$^CdX{`Gl={s@s3z`Rci=y*cVmECwMkgOvEwhxFS`qt$ z=khIR_g`1|M~V33=xDP2|H(NQwya2rfm!BRVTvSzfg~2R^3gqSEMwgDAWePQRjC}* zXPZyJv!uv2sH-X-2ajO(+3iFWS$dc@0emnj!SthxYwwT$a0J2QudYtgKS(^GaY7 z=5x>GEvp{Iu=w?<0HdMMhuGU3RbX#(C=_}H{hy@i(2U&%(1JFMg+jlIMZb&;2H$Al zg!yFn&2$jm^Ye@g(;5D$?Y~0AAIUX|{~@eJZOS@RqcJJ@gz6)!8__#G_#w6XQ_i_K z4TG+M-&=kk_EAGp&sUr)(d|E2W=yl0bmnseanfx%wO&MIc*PMeo=={53laNE1BzpcVv z;92dU=+_1{_Seku?pP=7V;-?+;*y0pPqeevs7VrgrW$FoAou*(OnacU#+b?e=)rK@ zD2ywHM8@B5RXeSO<}sZMz?A-9w1|In1pbt&oncC`9;Ex3kpDTPO>LMq8YzaxwOJ^I z6S{+MzeRe;tKC%E~ zW@$m0EK7P8bj;0cnY68-?A~wscj0zs&&9{g1y$whhr_+U?HBjpsNfGZwDj#KF7Ae& z^`=_pMMHi6W#ho?bUyC+;04xgx8f7dF1{qbMb6+BOMKfFOLSY#CC2T8UFwhYZOE^V zD0vl^Ann$*RawbGAMJ87s^Zx5-8k?($1%85#KcLWTu0XwQF% zS=wf@g+~ShUtIt_WGpl*s88GF3mS_Zdm=NTz|?SYf`hp2XD_9Wa~g@eIop0_w&jT` z=H{6=9=WdYj}`I9=5Uh#Q(pVdr3wC9WB<*i)G!C$dX{oc4;jMx74rg{E%CZCEo2dM zRBa$@jF52@(l1W5>l5uS9rGW+Hun&kCLH@W;n@F4w4X$~1MLp9X**BbeA>p-_MW!= z{*c%gpzU^XJisQjX+I$kc>?m4Gap}KGIGs^s$9gK{!7|Sm74?a>rcQ7fkoE$Z1-GR zWSw_$p=If%9COMg$jHRFnc=t#Fz$FcI#V0S4D*<|VIET%ptZj}EDxHlWnc>bj}!5a zi@;xpXnRa3g<(8}?7&>Bhdh3%mJAqc$iQqh-%h@FN$Qq#Mz`(8{y=nVv^!B69cMO1 zvoUe;ZPD(3UYKFta53I|?oy)VtBXqWrxy(*!7TP3??HFmxckqSXn$`4cUlH$8KLE8 zq72Z!8to6#eqylK#(lqg_PjDhhQLSgLb zd`-bWUc?`(*kt{u)M_+Ig3lnm*s~i+$78IY;Xs?y6xAI>d8u+f)&;~Jpo_w}p4t#~$EM&P~pZ&U(2= zej#BXLBkw^G-I-SAOZ9YrcX0M@a7P8(;SwEU33O1cRc;WqYchRXS95y^LRqJ+zjAQ+JU-!r(R|`U*$ZXf+=UWvNE^2hyiLdaJ);3C zlf16*pCRHuBLaU>CM$ux#NX>MW#ns`pX%-;%s!N><$!O+ITzUjl6Hql!*rNT1hfm6 zY42cz{MG+7vqAY)6NySY(8vZ?Tm{_|WJMYqym$q4(li@kgZ?WwrODE$-&g3z22bI= zJRI`3@V)nn1yj==a0vXf{rmtS3}CepM8 z=z9H18Igr^`u!CqEf;+bqmMkT3ZMEm`p{>-9L}A^+`kvjeN{O33s>I4+z*Fy--o%M zxpH0MpCIB-+3bY;e}cNZCaL@6Bz2!3r|u(@)O}{0y7z{uD?#cOhpB4{Q@8iZA*Ajj zB6geD;89E`VplVn`ovy=AIhPy(FCb(^vLl?j<62nET_C zx#vvgK5sJj72~i-~5%=m;7HT;*TPeN&F4Oi?y!m$^%H-bP>7D zY|u7I+iN2ewQhK1qSnQX)AkDt6Om)aX?tme)@eD?_H93{(`&=XWnH-&bN^z5*6Dqi zyKXY~k0*0KKAHQ#h*+olCUZZ5KD18%GMsx9=Kf4L_xf<|S4Ift-WSeYxdpim<|LN7 zmnL&}Pv-vGc-!*fmbwmN0b{+4p!Ua+|`m{cvOn(zZ=R4r%*4 zq@yd$jkk>yjC12nleB$&oVE@O6U)T|<9XMF^N!A5>Sa3^_mgBFk$CRcimi%~sIUb7 zXk;cn>8=qYmJ!ufHu%HIC#{-%(v6c(GLJuL@#K^E@h4@DM5XD{XnS$vh>I}(k3RhH zmSSudHjFGy+ky5%Ou+^dMzF7faVkfyEBup0{Hf4ug8#z;gYTv^N};(US!q#W%Bja` zIXb))X%rexH%FnJFfyv!C1ZnM53|8Kq%G^wrKK~BXGPk6vfh*r@8{1I*x=|e6K$T#s)vZ^oEg7xS&n;ZJ81W^E=4N0B{{-Zd!WUoV2vG zmjg!*AHGe(1$*x#Zk#_}jq}H4*i+HvztsJ1<@@=a#7z2DzFS#hg^Znb9b_4oGH)j_ z-)<@WU8N#F$^p7RR6ga9?h6g=x>iFnxiaFbG?P2?eU(PizTrvklgjG~|6~#WLvN0nq(tOk zn9MbzjxN4q+08g7v23*rY@<{YV)|ZF#(yay{wes$ z6ZM~el?5M;!|520j`ip`FNE?K9d;7Tu)8N3XZ#lr;4E-YQV(P=8JO_Po%CBe>6eFo z&xPaV4{U~4dlvP;qGcG$bCHz%=Wd<^E}>n{fbpV-rexqJIjvjBZ}q5q#E z{6CKH|0u%$!~RkzB>J`Ki~e`$f6Nr|pBaJwg5`-nGZ3!%l*sqopxM`$I8C-9sqpG1{B6g{*B1hVS;Ddr0@N82<9Z81gU;fBB)qLe_n3&Ldv^ zBlbtc(7{L0{}CEG_;9v8>j6p5CU4!NFFz`V>K;S?M`@_;(Zlwv?@4p+@)nbWj}1OH z{_Mvw5fBlS0dv?Db1 zA6Fkq%UZ_Fw=K`Qfz1JLc>uJiNvNIst#-bT1bhEXZYP95W?yd<1g%QF1a*UD_TLLn z359~J?)Ubm?1lE>TqgKl=t*t`)WWs$_*^z$fzq67orU{yj9NTy;Z&f z_HvL>3v+TuD?vAQGmUd7v;vmr$}tVSdm+?|b4sI?w{i-7D1Wr_q?MVc7W}9oJbkYz z{C~EH|7`r^3I2b=dVZ_qbHgd7FZ$oX|5YOXh}k6m zJfSrEQJ7}=JkF~djC?HroS9v=7ipC&yj-~3#|HIR=jt>lNBwvNXWPQA^EiT{K9@)4 z>eOhz8xEnaKVt~BFJVX;+HZtIsOva}Q2R87%t8C7;SlP28$+o5;fM5 zD7d=Pt~x4PruW|;SpfIoNb}PxxQYQw;B|$6s)#@K=_m2u=uFZk^J^`MLV`Zasmv_n z`|@J=YIBTGZDDlD0^k@KZKRwH?U`toj7W8}(Jn@N7TP96kyuxo`yDm)?)ua9bLu~H zM7pnV#b%E*#u=rh+5{x5O>l*ac6tEyigAvSKBInw%KhylR91gCV%k;5uk5PRqDeuY zHQ{nXT?>InSrSlK~-W#S1FJQzxuXC%53eW}V> z?bKQHRwnq_D03^MHWa|(fB8^OBUB43!2kBYkzbHkPzwYp&MTIlz2wAsUkFNh=tJX` z^38Zp#}Epf^QHRWacGp(#`$09lkf{R{sW{z3C5N1Qon@%QxwkoVmQ?mKkzgY9*f+* zW5dwnkH}IMZGWz0(br{F;fn)Sh#kKHWrQE7QDaGL{pp|vQ_<8^&$F#hrwEr3re+_=^ zMEmc`W1+S5$>|h zv`mg1@IsfK6mL<`7|NE&M z4b}6bIss3jq4UEN^T*Gko6Z~4Ic2JEK=uCUoN}~fC-yRwy(vy!U&*`j^t`;J4-R23 zLoG|PD60#8tI{}+LmR{xYxp=WnU`3e zMOsOOrlN0X?C*qP((KRlu||LGP%fm;FOqyu-akg(5B8 z2K}ih9W!qiLA`*yc7y6oo+5?vp4y8Uu0y&?16*z8uD%3~ldq^$HW@Fkw&ur|^E4kz*TK@h@ z+ZLVFDz(t`>~Gr*SA5hTFun_o%qqu`3pEu&yKquSwpZBO?M+24yx;=(rD*1IDn5PX zPM((FKAjX-FI3o)vE;e*%=M-JpDyB`9)bTJ+m!$eg1@OZ9w3=yu#$fdR*ih-cYxQBxlI&Yftwnt#jT7|| zI5q4iPw-Zf%+J+#5(i=VcHWJvJ!*-KblB)y;7ZV)9xPKgdKph`A$`w(A7;30a2xkI zpEM|Z5BjaBqwImpst&xr!K((juI!HKTl>Fb{5My`e{KZ+kafBLNu;XU1gci{ZOf*4%Mv()dx8J8T3 z&vq&x*Q)M-1-bzPB&v47{i;3WWoM@__|WNXa-IE-&TWvDF>t8mOLe_0W9C{@bZWFL zy~G@^Gj_U?RcR;%NNm2W>fEMmIvhxbYFwd|;tMTp-8S3Y>Y8beX?Aa$Y4K=k3u=r> z@i9#ufkg8;e4_X!zwb}5D9tC-ikodLAJ1Tp@#bn%W@t=utOM*}GFA1&LP6hcaZAKGd*&lHbAO z7~G_G9yG=n)!4(Le7C8W$&ebcPVNxfXI(_CpKF!DT+8R`%>}sXAU?fx7yQgw-t%D9 zPYh|et3dS~t!O8OZM`==Y-_#QXiG9$4N7maHMLpYv}2pdd1A-go2Fw&obe@VZC{}7 zupOdPcVS%*={mPXn>$?>15C!(0XE|+#G8IE{Z9H_luzORw7sUZ|2h$W9e(jd|DWPa zc{RnF#a@}+vABXao8rrc|${s#uzLNp`rf_{lrBPP%L}R z5Ios*EHtDY4>=Q>7siH$oIm*`(!3eVj|)wVHPwlw!FBJOF;2)9`xhFgh@)C?A#dT| z!OV5EeSa3;E3*CgKDHjRof18hspg;KTcHJ4BD`*uw5_dr-8x5j-70OjlAdCtL&06V z8y4Nz-_@e&!5NK#qyDak(Dwerui*T5qm51WF&Uc_G? zf&YDJ;7c4lRLr2p{1c6_4Zl=-)Grxkp2YdnMoGs`UTsM5?k|SmomZbRuxCE>J#F~o zY5pD54UJe0GQ<9Ppsryft_iJ89RRnfg1qSbixYykT>YWp-_C$fG6+kc|F8aXg3{pz zO+!18y4wxlJB>aHop5(~3rMSp(%4Z1` zg8>D%pBysDo?A!sI}V}t!a|VyE2cC2Q}O3S{J9AHJ5`io&mg^y5D(Uh->QCXkRxWV z`*D=WokQCGuYL~d$$rF6?Q#&=3v*oKb*sNDpl`tXyyM%mRdH#`CZSXQu2ciK=+ugFNe2kT;%a)51@`;Qn*Uf>4r*5a5` zK2QSnstd3z^}O?l;mfnEC&~rdxp$|!V9A;O6bRRDpAxl3y0*;&1*THmV8t@MFra{g zWL*R6zZI-$YA@~k4Zs(w7#xBA=iQHlxx<(J3?;?1y{6Rv3=#hf{Nf4zUu!rs`z>Kq zc)}p_R*VPVuX~4R=K;x?* znihQnN89+F_r95k2#Q?2jMTNnxbzL{!fgcT5`~>u2Hv>zv;j^HU+Ts@aE+N0+L-pJ z!0Ux@syI+W+X>|PtD0o|FXmz^z~lOn-o+)E4X{Y+jnOSE4h=0CYdV&*TB6GZ2+FQp z5<)}kM;BNn`u)Vq>6LzYnmw#Gk2ESF(>Pup;cBaeLv>^TC(mtwm@px81XrQ8E$H(K0_ zM=Uq2HvaCC1}E^TeUPD2Aoe#5%xhUKmZEv`u`F_Kd_Ox)WXI!z99EnPRVw;&fH@@2Ry{6zlPsD#7e)2^9|G=rh z8C5EiYdXg0&yGuJB$HB_%ZTMFEr7DuxGN^5-Jm{Ofit>>2HJm`z)wHfo2zMwmOk4P z9k<({@JhZ#ZImn@!xdL-igB$A`xXs5#r`{T28xB$%r@^O?fiRAMoVWM{~Dmr4EFwk zK<^XXQot1}!Ss;~RcQe%!c{yOOG@hv@_7#$hGyT8y0zfvhLlr@xPMXnNFJ<&B=sI# z88q_@NJzxg8m^CR%5wEEAp z5d31C9`rtD0QKhDXItrds#(P=vA4e<#N?jBHu)y##-0G(FQ}ri(;z+dd?P9R@I^Hr~u{=>7vmM9>D3750LG0^ONzrFv zS&`s-Ni zc)mwZo(L$$-9J4ETKW``EeEtb&~g!FHaabh%3i4xwCx7@ zsa=g8HI4sr7;}^_U|d!(3Q*is^y)n1EWNCS#s0y_i1L^hOUeRlXCzS27y})R)s4Hu z5@U(Adi=gMd{6U`TPhl_EBv!W{E;V4^8Zf5=&2~5)XMs68e0vl&)vAsFmeiaYD4-A zqx^Ptc+W0>?;aw9m`)X-?pW~mqcNUMFqh4<)~Di_3fG1^oIA)%ILg`}S0yoOA_C=cf?o&M7#fPS|(iEaqh;_mq93RX}l0`4-N` z{}Jb>B5-aE<7|k;8OvBZf6$ryU%r#UdC$J8i5&^2Cl{E~|H~2a&xyca#GKCmxq54@ zv!KQqg|fAs-JxtmT3-$|f<%9ZbxkSO816MEhg8&}o{*>|REAnY+0NCaHM|68jbjCN zJcdhOVx_g7qPtPBlynk{^=5G3Ub03~Tnx`c@D#4XSpnN{z3xj_3sD+e;|ygpr!1sK zaOn-z33ZnCT_PISid5`0>svl{iiV|l4gBD6bG@!edyW!7jW zRdDI@t!5|_IQu*+%^OZt<99H+43rk@En-|hr*umA>507fRa5X^DB`~mgD3j`+G2^2 z-6*i@5dB!d<#RWyhkvE#gJb|g!~Lc>b-x* z5XqwTC}WyPaS;8%cdtKZZU>QKn)@TtJR1&s%r8U#?dZP(2GxN34#+c}HXQXq))C+P zq?Y&ylgV~SUYzHD1vNS8TQpz60iW+j{&ndG$;3Nv!MuL#6W`K_;V+G+r75HR(-wUE zuqpU27V%#kfj?v&@|BbKkect4HKdhLsu+mTTxPToMqspg!W`k-qb$g}G>&G6FIu+( zBpK0}5L}5=e#a!Um5+P;h-{Jq-)sTd;_pAn6r^Smi|2SLdd8JCjbCQlnQUKVv2 zXWp_RAm!MM4)T`bMJ!hj1`ZPDmPf*FSKybp{%7nGqo<{&b-dK<2}m>V3tU(DFA?!y z5`q6KM3x~0C|zDAnLyFs9)z`Mipb)m-{E=2=o}(8SKRXe17l>CD2`U@pWse`LJsOR;7O z=TtiGfRHTEl%xF$?h?9(kKM-UaW%pb!)afaQzO&F?=*bk`-lF#^8}mBzi#;0_c`sW z;`;!9?HxrQwhMkVLExo0taouNY6`>}Pu5|-&L!i4Q=*iyW4C4x-5t7*%nIMDTG)I| z3-zITPuCUxxg!2Zs!9G&`+W)NDsKv|hLh$?0Wz6+9mGmhEvO6Zq;z6_XH4%XV3y1I zpBujN<-(*^*SulG;xH$-XqaR{mmkVMDM-yyYpPHYMwE^rSsuD^iw%M0n~M4fqES3H zG-K>*9HTe&MwLdb?lnZ8fDGo=2Q_ah6d!dIalkBl9Zw8sI!5P<-$q0AyX0myYIsFq z-%Saz&JHX;m^b47&PP|rXc-vWz6v=4mI3R)CfxJe=h928Cs>qd;xhpfZ6(@7o75u1 z^fJ^!7+_raG>5Ppia}CL+iME{Un=6i6u)>in(+A#NH>V2_7^C%MzIlGhg3@~RPn3C(yBJRA>KCBCC~a@wUbym9u67BxFtt8PYjJ5VBzQEk zoLQ%D1X##=Ux)9d{mllN{8SGVDE^LfjmK4Lp+->TccK)A;UuW((P8{U>g(_~+kI(E z7SHs0(Di!S;>F6a>vGzn#j#;m{+xx2xF^tjMBlNA$r<%Y^qJx)U?aVow&3H3O~F4;#Gg)jP2kVuLp!6> zTlB@!{Ls+eF&W3`pj~UxqCD-XG0?L*sW#mj8oGRyuAq@=)6o4ny6K)vYIFy}ZiRL> zx(BbixB-+I)6i&bGWy(&p-MbUM2>SqC$38I-#9u0b@MZZvEt#~PsZ48l;^p`aRBY_ z!xCpT?f=8xr;DHuZRGWQ^?gxvT)-O=fGL*h7^|wthP)whF&azzR*xe2gmQh^daB@z47EtHtOR>gwyfE5=hj(ULqiusDeGYxUsLeU7xB-> zZ=S$^&H6hx%-RqaDGEqy)PiQcGZ6zs4xLkAom{r7_=q$ z!uw8u_V@S-QN6g9uO%ji&HYRE72olmClu0?YzvPXBuXQ;2~Nghvq|Wj7H;e z)n1Yqx3kA;RK6jNvm4ubTs<~poiPnp9zm+|?P(1E)cU_n#D7@?{?!G^(xkXPRat@E zM5STHd;wp~&#=}yRYh@D4gC+-F!n^WbpY=o>(9SJGE2((i4w%1zNo5b_)oN$fH$gF z%!$Fh7NU9$kTe7LUdxvF46N!@Uwog;GU;Ps6HXnEKQ3eR1-$H&0-pJFhau`%3mV2~ zRxw_zy{f2V&>I>W2+`UM#~3v4CLE7~PjN551+%?h6UiH~_^usA^0#;JFRtJXuBA~Y zV&zdd8DNg-lPQe~qfo@;4hPIJyV!pQ=y%fcQ~)tE@Li|vHKqP97x7*rMAQJ09!zfV|rTryw87&30EHKvEMitVVx|6X@PAX1o4h`LWjR9t1j8}5j zrB6VuuFByV$Wi8Bn}Hlfc`T-+Qjf`l=HXcDck>CZ?!5hCAj}^y-wFP3G5`O9KhScj zdv7L^V%lC)@V`OC{|5ZxN&NF%d5o8}D8onte1ww&6K$+8-o`RvK7ieLyhl8-mWysY zVP^sOu>B6Ej9kUBfU-B*B7qd=lijdT;brvDDlaZKNM1cq#Av0ooex?@Ksypxi*>Jz zmJmEqNQw3ODz5*)b7+SCIr=}33Fvr$p6??8Wv>Dff!26Bt`qeHjE_Y@9G#!Sd4I^B zeKxi^CMN^utWj3AIN8f&W`ScZvpgnep0^iI4NT#X1x1xm68_KL<>lF5e-MF=aPB81t963zUY5P z|9^#uKg!Q0@&9IqHF#|H>OTi&A`N3W&o~|b{$J?Wg>i_ZG}Cad?(>2camZcvEiwHo7avvHcpcW%OBCg075-=+f0JGL%((GuJ>@ zINvz@&p_n6(f7Kp@V`mK|E381DK1SY+k@bys|?403@J(V`xk{2;kqUFI*`gTJYS7c zb{OkS(=qD0>>CMC`l0Rxw%^iW%7|u5XT2a(32|1JP9?-z%~HQD7FRXYnUbBcc%B^{ zpZaa#X93ul=IM<7chvuzMf`7$z+b5Zn{*l2QqbV$ZrFl?jqbKbSM7a<{8^5gSSaez zrvckOAS^FOql}l+C|Vl<%w|OCQ?BBE$9V z^rJLD+e%6ZxRdURF|tOrU5Ptyz#_F5GNk?jg0VLy8nW<|P?}b1ywC#+F5?_Pt)Y_? z<4j%twr|frxH5`K^~M;S?M^WsYX+#QAoyrA1zSmK4w=(VZ#zXx3U zPTWC$rVkd*^nFlRi7nQC)chYbSqpvR?>e1Jz?AlXrHKE^2>e0!L7~QLA>eVZ&3CHp zZ{S*~+Rb!V6*tZa>_II$slP0YQH&w}zhLxQARqNL8Mlq+*&hTX?rNghg5_Z02VTgM zct40@K8cQc;S)OV+DSSCpl>CCzz%XTP`#Pa;c8>u4%|Ix7Z^8U&BW7vQnwT!-jLPl zwUf^R46glt3-PKoVVmx;7XH-N9ANM)IJvi)6sNJBI}E#gJIIy*U{0q|`@fSo19U!^ zo-jzu?appDi93S32)04{3=fNINP#=Kp2;s0POh&&U8eXKAxAjNaMu<7t3>>1r+1?N zUu&o^;i}mTSWIhgG4_wLom6jwo~npaG}PzEvED+)mxcAYtFXX}(idZ*9Tv~`y7c_~ zeMI5MX(3Dn3s(AaobIiVo$NCZcU8^C*@iDMZ853c++FlLqZYuEv+dh!M>P$MJ9!PW ztg`T3-#-Gb`MmCAb;ZV1TxZN=RBWy{TnMypg`BUw$&>L0yxGp405cyull*q}CgU%0 zwVw}H_SG9c#(oFYR}(qcPAma3Z5*~Au^~AK3LUsoan0HlBhG8POi>`y@z^>IURg=E zLYcPLl=^>*i2p74#S{3uNjmngnvg#G4NFg@nL|T=zEYT0OM6`0`Sp6IG{1&tQI`XC zftZ6TTxCzr+pUDFL!B(`?KzUuwlF2%zH)%`+s<#j3GTucc#(8b+d z^vXBA%;94)e8wSs#)pC1u%GC9z5^ab$vxlv7?WUW>U~08k?t;-fu}?%^3+1SHNHeG z%mmK)f-xukif?@e>kKWEZTob6ir`q)k{??2spC1vC$`Ygj!@Gvx`!qB#rZ%*AJq>L z`QN*fmbqyb3;~e~`i5BKg&(eAEi`;Hh4$oAF?;Twrx; zakjbEsS`@bd}ma~2LAnJtN3MtBqNr`Q^%}UQ<{({;D`YC3E4IB?VjKo)a_q@9G$Ky z0DknH$Z|vV0W!1zv^?RO^(d4zf<%XVnB>ReN;h>gWRLh%YLrP$<7-O&=SBQ^{N@S% zkNqxZoF%?ww`#XhZ%FV}JEJYqeDO|!ce2SlY|W8+lyX;ngMrZ4*cjbog`Uan*L~CS zHdu}`$xeT7EblZ+Ayx>YmZe5wT!Z0_$ zB-=wn4~+_I7;R|ifzi;7{xt`*z4Q8HPZGyn)#iS7H;Lh^%nw4M@!(x|7N_JTF&@TY zI;c|78D+g&IE^h%E2+DS(Y5n7ORP3oxHk0ISgdxn`P$H)F*7;Dm0F})pRELS6wqMF zxpsZ={{j*Jf(ZP#kA-lC_u9}8LrmCSH&#y0-*uWz}EFImcv%p-9 z^|ez_+by_Dh#J>k-^$@Bta+^@lz(omBs;}>ZY_)T3P=2yBR@vZSI-SoyZ|YE|5#)% zpd5E@KUZ0I*W%(X{t8t!SlKC$z-|tRFjq86{rI7BFwBMJe z=GnIXDEQ}uwZ+%rdCiT6#)39n;rX7$(^Fw=CH--%>wwn9@u>MHZHuX)H2d$;SO`ti9~VHLdQbM9dh%6)d6(vhj?$C7Us86GZU+>e*mnO-A%GV4s`%5uYIJP)|8Vz=I5s#Vq+%0bi9RWAFHqqdc(d-jpT zMg;C$QpR`Lo>H!8(L?kt6*P^WkN5eY%8kEv@+1rxOH=MI4^>B4{ z6&BJ}!^~IrpdKk%M-DneOY3MZ4ITeoF3xpADL$!7z+9Z?1kA-=SMemS20!hpWX|?~4?bO1k6t`L%R^p2z%)=$_(E zZFp7}&zRPpuYt~5L)B7snx7xq@rB=r`+FpCOkOf?Aj!fi+&TW-S|5=cAvmI;p-Z{YZg3bD(E4IM#4@UzNVI;0EnL5AFzD z>A-us7T=>A=)s;_<+HRlkNd4aztB=gCCUrOefE#Xa-fg6p8IA!#zCI!4s(Hi+^>uE z%|t}cmDBSF&aV}HW`}(|5pm+FkBy#8FcBv)?6W^2PBitYq-Pe0alq@n6hq5l<%i>^ z9k)r{_QH$T75>E{{>2gaPxQuH?KfWJEE(2Br`5=XcLYfmPA5Q4{(zTLV*lK2*P!lv zE$*U}Ec_&(&}pn`#i_!uPU196VtKVWR**RT+CGMk@!vok2G<_Y?&fEj`(y^2e{E>! zrQt@TK6SZ=K^KeT!|p4w+8H=9|LYZ&bF8_DsLeflX>-qtw{%McqM- zlM{Dx;gZ9t8SS3~Q?Uqfl5OICAfQ)=Ya3D=DKjo`ezyg15SD^ok)vq|&! zgx3aeO>~~#jdT7BeBt+2;#|L#YJA~bu~v@jdRf8kGzk69Cj@oFdqPbEYxgu5aqnLM zmS>$9Lohhq!V}JZ;XRz$WE*Pih6eX|xCA*|AI@@4=W9y)ZxQjgMBv|UWKkmDR21i8 z__%!xkE>*G{?7vye4oAzl6f(-k4%IEp2asY%jQP;`>t+g-wa%1RcO}iNx^Bk3Y63v z&ElG?3SuzJ#@AoDaK7#PAr>`34u;+@dZj2d^v6(Uy56hc`gAA_)|zE?WKW}-R~c^e zIyA6oi4U?9ao#@B+sD<)P#=kN6FIi^^enhLi3elOH5gFO?_XY6nClA-{V3Fjd2R_m zVMi$XTl*8j!X>^weYvcyv6U$Fx09Q31H(n1#^-2GqlUsXzNX-B74f%5;2)eqnXf*9 zY^g5?I3wh|;qwJ@P}gq68@q*BmEVNxvu?;SeidL|Vs2U4%-k5ep7~KA0GSs96eUeK z4s$~%O_Ti>-x-v%nX$83SOq{(=uq*{efO{1taWVK>e#5=x?Y?4fOcc?!&--Z%a*P8 zYwOl&*EQQ6_iuVgYj-#s);+kc9^W%2J3CuzD_CtdYS9&gyFsFky8Yhz@7eENr?qdg z-@93xS&xo;*V*eI!mRBLo5Sg79IgGn`_|oiKa{WAw9bCtI;h*a_5QqjzL&RrTkZ;+ z9)P^Xd5h7Sw$~K=OGNxj@QWw<|CA0*4UVmp2&M<>?zG>#rD4kkY+oPPpv}wE=H=$y z0IRoZO{E1|hZBKXx9L7@nDh_Y8#dYNHm#!xzvFrgWrZ=Zaq%-yuqcOe+sWVq2Eg{5 zx(As9a3axrmJMMluO~Ni1cUS5UzOW44GEDo~y04wDm4Y%J3xWcl z%Y>he9RiTz$4y+KuXx8!c%m(daEBt?)6+Qxf212FSSfz)1pZ8Kv6ET)0|G@&Bzgs7 zZYg4pZBEDj*LPgaABp}pT#rL_?kmxhqbaRGL+dv#EHTo>CH%ML>bVLijwAavo4%$np!M&euLi-n}G4>exf2slPnj0}5fHpJryWi + +class DubWarning : public ComputerCard +{ +public: + static constexpr int32_t kEchoSize = 16384; + static constexpr int32_t kMaxAudio = 2047; + static constexpr int32_t kMinAudio = -2048; + static constexpr int32_t kPulseLength = 1600; + static constexpr int32_t kWheelLength = 24576; + + int16_t echo[kEchoSize] = {}; + + uint32_t oscPhase = 0; + uint32_t lfoPhase = 0; + uint32_t wobblePhase = 0; + uint32_t wheelPhase = 0; + uint32_t noise = 0x12345678; + + int32_t echoWrite = 0; + int32_t delayFilter = 0; + int32_t smearFilter = 0; + int32_t gateEnv = 0; + int32_t hitEnv = 0; + int32_t hitDecay = 8; + int32_t gateDecayClock = 0; + int32_t wheelTimer = 0; + int32_t pulseTimer1 = 0; + int32_t pulseTimer2 = 0; + int32_t lastTone = 0; + + bool latched = true; + + virtual void ProcessSample() + { + const int32_t mainKnob = KnobVal(Knob::Main); + const int32_t xKnob = KnobVal(Knob::X); + const int32_t yKnob = KnobVal(Knob::Y); + const int32_t mainControl = Connected(Input::Audio2) ? clampInt(AudioIn2() + 2048, 0, 4095) : mainKnob; + const int32_t xPlay = tameUpperTravel(xKnob); + const int32_t yPlay = tameUpperTravel(yKnob); + const int32_t cvPitch = clamp12(AudioIn1()) + clamp12(CVIn1()); + const int32_t cvColor = clamp12(CVIn2()); + const int32_t switchPosition = SwitchVal(); + + if(PulseIn1RisingEdge()) + { + oscPhase = 0; + gateEnv = 4095; + hitEnv = 4095; + hitDecay = 8; + latched = true; + } + + if(PulseIn2RisingEdge()) + { + oscPhase = 0; + lfoPhase = 0; + hitEnv = 4095; + hitDecay = 4; + latched = true; + } + + if(SwitchChanged() && switchPosition == Switch::Down) + { + oscPhase = 0; + lfoPhase = 0; + wheelPhase = 0; + wheelTimer = kWheelLength; + gateEnv = 4095; + hitEnv = 4095; + hitDecay = 8; + latched = true; + } + + if(switchPosition == Switch::Down && wheelTimer > 0) + { + latched = true; + } + else if(switchPosition == Switch::Middle) + { + latched = false; + } + + if(!latched && gateEnv > 0) + { + gateDecayClock += 1; + + if(gateDecayClock >= 6) + { + gateEnv -= 1; + gateDecayClock = 0; + } + } + else + { + gateDecayClock = 0; + } + + if(hitEnv > 0) + { + hitEnv -= hitDecay; + } + + if(gateEnv < 0) + { + gateEnv = 0; + } + + if(hitEnv < 0) + { + hitEnv = 0; + } + + const bool wheelActive = wheelTimer > 0; + const int32_t wheelEnv = wheelActive ? clampInt(wheelTimer >> 2, 0, 4095) : 0; + + if(wheelTimer > 0) + { + wheelTimer -= 1; + } + + const int32_t lfoRate = 120 + ((xPlay * xPlay) >> 8); + const int32_t wobbleRate = 55 + (yPlay >> 5); + const int32_t wheelRate = 90000000 + (xPlay << 14); + lfoPhase += (uint32_t)lfoRate; + wobblePhase += (uint32_t)wobbleRate; + wheelPhase += (uint32_t)wheelRate; + + const int32_t lfo = triangle(lfoPhase); + const int32_t wobble = triangle(wobblePhase); + const int32_t wheelTrill = triangle(wheelPhase); + const int32_t bend = wheelActive ? wheelTrill : (switchPosition == Switch::Up ? lfo : -lfo); + const int32_t baseStep = 2500000 + ((mainControl * mainControl) << 2); + const int32_t wheelDrop = wheelActive ? ((kWheelLength - wheelTimer) << 10) : 0; + const int32_t sweepStep = wheelActive ? ((xPlay + 2048) * bend) << 3 : (xPlay * bend) << 2; + const int32_t cvStep = cvPitch << 12; + const uint32_t phaseStep = (uint32_t)clampInt(baseStep + sweepStep + cvStep + wheelDrop, 250000, 140000000); + oscPhase += phaseStep; + + const int32_t tri = triangle(oscPhase); + const int32_t square = (oscPhase & 0x80000000u) ? 2047 : -2048; + const int32_t shape = clampInt(1900 + (cvColor >> 2), 800, 3300); + int32_t tone = ((tri * (4095 - shape)) + (square * shape)) >> 12; + + noise = (noise * 1664525u) + 1013904223u; + tone += (((int32_t)(noise >> 21) - 1024) * (hitEnv >> 7)) >> 6; + + const int32_t drive = 4300 + ((cvColor + 2048) >> 3); + tone = softClip((tone * drive) >> 12); + + const int32_t gatedAmp = gateEnv > hitEnv ? gateEnv : hitEnv; + const int32_t amp = wheelActive ? wheelEnv : (switchPosition == Switch::Middle ? gatedAmp : 4095); + tone = (tone * amp) >> 12; + tone = (tone + lastTone) >> 1; + lastTone = tone; + + const int32_t delayBase = 760 + ((yPlay * yPlay) >> 10); + const int32_t delayMod = (wobble * (768 + (yPlay >> 3))) >> 12; + const int32_t delaySamples = clampInt(delayBase + delayMod, 420, kEchoSize - 2); + const int32_t readIndex = (echoWrite - delaySamples) & (kEchoSize - 1); + const int32_t delayed = echo[readIndex]; + const int32_t darken = 2 + (yPlay >> 11); + delayFilter += (delayed - delayFilter) >> darken; + smearFilter += (delayFilter - smearFilter) >> 3; + + noise = (noise * 1664525u) + 1013904223u; + const int32_t clockHash = ((int32_t)(noise >> 22) - 512) * (yPlay >> 9); + const int32_t clockDirt = switchPosition == Switch::Middle ? ((clockHash * amp) >> 12) : clockHash; + const int32_t repeats = wheelActive ? 3000 + (yPlay >> 3) : 820 + ((yPlay * 7) >> 3); + int32_t echoIn = tone + ((smearFilter * repeats) >> 12) + (clockDirt >> 5); + echoIn = softClip(echoIn); + + const int32_t bitLoss = yPlay >> 11; + echoIn = (echoIn >> bitLoss) << bitLoss; + + echo[echoWrite] = (int16_t)echoIn; + echoWrite = (echoWrite + 1) & (kEchoSize - 1); + + const int32_t wet = wheelActive ? clampInt(1800 + (yPlay >> 1), 0, 3600) : clampInt(800 + ((yPlay * 3) >> 2), 0, 3400); + const int32_t mixed = softClip(((tone * (4095 - wet)) + (smearFilter * wet)) >> 12); + const int32_t out2 = softClip(tone + ((delayFilter * (1400 + (yPlay >> 2))) >> 12)); + + AudioOut1(mixed); + AudioOut2(out2); + CVOut1(clamp12(lfo)); + CVOut2(clamp12((gateEnv - 2048) + (hitEnv >> 2))); + + pulseTimer1 = gateEnv > 160 ? kPulseLength : pulseTimer1 - 1; + pulseTimer2 = hitEnv > 120 ? kPulseLength : pulseTimer2 - 1; + PulseOut1(pulseTimer1 > 0); + PulseOut2(pulseTimer2 > 0); + + LedBrightness(0, amp); + LedBrightness(1, mainControl); + LedBrightness(2, xKnob); + LedBrightness(3, yKnob); + LedBrightness(4, clampInt(lfo + 2048, 0, 4095)); + LedBrightness(5, wheelActive ? wheelEnv : ((hitEnv > 0) ? hitEnv : clampInt(smearFilter + 2048, 0, 4095))); + } + +private: + static int32_t clampInt(int32_t value, int32_t lo, int32_t hi) + { + if(value < lo) + { + return lo; + } + + if(value > hi) + { + return hi; + } + + return value; + } + + static int32_t clamp12(int32_t value) + { + return clampInt(value, kMinAudio, kMaxAudio); + } + + static int32_t tameUpperTravel(int32_t value) + { + if(value <= 3072) + { + return value; + } + + return 3072 + ((value - 3072) >> 2); + } + + static int32_t triangle(uint32_t phase) + { + int32_t value = (int32_t)((phase >> 20) & 4095); + + if(value >= 2048) + { + value = 4095 - value; + } + + return (value << 1) - 2048; + } + + static int32_t softClip(int32_t value) + { + if(value > 2300) + { + return kMaxAudio; + } + + if(value < -2300) + { + return kMinAudio; + } + + return clamp12(value); + } +}; + +DubWarning card; + +int main() +{ + set_sys_clock_khz(144000, true); + card.EnableNormalisationProbe(); + card.Run(); +} diff --git a/releases/999_Dub_Warning/pico_sdk_import.cmake b/releases/999_Dub_Warning/pico_sdk_import.cmake new file mode 100644 index 000000000..a0721d0d1 --- /dev/null +++ b/releases/999_Dub_Warning/pico_sdk_import.cmake @@ -0,0 +1,84 @@ +# 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() + +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 () + # GIT_SUBMODULES_RECURSE was added in 3.17 + if (${CMAKE_VERSION} VERSION_GREATER_EQUAL "3.17.0") + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + GIT_SUBMODULES_RECURSE FALSE + ) + else () + FetchContent_Declare( + pico_sdk + GIT_REPOSITORY https://github.com/raspberrypi/pico-sdk + GIT_TAG ${PICO_SDK_FETCH_FROM_GIT_TAG} + ) + endif () + + if (NOT pico_sdk) + message("Downloading Raspberry Pi Pico SDK") + FetchContent_Populate(pico_sdk) + 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/999_Dub_Warning/test_uf2/DubWarning_PT2399_TEST.uf2 b/releases/999_Dub_Warning/test_uf2/DubWarning_PT2399_TEST.uf2 new file mode 100644 index 0000000000000000000000000000000000000000..8b4536abfbb911bfb66ad17e891ab497395a70de GIT binary patch literal 39936 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 Date: Sat, 22 Aug 2026 15:23:26 +0100 Subject: [PATCH 2/2] Delete DubWarning_PT2399_TEST.uf2 --- .../test_uf2/DubWarning_PT2399_TEST.uf2 | Bin 39936 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 releases/999_Dub_Warning/test_uf2/DubWarning_PT2399_TEST.uf2 diff --git a/releases/999_Dub_Warning/test_uf2/DubWarning_PT2399_TEST.uf2 b/releases/999_Dub_Warning/test_uf2/DubWarning_PT2399_TEST.uf2 deleted file mode 100644 index 8b4536abfbb911bfb66ad17e891ab497395a70de..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 39936 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