# How Jitter Mitigation Works in Nallely's Signal Processing

> Understand jitter mitigation in Nallely's signal processing. Learn how configurable target cycle times and adaptive sleep calculations ensure stable execution intervals. Optimize your MIDI timing.

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

---

**Nallely mitigates timing jitter by enforcing a configurable target cycle time for each virtual device thread and applying adaptive sleep calculations to maintain stable execution intervals.**

The open-source Python framework [dr-schlange/nallely-midi](https://github.com/dr-schlange/nallely-midi) implements a virtual-device architecture where each device runs in its own thread. To combat the irregular timing variations (jitter) caused by Python's scheduler, OS load, and variable processing workloads, Nallely employs a dual-mechanism approach combining target-cycle enforcement with adaptive sleep management.

## Target-Cycle Enforcement

Every `VirtualDevice` instance stores a `target_cycle_time` attribute that defines the ideal duration for one complete iteration of its main processing loop.

According to the source code in [`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/virtual_device.py) (lines 190–223), this value defaults to **0.002 seconds** (2 milliseconds) but remains configurable per device during construction via the `VirtualDevice.__init__` method.

For clock-based devices, the target derives from musical tempo settings. In [`nallely/clocks.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/clocks.py) (lines 106–110), the `Clock._compute_target_cycle` method calculates the appropriate cycle time based on the BPM parameter, translating musical beats into precise thread timing targets.

## Adaptive Sleep After Each Cycle

The core jitter-reduction logic resides in the device's run loop at [`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/virtual_device.py) (lines 504–511). After processing all pending input queues and executing the device's `main` generator, the framework measures the elapsed wall-clock time and sleeps only the remaining fraction necessary to reach the target cycle time.

The calculation follows this logic:

```python
sleep_time = max(0, self.target_cycle_time - elapsed_time)

```

If processing exceeds the target duration, the device skips the sleep entirely and updates its `next_tick_time` to "catch up," preventing timing drift from accumulating across iterations.

## The Capped Sleep Helper

Nallely provides a helper method `VirtualDevice.sleep` (lines 337–342 in [`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/virtual_device.py)) that optionally caps requested pauses to the current `target_cycle_time`.

When calling `sleep(t, consider_target_time=True)`, the method limits the pause duration to `self.target_cycle_time * 1000` milliseconds. This prevents long explicit sleeps from destabilizing the device's rhythm.

However, clock devices use `consider_target_time=False` (as seen in [`nallely/clocks.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/clocks.py), lines 143–152) because pulse-width timing operates independently from the global target cycle, allowing precise control over musical pulse characteristics without jitter-mitigation interference.

## Code Examples

### Stabilizing a MIDI Clock

Create a tempo-based clock where jitter mitigation maintains steady BPM:

```python
from nallely.clocks import Clock

# Initialize clock at 120 BPM; target cycle computed automatically

clock = Clock(tempo=120)

# The run thread enforces stable timing via adaptive sleep

clock.start()

# ... musical processing occurs here ...

clock.stop()

```

The `Clock._compute_target_cycle` method (lines 106–110) derives the target from BPM, while `VirtualDevice.run` (lines 504–511) maintains that rhythm through adaptive sleep calculations.

### Respecting Target Cycles in Custom Devices

Use the capped sleep helper to prevent long stalls:

```python
from nallely.core.virtual_device import VirtualDevice, VirtualParameter, on

class StabilizedDevice(VirtualDevice):
    period_cv = VirtualParameter("period", range=(0.001, 1.0), default=0.01)

    @on(period_cv, edge="rising")
    def on_period_change(self, value, ctx):
        # Request 20ms pause but cap at target_cycle_time to prevent jitter

        yield from self.sleep(20, consider_target_time=True)

```

When `consider_target_time=True`, the sleep duration cannot exceed the device's configured `target_cycle_time`, preserving timing regularity.

### Visualizing Jitter Control

Demonstrate how the run loop smooths explicit sleeps:

```python
import time
from nallely.core.virtual_device import VirtualDevice, VirtualParameter

class Timer(VirtualDevice):
    tick_cv = VirtualParameter("tick", range=(0, 1))

    def main(self, ctx):
        while True:
            self.tick_cv = 1
            yield 1, [self.tick_cv]
            # Explicit 5ms sleep, but adaptive loop maintains 10ms target

            yield from self.sleep(5, consider_target_time=False)
            self.tick_cv = 0
            yield 0, [self.tick_cv]

# Configure 10ms target cycle

timer = Timer(target_cycle_time=0.01)
timer.start()
time.sleep(1)
timer.stop()

```

Even though the generator requests a 5-millisecond pause, the surrounding run loop guarantees the total cycle remains close to the 10-millisecond target, absorbing OS-induced timing variance.

## Summary

- **Target-cycle enforcement**: Each `VirtualDevice` maintains a configurable `target_cycle_time` (default 2ms) set during initialization or computed from BPM for clocks.
- **Adaptive sleep calculation**: The run loop measures elapsed processing time and sleeps only the remaining fraction needed to hit the target, skipping sleep entirely when catching up.
- **Capped sleep helper**: The `sleep` method accepts a `consider_target_time` parameter to optionally limit pauses to the target cycle, preventing long sleeps from increasing jitter.
- **Clock independence**: Clock devices bypass the target cap (`consider_target_time=False`) to maintain precise pulse-width control independent of the global timing target.

## Frequently Asked Questions

### What causes jitter in Nallely's virtual devices?

Jitter arises from Python's thread scheduler variability, operating system load fluctuations, and differences in processing time between loop iterations. Without mitigation, these factors cause irregular intervals between successive processing cycles, destabilizing MIDI timing.

### How does Nallely calculate the sleep duration after each cycle?

After executing the device's `main` generator and processing inputs, the framework calculates `sleep_time = max(0, self.target_cycle_time - elapsed_time)` where `elapsed_time` is the measured wall-clock time for the current iteration. This adaptive calculation ensures the total cycle duration remains consistent.

### Why does the Clock device use `consider_target_time=False`?

Clock devices control musical pulse width independently from the global timing target. By passing `consider_target_time=False` to the `sleep` method (as implemented in [`nallely/clocks.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/clocks.py) lines 143–152), the clock preserves precise control over pulse characteristics without the jitter-mitigation cap interfering with musical expression.

### Can I adjust the target cycle time for specific devices?

Yes. Pass `target_cycle_time` during device construction (default 0.002 seconds). High-priority devices benefit from shorter targets (1ms) for tighter timing, while background processors may use longer cycles (10ms) to reduce CPU load, as stored in [`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/virtual_device.py) lines 190–223.