diff --git a/crates/playback/src/lib.rs b/crates/playback/src/lib.rs
--- a/crates/playback/src/lib.rs
+++ b/crates/playback/src/lib.rs
@@ -23,9 +23,11 @@
 #![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
 
 pub mod devices;
+mod load;
 pub mod record;
 
 pub use devices::AudioConfig;
+pub use load::AudioLoad;
 
 use soundcraft_dsp::{Plugin, PluginEditor, PluginStateReader};
 use soundcraft_mix::{MAX_CHANNELS, MixEngine, PreparedInstance, StripMeter, itu_stereo_fold, main_channels};
@@ -557,6 +559,7 @@ impl Side {
 pub struct Player {
     shared: Arc<Shared>,
     fault: Arc<Fault>,
+    load: Arc<load::LoadMeter>,
     /// The block count last seen while playing, and since when.
     #[cfg(not(target_arch = "wasm32"))]
     watch: Mutex<Option<(u64, std::time::Instant)>>,
@@ -592,6 +595,17 @@ impl Player {
         p
     }
 
+    /// How much of each device callback's time the render takes, and how many were late
+    /// (dropouts). Not measured without a device.
+    pub fn audio_load(&self) -> AudioLoad {
+        self.load.snapshot()
+    }
+
+    /// Starts the worst load and the late count again.
+    pub fn reset_audio_load(&self) {
+        self.load.reset();
+    }
+
     /// What stopped the audio output, once: a bug in the engine, or the device went away or
     /// stopped asking for audio while playing.
     pub fn trouble(&self) -> Option<OutputTrouble> {
@@ -643,17 +657,19 @@ impl Player {
                         let (gtx, grx) = channel();
                         let state = AudioState::new(rx, Arc::clone(&shared), Arc::clone(&session), f64::from(rate), gtx);
                         let fault = Arc::new(Fault::default());
+                        let load = Arc::new(load::LoadMeter::default());
+                        let out = Output { channels, rate: f64::from(rate), fault: &fault, shared: &shared, load: &load };
                         let stream = match format {
-                            cpal::SampleFormat::I32 => build_output::<i32>(&device, *config, state, channels, &fault, &shared),
-                            cpal::SampleFormat::I16 => build_output::<i16>(&device, *config, state, channels, &fault, &shared),
-                            _ => build_output::<f32>(&device, *config, state, channels, &fault, &shared),
+                            cpal::SampleFormat::I32 => build_output::<i32>(&device, *config, state, &out),
+                            cpal::SampleFormat::I16 => build_output::<i16>(&device, *config, state, &out),
+                            _ => build_output::<f32>(&device, *config, state, &out),
                         }?;
-                        Ok(Opened { stream, tx, grx, fault })
+                        Ok(Opened { stream, tx, grx, fault, load })
                     },
                     |opened| start_stream(&opened.stream),
                 );
                 match opened {
-                    Some((config, Opened { stream, tx, grx, fault })) => {
+                    Some((config, Opened { stream, tx, grx, fault, load })) => {
                         let rate = config.sample_rate;
                         let channels = usize::from(config.channels);
                         let buffer = match config.buffer_size {
@@ -667,6 +683,7 @@ impl Player {
                         Player {
                             shared,
                             fault,
+                            load,
                             #[cfg(not(target_arch = "wasm32"))]
                             watch: Mutex::new(None),
                             tx,
@@ -745,6 +762,7 @@ impl Player {
         Player {
             shared,
             fault,
+            load: Arc::default(),
             watch: Mutex::new(None),
             tx,
             side,
@@ -769,6 +787,7 @@ impl Player {
         Player {
             shared,
             fault: Arc::default(),
+            load: Arc::default(),
             tx,
             side,
             _stream: None,
@@ -1057,6 +1076,7 @@ struct Opened {
     tx: Sender<Cmd>,
     grx: Receiver<Vec<Box<dyn Plugin>>>,
     fault: Arc<Fault>,
+    load: Arc<load::LoadMeter>,
 }
 
 /// Build and start the attempts in order; the first that builds and starts wins. A build error or
@@ -1077,28 +1097,37 @@ fn first_started<A: std::fmt::Debug, S, E: std::fmt::Display>(
     None
 }
 
-/// The output stream: the mix renders f32 into a scratch buffer allocated here (so the callback
-/// never allocates), converted to the device's sample type.
-fn build_output<T>(
-    device: &cpal::Device,
-    config: cpal::StreamConfig,
-    mut state: AudioState,
+/// What an output stream's callback shares with the player.
+struct Output<'a> {
     channels: usize,
-    fault: &Arc<Fault>,
-    shared: &Arc<Shared>,
-) -> Result<cpal::Stream, cpal::Error>
+    /// Device frames per second.
+    rate: f64,
+    fault: &'a Arc<Fault>,
+    shared: &'a Arc<Shared>,
+    load: &'a Arc<load::LoadMeter>,
+}
+
+/// The output stream: the mix renders f32 into a scratch buffer allocated here (so the callback
+/// never allocates), converted to the device's sample type. Each callback is timed for the load
+/// meter.
+fn build_output<T>(device: &cpal::Device, config: cpal::StreamConfig, mut state: AudioState, out: &Output) -> Result<cpal::Stream, cpal::Error>
 where
     T: cpal::SizedSample + cpal::FromSample<f32>,
 {
     use cpal::traits::DeviceTrait;
+    let (channels, rate) = (out.channels, out.rate);
     let frame = channels.max(1);
     let mut scratch = vec![0.0f32; 8192 * frame];
     let chunk = scratch.len();
-    let (failed, guard, gone) = (Arc::clone(fault), Arc::clone(shared), Arc::clone(fault));
+    let (failed, guard, gone) = (Arc::clone(out.fault), Arc::clone(out.shared), Arc::clone(out.fault));
+    let meter = Arc::clone(out.load);
+    let mut window = load::LoadWindow::default();
     device.build_output_stream(
         config,
         move |data: &mut [T], _: &cpal::OutputCallbackInfo| {
             mark_audio_thread();
+            #[cfg(not(target_arch = "wasm32"))]
+            let started = std::time::Instant::now();
             for out in data.chunks_mut(chunk) {
                 let Some(s) = scratch.get_mut(..out.len()) else { return };
                 guarded_block(&failed, &guard, s, |d| state.fill(d, channels));
@@ -1106,6 +1135,10 @@ where
                     *o = T::from_sample(*x);
                 }
             }
+            #[cfg(not(target_arch = "wasm32"))]
+            meter.record(&mut window, started.elapsed().as_secs_f64(), (data.len() / frame) as f64 / rate);
+            #[cfg(target_arch = "wasm32")]
+            let _ = (&meter, &mut window, rate);
         },
         // cpal may call this on the audio thread (on ALSA in a loop while the device stays
         // broken): marked, so the logger hands the record off instead of writing it.
diff --git a/crates/playback/src/load.rs b/crates/playback/src/load.rs
new file mode 100644
--- /dev/null
+++ b/crates/playback/src/load.rs
@@ -0,0 +1,156 @@
+//! The audio callback's load: how long each device callback takes to render, against how long its
+//! buffer lasts. The audio thread is the only writer and the UI reads; both lock-free and
+//! allocation-free.
+//!
+//! A callback that takes longer than its buffer lasts is *late*: the device ran out of audio
+//! before the callback returned, which is heard as a dropout. cpal doesn't report the device's
+//! own overload notices, so late callbacks are counted here instead.
+
+use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering::Relaxed};
+
+/// Audio the average and peak are taken over before they are published, in seconds.
+const WINDOW_S: f64 = 0.5;
+
+/// The callback load, for the UI and agents. Loads are render time over buffer duration: 1.0 is
+/// all of it.
+#[derive(Debug, Clone, Copy, PartialEq, Default, serde::Serialize)]
+pub struct AudioLoad {
+    /// False until a device callback has been timed (no device, the silent clock or the web).
+    pub measured: bool,
+    /// Averaged over the last half second of audio.
+    pub average: f32,
+    /// The heaviest callback of the last half second.
+    pub peak: f32,
+    /// The heaviest callback since the stream opened, or since the last reset.
+    pub worst: f32,
+    /// Callbacks that took longer than their buffer lasts (dropouts), since the stream opened or
+    /// the last reset.
+    pub late: u64,
+    /// Callbacks timed since the stream opened, or since the last reset.
+    pub callbacks: u64,
+    /// How long the last callback's buffer lasts, in milliseconds.
+    pub buffer_ms: f32,
+}
+
+/// The published load (f32 values as bits).
+#[derive(Default)]
+pub(crate) struct LoadMeter {
+    measured: AtomicBool,
+    average: AtomicU32,
+    peak: AtomicU32,
+    worst: AtomicU32,
+    late: AtomicU64,
+    callbacks: AtomicU64,
+    buffer_ms: AtomicU32,
+    /// Asked by the UI; the audio thread clears the counters at its next callback.
+    reset: AtomicBool,
+}
+
+/// The audio thread's own running window.
+#[derive(Debug, Default)]
+pub(crate) struct LoadWindow {
+    busy: f64,
+    audio: f64,
+    peak: f64,
+}
+
+impl LoadMeter {
+    /// One callback: `busy` seconds rendering `audio` seconds of output. Audio thread only.
+    pub(crate) fn record(&self, w: &mut LoadWindow, busy: f64, audio: f64) {
+        if !(audio > 0.0 && busy.is_finite() && busy >= 0.0) {
+            return;
+        }
+        if self.reset.swap(false, Relaxed) {
+            self.worst.store(0, Relaxed);
+            self.late.store(0, Relaxed);
+            self.callbacks.store(0, Relaxed);
+        }
+        let load = busy / audio;
+        self.callbacks.store(self.callbacks.load(Relaxed).saturating_add(1), Relaxed);
+        if load > 1.0 {
+            self.late.store(self.late.load(Relaxed).saturating_add(1), Relaxed);
+        }
+        if load as f32 > f32::from_bits(self.worst.load(Relaxed)) {
+            self.worst.store((load as f32).to_bits(), Relaxed);
+        }
+        self.buffer_ms.store(((audio * 1000.0) as f32).to_bits(), Relaxed);
+        self.measured.store(true, Relaxed);
+        w.busy += busy;
+        w.audio += audio;
+        w.peak = w.peak.max(load);
+        if w.audio >= WINDOW_S {
+            self.average.store(((w.busy / w.audio) as f32).to_bits(), Relaxed);
+            self.peak.store((w.peak as f32).to_bits(), Relaxed);
+            *w = LoadWindow::default();
+        }
+    }
+
+    pub(crate) fn snapshot(&self) -> AudioLoad {
+        let f = |a: &AtomicU32| f32::from_bits(a.load(Relaxed));
+        AudioLoad {
+            measured: self.measured.load(Relaxed),
+            average: f(&self.average),
+            peak: f(&self.peak),
+            worst: f(&self.worst),
+            late: self.late.load(Relaxed),
+            callbacks: self.callbacks.load(Relaxed),
+            buffer_ms: f(&self.buffer_ms),
+        }
+    }
+
+    /// Starts the worst load and the counts again (at the next callback).
+    pub(crate) fn reset(&self) {
+        self.reset.store(true, Relaxed);
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn averages_and_peaks_are_published_per_window_and_late_callbacks_counted() {
+        let m = LoadMeter::default();
+        let mut w = LoadWindow::default();
+        assert!(!m.snapshot().measured);
+        // 125 ms buffers: three at 20% and one late at 120% close a half-second window.
+        for busy in [0.025, 0.025, 0.15, 0.025] {
+            m.record(&mut w, busy, 0.125);
+        }
+        let s = m.snapshot();
+        assert!(s.measured);
+        assert!((s.average - 0.45).abs() < 1e-6, "{s:?}");
+        assert!((s.peak - 1.2).abs() < 1e-6, "{s:?}");
+        assert!((s.worst - 1.2).abs() < 1e-6, "{s:?}");
+        assert_eq!((s.late, s.callbacks), (1, 4));
+        assert!((s.buffer_ms - 125.0).abs() < 1e-3);
+
+        // Until the next window closes, the last one stays; then it is replaced. The worst load
+        // and the counts carry on.
+        for _ in 0..3 {
+            m.record(&mut w, 0.0125, 0.125);
+        }
+        assert!((m.snapshot().average - 0.45).abs() < 1e-6);
+        m.record(&mut w, 0.0125, 0.125);
+        let s = m.snapshot();
+        assert!((s.average - 0.1).abs() < 1e-6 && (s.peak - 0.1).abs() < 1e-6, "{s:?}");
+        assert!((s.worst - 1.2).abs() < 1e-6);
+        assert_eq!((s.late, s.callbacks), (1, 8));
+
+        m.reset();
+        m.record(&mut w, 0.0375, 0.125);
+        let s = m.snapshot();
+        assert_eq!((s.late, s.callbacks), (0, 1));
+        assert!((s.worst - 0.3).abs() < 1e-6);
+    }
+
+    #[test]
+    fn hostile_timings_are_ignored() {
+        let m = LoadMeter::default();
+        let mut w = LoadWindow::default();
+        for (busy, audio) in [(0.01, 0.0), (0.01, -1.0), (f64::NAN, 0.1), (f64::INFINITY, 0.1), (-0.01, 0.1), (0.01, f64::NAN)] {
+            m.record(&mut w, busy, audio);
+        }
+        assert_eq!(m.snapshot(), AudioLoad::default());
+    }
+}
diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs
--- a/crates/ui-egui/src/menus.rs
+++ b/crates/ui-egui/src/menus.rs
@@ -67,6 +67,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, Option<&str>)] = &[
     ("window.rtp", "MIDI Real-Time Properties", "", None),
     ("window.playback_engine", "Playback Engine", "", None),
     ("ui.audio_device", "Audio Device", "", None),
+    ("ui.audio_load", "Audio Load", "", None),
     ("window.io_setup", "I/O Setup", "", None),
     ("window.shortcuts", "Keyboard Shortcuts", "", None),
     ("view.mix_section", "Mix Window View", "", None),
@@ -518,6 +519,9 @@ pub fn run_ui_command(app: &mut SoundApp, id: &str, p: &Value) -> Option<Result<
     if id == "ui.audio_device" {
         return Some(crate::windows::audio_device_command(app, p));
     }
+    if id == "ui.audio_load" {
+        return Some(crate::windows::audio_load_command(app, p));
+    }
     let toggle = |v: &mut bool| {
         *v = p.get("value").and_then(Value::as_bool).unwrap_or(!*v);
         json!({"value": *v})
diff --git a/crates/ui-egui/src/windows.rs b/crates/ui-egui/src/windows.rs
--- a/crates/ui-egui/src/windows.rs
+++ b/crates/ui-egui/src/windows.rs
@@ -169,10 +169,27 @@ fn disk_usage(app: &mut SoundApp, ctx: &egui::Context) {
 
 fn system_usage(app: &mut SoundApp, ctx: &egui::Context) {
     let mut open = app.ui.show_system_usage;
-    win(ctx, &mut open, "System Usage", vec2(320.0, 140.0), |ui| {
+    win(ctx, &mut open, "System Usage", vec2(320.0, 200.0), |ui| {
         ui.label(format!("UI frame time: {:.1} ms", app.frame_ms));
         ui.label(format!("Audio: {}", app.player.as_ref().map_or("no engine".to_string(), |p| format!("{} @ {} Hz", p.device_name, p.device_rate))));
-        let plugins: usize = app.engine.session().tracks.iter().map(|t| t.mixer.inserts.iter().flatten().count()).sum();
+        let load = app.player.as_ref().map(|p| p.audio_load()).unwrap_or_default();
+        if load.measured {
+            ui.label(format!("Audio load: {:.0}% (peak {:.0}%) of {:.1} ms", load.average * 100.0, load.peak * 100.0, load.buffer_ms));
+            ui.add(egui::ProgressBar::new(load.peak.clamp(0.0, 1.0)).desired_height(6.0));
+            ui.horizontal(|ui| {
+                ui.label(format!("Late callbacks (dropouts): {} · worst {:.0}%", load.late, load.worst * 100.0));
+                if ui.small_button("Reset").clicked()
+                    && let Some(p) = &app.player
+                {
+                    p.reset_audio_load();
+                }
+            });
+            ctx.request_repaint_after(std::time::Duration::from_millis(250));
+        } else {
+            ui.label("Audio load: not measured");
+        }
+        let session = app.engine.session();
+        let plugins: usize = session.tracks.iter().map(|t| t.mixer.inserts.iter().flatten().count() + usize::from(t.instrument.is_some())).sum();
         ui.label(format!("Active plugin instances: {plugins}"));
         ui.label(format!("Tracks: {}", app.engine.session().tracks.len()));
         ui.label(format!("Undo steps: {}", app.engine.undo_history().len()));
@@ -587,6 +604,22 @@ pub fn playback_engine(app: &mut SoundApp, ctx: &egui::Context) {
     app.ui.show_playback_engine = open;
 }
 
+/// `ui.audio_load`: `{reset?: bool}` → the audio callback's load (`soundcraft_playback::AudioLoad`):
+/// render time over buffer duration, averaged (`average`) and at its heaviest (`peak`) over the
+/// last half second, the heaviest (`worst`) and the callbacks that ran past their buffer (`late`,
+/// dropouts) since the stream opened. `reset: true` starts `worst` and the counts again after
+/// returning them. Without an audio device `measured` is false.
+pub fn audio_load_command(app: &mut SoundApp, p: &serde_json::Value) -> Result<serde_json::Value, String> {
+    let Some(player) = &app.player else {
+        return serde_json::to_value(soundcraft_playback::AudioLoad::default()).map_err(|e| e.to_string());
+    };
+    let load = player.audio_load();
+    if p.get("reset").and_then(serde_json::Value::as_bool) == Some(true) {
+        player.reset_audio_load();
+    }
+    serde_json::to_value(load).map_err(|e| e.to_string())
+}
+
 /// `ui.audio_device`: `{list?: bool}` returns the audio systems and their devices;
 /// `{system?, output?, input?, sample_rate?, buffer_size?}` (a `null` value means "default") sets
 /// the choice and reopens the devices. Always returns the choice and the open device.
diff --git a/crates/ui-egui/tests/it/audio_load.rs b/crates/ui-egui/tests/it/audio_load.rs
new file mode 100644
--- /dev/null
+++ b/crates/ui-egui/tests/it/audio_load.rs
@@ -0,0 +1,16 @@
+//! `ui.audio_load`: the audio callback's load, for agents.
+use serde_json::json;
+use soundcraft_ui_egui::{Services, SoundApp};
+
+#[test]
+fn the_audio_load_is_unmeasured_without_an_audio_engine() {
+    let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default());
+    for p in [json!({}), json!({"reset": true})] {
+        let r = app.run("ui.audio_load", p).unwrap();
+        assert_eq!(r["measured"], false, "{r}");
+        for key in ["average", "peak", "worst", "late", "callbacks", "buffer_ms"] {
+            assert!(r[key].is_number(), "{key}: {r}");
+        }
+    }
+    assert!(soundcraft_ui_egui::menus::UI_COMMANDS.iter().any(|c| c.0 == "ui.audio_load"), "listed for agents");
+}
diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs
--- a/crates/ui-egui/tests/it/main.rs
+++ b/crates/ui-egui/tests/it/main.rs
@@ -5,6 +5,7 @@
 
 mod appearance;
 mod audio_device;
+mod audio_load;
 mod auto_rename;
 mod bounce;
 mod bounce_dialog;
diff --git a/docs/gaps.md b/docs/gaps.md
--- a/docs/gaps.md
+++ b/docs/gaps.md
@@ -1,6 +1,6 @@
 # Where SoundCraft falls short of Pro Tools
 
-> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-10-25 · **Change:** trivial (G-04: Clip Looping crossfade and loop count; previously trivial: G-08 and G-20: built-in plugin count, the sampler shipped) · **Target:** Avid Pro Tools Ultimate 2026.4.1
+> **Last reviewed:** 2026-10-19 · **Last updated:** 2026-10-25 · **Change:** trivial (G-01: audio load and dropout meter; G-04: Clip Looping crossfade and loop count) · **Target:** Avid Pro Tools Ultimate 2026.4.1
 
 Every known shortfall, one entry each, ranked by how much it stops a Pro Tools user from doing
 real work in SoundCraft. This is the work list: pick from the top. Rows marked **alpha blocker**
@@ -46,6 +46,8 @@ When you close a gap, delete its entry (or shrink it) in the same PR, and bump t
   device, sample rate and device buffer size (kept with the preferences, per machine; `ui.audio_device`
   for agents); f32 and 16/32-bit integer devices; ASIO behind the opt-in `asio` build feature
   (verified on Windows with an SSL 2+ at 128 samples).
+- **Done (2026-10-25):** Window › System Usage shows the audio callback's load (render time over
+  buffer duration, average and peak) and counts late callbacks (dropouts); `ui.audio_load` for agents.
 - **Missing:** routing to physical channels (I/O Setup), one duplex stream on one clock (input and
   output are still separate cpal streams), latency reporting and record-offset compensation, ASIO
   in official builds (owner decision: Steinberg's SDK is proprietary or GPLv3), JACK, and
@@ -226,6 +228,7 @@ missing. Out of scope unless the owner decides otherwise.
 | Date | Change | Summary |
 |---|---|---|
 | 2026-10-25 | trivial | G-04: Clip Looping (#1030) lacks the loop crossfade option, and Number of Loops stops at 512 while length and fill make up to 1000 loops |
+| 2026-10-25 | trivial | G-01: Window › System Usage shows the audio callback load and late callbacks (dropouts); `ui.audio_load` |
 | 2026-10-24 | trivial | G-08: 27 processors + 3 instruments, as the plugin registry holds; G-20: the sampler shipped (#108), so it is no longer listed as missing |
 | 2026-10-19 | trivial | G-04: Float32 encoding retains finite headroom (#791); integer clipping and non-finite handling retained |
 | 2026-10-18 | trivial | G-04: mono device stereo/surround fold-down fixed and regression-tested; estimates unchanged |
diff --git a/docs/hardware-parity.md b/docs/hardware-parity.md
--- a/docs/hardware-parity.md
+++ b/docs/hardware-parity.md
@@ -1,6 +1,6 @@
 # Hardware parity
 
-> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (bounces of a bus fed by an aux compensate its latency (#246); previously minor: audio device, rate and buffer choice; ASIO feature) · **Target:** Avid Pro Tools Ultimate 2026.4.1
+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** trivial (audio callback load and dropout meter; previously trivial: bounces of a bus fed by an aux compensate its latency (#246); previously minor: audio device, rate and buffer choice; ASIO feature) · **Target:** Avid Pro Tools Ultimate 2026.4.1
 
 A DAW is judged by how it drives audio interfaces, MIDI gear, control surfaces and sync. This is
 SoundCraft's weakest dimension. Ours is measured from `crates/playback` (cpal) and the absence of
@@ -21,6 +21,7 @@ Legend: ✅ works · 🟡 partial · ❌ missing.
 | Buffer size (32–2048) | ✅ | 🟡 device buffer when the driver allows it (not yet verified); mix in 512-sample blocks | 🟡 device buffer 16–8192 (verified 128 on ASIO, 256 on WASAPI); mix in 512-sample blocks | 🟡 (not yet verified) | 🟡 | ❌ |
 | Duplex stream on one clock | ✅ | ❌ separate input/output streams | ❌ | ❌ | ❌ | ❌ |
 | Hardware I/O mapping (I/O Setup to physical channels) | ✅ | 🟡 multichannel output in order; no per-channel map | 🟡 | 🟡 | 🟡 | ❌ |
+| Audio load and dropout meter (System Usage) | ✅ | 🟡 (not yet verified) | 🟡 (not yet verified) | 🟡 (not yet verified) | 🟡 (not yet verified) | ❌ not measured |
 | Low-latency monitoring | ✅ | ❌ (`options.low_latency_monitoring` is a stored flag) | ❌ | ❌ | ❌ | ❌ |
 | Latency reporting and record offset compensation | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
 | Sample rate follows the session; resample when it can't | ✅ (switches device) | 🟡 resamples (input pitch bug fixed, #28) | 🟡 | 🟡 | 🟡 | 🟡 |
@@ -72,6 +73,7 @@ Legend: ✅ works · 🟡 partial · ❌ missing.
 
 | Date | Change | Summary |
 |---|---|---|
+| 2026-10-25 | trivial | Audio callback load (average, peak, worst) and late callbacks (dropouts) in System Usage; `ui.audio_load` |
 | 2026-10-13 | trivial | bounces of a bus fed by an aux compensate its latency (#246) |
 | 2026-10-12 | minor | Audio device settings: system, output, input, sample rate and buffer size; opt-in ASIO |
 | 2026-10-10 | major | Created: audit of audio interface, MIDI, control-surface, sync and performance support against Pro Tools 2026.4.1 |
