diff --git a/releases/999_Dub_Warning/.gitignore b/releases/999_Dub_Warning/.gitignore new file mode 100644 index 00000000..6124a32d --- /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 00000000..916ba623 --- /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 00000000..1da6de33 --- /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 00000000..ada66941 --- /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 00000000..a2e22540 --- /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 00000000..f83bbb64 --- /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 00000000..8b4536ab Binary files /dev/null and b/releases/999_Dub_Warning/UF2/DubWarning.uf2 differ diff --git a/releases/999_Dub_Warning/info.yaml b/releases/999_Dub_Warning/info.yaml new file mode 100644 index 00000000..db0edcfb --- /dev/null +++ b/releases/999_Dub_Warning/info.yaml @@ -0,0 +1,125 @@ +draft: false +Name: Dub Warning +short-description: Playable dub warning siren with PT2399-style delay. +summary: | + A small, immediate dub warning siren for the Workshop Computer. Main sets the base + pitch, X drives the sweep, CV In 2 colors the oscillator, and Y opens up the + PT2399-style delay. Use Z Down for a compact Wheel Up one-shot gesture, + Pulse In 1 for a gated siren, or Pulse In 2 for reset plucks and fast-trigger + textures. Audio In 2 can override the Main pitch knob when patched. +Language: C++ (Pico SDK) +Creator: Adrian Vos +Version: "0.1.0-beta" +Status: Beta - stable in single-developer testing +License: MIT +date-created: 2026-08-21 +date-updated: 2026-08-22 +contact: {} + +tags: + - synthesizer + - effect + - drone + - trigger + +uf2: + - path: UF2/DubWarning.uf2 + name: Dub Warning PT2399 Beta + +panel: + inputs: + - id: AudioIn1 + name: Pitch Mod + description: Audio-rate or CV pitch modulation mixed with CV In 1 + type: audio + - id: AudioIn2 + name: Main CV + description: Overrides the Main pitch knob when patched + type: audio + - id: CVIn1 + name: Pitch CV + description: Adds to the base siren pitch + type: cv + - id: CVIn2 + name: Color CV + description: Modulates siren wave color and drive + type: cv + - id: PulseIn1 + name: Fire + description: Rising edge fires the normal gated siren envelope + type: pulse + - id: PulseIn2 + name: Reset Pluck + description: Rising edge resets oscillator/LFO phase and fires a short pluck; fast triggers create useful textures + type: pulse + outputs: + - id: AudioOut1 + name: Warning Mix + description: Main warning siren output through the wet/dry effect mix + type: audio + - id: AudioOut2 + name: Direct + Echo + description: Alternate direct-plus-echo output + type: audio + - id: CVOut1 + name: LFO + description: Internal siren sweep LFO + type: cv + - id: CVOut2 + name: Envelope + description: Gate and hit envelope for patching other modules + type: cv + - id: PulseOut1 + name: Active Gate + description: High while the siren envelope or Wheel Up gesture is active + type: pulse + - id: PulseOut2 + name: Hit + description: Short pulse on trigger, reset, or Wheel Up + type: pulse + +controls: + switch: + up: + name: Continuous + description: Free-running siren + middle: + name: Gated + description: Triggered siren that fades after Pulse In 1 + down: + name: Wheel Up / Reset + description: Tap to fire the Wheel Up one-shot and reset phase + tap: + name: Wheel Up + description: Briefly tap Down to fire the Wheel Up gesture from the panel + knobs: + - main: + name: Pitch + description: Base siren pitch, defeated by Audio In 2 when patched + x: + name: Sweep + description: Sweep rate and pitch bend depth + y: + name: Effect + description: Raises delay time, modulation, repeats, dirt, and wet level + leds: + - display: list + items: + - id: LED0 + name: Gate + description: Brightness follows siren level + - id: LED1 + name: Pitch + description: Shows active Main pitch source, either the Main knob or Audio In 2 + - id: LED2 + name: Sweep + description: Shows X knob position + - id: LED3 + name: Effect + description: Shows Y knob position + - id: LED4 + name: LFO + description: Follows the internal sweep LFO + - id: LED5 + name: Hit / Echo + description: Flashes on trigger, then follows echo return level diff --git a/releases/999_Dub_Warning/main.cpp b/releases/999_Dub_Warning/main.cpp new file mode 100644 index 00000000..40f3071e --- /dev/null +++ b/releases/999_Dub_Warning/main.cpp @@ -0,0 +1,275 @@ +// Dub Warning +// A lightweight playable siren, PT2399-style echo, and modulation source for the +// Music Thing Modular Workshop Computer. + +#include "ComputerCard.h" +#include "hardware/clocks.h" + +#include + +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 00000000..a0721d0d --- /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})