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).
The design is stateless in time: at each moment x in I⁶ (the six gate bits), the set of available notes and the choice of note are pure functions of x. Modulating the Theory of Time (e.g. phase modulation) therefore modulates this polyphonic process without breaking it.
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 monodromy (state-change count) of a chosen clock loop into a point in a range that is then used to pick a note from F^M_x(U).
m_gateChanged), the Nonagon setsm_arpInput.m_totalIndex[trio] = TheoryOfTime::MonodromyNumber(clockSelect[trio], resetSelect[trio]).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.m_totalIndex % m_rhythmLength — position on the rhythm loop.m_totalIndex / m_rhythmLength — which “page” or cycle through the rhythm.m_rhythm[m_rhythmIndex] is true and the clock has just advanced (we’re in the m_clock / m_triggered path). Then we compute m_index: the physical index among the on steps (0 to NumNotes()-1), i.e. how many rhythm steps that are on have been passed up to and including the current step.GetOutput(m_index, m_motiveIndex) =m_offset + m_index * m_interval + m_motiveIndex * m_pageInterval,So the index (physical step among on steps) and motive index (rhythm page) together determine a single float in a range. That float is passed to LameJuis as m_choiceArg and interpreted by the chosen strategy (e.g. percentile or closest-mod-octave).
SetIndexArpInputs then m_indexArp.Process. For each trio, m_totalIndex is set to the monodromy when the selected clock loop’s gate has just changed (m_gateChanged), or to 0 if no clock is selected; m_clocks[i] is set to m_gateChanged for loop i; and m_read is set for voices whose lens reads a dimension that just ticked. When there is no Theory of Time change, the arp and LameJuis are not run that frame.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_choiceArg.
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_choiceArg and runs the main strategy. This two-stage approach allows composing strategies.
The available strategies (HarmonicSheaf::SectionChooser):
m_choiceArg.m_choiceArg, with ±1 octave adjustment if that is closer. This is the default strategy.m_choiceArg as a percentile in [0, 1). The integer part of m_choiceArg 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 isEach 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).So M(x) is built from 6 such 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 operations targeting that accumulator (m_high[acc] / m_total[acc]). This yields 3 discontinuous values in [0, 1] per time slice, which the Nonagon exposes as m_extraTimbre (after slewing). 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.
Because:
the whole polyphonic note-generation process is a pure function of time. 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.