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 it
  • reset_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 value
  • ratios — 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:

  1. Converts BPM to quarter-note period (quarter_note_s)
  2. Determines tick length (tick_s) respecting the minimum millisecond constraint
  3. Advances each subdivision phase by ratio * tick_s/quarter_note_s when playing
  4. Emits a high pulse (≤5ms width) on corresponding outputs when phases cross integer boundaries
  5. Schedules the next tick using next_tick_time to 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 test
  • probability_cv — Base probability threshold ranging from 0 to 1 (default 0.5)
  • bias_cv — Additive bias value (0–1) that skews the probability calculation
  • quantized_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:

  1. Compute biased probability: pbias = p + b·(1−p)
  2. Optionally snap to nearest quantised value: pquant
  3. Draw uniform random number; if < pquant fire outA_cv, else fire outB_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 VirtualDevice in nallely/core.py, utilizing the @on decorator system and CV parameter infrastructure for event-driven processing.
  • The Clock (nallely/clocks.py lines 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.py lines 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_cv normalization).
  • 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →