LameJuis is an esoteric, layered sequencer that turns the six gate bits from the Theory of Time into polyphonic pitch. The implementation is in private/src/LameJuis.hpp, private/src/HarmonicSheaf.hpp, and private/src/IndexArp.hpp; the Nonagon wires the six time-loop gates into LameJuis and uses one LameJuis lane per trio (three voices share one lane’s pitch logic).
For fixed accepted configuration, the six gate bits x in I⁶ determine the set of available notes. The selected note also depends on the index arp’s choice argument, derived from the loop-cycle position. Modulating the Theory of Time therefore moves both the gate bits and the arp through this polyphonic process. Live loop-rhythm edits are held until the edited loop’s next tick.
Let M : I⁶ → pitch. Here “pitch” is represented as volt-per-octave (or equivalently log₂ of a just-intonation ratio). Composing M with the Theory of Time would give a single melody; we want many interlocking melodies, so we introduce a lens and a sheaf.
HarmonicSheaf::Lens, extending HarmonicSheaf::BitVector).
Lane::CoMuteState::GetLens() sets lens.Set(i, !m_coMutes[i])).Lens::Equivalent(a,b) is (a.m_bits ^ b.m_bits) & m_bits == 0.| Harmonic Sheaf — Define **F^M_x(U) = { M(y) | y ~_U x }. So at time **x, for a given lens U, we take all time slices equivalent to x under U and collect their M-values. This set is chosen statelessly from x; if x jumps (e.g. from time modulation), the set changes accordingly. |
There are 9 voices in 3 trios of 3 voices each. Each trio is assigned one LameJuis lane (there are 3 lanes, one per trio). The performer assigns a lens U to that lane via the co-mute UI: which of the 6 dimensions are “read” vs “co-mute”. At each time x in I⁶, the trio must pick a note from F^M_x(U). That choice is made by the index arp (see below) and a section choice strategy.
The index arp (IndexArp, used per voice inside NonagonIndexArp) turns the signed loop-cycle position of a chosen clock loop into a choice value that is then used to pick a note from F^M_x(U).
AnyTick(clockLoop) reports a whole-cycle crossing in the microblock, the Nonagon sets m_clockPosition from GetLoopCyclePosition(clockLoop, 0, resetLoop).This per-voice arp rhythm is separate from the per-loop Theory of Time rhythm that provides each LameJuis input bit. A loop tick clocks the arp even if that loop’s gate value repeats; editing the loop rhythm does not create an extra tick.
m_rhythm[0..m_rhythmLength-1] with m_rhythmLength default 8 (IndexArp::x_rhythmLength). Only some steps are “on”; the rest gate the voice off.PhaseUtils::FloorMod(m_clockPosition, m_rhythmLength) — the bounded slot in the rhythm.PhaseUtils::FloorDiv(m_clockPosition, m_rhythmLength) — the signed “page” or cycle through the rhythm.m_rhythm[m_rhythmSlotIndex] is true and the clock has just advanced (we’re in the m_clock / m_triggered path). Then we compute m_noteIndex: the bounded ordinal among the enabled steps, i.e. how many enabled rhythm steps have been passed up to and including the current slot.GetChoiceValue(m_noteIndex, m_motivePosition) =
m_offset + m_noteIndex * m_interval + m_motivePosition * m_pageInterval,
then optionally wrapped (cycle) or inverted, then scaled from [0,1] to [m_min, m_max].The note index and motive position together determine a single choice value. That value is passed to LameJuis as m_choiceValue and interpreted by the chosen strategy (e.g. percentile or closest-mod-octave).
AnyChangeInMicroBlock() causes the Nonagon to refresh arp inputs and run the arp and LameJuis. The selected clock’s AnyTick drives clock updates; no clock selection sets the clock position to zero. Read updates follow crossing dimensions selected by the lens. The clock and motive positions stay signed 64-bit values; bounded output mapping uses double until its final float result.Once we have the set F^M_x(U) (all M(y) for y ~_U x), we select a note from it using a section choice strategy (HarmonicSheaf::SectionChoiceStrategy) with the index-arp output as m_choiceValue.
Each lane has a strategy (toggled in the UI) and an optional base strategy (defaults to None). The Lane::Chooser first runs the base strategy to get a base section value, then adds that to m_choiceValue and runs the main strategy. This two-stage approach allows composing strategies.
The available strategies (HarmonicSheaf::SectionChooser):
m_choiceValue.m_choiceValue, with ±1 octave adjustment if that is closer. This is the default strategy.m_choiceValue as a percentile in [0, 1). The integer part of m_choiceValue is added as an octave offset.The result is a single pitch (volt-per-octave) per voice; that pitch is then used by the rest of the synth (e.g. V/O output, possible octave shift from the UI). Whether a trigger is emitted (note on) for that pitch is decided by the Multi-Phasor Gate (pitch-changed vs sub-trigger, mutes, interrupt).
M(x) is not a single ratio; it is computed by a matrix of logic operations feeding accumulators, whose outputs are combined additively in volt-per-octave (i.e. multiplicatively as ratios).
m_total[acc]) and how many of those are high (m_high[acc]). The pitch in volt-per-octave is
pitch = Σ_acc accumulators[acc].m_intervalValue * m_high[acc]Each LogicOperation (the “simple functions” in the user’s description) does the following:
m_active (which bits are used) and m_inverted (which of those are inverted). GetTotalAndHigh does inputVector &= m_active, inputVector ^= m_inverted, then counts countTotal = number of active bits and countHigh = number of 1s in the result.m_rhs[j] = (j % 2 == 1), so only odd counts pass. That is parity (Xor), i.e. a Walsh function. By changing the RHS table, the performer can select which counts (0..6) pass; these behave like generalized Walsh functions on the 6-bit input (with the given active/inverted mask).LameJuisRHSPage) is six operations by seven count columns. Toggling a cell still edits m_rhs[k]. A column flashes when count k is reachable in the active trio’s sheaf fiber: there exists an assignment of that trio’s co-muted bits which, paired with the current read (non-co-muted) bits, yields countHigh == k. Each co-muted active bit independently contributes 0 or 1, so the lit columns are the interval [base, base + f] where base is countHigh from the read ∩ active bits and f is the number of active ∩ co-muted bits. If nothing relevant is co-muted this degenerates to the single current count. Columns with k > countTotal stay dim (impossible from the matrix switches alone).An operation owns its six matrix elements, active and inverted masks, RHS table, and output target. Its m_countTotal counts accepted non-muted input bits. An accumulator owns an interval; each section’s m_total[acc] counts active operation rows targeting it. An empty row contributes to no accumulator, regardless of its stored target.
So M(x) is built from up to 6 active boolean functions; each contributes 0 or 1 to one of 3 accumulators; the accumulators have fixed intervals (octave, fifth, third, etc.); and the final pitch is the sum in V/O of (interval × exponent) per accumulator.
In addition to pitch, the logic matrix provides extra timbre modulators. For each of the 3 accumulators, the matrix computes the ratio of operations that evaluated to high versus the total number of active operations targeting that accumulator (m_high[acc] / m_total[acc]). An accumulator with no active rows yields zero. The Nonagon captures these three values in [0, 1] as each voice’s m_extraTimbre when that voice triggers and holds them until its next trigger. These can be routed to DSP parameters (like filter cutoff or wavefolder depth) to provide rhythmic modulation that is perfectly synchronized with the pitch sequence.
The Nonagon supplies each input’s gate value and a separate m_ticked flag from AnyTick(i). A modulated loop crossing counts even when consecutive rhythm steps have the same gate value.
A channel selects a new section only on its existing read flag or arp trigger. Section equality compares both m_high and m_total for every accumulator; the selected result also compares evaluated pitch. Thus a denominator-only change can request another note at the same pitch, on the next permitted channel update. This includes zero-high sections such as 0/4 and 0/3, even though both timbre ratios are zero. Cache rebuilding and equality checks do not add reads or move them off the rhythmic grid. The existing trigger, mute, and interrupt controls still determine whether a requested note starts.
Because:
the pitch-selection mapping is deterministic for a fixed accepted configuration and choice argument. Pending edits, accepted configuration, and held channel selections are stateful as described above. Live Theory of Time rhythm edits take effect only at that loop’s next modulated tick, so the output gate is explicitly held between ticks. Modulating the Theory of Time (e.g. phase modulation, different clock/reset, or different topology) only changes x and the index over time; the logic remains consistent.