xmrsplayer 0.14.4

Safe, no_std SoundTracker music player — plays MOD/XM/S3M/IT/DW with cycle-accurate SID and OPL/AdLib FM synthesis.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
# Subscribing to player events

Starting with 0.10, `xmrsplayer` exposes a subscription API: anything that
wants to know "which row is playing right now, on which pattern, with which
notes and effects" can register an observer with the player and receive
callbacks as the song progresses. This document walks through the contract,
the threading rules, and a couple of recipes.

## The big picture

Under the hood the player is now three pieces:

1. **`Sequencer`** — knows where we are in the song (pattern order, row,
   tempo clock) and interprets navigation effects (`Fxx`, `Bxx`, `Dxx`,
   `E6y`, `EEy`…).
2. **`Voices`** — the audio engine (per-channel state + mixer). It is the
   *default row consumer*: every row the sequencer produces is forwarded to
   voices so the module actually plays.
3. **Your observers** — anything else that wants to be notified. Voices is
   called directly by the facade; observers are called right after, via
   trait-object dispatch.

From an observer's point of view, `Voices` is invisible. You just implement
a trait, register an instance, and receive events.

## The trait

```rust
pub trait PlayerObserver {
    fn on_row(&mut self, ctx: &RowContext<'_>);

    fn on_tick(&mut self, _ctx: &TickContext<'_>) {}
    fn subscribes_to_ticks(&self) -> bool { false }

    fn on_pattern_change(&mut self, _ctx: &PatternChangeContext<'_>) {}
    fn on_song_end(&mut self) {}
}
```

`on_row` is the only mandatory method. The others have no-op defaults, so a
"just tell me when the row changes" observer is three lines of code.

### The contexts

```rust
pub struct RowContext<'a> {
    pub module: &'a Module,
    pub song: usize,
    pub pattern: usize,      // pattern index as resolved by Module::row_at
    pub row: usize,
    pub cells: &'a [Cell],   // one per channel, zero-copy borrow
    pub tempo: usize,
    pub bpm: usize,
}
```

`cells` is materialised through `Module::row_at` (which walks the DAW
timeline layer) and borrowed for the duration of the callback. Each
[`Cell`] exposes a typed `event: CellEvent`
(`NoteOn { pitch, velocity }`, `NoteOnGhost { pitch, velocity }`,
`InstrReset`, `NoteOff { retrig }`, `NoteCut`, `NoteFade`, `None`)
plus a `Vec<TrackEffect>` of discrete per-trigger effects. The
**instrument** that a row plays comes from the active `Track`
(`Track::instrument()`) — it is no longer a per-cell field. The
audio-side dispatch threads the track-level instrument separately
(see `Module::row_at`, which returns `Vec<(Cell, Option<usize>)>`
on the audio path).

```rust
for (ch, cell) in ctx.cells.iter().enumerate() {
    if let Some(pitch) = cell.event.pitch() {
        println!("ch{ch}: pitch={:?} velocity={:?}", pitch, cell.event.velocity());
    }
}
```

`TickContext` and `PatternChangeContext` follow the same pattern; see
`src/observer/mod.rs` for the full definitions.

### Opting into ticks

`on_tick` fires on every sustained (non-row-start) tick — roughly 50 times
per second at the default BPM. That's wasteful for a UI that only redraws
on row changes, so the default `subscribes_to_ticks()` returns `false` and
the player skips the callback entirely for observers that don't override
it.

Override it to return `true` *only* when you actually need tick-level
granularity (a vibrato visualiser, a per-tick LED strobe, a recording probe
measuring something that changes between rows).

```rust
impl PlayerObserver for MyVibratoViewer {
    fn on_row(&mut self, _ctx: &RowContext<'_>) { /* ... */ }
    fn on_tick(&mut self, ctx: &TickContext<'_>) { /* ... */ }
    fn subscribes_to_ticks(&self) -> bool { true }
}
```

## Registering an observer

```rust
use xmrsplayer::prelude::*;

let mut player = XmrsPlayer::new(&module, 48_000.0, 0);
player.add_observer(Box::new(MyObserver::new()));
```

Observers are stored in a `Vec<Box<dyn PlayerObserver + Send>>` and invoked in
registration order. You can register as many as you want. To remove all of
them at once, call `player.clear_observers()`; to count them,
`player.observer_count()`.

There is no "remove a specific observer" method by design — if you need to
toggle a subscriber on and off, put the toggle inside the observer itself
(a `self.enabled: bool` flag read at the top of `on_row`).

### The `Send` bound

`PlayerObserver` has `Send` as a supertrait: `trait PlayerObserver: Send`.
This is because the typical deployment pattern wraps the player in
`Arc<Mutex<XmrsPlayer>>` and hands it to an audio callback thread (cpal,
rodio, …). For the `Mutex<XmrsPlayer>` to be `Sync` (and the `Arc` sendable),
every owned field — including the observer list — must be sendable.

Most observers satisfy this automatically (they hold plain data — counters,
pre-allocated `Vec`s, channel senders). If yours holds a `!Send` type —
a raw pointer, an `Rc<_>`, a `RefCell<_>` — either restructure to move the
non-sendable state behind an `Arc<Mutex<_>>`, or don't share the player
across threads at all (single-threaded usage removes the bound practically,
though the type still enforces it).

`Sync` is **not** required: observer methods are always called through
`&mut self` from a single thread at a time (the one holding the mutex).

## Threading contract

Every observer callback runs **synchronously, on the thread that pulled the
last sample from the player**. In a typical audio setup that's the audio
callback thread (cpal / rodio). That has three consequences:

1. **Do not block.** No mutex acquisitions you can't prove will succeed
   instantly, no file I/O, no network, no long CPU loops. The audio callback
   has a deadline (usually a few ms); if you miss it the stream xruns.

2. **Do not allocate on hot paths.** `on_tick` fires ~50 Hz; `on_row` a few
   Hz. The latter is fine for the occasional `format!()`; the former should
   stay allocation-free.

3. **Send data elsewhere to do real work.** The canonical pattern is a
   single-producer single-consumer queue:

   ```rust
   use std::sync::mpsc::{self, Sender};

   struct RowForwarder { tx: Sender<RowSnapshot> }

   impl PlayerObserver for RowForwarder {
       fn on_row(&mut self, ctx: &RowContext<'_>) {
           // Build a small owned snapshot (no borrowed references).
           let snap = RowSnapshot {
               pattern: ctx.pattern,
               row: ctx.row,
               events: ctx.cells.iter().map(|c| c.event).collect(),
           };
           let _ = self.tx.send(snap);  // non-blocking send
       }
   }
   ```

   The UI thread drains the receiver and updates the display. The audio
   thread does only the minimum: build the snapshot and enqueue it.

   For real-time-sensitive setups, prefer a lock-free SPSC queue
   (`rtrb`, `crossbeam` `ArrayQueue`) over `std::sync::mpsc`.

## No back-references to the player

An observer receives `&RowContext`, not a handle to the player. This is
deliberate: if an observer needed to call `player.goto(...)` from inside
`on_row`, the player is already borrowed mutably up the stack (you're
inside its `sample()` call), and you'd deadlock the moment you tried to
lock it.

If you want to react to an event by jumping, push the command to a queue
and let the thread that owns the player drain it between samples. For
example, your main thread can do:

```rust
if let Some(cmd) = command_rx.try_recv().ok() {
    match cmd {
        Command::JumpTo(pos) => { player.lock().unwrap().goto(pos, 0, 0); }
        // ...
    }
}
```

## The `DebugObserver`

Gone are the days of `player.debug(true)`. Register the built-in observer
instead:

```rust
use xmrsplayer::prelude::*;

player.add_observer(Box::new(DebugObserver::new()));
```

It prints pattern-order changes and one row per notification. For a
firehose that also prints every tick, flip the flag:

```rust
player.add_observer(Box::new(DebugObserver::new().with_trace_ticks(true)));
```

Only available when the `std` feature is on, since it uses `println!`.

## A complete tiny example

```rust
use xmrs::prelude::*;
use xmrsplayer::prelude::*;

struct NoteHighlighter;

impl PlayerObserver for NoteHighlighter {
    fn on_row(&mut self, ctx: &RowContext<'_>) {
        for (ch, cell) in ctx.cells.iter().enumerate() {
            if let Some(pitch) = cell.event.pitch() {
                // Send `(ch, pitch)` to your UI somehow.
                // For the sake of the example we just print.
                println!("row {}: ch{ch} -> {:?}", ctx.row, pitch);
            }
        }
    }
}

fn main() {
    let bytes = std::fs::read("song.xm").unwrap();
    let module = Module::load(&bytes).unwrap();
    let mut player = XmrsPlayer::new(&module, 48_000.0, 0);

    player.add_observer(Box::new(NoteHighlighter));

    for sample in player.by_ref().take(48_000 * 10) {
        // feed `sample` to your audio backend
        let _ = sample;
    }
}
```

## Migrating from 0.9.x

The public API is almost identical. The only breaking changes:

| 0.9.x                            | 0.10+                                                        |
| -------------------------------- | ------------------------------------------------------------ |
| `player.amplification = 0.25;`   | `player.set_amplification(0.25);`                            |
| `player.global_volume = 0.5;`    | `player.set_global_volume(0.5);`                             |
| `player.generated_samples`       | `player.generated_samples()`                                 |
| `player.debug(true);`            | `player.add_observer(Box::new(DebugObserver::new()));`       |
| `player.channel[n].muted = ...;` | `player.set_mute_channel(n, ...);` (already existed in 0.9)  |

`player.module` stays public — it's a `&Module` reference and there is no
invariant to guard.

## Testing story

The `Sequencer` is now independently driveable: you can feed it a
hand-built `Module` and check that `advance_tick()` emits the expected
`TickOutcome`s, without instantiating `Voices` and without producing any
audio. Since DAW migration Phase 3c.2 the sequencer is fully
TimelineMap-driven — navigation effects (`Bxx` / `Dxx` / `E6y` / `EEy`)
are absorbed at import time and the runtime just walks
`module.timeline_map.entries` linearly, honouring `module.song_loop_to`
for the end-of-song wrap. A test module is a good place to start
growing the regression suite.

## Audio observers

So far we've covered `PlayerObserver`, which fires on *song* events (rows,
ticks, pattern changes). Starting in 0.10 there is a complementary
subscription path that fires on **produced samples** — once per stereo
frame of audio coming out of the mixer. The use cases are different:
VU-meters, oscilloscopes, FFT analyzers, recording probes, loudness
meters — anything that wants to consume (not modify) the audio.

Two traits cover the two levels of detail:

```rust
pub trait MixObserver: Send {
    fn on_mix(&mut self, ctx: &MixContext);
}

pub trait ChannelsObserver: Send {
    fn on_channels(&mut self, ctx: &ChannelsContext<'_>);
}
```

`MixObserver` receives the **final stereo mix**, pre-gain. Use it for
anything that operates on the summed signal.

`ChannelsObserver` receives the **per-channel pre-sum slice** — one
`(left, right)` pair per song channel, with the channel's panning / mute
already applied but before the sum and the global gain. Use it for
multi-track visualisers or per-channel meters.

### Contexts

```rust
pub struct MixContext {
    pub left: f32,
    pub right: f32,
    pub global_volume: f32,   // 0.0..=1.0, song-driven
    pub amplification: f32,   // user-driven, unbounded
}

pub struct ChannelsContext<'a> {
    pub channels: &'a [(f32, f32)],   // one per song channel
    pub global_volume: f32,
    pub amplification: f32,
}
```

Both contexts carry `global_volume` and `amplification` alongside the
samples so the observer is **self-contained**: no need to query the
player (which is borrowed mutably during the callback anyway) and no
need to cache the gain through side-channel notifications.

### Pre-gain by design

Both contexts expose samples *before* the `global_volume * amplification`
multiplication. An observer that wants the post-gain value (what the
listener actually hears) multiplies itself:

```rust
fn on_mix(&mut self, ctx: &MixContext) {
    let gain = ctx.global_volume * ctx.amplification;
    let post_l = ctx.left * gain;
    let post_r = ctx.right * gain;
    // ...
}
```

An observer wanting the raw mix (e.g. a loudness analyzer that must not
be influenced by the user's volume slider) uses `ctx.left` / `ctx.right`
as-is. This keeps the design symmetric, lets each observer pick its own
semantics, and makes the pipeline transparent — nobody has to wonder
what `post-gain` means exactly when new stages are inserted.

### Registration

```rust
use xmrsplayer::prelude::*;

player.add_audio_mix_observer(Box::new(MyVuMeter::default()));
player.add_audio_channels_observer(Box::new(MyMultiTrackScope::default()));

// Symmetric clear / count methods:
player.clear_audio_mix_observers();
player.clear_audio_channels_observers();
let n = player.audio_mix_observer_count();
let m = player.audio_channels_observer_count();
```

### Cost model

A `MixObserver` is cheap: one vtable dispatch per sample per registered
observer. At 48 kHz with N observers, that's `48000 × N` calls per
second — well under 1% CPU for small N.

A `ChannelsObserver` costs more in two ways. First, each sample now
fills a per-channel capture buffer in the mixer (one write per channel
per sample). Second, the observer's `on_channels` receives a slice of
that buffer to iterate. The player gates the capture on the channels-
observer list being non-empty, so the cost is paid only when someone
consumes it — but *any* channels observer enables the capture for *all*
samples. If you only need the summed mix, always prefer `MixObserver`.

### Pause behaviour

`player.pause(true)` still fires audio observers, but with samples
forced to `(0.0, 0.0)`. This is the documented contract:

- Recorders capture silence for the duration of the pause (so the
  output file's length matches the perceived musical time).
- Oscilloscopes / VU-meters draw a flatline.
- FFT analyzers see a silent window and typically emit a zero spectrum.

An observer that wants to distinguish "paused silence" from "musical
silence" must track it externally — keep a `paused: bool` on your side
and flip it when you call `player.pause()`.

### Ordering

When both observer kinds are registered, the player dispatches in this
order within a single sample:

1. `voices.mix(...)` — produces the stereo mix and (if any channels
   observer is registered) fills the per-channel capture buffer.
2. Every `ChannelsObserver::on_channels` (registration order).
3. Every `MixObserver::on_mix` (registration order).

This way a channels observer and a mix observer can cooperate: the
channels one sees the fine-grained state, then the mix one sees the
summed output, for the same sample.

### Example — a minimalist VU-meter

```rust
use xmrsplayer::prelude::*;
use std::sync::{Arc, Mutex};

#[derive(Default)]
struct VuMeter {
    peak_left: f32,
    peak_right: f32,
}

impl MixObserver for VuMeter {
    fn on_mix(&mut self, ctx: &MixContext) {
        let gain = ctx.global_volume * ctx.amplification;
        self.peak_left = self.peak_left.max((ctx.left * gain).abs());
        self.peak_right = self.peak_right.max((ctx.right * gain).abs());
    }
}

// The observer goes into a `Box<dyn MixObserver + Send>`, so `VuMeter`
// itself must be `Send`. `f32` is `Send`, so the derived auto-`Send`
// covers us here. If you want to *read* the meter from another thread,
// wrap it in `Arc<Mutex<VuMeter>>` yourself and install a thin
// forwarder observer that pushes into it — the player only needs to
// own a `Send` object, not a `Sync` one.
```

### Example — recording to a WAV file

```rust
use hound::{WavWriter, WavSpec, SampleFormat};

struct Recorder {
    writer: WavWriter<std::io::BufWriter<std::fs::File>>,
}

impl MixObserver for Recorder {
    fn on_mix(&mut self, ctx: &MixContext) {
        let gain = ctx.global_volume * ctx.amplification;
        let l = (ctx.left * gain).clamp(-1.0, 1.0);
        let r = (ctx.right * gain).clamp(-1.0, 1.0);
        // `write_sample` is O(1), no allocation — safe on audio thread.
        self.writer.write_sample((l * 32767.0) as i16).ok();
        self.writer.write_sample((r * 32767.0) as i16).ok();
    }
}
```

Note: `hound`'s `write_sample` buffers internally, so it doesn't block
on each call. For very long recordings, the buffer will eventually need
to flush to disk — if that's a concern, record into an in-memory queue
from the observer and do the actual disk I/O from another thread.

### Alternative consumption path

The crate also exposes
[`XmrsPlayer::samples_from_channels`](crate::xmrsplayer::XmrsPlayer::samples_from_channels)
for code that wants to drive the player while getting per-channel data
back as a `Vec<(f32, f32)>`. That path **does not** fire audio observers —
it is considered an alternative to `sample()` / the `Iterator` impl,
and firing observers on both would cause double dispatch if the two
paths were interleaved. Pick one path or the other for a given player
instance.

## MIDI observers

Impulse Tracker modules can embed MIDI macros that, when triggered via
`Zxx` (parametric / fixed macro) or `SFx` (macro selector), emit MIDI
data. xmrsplayer's internal interpreter handles the `F0 F0 nn xx`
"filter control" frames natively (they drive the per-voice resonant
filter) but everything else — standard channel messages (Note On/Off,
CC, Program Change, Pitch Bend, Aftertouch) and SysEx — has no internal
meaning to attach, so the bytes are emitted as `MidiEvent`s to any
subscribed `MidiObserver`.

This is the third subscription trait, parallel to `PlayerObserver`
(song events) and the audio observers (produced samples). The use case
is different: bridging a module to real MIDI hardware, routing to a
soft synth, logging, or just inspecting what a given IT file's macros
actually emit.

### The trait

```rust
pub trait MidiObserver: Send {
    fn on_midi_event(&mut self, event: &MidiEvent, source_channel: usize);
}
```

### The event

```rust
pub enum MidiEvent {
    NoteOff       { channel: u8, note: u8, velocity: u8 },
    NoteOn        { channel: u8, note: u8, velocity: u8 },
    PolyAftertouch{ channel: u8, note: u8, pressure: u8 },
    ControlChange { channel: u8, controller: u8, value: u8 },
    ProgramChange { channel: u8, program: u8 },
    ChannelAftertouch { channel: u8, pressure: u8 },
    PitchBend     { channel: u8, value: u16 },  // 14-bit, centre = 8192
    SysEx(Vec<u8>),
    Raw(Vec<u8>),
}
```

`NoteOn` with velocity 0 is aliased to `NoteOff` before dispatch (per
MIDI convention), so `NoteOn { velocity: 0 }` never reaches the
callback.

`SysEx` carries the leading `F0` but strips the terminating `F7` —
consumers that need the raw wire stream append the `F7` themselves.
The IT-internal `F0 F0 nn xx` frames are consumed by the filter and
never reach this variant.

`Raw` is a fallback for byte sequences that look like MIDI data but
don't parse cleanly (truncated message, unknown status byte). Most
consumers can ignore it; forwarders that want to pass everything
through reuse the payload verbatim.

### Two different "channels"

The trait signature carries two channel-like parameters that are
**independent**:

- `source_channel: usize` (the callback's second argument) is the
  **tracker pattern channel** the macro was triggered on. Range:
  `0..module.get_num_channels()`. Use this to associate the event
  with its origin (e.g., route tracker channel 3 to MIDI port A,
  channel 5 to port B).

- `channel: u8` (inside each event variant) is the **MIDI channel**
  encoded in the low nibble of the status byte. Range: `0..16`. This
  is what the macro's author wrote in the macro string (`9n nn vv`
  where `n` is the MIDI channel).

The two can differ freely: a macro on tracker channel 3 can address
MIDI channel 7, or MIDI channel 0, or whatever the author encoded.

### Registration

```rust
use xmrsplayer::prelude::*;

let mut player = XmrsPlayer::new(&module, 48_000.0, 0);
player.add_midi_observer(Box::new(MyMidiBridge::new(port)));

// Symmetric clear / count methods:
player.clear_midi_observers();
let n = player.midi_observer_count();
```

### Dispatch timing

MIDI events are dispatched **once per row**, immediately after the
row's effects have been processed and before the row's first tick
produces audio. This is because `Zxx` / `SFx` are row-start effects:
the macro fires at tick 0, and queuing events until row-end would
collapse distinct macro invocations from different channels into a
single batch.

If no observer is registered, the internal event buffer is still
drained (to bound memory) but no allocation or dispatch happens. Cost
of registering the trait is one `Vec<Box<dyn MidiObserver + Send>>`
on the player and one vtable dispatch per emitted event.

### Cost model

Very cheap: a macro emits at most a handful of events per row, and
rows fire at a few Hz — the aggregate is orders of magnitude below
the audio observers. No hot-path gating is needed.

Modules without embedded MIDI macros (every XM / MOD / S3M, and IT
files without the macro flag) never enter the dispatch path at all —
the buffer stays empty because the interpreter is only invoked for
`Zxx` / `SFx`.

### Pause behaviour

When the player is paused, row advancement stops, so no `Zxx` macros
fire and no events are emitted. This is the correct behaviour: MIDI
hardware that received a Note On before the pause keeps sounding
unless the observer itself emits a compensating Note Off.

If your bridge needs "all notes off" on pause, track `player.pause()`
transitions externally and send the All-Notes-Off CC (controller 123)
to every MIDI channel your module uses.

### The `DebugMidiObserver`

The crate ships a built-in MIDI observer that prints every emitted
event to stdout, mirroring `DebugObserver` for song events:

```rust
use xmrsplayer::prelude::*;

player.add_midi_observer(Box::new(DebugMidiObserver::new()));
```

Output is one line per event, prefixed with the tracker channel:

```text
ch03 NoteOn        midi_ch=0 note=60 vel=100
ch05 ControlChange midi_ch=0 cc=74 val=64
ch07 SysEx         len=6 data=[F0, 7E, 7F, 09, 01, ...]
```

Long SysEx payloads can be truncated for readability:

```rust
player.add_midi_observer(
    Box::new(DebugMidiObserver::new().with_truncate_sysex(true))
);
```

Only available when the `std` feature is on.

### Example — bridge to `midir`

```rust
use midir::{MidiOutput, MidiOutputConnection};

struct MidirBridge {
    conn: MidiOutputConnection,
}

impl MidiObserver for MidirBridge {
    fn on_midi_event(&mut self, event: &MidiEvent, _src_ch: usize) {
        // Serialise the event back to wire bytes and send.
        let bytes: Vec<u8> = match event {
            MidiEvent::NoteOn { channel, note, velocity } =>
                vec![0x90 | (channel & 0x0F), *note, *velocity],
            MidiEvent::NoteOff { channel, note, velocity } =>
                vec![0x80 | (channel & 0x0F), *note, *velocity],
            MidiEvent::ControlChange { channel, controller, value } =>
                vec![0xB0 | (channel & 0x0F), *controller, *value],
            MidiEvent::ProgramChange { channel, program } =>
                vec![0xC0 | (channel & 0x0F), *program],
            MidiEvent::PitchBend { channel, value } => {
                let lsb = (*value & 0x7F) as u8;
                let msb = ((*value >> 7) & 0x7F) as u8;
                vec![0xE0 | (channel & 0x0F), lsb, msb]
            }
            MidiEvent::SysEx(data) => {
                let mut v = data.clone();
                v.push(0xF7);
                v
            }
            // Aftertouch, Raw — omitted for brevity.
            _ => return,
        };
        let _ = self.conn.send(&bytes);
    }
}
```

Real deployments should probably post events to a ring buffer read by a
dedicated MIDI thread rather than calling `conn.send` on the audio
thread — the dispatch runs on the sample-producing thread (same as the
other observer traits), and `midir`'s `send` can block briefly
depending on the backend.