# VirtualDevice vs MidiDevice in Nallely-MIDI: Core Architectural Differences Explained

> Understand the core architectural differences between VirtualDevice and MidiDevice in Nallely MIDI. Learn how software generators and hardware adapters connect your MIDI system.

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

---

**VirtualDevice is a software-only thread-based generator that produces control values internally, while MidiDevice is a hardware-aware adapter that bridges the Nallely link system with physical MIDI interfaces via the mido library.**

The `nallely-midi` framework provides two fundamental abstractions in its `nallely/core/` module for building modular MIDI ecosystems. Understanding the difference between VirtualDevice and MidiDevice is essential for deciding whether to create algorithmic software generators or hardware controller wrappers. Both expose parameters that participate in the Nallely runtime's linking system, but they represent fundamentally different execution models, I/O strategies, and lifecycle requirements.

## Base Class and Threading Architecture

The primary distinction lies in how each device manages execution. **VirtualDevice** inherits from Python's `threading.Thread` class, as implemented in [`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/virtual_device.py). It runs a continuous cycle through its `run()` method, processing input queues, handling pause states, and executing the internal link engine. Device logic is implemented in a user-defined `main()` generator method that yields values at each cycle.

**MidiDevice**, defined in [`nallely/core/midi_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/midi_device.py), is a regular Python class that **does not inherit from `Thread`**. Instead of managing its own execution thread, it relies on the *mido* library's callback mechanism to react to incoming MIDI messages. This architectural choice reflects its role as a passive adapter rather than an active generator.

## Parameter Types and Data Flow

Each device type utilizes distinct parameter descriptors optimized for their respective domains.

**VirtualDevice** uses **VirtualParameter** instances declared with `VirtualParameter(...)`. These represent virtual control values that can operate in streaming mode (`stream=True`) or as discrete triggers. Parameters are linked through Nallely's internal routing system using `send_out`, `stream_links`, and `nonstream_links` attributes.

**MidiDevice** employs hardware-oriented descriptors including **ModuleParameter**, **ModulePadsOrKeys**, and **ModulePitchwheel**. These map directly to MIDI protocol elements:
- `ModuleParameter` handles Continuous Controller (CC) and Program Change messages
- `ModulePadsOrKeys` manages Note On/Off and Velocity data  
- `ModulePitchwheel` captures pitch bend information

Incoming MIDI messages are parsed and routed to these parameters via `self.links`, while outgoing values are transmitted through `self.outport.send(msg)`.

## Lifecycle and Initialization Patterns

VirtualDevice instances are instantiated with `VirtualDevice(..., autoconnect=False)` and must be explicitly started using `device.start()`. This launch sequence creates the background thread and begins executing the `main()` generator. The class must be decorated with `@register_virtual_device_class` from [`nallely/core/world.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/world.py) to participate in the device registry.

MidiDevice concrete subclasses (such as `NTS1` or `MPD32`) automatically register via `midi_device_classes.append(cls)` in their `__post_init_subclass__` method. Initialization requires calling `self.connect()` to open MIDI ports via mido, followed by `self.listen()` to attach the input callback (`self.inport.callback = self._sync_state`). Unlike VirtualDevice, there is no thread to start— hardware communication begins immediately upon connection.

## Practical Implementation Examples

### Creating a Software-Only VirtualDevice

The following example implements a simple low-frequency oscillator entirely in software:

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

class LFO(VirtualDevice):
    """Simple low-frequency oscillator generating sawtooth waves."""
    freq_cv = VirtualParameter('freq', range=(0.1, 10.0), default=1.0)
    out_cv = VirtualParameter('out', range=(0, 127))

    def main(self, ctx):
        """Generate a saw-tooth wave based on frequency."""
        while True:
            t = ctx.get('t', 0) + self.freq_cv / self.target_cycle_time
            value = int((t % 1) * 127)
            ctx['t'] = t
            return value

    @on(freq_cv, edge='rising')
    def on_freq_change(self, value, ctx):
        """Optional handler for frequency parameter changes."""
        print(f'Frequency changed to {value}')

```

Usage requires starting the thread:

```python
lfo = LFO(target_cycle_time=0.01)  # 10ms cycle time

lfo.start()  # Launches threading.Thread.run()

```

Relevant source: [`nallely/core/virtual_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/virtual_device.py) lines 70-80 (class definition) and lines 170-190 (parameter handling).

### Integrating Hardware with MidiDevice

Concrete MidiDevice subclasses interface with physical hardware through the mido library:

```python
from nallely.devices.nts1 import NTS1  # Concrete subclass in nallely/devices/

# Instantiate and connect to physical hardware

synth = NTS1(device_name='Korg NTS-1')
synth.channel = 0        # Optional: force MIDI channel

synth.connect()          # Opens output port via mido

synth.listen()           # Starts input callback

# Send direct MIDI CC message (e.g., filter cutoff)

synth.control_change(control=74, value=100)

# Route virtual device output to hardware parameter

from nallely.core.links import Link
Link.create(synth.modules.filter.cutoff_cv, lfo.out_cv)

```

Relevant source: [`nallely/core/midi_device.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/midi_device.py) lines 82-94 (base class definition) and lines 370-420 (I/O methods).

## Summary

- **VirtualDevice** inherits from `threading.Thread` and executes a user-defined generator in a background thread, while **MidiDevice** uses mido callbacks without threading for hardware communication.
- **VirtualParameter** instances handle internal software values with streaming support, whereas **ModuleParameter** and related classes map directly to MIDI protocol messages.
- **VirtualDevice** communicates exclusively through Nallely's internal link system, while **MidiDevice** bridges to physical MIDI ports via `outport.send()` and input callbacks.
- **VirtualDevice** requires explicit `start()` to launch the thread, while **MidiDevice** uses `connect()` and `listen()` to initialize hardware interfaces.

## Frequently Asked Questions

### Can a VirtualDevice send MIDI messages directly to hardware?

No. VirtualDevice instances have no external I/O capabilities and cannot access MIDI ports directly. To control hardware, create a Link between the VirtualDevice's VirtualParameter and a MidiDevice's ModuleParameter, allowing the internal value to drive MIDI CC or note messages sent by the MidiDevice.

### Why does MidiDevice not inherit from Thread like VirtualDevice?

MidiDevice relies on the *mido* library's callback-based I/O model rather than a polling loop or generator pattern. Since mido handles asynchronous message reception through port callbacks, and transmission occurs on-demand via method calls, a background thread would be redundant and potentially conflict with mido's internal threading model.

### Can the same device class inherit from both VirtualDevice and MidiDevice?

No, these classes represent mutually exclusive architectural patterns. A device cannot simultaneously be a background thread generator and a mido-based hardware adapter. For hybrid functionality, create separate device instances and link them using the Nallely Link API in [`nallely/core/links.py`](https://github.com/dr-schlange/nallely-midi/blob/main/nallely/core/links.py).

### Which device type should I use for a custom clock or sequencer?

Use **VirtualDevice** for algorithmic generation (LFOs, envelope followers, probability sequencers) that produces control data internally. Use **MidiDevice** when you need to send that sequenced data to external synthesizers or when wrapping hardware that generates its own clock signals. The VirtualDevice generates the pattern; the MidiDevice transmits it to hardware.