diff --git a/example-colorlight-i5.py b/example-colorlight-i5.py index 990a5b9..9899502 100755 --- a/example-colorlight-i5.py +++ b/example-colorlight-i5.py @@ -24,6 +24,7 @@ from rtl.dsp_wrapper import * from rtl.spi_dma import Wishbone2SPIDMA from rtl.dma_router import * +from rtl.dsp import create_voices _io_eurolut_proto1 = [ ("eurorack_pmod_clk0", 0, @@ -122,21 +123,7 @@ def into_shifter(soc, eurorack_pmod): N_VOICES = 4 - for voice in range(N_VOICES): - pitch_shift = PitchShift(soc.platform) - lpf = KarlsenLowPass(soc.platform) - dc_block = DcBlock(soc.platform) - - soc.comb += [ - pitch_shift.sample_in.eq(eurorack_pmod.cal_in0), - lpf.sample_in.eq(pitch_shift.sample_out), - dc_block.sample_in.eq(lpf.sample_out), - getattr(eurorack_pmod, f"cal_out{voice}").eq(dc_block.sample_out), - ] - - soc.add_module(f"pitch_shift{voice}", pitch_shift) - soc.add_module(f"karlsen_lpf{voice}", lpf) - soc.add_module(f"dc_block{voice}", dc_block) + create_voices(soc, eurorack_pmod, N_VOICES) add_dma_router(soc, eurorack_pmod, output_capable=False) diff --git a/firmware/polyboot/Cargo.lock b/firmware/polyboot/Cargo.lock index 6ec9910..5704c6c 100644 --- a/firmware/polyboot/Cargo.lock +++ b/firmware/polyboot/Cargo.lock @@ -73,6 +73,12 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.5.0" @@ -120,6 +126,12 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "cty" version = "0.2.2" @@ -207,6 +219,18 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fixed" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c69ce7e7c0f17aa18fdd9d0de39727adb9c6281f2ad12f57cbe54ae6e76e7d" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -222,6 +246,16 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -437,6 +471,7 @@ dependencies = [ name = "polyvec-hal" version = "0.1.0" dependencies = [ + "fixed", "heapless", "litex-hal", "litex-pac", @@ -761,6 +796,12 @@ dependencies = [ "glob", ] +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + [[package]] name = "ufmt" version = "0.2.0" diff --git a/firmware/polyvec-hal/Cargo.toml b/firmware/polyvec-hal/Cargo.toml index cc506a6..b3668be 100644 --- a/firmware/polyvec-hal/Cargo.toml +++ b/firmware/polyvec-hal/Cargo.toml @@ -17,6 +17,7 @@ paste = "1.0.14" riscv = { version = "0.10.1", features = ["critical-section-single-hart"] } riscv-rt = { path = "../deps/riscv-rt", features = ["single-hart"] } vexriscv = "0.0.3" +fixed = "1.24.0" [profile.release] lto = true diff --git a/firmware/polyvec-hal/src/gw.rs b/firmware/polyvec-hal/src/gw.rs index 68e5895..ab00b93 100644 --- a/firmware/polyvec-hal/src/gw.rs +++ b/firmware/polyvec-hal/src/gw.rs @@ -1,5 +1,7 @@ #![allow(unused_macros)] +use fixed::{FixedI32, types::extra::U16}; + use litex_hal::prelude::*; use litex_pac as pac; use litex_hal::uart::UartError; @@ -25,14 +27,14 @@ pub trait WavetableOscillator { } pub trait PitchShift { - fn set_pitch(&self, value: i16); - fn pitch(&self) -> i16; + fn set_pitch(&self, value: FixedI32); + fn pitch(&self) -> FixedI32; } pub trait KarlsenLpf { - fn set_cutoff(&self, value: i16); - fn cutoff(&self) -> i16; - fn set_resonance(&self, value: i16); + fn set_cutoff(&self, value: FixedI32); + fn cutoff(&self) -> FixedI32; + fn set_resonance(&self, value: FixedI32); } macro_rules! eurorack_pmod_reset { @@ -119,28 +121,16 @@ macro_rules! eurorack_pmod { } -macro_rules! wavetable_oscillator { - ($($t:ty),+ $(,)?) => { - $(impl WavetableOscillator for $t { - fn set_skip(&self, value: u32) { - unsafe { - self.csr_wavetable_inc().write(|w| w.csr_wavetable_inc().bits(value)); - } - } - })+ - }; -} - macro_rules! pitch_shift { ($($t:ty),+ $(,)?) => { $(impl PitchShift for $t { - fn set_pitch(&self, value: i16) { + fn set_pitch(&self, value: FixedI32) { unsafe { - self.csr_pitch().write(|w| w.csr_pitch().bits(value as u16)); + self.csr_pitch().write(|w| w.csr_pitch().bits(value.to_bits() as u32)); } } - fn pitch(&self) -> i16 { - (self.csr_pitch().read().bits() as u16) as i16 + fn pitch(&self) -> FixedI32 { + FixedI32::::from_bits(self.csr_pitch().read().bits() as i32) } })+ }; @@ -149,17 +139,17 @@ macro_rules! pitch_shift { macro_rules! karlsen_lpf { ($($t:ty),+ $(,)?) => { $(impl KarlsenLpf for $t { - fn set_cutoff(&self, value: i16) { + fn set_cutoff(&self, value: FixedI32) { unsafe { - self.csr_g().write(|w| w.csr_g().bits(value as u16)); + self.csr_g().write(|w| w.csr_g().bits(value.to_bits() as u32)); } } - fn cutoff(&self) -> i16 { - (self.csr_g().read().bits() as u16) as i16 + fn cutoff(&self) -> FixedI32 { + FixedI32::::from_bits(self.csr_g().read().bits() as i32) } - fn set_resonance(&self, value: i16) { + fn set_resonance(&self, value: FixedI32) { unsafe { - self.csr_resonance().write(|w| w.csr_resonance().bits(value as u16)); + self.csr_resonance().write(|w| w.csr_resonance().bits(value.to_bits() as u32)); } } })+ diff --git a/firmware/polyvec/Cargo.lock b/firmware/polyvec/Cargo.lock index 3f40be5..8d11dbc 100644 --- a/firmware/polyvec/Cargo.lock +++ b/firmware/polyvec/Cargo.lock @@ -53,18 +53,36 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bytemuck" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "374d28ec25809ee0e23827c2ab573d729e293f281dfe393500e7ad618baa61c6" + [[package]] name = "byteorder" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + [[package]] name = "critical-section" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "defmt" version = "0.3.5" @@ -146,6 +164,18 @@ dependencies = [ "nb 1.1.0", ] +[[package]] +name = "fixed" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c69ce7e7c0f17aa18fdd9d0de39727adb9c6281f2ad12f57cbe54ae6e76e7d" +dependencies = [ + "az", + "bytemuck", + "half", + "typenum", +] + [[package]] name = "float-cmp" version = "0.9.0" @@ -165,6 +195,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "half" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc52e53916c08643f1b56ec082790d1e86a32e58dc5268f897f313fbae7b4872" +dependencies = [ + "cfg-if", + "crunchy", +] + [[package]] name = "hash32" version = "0.2.1" @@ -312,6 +352,7 @@ dependencies = [ "critical-section", "embedded-graphics", "embedded-midi", + "fixed", "heapless", "irq", "litex-hal", @@ -335,6 +376,7 @@ dependencies = [ name = "polyvec-hal" version = "0.1.0" dependencies = [ + "fixed", "heapless", "litex-hal", "litex-pac", diff --git a/firmware/polyvec/Cargo.toml b/firmware/polyvec/Cargo.toml index fe04ada..5c4470a 100644 --- a/firmware/polyvec/Cargo.toml +++ b/firmware/polyvec/Cargo.toml @@ -31,6 +31,7 @@ litex-interrupt = { path = "../deps/litex-interrupt" } critical-section = "1.1.2" strum_macros = "0.25.3" strum = {version = "0.25.0", features = ["derive"], default-features=false} +fixed = "1.24.0" [profile.release] lto = true diff --git a/firmware/polyvec/src/main.rs b/firmware/polyvec/src/main.rs index 4060975..bef902e 100644 --- a/firmware/polyvec/src/main.rs +++ b/firmware/polyvec/src/main.rs @@ -17,6 +17,7 @@ use core::cell::RefCell; use critical_section::Mutex; use irq::{handler, scope, scoped_interrupts}; use litex_interrupt::return_as_is; +use fixed::{FixedI32, types::extra::U16}; use ssd1322 as oled; @@ -252,6 +253,7 @@ impl State { } } + /* if opts.touch.note_control.value == opt::NoteControl::Midi { while let Ok(event) = self.midi_in.read() { self.voice_manager.event(event, uptime_ms); @@ -264,6 +266,7 @@ impl State { lpf[n_voice].set_resonance(opts.adsr.resonance.value); } } else { + */ let pmod1 = &peripherals.EURORACK_PMOD1; let pmod2 = &peripherals.EURORACK_PMOD2; let pmod3 = &peripherals.EURORACK_PMOD3; @@ -298,13 +301,14 @@ impl State { let mut update_hw_voice = |n_voice: usize, midi_note: u8, touch_raw: u8| { let ampl = (touch_raw as f32) / 256.0f32; let pitch = note_to_pitch(midi_note); - shifter[n_voice].set_pitch(pitch); + //shifter[n_voice].set_pitch(pitch); + shifter[n_voice].set_pitch(FixedI32::::from_num(0.0f32)); // Low-pass filter to smooth touch on/off - let ampl_old = (lpf[n_voice].cutoff() as f32) / 8000f32; + let ampl_old: f32 = lpf[n_voice].cutoff().to_num(); let ampl_new = ampl*0.05 + ampl_old*0.95; - lpf[n_voice].set_cutoff((ampl_new * 8000f32) as i16); - lpf[n_voice].set_resonance(opts.adsr.resonance.value); + lpf[n_voice].set_cutoff(FixedI32::::from_num(ampl_new)); + lpf[n_voice].set_resonance(FixedI32::::from_num(0)); // Push to voice manager to visualizations work self.voice_manager.voices[n_voice].amplitude = ampl_new; @@ -349,7 +353,9 @@ impl State { } } + /* } + */ self.last_control_type = Some(opts.touch.note_control.value); } diff --git a/rtl/dsp.py b/rtl/dsp.py index 517ba01..42ce3c7 100644 --- a/rtl/dsp.py +++ b/rtl/dsp.py @@ -6,6 +6,7 @@ from litex.gen.fhdl import verilog from litex.gen.sim import * from litex.soc.interconnect.stream import * +from litex.soc.interconnect.csr import * from functools import reduce @@ -427,11 +428,11 @@ def __init__(self, n_shifters=2, max_delay=512, xfade=64, sw=16, dw=32): self.comb += [shifter.sample_strobe.eq(self.sink.valid) for shifter in self.shifters] class PitchShifterDecorator(Module, AutoCSR): - def __init__(self, shifter, dw=32, ww=16): + def __init__(self, shifter, dw=32, ww=16, default_window_size=256): # Create some CSRs and link them to the provided (sub) shifter. - self.csr_pitch = CSRStorage(dw) - self.csr_window_sz = CSRStorage(ww) + self.csr_pitch = CSRStorage(dw, reset=Constant(float_to_fp(0.5), (32, True))) + self.csr_window_sz = CSRStorage(ww, reset=default_window_size) self.comb += [ shifter.pitch.eq(self.csr_pitch.storage), shifter.window_sz.eq(self.csr_window_sz.storage), @@ -440,25 +441,22 @@ def __init__(self, shifter, dw=32, ww=16): class MultiDcBlockedLpf(Module): def __init__(self, n_lpfs=2): - self.sources = [] - self.sinks = [] - self.dc_blocks = [] self.lpfs = [] + self.dc_blocks = [] self.submodules.rrmac = RRMux(n=n_lpfs*2, inner=FixMac()) for ix in range(n_lpfs): - self.dc_blocks.append(DcBlock(mac=self.rrmac)) self.lpfs.append(LadderLpf(mac=self.rrmac)) - setattr(self.submodules, 'dc'+str(ix), self.dc_blocks[-1]) + self.dc_blocks.append(DcBlock(mac=self.rrmac)) setattr(self.submodules, 'lpf'+str(ix), self.lpfs[-1]) - + setattr(self.submodules, 'dc'+str(ix), self.dc_blocks[-1]) # Route the LPF --> the DC block. - self.comb += self.lpfs[-1].connect(self.dc_blocks[-1].sink) + self.comb += self.lpfs[-1].source.connect(self.dc_blocks[-1].sink) class LpfDecorator(Module, AutoCSR): - def __init__(self, lpf, dw=32, ww=16): - self.csr_g = CSRStorage((dw, True)) - self.csr_resonance = CSRStorage((dw, True)) + def __init__(self, lpf, dw=32): + self.csr_g = CSRStorage(dw, reset=Constant(float_to_fp(1.0), (32, True))) + self.csr_resonance = CSRStorage(dw, reset=Constant(float_to_fp(0.0), (32, True))) self.comb += [ lpf.g.eq(self.csr_g.storage), lpf.resonance.eq(self.csr_resonance.storage), @@ -470,56 +468,52 @@ def create_voices(soc, eurorack_pmod, n_voices=4): multi_shift = MultiPitchShifter(n_shifters=n_voices) multi_lpf = MultiDcBlockedLpf(n_lpfs=n_voices) + soc.comb += [multi_shift.sources[n].connect(multi_lpf.lpfs[n].sink) for n in range(n_voices)] + soc.add_module("multi_shift0", multi_shift); soc.add_module("multi_lpf0", multi_lpf); for n in range(n_voices): shifter_csr = PitchShifterDecorator(multi_shift.shifters[n]) - soc.add_module(shifter_csr, f'pitch_shift{n}') + soc.add_module(f'pitch_shift{n}', shifter_csr) lpf_csr = LpfDecorator(multi_lpf.lpfs[n]) - soc.add_module(lpf_csr, f'karlsen_lpf{n}') + soc.add_module(f'karlsen_lpf{n}', lpf_csr) # CDC: PMOD -> shifter delayline write - cdc_in0 = ClockDomainCrossing( - layout=[("sample_in", 16)], + cdc_vin0 = ClockDomainCrossing( + layout=[("sample", 16)], cd_from="clk_fs", cd_to="sys" ) - soc.add_module("cdc_voice_in0", cdc_in0) + soc.add_module("cdc_voice_in0", cdc_vin0) soc.comb += [ - cdc_in0.sink.valid.eq(1), - cdc_in0.sink.sample_in.eq(eurorack_pmod.cal_in0), - cdc_in0.source.connect(multi_shift.sink), + cdc_vin0.sink.valid.eq(1), + cdc_vin0.sink.sample.eq(eurorack_pmod.cal_in0), + cdc_vin0.source.connect(multi_shift.sink), ] # CDC: DcBlock out -> PMOD channels - cdc_out0 = ClockDomainCrossing( - layout=[("out0", 16), - ("out1", 16), - ("out2", 16), - ("out3", 16)], + cdc_vout0 = ClockDomainCrossing( + layout=[(f"out{n}", 16) for n in range(n_voices)], cd_from="sys", cd_to="clk_fs" ) - soc.add_module("cdc_out0", cdc_out0) + soc.add_module("cdc_vout0", cdc_vout0) outputs_valids = [b.source.valid for b in multi_lpf.dc_blocks] - soc.comb += cdc_out0.sink.valid.eq(reduce(lambda x, y: x & y, outputs_valids)) + soc.comb += cdc_vout0.sink.valid.eq(reduce(lambda x, y: x & y, outputs_valids)) # Route DC block outputs to CDC entry for n in range(n_voices): - soc.comb += cdc_out0.sink.out0.eq(multi_lpf.dc_blocks[n].source) + soc.comb += getattr(cdc_vout0.sink.payload, f"out{n}").eq(multi_lpf.dc_blocks[n].source.sample) + soc.comb += multi_lpf.dc_blocks[n].source.ready.eq(cdc_vout0.sink.ready) # Route CDC exit to eurorack-pmod - soc.comb += [ - cdc_out0.source.ready.eq(1), - eurorack_pmod.cal_out0.eq(cdc_out0.source.out0), - eurorack_pmod.cal_out1.eq(cdc_out0.source.out1), - eurorack_pmod.cal_out2.eq(cdc_out0.source.out2), - eurorack_pmod.cal_out3.eq(cdc_out0.source.out3), - ] + soc.comb += cdc_vout0.source.ready.eq(1), + for n in range(n_voices): + soc.comb += getattr(eurorack_pmod, f"cal_out{n}").eq(getattr(cdc_vout0.source, f"out{n}")) class TestDSP(unittest.TestCase): def test_fixmac(self):