| name | matlab-play-record-audio |
| description | Reference for MATLAB audiostreamer (Audio Toolbox R2025a+). Without this skill, agents consistently default to legacy audioDeviceWriter/audioDeviceReader or base MATLAB sound(), producing less capable code. Use when writing code for audio playback, recording, full-duplex device I/O, real-time audio measurements, or audio I/O processing with callbacks. Also use when debugging audiostreamer errors, dropouts, or latency issues, or migrating from audioDeviceReader, audioDeviceWriter, audioPlayerRecorder, or audioplayer/audiorecorder. |
| license | https://www.mathworks.com/content/dam/mathworks/license/pmrl/license.md |
| metadata | {"author":"MathWorks","version":"1.0"} |
audiostreamer — MATLAB Audio Device I/O (R2025a+)
audiostreamer is the unified replacement for audioDeviceWriter, audioDeviceReader, and audioPlayerRecorder. It provides player-only, recorder-only, or full-duplex modes with callbacks, pre-buffering, transport control, and measurement helpers.
Version requirements: audiostreamer requires Audio Toolbox R2025a or later. The start and write methods were added in R2026a.
When to Use
- Playing audio through a sound card or USB audio device
- Recording audio from a microphone or audio interface
- Full-duplex playback + recording (e.g., acoustic measurements, loopback tests)
- Listing or selecting audio devices and drivers
- Any workflow that involves audio hardware I/O in MATLAB
When NOT to Use
- Playing a single isolated sound —
sound or soundsc is fine for one-shot playback of a short clip with no sequencing. For sequential playback (e.g., before/after comparison), use audiostreamer — its play() calls queue automatically, whereas overlapping sound calls play simultaneously.
- Code generation (codegen) —
audiostreamer does not yet support codegen; use legacy APIs if targeting codegen
- Simulink models — Simulink still uses the existing audio I/O blocks, not
audiostreamer
- Audio Toolbox not available — fall back to
sound/soundsc, audioplayer/audiorecorder, or audioDeviceWriter (DSP System Toolbox) if the user lacks Audio Toolbox
- File I/O only — reading/writing audio files without device playback or recording uses
audioread/audiowrite, not this skill
- DAQ hardware — National Instruments or similar data acquisition devices use DAQ Toolbox and the
daq object
- MIDI-only devices — MIDI control uses
mididevice/midicontrols, not audiostreamer
Construction
Use Name-Value pairs for Mode and SampleRate (positional shorthand exists but does not support tab-completion):
The default Mode is "player". You MUST set Mode explicitly if recording — either Mode="recorder" or Mode="full-duplex". Mode is not inferred from other properties like Recorder or RecorderChannels.
as = audiostreamer % default: player mode, 44100 Hz
as = audiostreamer(Mode="player", SampleRate=fs) % player at fs Hz
as = audiostreamer(Mode="recorder", SampleRate=fs) % recorder at fs Hz
as = audiostreamer(Mode="full-duplex", SampleRate=fs) % simultaneous play + record
as = audiostreamer(Mode="full-duplex", SampleRate=48000, Driver="ASIO", ...
Player="Focusrite USB ASIO", Recorder="Focusrite USB ASIO", ...
PlayerChannels=[1 2], RecorderChannels=[1 2])
Properties
Device Configuration (set BEFORE streaming starts)
| Property | Type | Default | Notes |
|---|
Mode | "player" / "recorder" / "full-duplex" | "player" | Set at construction or via property |
Driver | "DirectSound" / "ASIO" / "WASAPI" (Win); "CoreAudio" (Mac); "ALSA" (Linux) | OS default | Only set on Windows (Mac/Linux have one driver each). Setting at construction selects the default device for that driver. |
Player | string | System default for driver | Output device name. Omit to use the default device for the selected driver. |
Recorder | string | System default for driver | Input device name. Omit to use the default device for the selected driver. |
SampleRate | positive scalar | 44100 | Hz |
DeviceBufferSize | positive int or "auto" | "auto" | Fixed for ASIO (use asiosettings). |
DeviceBitFormat | "single" / "int24" / "int16" | "int24" | int16 on ASIO silently uses int24 |
PlayerChannels | row vector or "auto" | "auto" | 1-based mapping. "auto" upmixes mono→stereo; for N≥2 channels, opens N channels on the device |
RecorderChannels | row vector | 1 | 1-based mapping. Records 1 channel by default — set e.g. 1:2 for stereo |
ExclusiveMode | on/off | "on" | WASAPI only — disables OS mixing/resampling |
|
IMPORTANT: Mode, SampleRate, Driver, DeviceBufferSize, DeviceBitFormat, ExclusiveMode, ConstantLatency, PlayerChannels, and RecorderChannels lock once streaming starts. Call release(as) before changing any of these properties to avoid an automatic release with a warning.
Callback Properties
| Property | Signature | Trigger |
|---|
PlayerFcn | @(obj, event) | Player buffer drops below PlayerMinSamples |
PlayerMinSamples | positive int (default 16384) | Threshold for PlayerFcn trigger |
RecorderFcn | @(obj, event) | Recorder buffer exceeds RecorderMinSamples |
RecorderMinSamples | positive int (default 1024) | Threshold for RecorderFcn trigger |
PlayerCompletedFcn | @(obj, event) | Output queue empties |
RecorderCompletedFcn | @(obj, event) | Fixed-length recording finishes |
PlayerUnderrunFcn | @(obj, event) | Player underrun occurs |
ALL callbacks MUST accept exactly 2 arguments. First arg = the audiostreamer object. Second arg = event struct with .Type field. Use @(obj, ~) if you don't need the event.
Event struct fields by type:
PlayerFcn: event.Type = "Player", event.NumPlayerSamples
RecorderFcn: event.Type = "Recorder", event.NumRecorderSamples
PlayerCompletedFcn: event.Type = "PlayerCompleted", event.StreamTime
RecorderCompletedFcn: event.Type = "RecorderCompleted", event.StreamTime
PlayerUnderrunFcn: event.Type = "PlayerUnderrun", event.SamplesUnderrun
Read-Only Status
| Property | Description |
|---|
NumPlayerSamples | Samples currently queued in output buffer |
NumRecorderSamples | Samples available to read() without blocking |
MaxPlayerChannels | Max output channels on selected device |
MaxRecorderChannels | Max input channels on selected device |
Methods
Playback
| Method | Description |
|---|
play(obj, x) | Queue x and play. Blocks until output buffer <= PlayerMinSamples (up to PlayerMinSamples samples remain unplayed when it returns). Call waitfor(as) after the last play to ensure complete playback before release. |
play(obj, x, "non-blocking") | Queue x and return immediately regardless of buffer level. |
play(obj) | Start PlayerFcn callback loop (no data argument). |
write(obj, x) | [R2026a+] Queue x to output buffer WITHOUT starting playback. Use with start(). |
write(obj, x, "non-blocking") | [R2026a+] Queue x and return immediately regardless of buffer level. |
Recording
| Method | Description |
|---|
record(obj) | Start recording indefinitely. Warns if unread samples remain in the buffer. To avoid: stop(as) (or stop(as, "recorder") in full-duplex), then read(as) to flush. Not needed if samples were already consumed by a callback or read. |
record(obj, numSamples) | Record exactly numSamples then stop. Same unread-samples warning applies. |
read(obj) | Return all available recorded samples immediately (non-blocking). Returns empty if none available. |
read(obj, numSamples) | Blocks until numSamples available, then returns them. |
Full-Duplex
| Method | Description |
|---|
playrec(obj, x) | Play x and record simultaneously. Non-blocking — recording continues in the background; retrieve data with read. |
playrec(obj, x, numSamples) | Play x and record numSamples. Blocking — returns recorded matrix. |
playrec(obj) | Start callback-driven full-duplex (requires RecorderFcn and/or PlayerFcn). |
playrec pauses both player and recorder, queues audio, then resumes both simultaneously for repeatable latency. This is critical for measurements with impzest.
Transport Control
| Method | Description |
|---|
start(obj) | [R2026a+] Start streaming in current mode. |
start(obj, Mode="player") | [R2026a+] Start only player (full-duplex). |
start(obj, Mode="recorder", SamplesToRecord=N) | [R2026a+] Start recorder with fixed count. |
stop(obj) | Stop all streaming. Preserves unread input samples. Resets underrun count (as does getUnderrunCount). |
stop(obj, "player"/"recorder"/"both") | Stop specific side. |
pause(obj) / pause(obj, "player"/"recorder"/"both") | Pause with state preservation. |
resume(obj) / resume(obj, "player"/"recorder"/"both") | Resume from pause. |
waitfor(obj) / waitfor(obj, "player"/"recorder"/"both") | Block until complete. |
release(obj) | Stop, flush, close device, tear down. Deletes unread samples. |
Query / Diagnostics
| Method | Description |
|---|
isPlaying(obj) | Returns OnOffSwitchState |
isRecording(obj) | Returns OnOffSwitchState |
isPlayerPaused(obj) | Returns OnOffSwitchState |
isRecorderPaused(obj) | Returns OnOffSwitchState |
getUnderrunCount(obj) | Underrun sample count since last call. Resets counter (as does stop). |
getStreamTime(obj) | Elapsed stream time in seconds. |
getStreamTime(obj, "reset") | Reset stream timer. |
measureLoopbackLatency(obj) | Full-duplex only, single channel. Returns delay in samples. |
Static Device Enumeration
audiostreamer.getDrivers() % Available drivers for this OS
audiostreamer.getPlayerNames() % All output devices
audiostreamer.getPlayerNames("ASIO") % Output devices for specific driver
audiostreamer.getRecorderNames() % All input devices
audiostreamer.getRecorderNames("ASIO") % Input devices for specific driver
audiostreamer.getAudioDevices() % Struct array: Name, Driver, MaxRecorderChannels, MaxPlayerChannels, SampleRate (channel counts are int32)
Note: getAudioDevices() returns int32 for MaxRecorderChannels and MaxPlayerChannels. Cast to double() before using these values in UI components (e.g., uispinner Limits) or arithmetic that expects double.
CRITICAL: There is NO setup() Method
The audiostreamer does NOT have a public setup() method. Device initialization happens implicitly on the first play(), record(), playrec(), or start() call. Do NOT call setup() — it will error.
If PlayerFcn is set, the first streaming call invokes it repeatedly to pre-buffer at least 8192 samples (or PlayerMinSamples, whichever is greater) before the device opens.
Common Patterns
Pattern 1: Simple Blocking Measurement (Sweep + IR)
as = audiostreamer(Mode="full-duplex", SampleRate=48000, ...
PlayerChannels=1, RecorderChannels=1);
x = sweeptone(2, 1, 48000);
y = playrec(as, x, size(x, 1)); % blocking: returns recorded audio
underruns = getUnderrunCount(as);
ir = impzest(x, y);
release(as);
Pattern 2: Non-Blocking Play + Record with waitfor
as = audiostreamer(Mode="full-duplex", SampleRate=48000);
x = sweeptone(3, 2, 48000);
playrec(as, x); % non-blocking (no output arg)
waitfor(as); % block until done
y = read(as); % retrieve recorded data
release(as);
Pattern 3: Pre-Buffered Playback (write + start) — R2026a+
Use write+start when you need to control exactly when playback begins (e.g., synchronized full-duplex start). For simple playback, play(as, signal) achieves the same result — it queues and starts automatically.
as = audiostreamer(Mode="player", SampleRate=48000);
write(as, signal); % queue without starting
start(as); % begin playback
waitfor(as); % wait for completion
release(as);
Pattern 4: Callback-Driven Streaming Player
as = audiostreamer(Mode="player", SampleRate=48000, DeviceBufferSize=1024);
gen = dsp.ColoredNoise("pink", NumChannels=2, SamplesPerFrame=1024);
as.PlayerFcn = @(obj, ~) play(obj, gen());
as.PlayerMinSamples = 4096;
play(as); % or start(as) [R2026a+] — begins callback loop
% ... later ...
stop(as);
release(as);
Pattern 5: Callback-Driven Level Metering (Recorder)
as = audiostreamer(Mode="recorder", SampleRate=48000);
as.RecorderFcn = @(obj, ~) updateMeter(read(obj));
as.RecorderMinSamples = 1024;
start(as); % or record(as) before R2026a
% ... meter updates in background ...
stop(as);
release(as);
Pattern 6: Frame-at-a-Time Processing Loop (Full-Duplex)
The most direct replacement for legacy audioDeviceReader/audioDeviceWriter loops. Call start(as) before the loop so that read has samples available.
as = audiostreamer(Mode="full-duplex", SampleRate=48000, RecorderChannels=1:2);
start(as);
for iter = 1:numIterations
in = read(as, frameLength); % blocks until frameLength samples available
out = process(myPlugin, in);
write(as, out); % blocks until buffer has room
end
nUnderruns = getUnderrunCount(as); % total underruns since last call (resets counter)
release(as);
Before R2026a, use record(as) + play(as, out) instead of start/write.
For player-only (e.g., file input → device output), use play(as, out) with no start needed — play queues and starts automatically.
Pattern 7: Repeated Measurements with Callbacks (Full-Duplex)
as = audiostreamer(Mode="full-duplex", SampleRate=48000, PlayerChannels=1, RecorderChannels=1);
x = sweeptone(2, 1, 48000);
as.RecorderMinSamples = size(x, 1);
as.RecorderFcn = @(obj, ~) processMeasurement(obj, x);
as.PlayerFcn = @(obj, ~) write(obj, x); % or play(obj, x) before R2026a
as.PlayerMinSamples = size(x, 1);
as.ConstantLatency = "dropPlayer"; % keep in sync for impzest
playrec(as); % starts callback-driven measurement loop
% ... runs continuously ...
stop(as);
release(as);
Pattern 8: App with Timer-Based GUI Updates
as = audiostreamer(Mode="player", SampleRate=fs, DeviceBufferSize=1024);
as.PlayerFcn = @(obj, ~) play(obj, getNextFrame());
as.PlayerMinSamples = 20 * 1024;
as.PlayerUnderrunFcn = @(~, ev) fprintf("Dropped %d samples\n", ev.SamplesUnderrun);
figTimer = timer(ExecutionMode="fixedRate", Period=0.05, ...
TimerFcn=@(~,~) updatePlot(as));
play(as); % starts callback loop
start(figTimer); % starts GUI updates
% ...
stop(as);
release(as);
stop(figTimer);
delete(figTimer);
In the timer callback, check buffer health before expensive GUI operations:
function updatePlot(as)
if as.NumPlayerSamples < 0.5 * as.PlayerMinSamples
return % skip GUI update to prevent dropout
end
% ... update plots ...
if as.NumPlayerSamples > 0.9 * as.PlayerMinSamples
drawnow("limitrate");
end
end
Pattern 9: Full-Duplex with write/start for Control — R2026a+
as = audiostreamer(Mode="full-duplex", SampleRate=48000);
write(as, excitation); % queue output
start(as, SamplesToRecord=size(excitation,1)); % start both
waitfor(as, "both");
y = read(as);
release(as);
Teardown Best Practice
release(as) is sufficient — it implicitly stops streaming, flushes buffers, and closes the device. No need to call stop first. However, release discards any unplayed samples — call waitfor(as) first if playback must complete.
waitfor(as); % ensure all queued audio finishes playing
release(as);
In apps, wrap in try-catch and nil the reference:
try
release(as);
catch
end
as = [];
The destructor calls release() automatically, but explicit cleanup is preferred in apps to avoid device lock-up. Calling release from within PlayerCompletedFcn is safe and does not deadlock.
Note: isvalid(as) returns true even after release — it cannot be used to detect a released audiostreamer. To track released state, nil the object reference and check with isempty.
ConstantLatency Modes (Full-Duplex)
| Value | Behavior | Use For |
|---|
"off" | After dropout, inserts silence frame (latency increases) | General use |
"dropPlayer" | Late output frames dropped; latency stays constant | Measurements with impzest (sweep-based) |
"dropRecorder" | Input frames dropped; latency constant | Adaptive filters (NOT compatible with impzest) |
Error Conditions
| Error ID | Cause |
|---|
audio:device:methodRequiresModes | Calling method invalid for current Mode (e.g., record() in player mode). Set Mode to "full-duplex" if you need both playback and recording methods. |
audio:device:invalidChannelMap | Channel indices exceed device max. Check MaxPlayerChannels or MaxRecorderChannels and adjust mapping. |
audio:device:callbackNargin | Callback doesn't accept exactly 2 arguments. Use @(obj, ~) or @(obj, event) signature. |
audio:device:playrecRecorderFcnConflict | playrec called with output argument while RecorderFcn is set — callback consumes samples via read(), leaving nothing for the return value. Clear RecorderFcn before blocking playrec. |
audio:device:startModePlayerNotValid | start(Mode="player") in recorder-only mode |
audio:device:startModeRecorderNotValid | start(Mode="recorder") in player-only mode |
MATLAB:validators:mustBeFinite | Audio data contains NaN or Inf |
MATLAB:validators:mustBeReal | Audio data is complex |
audiostreamer vs. Legacy Audio APIs
audiostreamer is strongly preferred for all audio device I/O when Audio Toolbox is available. Legacy alternatives may be useful as fallbacks when Audio Toolbox is not installed or in edge cases.
| Legacy API | Limitation | audiostreamer Equivalent |
|---|
audiodevinfo | Does not support ASIO; incomplete device list | audiostreamer.getAudioDevices(), audiostreamer.getPlayerNames(), etc. |
audioplayer / audiorecorder | No ASIO/WASAPI exclusive; limited driver model; no callbacks | audiostreamer in player/recorder/full-duplex mode |
sound / soundsc | Creates an audioplayer under the hood; concurrent calls overlap (do NOT queue) | audiostreamer with play() for sequential playback |
audioDeviceWriter / audioDeviceReader | Separate objects; no callbacks; no pre-buffering; frame-at-a-time loops only | Single audiostreamer object with blocking/non-blocking modes |
audioPlayerRecorder | Limited full-duplex; no transport control; no latency measurement | playrec, measureLoopbackLatency, start/stop/pause/resume |
audiodevreset is fine to call — it resets the audio subsystem and can help recover from device errors regardless of which API you use.
When sound/soundsc is acceptable: Only for a single isolated playback with no sequencing. If you need to play two clips back-to-back (e.g., before/after comparison), use audiostreamer — its play() calls queue automatically.
Migration Pitfalls (audioDeviceReader/Writer → audiostreamer)
| Legacy | audiostreamer | Gotcha |
|---|
audioDeviceReader with NumChannels=2 | RecorderChannels=1:2 | audiostreamer records 1 channel by default. You must set RecorderChannels explicitly for stereo/multichannel. |
audioDeviceWriter returns underrun count per frame | getUnderrunCount(as) after loop | play() has no return value. Call getUnderrunCount when you need the total — it resets the counter each call. |
audioDeviceReader returns overrun count per frame | No equivalent needed | audiostreamer buffers all recorded samples internally — recorder cannot overrun. |
Device='Default' | Omit Player/Recorder | No "Default" string — omitting the property selects the system default for the current driver. |
[data, nOverrun] = deviceReader() | record(as) then data = read(as, N) | Must call record(as) (or start(as) [R2026a+]) before the loop — otherwise read blocks forever waiting for samples. |
| Two separate objects for reader+writer | Single audiostreamer(Mode="full-duplex") | One object handles both directions. Use two separate objects if devices require different drivers or conflict when opened together. |
Diagnostics
For debugging streaming issues, enable the diagnostic trace:
as = audiostreamer(Mode="full-duplex", SampleRate=44100);
as.TraceEnabled = true; % logs internal timing and buffer state
Copyright 2026 The MathWorks, Inc.