diff --git a/crates/playback/src/devices.rs b/crates/playback/src/devices.rs
--- a/crates/playback/src/devices.rs
+++ b/crates/playback/src/devices.rs
@@ -96,7 +96,7 @@ pub fn audio_systems() -> Vec<AudioSystem> {
     cpal::available_hosts()
         .into_iter()
         .filter_map(|id| {
-            let host = cpal::host_from_id(id).ok()?;
+            let host = host_for(id)?;
             let def_out = host.default_output_device().as_ref().and_then(device_name);
             let def_in = host.default_input_device().as_ref().and_then(device_name);
             let outputs = host.output_devices().map(|ds| ds.filter_map(|d| describe(&d, def_out.as_deref(), true)).collect()).unwrap_or_default();
@@ -106,22 +106,54 @@ pub fn audio_systems() -> Vec<AudioSystem> {
         .collect()
 }
 
+/// An audio system handle: ASIO's is shared by the whole process, the others are made per call.
+pub(crate) enum HostRef {
+    Owned(cpal::Host),
+    Shared(&'static cpal::Host),
+}
+
+impl std::ops::Deref for HostRef {
+    type Target = cpal::Host;
+    fn deref(&self) -> &cpal::Host {
+        match self {
+            HostRef::Owned(h) => h,
+            HostRef::Shared(h) => h,
+        }
+    }
+}
+
+/// The host for `id`. ASIO gets one host for the process: asio-sys remembers the loaded driver
+/// per host, so a second host would load and initialise the driver again under the streams
+/// already running on it (and drop it again with its last device), silencing them.
+fn host_for(id: cpal::HostId) -> Option<HostRef> {
+    if id.name().eq_ignore_ascii_case("ASIO") {
+        static ASIO: std::sync::OnceLock<Option<cpal::Host>> = std::sync::OnceLock::new();
+        return ASIO.get_or_init(|| cpal::host_from_id(id).ok()).as_ref().map(HostRef::Shared);
+    }
+    cpal::host_from_id(id).ok().map(HostRef::Owned)
+}
+
 /// The chosen audio system, else the default one.
-pub(crate) fn host(cfg: &AudioConfig) -> cpal::Host {
+pub(crate) fn host(cfg: &AudioConfig) -> HostRef {
     cfg.system
         .as_deref()
         .and_then(|want| cpal::available_hosts().into_iter().find(|id| id.name().eq_ignore_ascii_case(want)))
-        .and_then(|id| cpal::host_from_id(id).ok())
+        .and_then(host_for)
         .unwrap_or_else(|| {
             if let Some(s) = &cfg.system {
                 log::warn!("audio system `{s}` is not available; using the default");
             }
-            cpal::default_host()
+            host_for(cpal::default_host().id()).unwrap_or_else(|| HostRef::Owned(cpal::default_host()))
         })
 }
 
-/// The named output (or input) device of `host`, else its default device.
-pub(crate) fn device(host: &cpal::Host, name: Option<&str>, output: bool) -> Option<cpal::Device> {
+/// The named output (or input) device of `host`, else its default device, and the lease to keep
+/// with the stream opened from it (see [`shared_asio`]).
+pub(crate) fn device(host: &cpal::Host, name: Option<&str>, output: bool) -> Option<(cpal::Device, DeviceLease)> {
+    shared_asio(host, find_device(host, name, output))
+}
+
+fn find_device(host: &cpal::Host, name: Option<&str>, output: bool) -> Option<cpal::Device> {
     if let Some(want) = name {
         let found = if output { host.output_devices().ok() } else { host.input_devices().ok() }
             .and_then(|mut ds| ds.find(|d| device_name(d).as_deref() == Some(want)));
@@ -133,6 +165,63 @@ pub(crate) fn device(host: &cpal::Host, name: Option<&str>, output: bool) -> Opt
     if output { host.default_output_device() } else { host.default_input_device() }
 }
 
+/// One device value per ASIO driver for the whole process. An ASIO driver has a single set of
+/// buffers for input and output together, and cpal keeps the record of that set in the device
+/// value: an input opened from a second value of the same driver would recreate the buffers for
+/// input only, leaving the output stream (opened from the first value) writing into buffers the
+/// driver has disposed, and silent. Sharing the value makes cpal create both sides together.
+///
+/// The value is kept only while a lease on it lives: ASIO loads one driver per process, and a
+/// value kept for good would keep its driver loaded, hiding every other driver from the device
+/// lists and from a later choice in the settings.
+fn shared_asio(host: &cpal::Host, found: Option<cpal::Device>) -> Option<(cpal::Device, DeviceLease)> {
+    let d = found?;
+    if !host.id().name().eq_ignore_ascii_case("ASIO") {
+        return Some((d, DeviceLease(None)));
+    }
+    let Some(name) = device_name(&d) else { return Some((d, DeviceLease(None))) };
+    let d = acquire(&mut shared_devices(), &name, d);
+    Some((d, DeviceLease(Some(name))))
+}
+
+/// One kept device value per driver name, with the number of leases on it.
+type SharedDevices = Vec<(String, cpal::Device, usize)>;
+
+fn shared_devices() -> std::sync::MutexGuard<'static, SharedDevices> {
+    static SHARED: std::sync::OnceLock<std::sync::Mutex<SharedDevices>> = std::sync::OnceLock::new();
+    SHARED.get_or_init(Default::default).lock().unwrap_or_else(std::sync::PoisonError::into_inner)
+}
+
+/// The kept value for `name` (else `found`, kept from now on), with one more lease on it.
+fn acquire<D: Clone>(shared: &mut Vec<(String, D, usize)>, name: &str, found: D) -> D {
+    if let Some((_, kept, leases)) = shared.iter_mut().find(|(n, ..)| n == name) {
+        *leases = leases.saturating_add(1);
+        return kept.clone();
+    }
+    shared.push((name.to_string(), found.clone(), 1));
+    found
+}
+
+/// Keeps the shared value of an ASIO driver while the stream opened from it lives (nothing for
+/// other systems). Dropping the last lease on a driver forgets the value, so the driver can unload.
+#[must_use]
+pub(crate) struct DeviceLease(Option<String>);
+
+impl Drop for DeviceLease {
+    fn drop(&mut self) {
+        let Some(name) = self.0.take() else { return };
+        release(&mut shared_devices(), &name);
+    }
+}
+
+/// One lease on `name` ends; the last one removes its value.
+fn release<D>(shared: &mut Vec<(String, D, usize)>, name: &str) {
+    if let Some((_, _, leases)) = shared.iter_mut().find(|(n, ..)| n == name) {
+        *leases = leases.saturating_sub(1);
+    }
+    shared.retain(|(_, _, leases)| *leases > 0);
+}
+
 /// The buffer to request: the chosen size when the device's range allows it, else the default.
 pub(crate) fn buffer_size(want: Option<u32>, supported: &cpal::SupportedBufferSize) -> cpal::BufferSize {
     match (want, supported) {
@@ -191,10 +280,26 @@ mod tests {
         assert_eq!(host(&cfg).id(), cpal::default_host().id());
         // No device with this name exists: the default (or none, on a machine without audio).
         let h = cpal::default_host();
-        let fallback = device(&h, Some("No Such Device 1234"), true).as_ref().and_then(device_name);
+        let fallback = device(&h, Some("No Such Device 1234"), true).and_then(|(d, _lease)| device_name(&d));
         assert_eq!(fallback, h.default_output_device().as_ref().and_then(device_name));
     }
 
+    /// Input and output of one driver share a value while either stream is open; once both
+    /// close the value is forgotten, so another driver can load.
+    #[test]
+    fn a_shared_driver_is_released_with_its_last_stream() {
+        let mut shared: Vec<(String, u32, usize)> = Vec::new();
+        assert_eq!(acquire(&mut shared, "A", 1), 1, "output opens A");
+        assert_eq!(acquire(&mut shared, "A", 2), 1, "input reuses the output's value");
+        release(&mut shared, "A");
+        assert_eq!(shared, [("A".to_string(), 1, 1)], "the input still holds A");
+        release(&mut shared, "A");
+        assert!(shared.is_empty(), "A is no longer kept");
+        assert_eq!(acquire(&mut shared, "B", 3), 3, "B is kept fresh");
+        release(&mut shared, "missing");
+        assert_eq!(shared.len(), 1);
+    }
+
     #[test]
     fn enumeration_does_not_fail() {
         let systems = audio_systems();
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
@@ -465,6 +465,9 @@ pub struct Player {
     _stream: Option<cpal::Stream>,
     #[cfg(target_arch = "wasm32")]
     _stream: Option<cpal::Stream>,
+    /// Keeps the stream's device value shared while the stream lives (declared after it, so the
+    /// stream closes first).
+    _lease: Option<devices::DeviceLease>,
     pub device_name: String,
     pub device_rate: u32,
     /// Output channels the device was opened with (0 for the silent clock).
@@ -520,7 +523,7 @@ impl Player {
         let side_rate = session.sample_rate.as_f64() as f32;
         let new_side = |garbage: Receiver<Vec<Box<dyn Plugin>>>| Mutex::new(Side::new(side_rate, garbage));
         match open_device(want, want_ch, cfg) {
-            Ok((device, config, format, name)) => {
+            Ok((device, lease, config, format, name)) => {
                 // The chosen buffer size first; if the device refuses it, its default.
                 let mut attempts = vec![config.clone()];
                 if config.buffer_size != cpal::BufferSize::Default {
@@ -564,6 +567,7 @@ impl Player {
                             tx,
                             side: new_side(grx),
                             _stream: Some(stream),
+                            _lease: Some(lease),
                             device_name: name,
                             device_rate: rate,
                             device_channels: config.channels,
@@ -641,6 +645,7 @@ impl Player {
             tx,
             side,
             _stream: None,
+            _lease: None,
             device_name: "No audio device (silent)".into(),
             device_rate: rate,
             device_channels: 0,
@@ -664,6 +669,7 @@ impl Player {
             tx,
             side,
             _stream: None,
+            _lease: None,
             device_name: "No audio device".into(),
             device_rate: session.sample_rate.hz(),
             device_channels: 0,
@@ -964,10 +970,14 @@ where
 /// rate) if possible, in the best sample format it offers (f32, else 32- or 16-bit integers),
 /// with as many channels as it offers up to `want_ch` (at least stereo when available), and the
 /// chosen buffer size when the device allows it.
-fn open_device(want_rate: u32, want_ch: u16, cfg: &AudioConfig) -> Result<(cpal::Device, cpal::StreamConfig, cpal::SampleFormat, String), String> {
+fn open_device(
+    want_rate: u32,
+    want_ch: u16,
+    cfg: &AudioConfig,
+) -> Result<(cpal::Device, devices::DeviceLease, cpal::StreamConfig, cpal::SampleFormat, String), String> {
     use cpal::traits::DeviceTrait;
     let host = devices::host(cfg);
-    let device = devices::device(&host, cfg.output.as_deref(), true).ok_or_else(|| "no output device".to_string())?;
+    let (device, lease) = devices::device(&host, cfg.output.as_deref(), true).ok_or_else(|| "no output device".to_string())?;
     #[allow(deprecated)]
     let name = device.name().unwrap_or_else(|_| "Audio device".into());
     // Prefer a config at the chosen (else session) rate; else the device default (we resample).
@@ -1015,7 +1025,7 @@ fn open_device(want_rate: u32, want_ch: u16, cfg: &AudioConfig) -> Result<(cpal:
             (c, format)
         }
     };
-    Ok((device, config, format, name))
+    Ok((device, lease, config, format, name))
 }
 
 #[cfg(test)]
diff --git a/crates/playback/src/record.rs b/crates/playback/src/record.rs
--- a/crates/playback/src/record.rs
+++ b/crates/playback/src/record.rs
@@ -128,6 +128,9 @@ pub struct Recorder {
     pub monitor: Arc<InputRing>,
     #[cfg(not(target_os = "freebsd"))]
     _stream: Option<cpal::Stream>,
+    /// Keeps the stream's device value shared while the stream lives (after it: dropped later).
+    #[cfg(not(target_os = "freebsd"))]
+    _lease: Option<crate::devices::DeviceLease>,
     pub device_name: String,
     pub sample_rate: u32,
     pub channels: usize,
@@ -201,7 +204,7 @@ impl Recorder {
     pub fn open_with(audio: &crate::AudioConfig) -> Result<Recorder, String> {
         use cpal::traits::{DeviceTrait, StreamTrait};
         let host = crate::devices::host(audio);
-        let device = crate::devices::device(&host, audio.input.as_deref(), false).ok_or("no audio input device")?;
+        let (device, lease) = crate::devices::device(&host, audio.input.as_deref(), false).ok_or("no audio input device")?;
         #[allow(deprecated)]
         let name = device.name().unwrap_or_else(|_| "Input".into());
         // At the chosen rate when the device offers it, so input and output run together: the
@@ -245,7 +248,17 @@ impl Recorder {
         };
         stream.play().map_err(|e| e.to_string())?;
         let seen = (0, Instant::now());
-        Ok(Recorder { monitor, shared, _stream: Some(stream), device_name: name, sample_rate: config.sample_rate.0, channels, writer: None, seen })
+        Ok(Recorder {
+            monitor,
+            shared,
+            _stream: Some(stream),
+            _lease: Some(lease),
+            device_name: name,
+            sample_rate: config.sample_rate.0,
+            channels,
+            writer: None,
+            seen,
+        })
     }
 
     #[cfg(target_os = "freebsd")]
@@ -469,6 +482,8 @@ mod tests {
             monitor: InputRing::new(1, 48_000),
             #[cfg(not(target_os = "freebsd"))]
             _stream: None,
+            #[cfg(not(target_os = "freebsd"))]
+            _lease: None,
             device_name: "Mic In".into(),
             sample_rate: 48_000,
             channels: 1,
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -812,6 +812,9 @@ impl SoundApp {
         // The input reopens on the next record or monitor with the new choice.
         self.recorder = None;
         self.recorder_failed = false;
+        // Closed before the new one opens: an ASIO driver stays loaded while a stream uses it,
+        // and only one can be loaded at a time.
+        self.player = None;
         let player = soundcraft_playback::Player::with_config(self.engine.session_arc(), &self.ui.audio);
         player.locate(at);
         self.player = Some(player);
@@ -907,6 +910,8 @@ impl SoundApp {
         }
         self.recorder = None;
         self.recorder_failed = false;
+        // Closed before the new one opens (see `apply_audio_config`).
+        self.player = None;
         self.player = Some(Player::with_config(self.engine.session_arc(), &self.ui.audio));
     }
 
