Add the Edges pulse oscillators to Blocks #3

Merged
peterswimm merged 1 commit from claude/mutable-edges-feasibility-b6y1i0 into master 2026-09-03 21:55:52 +00:00
peterswimm commented 2026-08-19 23:09:51 +00:00 (Migrated from github.com)

Description

Blocks ports channel 4 of Mutable Instruments Edges — the sampled digital oscillator in digital_oscillator.cc — four times over. Channels 1 through 3 of the hardware are not DSP at all: they are XMEGA timers driving output pins (timer_oscillator.cc), which is why they were never part of the port. Their timbres are the rest of the module, so this renders them as six more shapes available on every channel, rather than as a module of its own with a new panel and manual.

  • The five duty cycles are the pulse_widths table from timer_oscillator.cc. The panel of the hardware labels them 50%, 66%, 75%, 87%, and 95%, each of which is the complement of the duty cycle the timer produces; a pulse and its complement differ only in sign, so they sound the same.
  • The sixth width is CV controlled, and takes the channel's frequency modulation input in place of its pitch, as Settings::dac_to_fm() does on the hardware. The width is centered on a square, so an unpatched port renders one and the attenuverter sets the depth and direction of the sweep.
  • The shapes are appended to DigitalOscillator::Shape, so a patch that stores a shape index still loads.
  • A timer toggling a pin places its edges between samples and aliases none of its own, so comparing a sampled phase against the width would be the unfaithful rendering, not the naive one. The edges are corrected with PolyBLEP instead.

Both front-ends are covered: the Rack module and the disting NT algorithm, which gains the six enum strings and the same width routing.

Incidental fixes along the way, each called out in the changelog:

  • The shape indicator light now takes its color from a table. The three bit encoding it used (shape + 1 packed into R/G/B) only reached seven shapes.
  • dataFromJson() clamps the shape index a patch supplies, which now indexes the color and label tables.
  • A closed gate renders mid-scale, i.e., silence, as RenderSilence() does on the hardware, rather than the negative rail. Neither front-end closes the gate, so no patch changes.
  • dsp/mi_edges/wavetable.hpp includes <limits>, which it uses and had been getting from rack.hpp.

Not included, because both need panel controls that this deliberately avoids: the hard sync switch (channel 1's timer restarting channel 2's) and per-channel gate inputs. The sub-oscillator that follows channel 1 an octave down is already reachable through the normalled pitch chain.

No issue to close — this came out of a question about how much of Edges was still missing.

Type of change

  • New feature (non-breaking change which adds functionality)
  • Bug fix (non-breaking change which fixes an issue) — the latent gate, patch-index, and include fixes above

How Has This Been Tested?

  • New unit tests, test/dsp/mi_edges/test_wavetable.cpp, run under ASan and UBSan: the shape indexes of the digital oscillator are unchanged, cycleShape() visits all twelve, each fixed pulse renders the duty cycle of the hardware's table, the CV controlled width clamps to the bounds TimerOscillator::set_cv_pw() uses, every shape stays inside the output range across a sweep of pitches from 0Hz to 40kHz, and a closed gate is silent.
  • Band-limiting is measured, not assumed. The same suite renders each pulse against a band-limited reference built by additive synthesis and requires the error to be well below that of an uncorrected pulse. Measured off-branch across pitch: the renderer's RMS error is ~2.3x smaller than an uncorrected pulse and ~4.5x smaller than the same corrections applied with their signs reversed, so the test fails if an edge correction is ever inverted.
  • The disting NT host harness passes for every algorithm, make -C distingNT/test, which builds under ASan and UBSan and checks the parameter tables, the declared memory, and rendering from defaults, saturated inputs, and random parameters.
  • src/Blocks.cpp compiles clean against the Rack SDK with -Wall -Wextra.

Two notes on running this yourself. scons test needs -DCATCH_CONFIG_NO_POSIX_SIGNALS on a recent glibc, because the pinned Catch2 predates the MINSIGSTKSZ change; that affects the existing suites equally. And a full plugin build against a v2 SDK fails in src/SuperEcho.cpp on master as well, untouched by this branch.

Test Configuration

  • Operating System: Linux 6.18.5 (x86-64)
  • Python version: 3.13 (SCons only)
  • C++ compiler version: g++ 13, -std=c++11

Checklist

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation following the
    Doxygen style
  • I have tested that my fix is effective or that my feature works

The prose documentation moved too: the README feature list, the Blocks manual (the shape table gains its six rows, and the frequency modulation section explains the width routing), the disting NT algorithm table, and the changelog. The manual's moduleName said Infinite Stairs; it now says Blocks. The panel art is untouched, since no control moved.

### Description Blocks ports channel 4 of [Mutable Instruments Edges](https://github.com/pichenettes/eurorack/tree/master/edges) — the sampled digital oscillator in `digital_oscillator.cc` — four times over. Channels 1 through 3 of the hardware are not DSP at all: they are XMEGA timers driving output pins (`timer_oscillator.cc`), which is why they were never part of the port. Their timbres are the rest of the module, so this renders them as six more shapes available on every channel, rather than as a module of its own with a new panel and manual. - The five duty cycles are the `pulse_widths` table from `timer_oscillator.cc`. The panel of the hardware labels them _50%_, _66%_, _75%_, _87%_, and _95%_, each of which is the complement of the duty cycle the timer produces; a pulse and its complement differ only in sign, so they sound the same. - The sixth width is CV controlled, and takes the channel's frequency modulation input in place of its pitch, as `Settings::dac_to_fm()` does on the hardware. The width is centered on a square, so an unpatched port renders one and the attenuverter sets the depth and direction of the sweep. - The shapes are **appended** to `DigitalOscillator::Shape`, so a patch that stores a shape index still loads. - A timer toggling a pin places its edges between samples and aliases none of its own, so comparing a sampled phase against the width would be the unfaithful rendering, not the naive one. The edges are corrected with PolyBLEP instead. Both front-ends are covered: the Rack module and the disting NT algorithm, which gains the six enum strings and the same width routing. Incidental fixes along the way, each called out in the changelog: - The shape indicator light now takes its color from a table. The three bit encoding it used (`shape + 1` packed into R/G/B) only reached seven shapes. - `dataFromJson()` clamps the shape index a patch supplies, which now indexes the color and label tables. - A closed gate renders mid-scale, i.e., silence, as `RenderSilence()` does on the hardware, rather than the negative rail. Neither front-end closes the gate, so no patch changes. - `dsp/mi_edges/wavetable.hpp` includes `<limits>`, which it uses and had been getting from `rack.hpp`. Not included, because both need panel controls that this deliberately avoids: the hard sync switch (channel 1's timer restarting channel 2's) and per-channel gate inputs. The sub-oscillator that follows channel 1 an octave down is already reachable through the normalled pitch chain. No issue to close — this came out of a question about how much of Edges was still missing. ### Type of change - [x] New feature (non-breaking change which adds functionality) - [x] Bug fix (non-breaking change which fixes an issue) — the latent gate, patch-index, and include fixes above ### How Has This Been Tested? - [x] **New unit tests**, `test/dsp/mi_edges/test_wavetable.cpp`, run under ASan and UBSan: the shape indexes of the digital oscillator are unchanged, `cycleShape()` visits all twelve, each fixed pulse renders the duty cycle of the hardware's table, the CV controlled width clamps to the bounds `TimerOscillator::set_cv_pw()` uses, every shape stays inside the output range across a sweep of pitches from 0Hz to 40kHz, and a closed gate is silent. - [x] **Band-limiting is measured, not assumed.** The same suite renders each pulse against a band-limited reference built by additive synthesis and requires the error to be well below that of an uncorrected pulse. Measured off-branch across pitch: the renderer's RMS error is ~2.3x smaller than an uncorrected pulse and ~4.5x smaller than the same corrections applied with their signs reversed, so the test fails if an edge correction is ever inverted. - [x] **The disting NT host harness passes for every algorithm**, `make -C distingNT/test`, which builds under ASan and UBSan and checks the parameter tables, the declared memory, and rendering from defaults, saturated inputs, and random parameters. - [x] **`src/Blocks.cpp` compiles clean** against the Rack SDK with `-Wall -Wextra`. Two notes on running this yourself. `scons test` needs `-DCATCH_CONFIG_NO_POSIX_SIGNALS` on a recent glibc, because the pinned Catch2 predates the `MINSIGSTKSZ` change; that affects the existing suites equally. And a full plugin build against a v2 SDK fails in `src/SuperEcho.cpp` on `master` as well, untouched by this branch. ### Test Configuration - Operating System: Linux 6.18.5 (x86-64) - Python version: 3.13 (SCons only) - C++ compiler version: g++ 13, `-std=c++11` ### Checklist - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [x] I have made corresponding changes to the documentation following the [Doxygen](https://www.doxygen.nl/manual/docblocks.html) style - [x] I have tested that my fix is effective or that my feature works The prose documentation moved too: the README feature list, the Blocks manual (the shape table gains its six rows, and the frequency modulation section explains the width routing), the disting NT algorithm table, and the changelog. The manual's `moduleName` said _Infinite Stairs_; it now says _Blocks_. The panel art is untouched, since no control moved.
peterswimm changed title from WIP: Add the Edges pulse oscillators to Blocks to Add the Edges pulse oscillators to Blocks 2026-09-03 21:55:48 +00:00
peterswimm deleted branch claude/mutable-edges-feasibility-b6y1i0 2026-09-03 21:55:53 +00:00
Sign in to join this conversation.
No description provided.