# Nallely-MIDI Clock System and BernoulliTrigger Architecture: A Deep Dive

> Explore the Nallely-MIDI Clock system and BernoulliTrigger architecture. Discover deterministic tempo generation and probabilistic gating for your MIDI-to-CV patches.

- Repository: [dr-schlange/nallely-midi](https://github.com/dr-schlange/nallely-midi)
- Tags: deep-dive
- Published: 2026-02-28

---

**The Clock system and BernoulliTrigger are specialized `VirtualDevice` subclasses implemented in [`nallely/clocks.py`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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.

```python
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

```python
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

```python
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

```python
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`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core.py), utilizing the `@on` decorator system and CV parameter infrastructure for event-driven processing.
- **The Clock** ([`nallely/clocks.py`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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`](https://github.com/dr-schlange/nallely-midi/blob/main/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.