Nallely-MIDI Clock System and BernoulliTrigger Architecture: A Deep Dive
The Clock system and BernoulliTrigger are specialized VirtualDevice subclasses implemented in nallely/clocks.py that provide deterministic tempo generation and probabilistic gating for modular MIDI-to-CV patches.
Both modules inherit from the event-driven VirtualDevice base class defined in nallely/core.py, enabling them to integrate seamlessly into the Nallely-MIDI virtual environment. The Clock manages continuous-time phase accumulators for rhythmic subdivisions, while the BernoulliTrigger implements stochastic gate generation with optional probability quantization.
Core Architecture Overview
All timing and probabilistic devices in the repository extend the VirtualDevice abstraction. This base class provides the @on decorator infrastructure, parameter CV (Control Voltage) handling, and the main execution loop that drives real-time processing. In nallely/clocks.py, the Clock and BernoulliTrigger classes leverage this foundation to offer precise musical timing and randomization capabilities respectively.
Clock System Implementation
Class Definition and Inheritance
The Clock class is defined at line 9 of nallely/clocks.py as a continuous-time virtual device. It inherits from VirtualDevice and generates clock pulses at configurable tempos ranging from 20 to 600 BPM.
from nallely.clocks import Clock
# Create a clock at 120 BPM with 5ms minimum tick resolution
clk = Clock(tick_min_ms=5, tempo_cv=120, play_cv=1)
Inputs and CV Parameters
The Clock exposes three primary control inputs between lines 14 and 19:
tempo_cv— Sets the BPM (default 120, range 20–600)play_cv— Transport control where 0 stops the clock and any positive value starts itreset_cv— Rising-edge trigger that synchronizes all phase accumulators instantly
Timing Engine and Phase Accumulators
The internal timing mechanism relies on Decimal-based phase accumulators to prevent floating-point drift during long runs. Lines 63 through 87 define:
phases— Per-subdivision phase accumulators tracking fractional progress through each note valueratios— Musical-time ratios relative to a quarter note (e.g.,mul2 = 2,div3 = 1/3)
The system maintains a maximum resolution of 1/32nd notes via smallest_subdivision = 32 (line 88). The _compute_target_cycle method (lines 104–110) calculates target_cycle_time from the current BPM to drive the scheduler, while tick_min_ms enforces a lower bound on the real-time loop interval.
Main Processing Loop
The main method (lines 122–155) implements the core tick generation algorithm:
- Converts BPM to quarter-note period (
quarter_note_s) - Determines tick length (
tick_s) respecting the minimum millisecond constraint - Advances each subdivision phase by
ratio * tick_s/quarter_note_swhen playing - Emits a high pulse (≤5ms width) on corresponding outputs when phases cross integer boundaries
- Schedules the next tick using
next_tick_timeto compensate for processing drift
This design ensures steady tempo maintenance even under variable system load.
Reset and Transport Control
The Clock implements hard synchronization via the reset handler at lines 177–181. A rising edge on reset_cv zeros all phases accumulators instantly, realigning all musical subdivisions to beat one.
The play property (lines 94–101) normalizes the transport state: any positive input maps to 1 (running), while zero or negative values map to 0 (stopped).
BernoulliTrigger Implementation
Class Definition and Output Configuration
The BernoulliTrigger class (lines 57–59) generates probabilistic gate events. Unlike continuous devices, it returns {"disable_output": True} in __post_init__ (lines 78–79) to suppress the base VirtualDevice default output, ensuring only explicit trigger pulses appear on outA_cv and outB_cv.
Probabilistic Inputs
The trigger accepts four control voltages:
trigger_cv— Rising-edge detector that initiates the probability testprobability_cv— Base probability threshold ranging from 0 to 1 (default 0.5)bias_cv— Additive bias value (0–1) that skews the probability calculationquantized_cv— Optional mode snapping probabilities to rational values like "1/4" or "1/8"
Quantization System
During initialization (__post_init__, lines 69–78), the class builds a quantize_scale lookup table containing the accepted rational values plus the extremes 0 and 1. When quantization is active (not "off"), the effective probability pquant becomes the nearest table value to the biased probability, enabling deterministic stepped randomness.
Trigger Processing Logic
The edge-detection and firing logic resides in lines 80–96. On a rising trigger_cv edge:
- Compute biased probability:
pbias = p + b·(1−p) - Optionally snap to nearest quantised value:
pquant - Draw uniform random number; if
< pquantfireoutA_cv, else fireoutB_cv
Both outputs produce short high-pulses lasting a single processing frame.
Practical Implementation Examples
Basic Clock Operation
from nallely.clocks import Clock
# Initialize and run the clock
clk = Clock(tick_min_ms=5, tempo_cv=120, play_cv=1)
for _ in clk.run(limit=10):
# Yields (value, [output_cv_objects]) for each tick
pass
The device emits pulses on lead_cv, div2_cv, div4_cv, mul2_cv, and other subdivision outputs according to the configured tempo.
Quantized Probability Trigger
from nallely.clocks import BernoulliTrigger
# 30% base probability with 0.2 bias, quantized to 1/4, 1/2, or 1
trigger = BernoulliTrigger(
probability_cv=0.3,
bias_cv=0.2,
quantized_cv="1/4",
)
# Simulate trigger events
for _ in range(20):
trigger.trigger_cv = 1 # rising edge
for value, outputs in trigger.run_one_step():
print(value, outputs[0].name)
With these settings, the effective probability snaps to the nearest value in [0, 0.25, 0.5, 1], creating deterministic rhythmic patterns from random sources.
Patching Clock to BernoulliTrigger
from nallely.clocks import Clock, BernoulliTrigger
from nallely.core import VirtualEnvironment
env = VirtualEnvironment()
# Create devices
clk = Clock(tempo_cv=100, play_cv=1)
rnd = BernoulliTrigger(probability_cv=0.4, bias_cv=0.1, quantized_cv="off")
# Route clock pulses to trigger input
env.connect(clk.lead_cv, rnd.trigger_cv)
env.add_devices(clk, rnd)
env.run(duration=5) # Run for 5 seconds
This composition converts steady tempo into probabilistic gate sequences, useful for generative drum patterns or randomized modulation sources.
Summary
- Both devices inherit from
VirtualDeviceinnallely/core.py, utilizing the@ondecorator system and CV parameter infrastructure for event-driven processing. - The Clock (
nallely/clocks.pylines 9–181) uses Decimal phase accumulators and a 1/32nd note resolution engine to generate stable tempo across nine musical subdivisions with drift compensation. - The BernoulliTrigger (
nallely/clocks.pylines 57–96) implements biased probability logic with optional rational quantization, firing gated outputs on rising trigger edges. - Reset and transport control in the Clock provide hard synchronization (rising-edge
reset_cv) and start/stop functionality (play_cvnormalization). - Quantization scales in the BernoulliTrigger enable stepped probability modes for deterministic pseudo-random sequences.
Frequently Asked Questions
How do the Clock and BernoulliTrigger relate to the base VirtualDevice class?
Both classes extend VirtualDevice from nallely/core.py, inheriting the event-loop infrastructure, CV parameter binding, and the run() method for real-time execution. The BernoulliTrigger additionally overrides __post_init__ to disable default continuous output, ensuring only explicit trigger pulses emit from outA_cv and outB_cv.
What is the purpose of the Decimal type in the Clock's phase accumulators?
The phases dictionary stores per-subdivision progress as Decimal objects (lines 63–87) to eliminate floating-point accumulation errors during long-running sessions. This precision ensures that musical subdivisions remain phase-accurate over hours of continuous operation without drift.
How does the BernoulliTrigger's quantization mode work?
When quantized_cv is set to a value like "1/4" or "1/8", the __post_init__ method builds a quantize_scale lookup table including 0, 1, and the specified rational fractions. During trigger processing, the biased probability pbias snaps to the nearest entry in this table, converting continuous probability into discrete steps suitable for deterministic generative sequencing.
Can the Clock be reset without stopping playback?
Yes. The reset_cv input responds to rising edges independently of the play_cv transport state (lines 177–181). Sending a high pulse to reset_cv zeros all internal phase accumulators instantly, realigning all subdivisions to beat one while maintaining the current play/stop status.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →