mcpbeat

Matlab Play Record Audio

matlab/matlab-play-record-audio

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.

5k tokens
context cost
the whole folder, loaded on every use
2
files
instructions only
0
copies elsewhere
how many repositories repackaged it
865
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/matlab/matlab-agentic-toolkit --skill matlab-play-record-audio

The instruction itself

32 sections, as written by the author

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 soundsound 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 |

| ConstantLatency | "off" / "dropPlayer" / "dropRecorder" | "off" | Full-duplex dropout handling |

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.

----

How to use it

Copy the folder

Take matlab/matlab-play-record-audio from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.