Lets an AI client edit, analyze, and convert audio files using non-destructive manipulations and high-level plugins.
Also known as audiojs/audio
- Stars
- 334
- Tools
- —
- Downloads, 30 days
- —
- Latest version
- 2.10.0
- Last commit
- Nov 10, 2026
- License
- MIT
What is Audio MCP?
Audio lets an AI client edit, analyze, and convert audio files using non-destructive manipulations and high-level plugins.
Summary generated from the project's README
About Audio MCP server
Audio enables developers and AI agents to perform high-level audio manipulations including loading, playback, analysis, and editing. It supports over 140 plugins for tasks such as denoising, equalization, reverb, and music analysis. The system uses non-destructive virtual edits, allowing for infinite undo and instant cloning of files without altering the original source. Because it is stream-first and paged, it can handle massive files exceeding 10GB without hitting the standard 2GB memory limit.
Users can integrate this server into various environments including Node, Bun, Deno, and browsers. It is designed to work with AI clients like Claude Code, Cursor, and Gemini CLI through an MCP bridge. The server provides specific tools for measurement, such as checking loudness and true peak against Apple Podcast specifications. It also supports advanced features like generating audio from mathematical functions or sonifying data through custom sample functions.
What you can do with it
- 1Prepare podcast audioAn AI agent can trim, normalize, and check a recording against specific loudness rules for podcast platforms.
- 2Analyze musical featuresDevelopers can extract technical data such as notes, chords, and musical keys from an audio file.
- 3Automate audio masteringYou can instruct an agent to master a mix by matching the tone and loudness of a reference track.
Written from the project's README. Every use case is checked against a quote from it.
How to install Audio MCP
Formats verified 10 Nov 2026Install Audio MCP in Claude Code
$ claude mcp add audio -- npx -y audioInstall Audio MCP in Cursor
~/.cursor/mcp.json
{
"mcpServers": {
"audio": {
"command": "npx",
"args": [
"-y",
"audio"
]
}
}
}Install Audio MCP in VS Code
.vscode/mcp.json
{
"servers": {
"audio": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"audio"
]
}
}
}Install Audio MCP in Claude Desktop
claude_desktop_config.json
{
"mcpServers": {
"audio": {
"command": "npx",
"args": [
"-y",
"audio"
]
}
}
}Install Audio MCP in Codex
~/.codex/config.toml
[mcp_servers.audio] command = "npx" args = ["-y", "audio"]
Install Audio MCP in Gemini CLI
~/.gemini/settings.json
{
"mcpServers": {
"audio": {
"command": "npx",
"args": [
"-y",
"audio"
]
}
}
}Configuration
ANTHROPIC_BASE_URL
| Name | Description | |
|---|---|---|
| ANTHROPIC_BASE_URL | — |
Automated checks
Ran Nov 12, 2026Full report →Passed our automated checks. That is not a security audit: read the code and give the server the narrowest access it needs. How checks work
- Source repository linked and reachablePass
- Open source license (OSI)Pass
- Commit or release in the last yearPass
- Package name not close to a popular packagePass
- Remote endpoint uses HTTPSNot checked
- Destructive tools are labelledNot checked
- No hidden instructions to the modelPass
- No critical advisory in the package or its dependenciesPass
README
View on GitHubaudio

High-level audio manipulations: loading, playback, analysis and editing. 140+ plugins: denoise, dynamics, EQ, filters, effects, reverb, time and pitch, spatial, synthesis, music analysis.
- Any Format — fast wasm codecs, no ffmpeg.
- Non-destructive — virtual edits, infinite undo, instant clone.
- Stream-first — playback/encode during decode, realtime editing.
- Paged — no 2Gb memory limit, open 10Gb+ files.
- Analysis — loudness, spectrum, beats, pitch, chords, key.
- Modular – pluggable ops, tree-shakable.
- CLI — playback, batch processing, scripting, unix pipes, tab completion.
- Cross-platform — browsers, node, deno, bun.
Start Recipes API CLI FAQ Plugins Architecture Comparison RX 12
Start
Node
npm i audio
import audio from 'audio'
let song = audio('song.mp3')
song.play() // the speakers, as a page plays it: no ffmpeg, no player to spawn
song.pause(); song.seek(60); song.resume()
let voice = audio('voice.wav').trim().normalize('podcast')
await voice.save('clean.mp3')
let { pass, rules } = await voice.check('podcast') // each rule of Apple's spec, measured
Browser
<script type="module">
import audio from 'https://esm.sh/audio'
audio('./song.mp3').trim().normalize().fade(0.5, 2).clip({ at: 60, duration: 30 }).play()
</script>
Audio anywhere
import 'audio/polyfill' // the page's HTMLAudioElement, in Node, Bun and Deno
let song = new Audio('song.mp3')
song.onended = () => console.log('done')
await song.play()
song.currentTime = 30; song.volume = 0.5; song.loop = true
Events, their order, promises and errors as Chromium's own element has them, compared observation by observation (test/polyfill.js). import { Audio } from 'audio/polyfill' leaves the global alone.
CLI
npm i -g audio # or: npx audio …
audio voice.wav trim normalize podcast save clean.mp3
audio clean.mp3 check podcast
# Apple Podcasts https://podcasters.apple.com/support/893-audio-requirements
# ✓ Loudness -16.44 LUFS -17 to -15
# ✓ True peak -1.18 dBTP ≤ -1
# pass
A fail exits 1, so the same line guards a build: Check in CI.
Skill
npx skills add audiojs/audio
MCP
npx add-mcp "npx -y audio --mcp" # asks which of your agents: Claude Code, Codex, Cursor, Gemini CLI, Kimi Code, Pi, OpenCode, Zed…
Prompt: make ~/Desktop/interview.m4a podcast-ready and tell me the loudness before and after
One agent at a time: claude mcp add audio -- npx -y audio --mcp, qwen mcp add audio npx -y audio --mcp, droid mcp add audio "npx -y audio --mcp".
The repl and agents
npx audio --bridge
# audio bridge on http://127.0.0.1:7777
#
# key 3e37b8c61acf5b50f010852168f4843d
# agents Claude Code, Codex, Pi, Kimi Code
#
# In the repl opened with ?ui={"agent":true}, paste the key in the line under the code, then Connect. It stays the same next time.
Open the repl with its agent line, every edit of it a line of audio code, and connect it to the bridge with the key (once: the bridge keeps it), and the line under its code runs an agent of yours, which measures, looks at, edits and plays the sound open there, and finds which edit did what: it measures the sound before and after each, and what each took out; its conversations are kept, each with the agent, and its model, picked under the line. The bridge finds Claude Code, Codex, Pi, Gemini CLI, Qwen Code, Kimi Code, OpenCode, Kilo Code, Cline, Goose, Factory Droid, Cursor, Augment, Kiro and Mistral Vibe on PATH; any other that speaks ACP runs by its command line, --agent "my-agent --acp".
Any MCP agent gets the repl's tools (state, measure, look, edit, select, play, check, …), the running bridge found by itself: npx add-mcp "npx -y audio --mcp --repl".
An agent thinks with the model its own settings name: Pi takes local ones for good from ollama launch pi --config; Claude Code takes any Anthropic-compatible endpoint from the bridge's environment:
ANTHROPIC_BASE_URL=http://localhost:11434 ANTHROPIC_AUTH_TOKEN=ollama ANTHROPIC_API_KEY= ANTHROPIC_MODEL=qwen3-coder npx audio --bridge
| Models | ANTHROPIC_BASE_URL, with the provider's key as ANTHROPIC_AUTH_TOKEN |
|---|---|
| Ollama | http://localhost:11434, token ollama (docs) |
| Z.ai GLM | https://api.z.ai/api/anthropic (docs) |
| Kimi | https://api.moonshot.ai/anthropic (docs) |
| Qwen | https://coding-intl.dashscope.aliyuncs.com/apps/anthropic, Coding Plan (docs) |
| DeepSeek | https://api.deepseek.com/anthropic (docs) |
Embed
The repl in any page, its script the element's own text: it runs where the reader scrolls to it, plays, takes edits, and opens whole in the repl. Nothing of the reader's is kept.
<script type="module" src="https://audio.js.org/embed.js"></script>
<audio-repl panel="code" display="wave" height="320">
<pre><code>audio('chime.wav')
.trim()
.normalize(-1)</code></pre>
</audio-repl>
What it shows, the frame takes as one JSON: the element's ui attribute, with these attributes over it, each a key of it. panel: code (default), edits (its steps, each with its settings) or none; display: wave or spec; controls: the parts shown, each in its own place: over the picture sound (its name, a menu of its ways in: a file, a take, a sample), link and name (the way to the repl), ruler (the time row; left out, none: the big time says it), look (a band's dropdown of how it draws); in the bar play record loop clock history message heard, a level (level, or what it reads: peak rms loudness), rate channels depth before, view (a switch: the waveform, the spectrogram), open and save (a file of the reader's in; the result out, in the format picked from its menu) — without it, all but sound, view, open and save; ops: edits a press away, one button after the history, a menu of them with their values, as trim normalize(-1) stretch(1.25) reverse (a call's own arguments, or the defaults the menus write); background: the ground it sits on and fades into, a CSS colour, or page, the page's own under the element (the element reads it; a frame can't); height in px. A sound named by its path from the site's root (audio('/sounds/take.mp3')) is that site's file. The <pre> shows until the frame comes, and where scripts don't run. Every key, zoom and overshoot too: repl/README.md.
Recipes
Clean up
// master a raw take
let a = audio('raw-take.wav')
a.trim(-30).normalize('podcast').fade(0.3, 0.5)
await a.save('clean.wav')
// full restoration chain via ecosystem plugins (see API › Plugins)
a.gate(-45).dehum().deesser().compressor({ threshold: -18 }).limiter({ ceiling: -1 })
// cut 2:00–2:15, smooth the splice
a.remove({ at: 120, duration: 15 }).fade(0.1, { at: 120 })
// find clipped blocks
let clips = await a.stat('clipping')
// does it pass? each rule of the spec, measured
let { pass, rules } = await a.check('podcast') // acx, podcast, spotify, broadcast, netflix
Master & deliver
// master a song to a reference track: its tone (mid and side), width and loudness, under -1 dBTP
audio('mix.wav').master(await audio('reference.wav')).save('master.wav')
// audiobook chapter for ACX: RMS -23..-18 dB, peaks under -3 dB, floor under -60 dB, room tone at each end, 44.1 kHz
let ch = audio('chapter-01.wav')
.highpass(80).omlsa({ gMin: -12 }).compressor({ threshold: -24, ratio: 2.5 })
.normalize(-20, 'rms', { ceiling: -3.5 })
.trim().pad(1.5, 2).roomtone() // room tone, not digital silence
.resample(44100)
console.log(await ch.check('acx')) // passed 10 of 10 real narrations (.work/pro.md)
await ch.save('chapter-01.mp3', { bitrate: 192 })
// tighten pauses in a talking-head video, then hand the cuts to the video editor
let talk = audio('talk.mp4').shrink(0.3)
await talk.save('talk.m4a') // the sound, cut
await talk.save('talk.edl') // the same cuts for the picture (Premiere, Resolve)
Compose
// podcast montage
let ep = audio([intro, interview.trim().normalize('podcast'), outro], { crossfade: 0.5 })
await ep.save('episode.mp3')
// voiceover over music
music.gain(-12).mix(voice, { at: 2 })
// ringtone: the chorus + fades
audio('song.mp3').crop({ at: 45, duration: 30 }).fade(0.5, 2).normalize().save('ringtone.mp3')
// split an audiobook into chapters
let [ch1, ch2, ch3] = audio('audiobook.mp3').split(1800, 3600)
// glitch: stutter + reverse
let v = a.clip({ at: 1, duration: 0.25 })
audio([v, v, v, v]).reverse({ at: 0.25, duration: 0.25 })
Analyze
// waveform bars — and progressively, as it decodes
let [mins, peaks] = await a.stat(['min', 'max'], { bins: canvas.width })
a.on('data', ({ delta }) => appendBars(delta.max[0], delta.min[0]))
// features for ML
let mfcc = await a.stat('cepstrum', { bins: 13 })
let [loud, rms] = await a.stat(['loudness', 'rms'])
// notes, chords, key
let notes = await a.stat('notes') // [{time, duration, freq, midi, note, clarity}]
let chords = await a.stat('chords') // [{time, duration, label, root, quality, confidence}]
let key = await a.stat('key') // {tonic, mode, label, confidence}
Record & generate
// mic take
let a = audio()
a.record()
// …later
a.stop()
a.trim().normalize()
// tone — any t => sample function
let tone = audio.from(t => Math.sin(440 * Math.PI * 2 * t), { duration: 2 })
// sonify data
let s = audio.from(t => Math.sin((200 + data[t / 0.2 | 0]) * Math.PI * 2 * t) * 0.5, { duration: data.length * 0.2 })
Automate
a.gain(t => -12 * (0.5 + 0.5 * Math.cos(t * Math.PI * 4))) // 2Hz tremolo in dB
a.lowpass(t => 400 + 4000 * t) // filter sweep
a.pan({ t: [0, 2, 4], v: [-1, 1, -1] }) // curve L→R→L over 4s, serializable
music.ducker({ key: voice }) // sidechain (plugin)
Stream & persist
// stream to network — encode/playback during decode
for await (let chunk of audio('2hour-mix.flac').highpass(40)) socket.send(chunk[0].buffer)
// serialize edits, restore later
let json = JSON.stringify(a) // { source, edits, ... }
let b = audio(JSON.parse(json)) // re-decode + replay edits
API
Create
| Method | Description |
|---|---|
audio(source, opts?) |
decode from file, URL, bytes, or a byte stream. Returns instantly — decodes in background; streams decode as they arrive. |
audio.from(source, opts?) |
wrap existing PCM, AudioBuffer, silence, or function. Sync, no I/O. |
let a = audio('voice.mp3') // file path
let b = audio('https://cdn.ex/track.mp3') // URL
let c = audio(inputEl.files[0]) // Blob, File, Response, ArrayBuffer
let d = audio() // empty, ready for .push() or .record()
let e = audio([intro, body, outro]) // concat (virtual, no copy)
let f = audio([a, b, c], { crossfade: 2 }) // concat with 2s crossfade
let g = audio(process.stdin) // byte stream: pipe, socket, fetch body – decodes as it arrives
// opts: { sampleRate, channels, crossfade, curve, storage: 'memory' | 'persistent' | 'auto' }
await a // await for decode — if you need .duration, full stats etc
let a = audio.from([left, right]) // Float32Array[] channels
let b = audio.from(3, { channels: 2 }) // 3s silence
let c = audio.from(t => Math.sin(440*TAU*t), { duration: 2 }) // generator
let d = audio.from(audioBuffer) // Web Audio AudioBuffer
let e = audio.from(int16arr, { format: 'int16' }) // typed array + format
let f = audio.from(store, { length, channels, sampleRate }) // pages in a store ({ read(i), has(i), write(i, page) }), read as needed
Properties
| Property | Description |
|---|---|
.duration |
total seconds, after edits. |
.channels |
channel count. |
.sampleRate |
sample rate. |
.length |
samples per channel. |
.currentTime |
what the speakers play now, in seconds: their latency compensated, smooth; at pause it holds where playback resumes. |
.playing |
true during playback. |
.paused |
true when paused. |
.volume |
0..1 linear. Settable. |
.muted |
mute, independent of volume. Settable. |
.loop |
settable, mid-playback too: the span repeats, each seam a 10 ms equal-power crossfade. |
.playbackRate |
0.0625..16, settable mid-playback, click-free. The pitch stays (WSOLA, as a browser's media element plays at a speed) unless .preservesPitch is false: then it glides over ~50 ms and the pitch follows, tape-style. .speed() bakes it. |
.preservesPitch |
true: at a rate other than 1, the pitch kept. false: varispeed, the pitch with the speed. Settable mid-playback. |
.ended |
true when playback reached the end, not after stop(). |
.seeking |
true during a seek. |
.played |
promise, resolves when playback sounds. |
.recording |
true during mic recording. |
.ready |
promise, resolves when fully decoded. |
.source |
original source. |
.bitDepth |
stored sample depth of the source: 16, 24, 32 (float); null for lossy or generated audio. Lossless save keeps it. |
.pages |
Float32Array page store. |
.stats |
per-block stats (peak, rms, etc.). |
.edits |
edit list. |
.version |
increments on each edit. |
Structure
| Method | Description |
|---|---|
.trim(threshold?) |
strip leading/trailing silence (dB, default auto). On a live stream a given threshold streams, holding a silent tail until sound resumes; the automatic one reads the whole input, so it waits for the end. |
.shrink(gap?, threshold?) |
shorten silent pauses to gap seconds (default 0.3); 0 removes them. Streams with a given threshold, like trim.≡ FFmpeg silenceremove, Audacity truncate-silence |
.crop({at, duration}) |
keep range, discard rest. |
.remove(at, duration, crossfade?) |
delete range, close gap. crossfade ('10ms') makes the splice an equal-power crossfade centered on the cut; the length stays the same. |
.insert(source, at?, crossfade?) |
insert audio (default: at end), or a number of seconds of silence; crossfade fades both seams. |
.copy({at?, duration?}) |
copy range (default: all) to this instance's clipboard. |
.cut({at?, duration?, crossfade?}) |
copy, then remove. |
.paste({at?, crossfade?}) |
insert the clipboard (default: at end). |
.move({at, duration, to, crossfade?}) |
slide a range to to, over what is there; silence where it was, the length kept (past the end, extended). crossfade crossfades each edge, centered.≡ a DAW's clip moved in slip mode |
.clip({at, duration}) |
zero-copy excerpt as a new instance. |
.split(...offsets) |
zero-copy excerpts between timestamps. |
.pad(before, after?) |
silence at edges (seconds). |
.repeat(n) |
repeat n times. |
.reverse({at?, duration?}) |
reverse audio or range. |
.speed(rate, {type?}) |
changes pitch and duration together; type: 'sinc' reads between samples by a Lanczos sinc (16 zero crossings) instead of linearly, the top octave kept at a rate near 1. |
.stretch(factor, {voice?, method?}) |
changes duration, keeps pitch (phase-locked vocoder). A t => f or {t, v} factor slides the tempo; duration becomes ∫factor dt. A range {at, duration} comes out round(round(duration · sampleRate) · factor) samples long, to the sample; the audio around it as it was. { voice: true } keeps a voice's pulse shape and consonants, which the vocoder makes distant: shortened, its waveform copied a segment at a time (WSOLA, @audio/stretch-wsola); slowed, the vocoder's frames restarted from the waveform where it fits (PVSOLA, @audio/stretch-pvsola), so breath and reverberation are not repeated into a flanger. One voice, not chords. { method } stretches another way, its module loaded only then: 'vocoder', 'pghi', 'wsola', 'psola', 'sms', 'transient', 'hybrid', 'paulstretch' (@audio/stretch-*); 'phase-lock', the default, is the one that streams, slides and takes voice. The others render what they stretch at once, from a constant factor; a range, with a crossfade into the audio either side.≡ Logic Flex Time Monophonic, Ableton Tones |
.warp(markers) |
move moments in time: [[from, to], …] in seconds. Between markers the audio stretches to fit, pitch kept; start and end stay.≡ Logic Flex Time, Ableton warp markers |
.pitch(semitones, {voice?, method?}) |
changes pitch, keeps duration. Semitones may be a curve {t, v} (seconds → semitones, straight between points, flat past the ends, as the gain line's) or t => semitones; where it is zero the audio is as it was. { voice: true } re-spaces a voice's own glottal cycles, found on its pYIN pitch track (TD-PSOLA, the optional @audio/tune-curve): formants and consonants kept, one voice; a range's edges glide as fast as a voice moves its pitch, and its cycles land back on the input's. { method } shifts another way, its module loaded only then (@audio/shift-*): 'vocoder', 'formant' (the voice's tone kept), 'psola', 'wsola', 'lpc', 'transient', 'hybrid', 'hpss', 'sms', 'sample', 'delay', 'ola', 'granular', 'paulstretch', or 'auto' (@audio/shift chooses by the sound, loading them all); 'phase-lock', the default, is the one that streams, follows a curve and takes voice. The others render what they shift at once, by a constant number of semitones; a range crossfades in and out of it over 10 ms.≡ Melodyne pitch drawing |
.intonation(factor) |
a voice's rises and falls wider or flatter about its median pitch: 1 as it was, 0 a monotone, 2 twice as wide. Its own cycles re-spaced (as pitch({ voice: true })): timing, formants and consonants kept, one voice.≡ Praat's pitch range factor |
.modulation(factor) |
each note's vibrato and wobble about its own slow line: 1 as sung, 0 a smooth line, 2 twice as wide, -1 inverted. The notes are Tony's (as stat('notes')); a note's line, its pitch smoothed over its own vibrato period twice. Its cycles re-spaced as intonation's.≡ Melodyne pitch modulation tool |
.drift(factor) |
each note's slow line about its centre: 1 as sung, 0 held at its centre with its vibrato kept, 2 twice the wander.≡ Melodyne pitch drift tool |
.center(factor, {scale?, root?, a4?}) |
each note's centre (the median of its middle half) about the nearest note of scale from root: 1 as sung, 0 on the note, 0.5 half way, all that moves inside it kept; chromatic by default, @audio/note's scale names or degrees. A note is moved from its first frame where it starts after silence; where two meet, the move crosses over 100 ms.≡ Melodyne Correct Pitch, pitch center |
.formant(semitones) |
moves the formants (the spectral envelope: a voice's vowels, the size of its head), keeps the pitch. A number, a curve {t, v} or t => semitones. Any sound; with pitch(), a voice kept its own or made another's.≡ Melodyne formant tool, Praat's formant shift ratio |
.remix(channels) |
channel count (down per ITU-R BS.775: 7.1 → 5.1, stereo, mono), or a map: [1, 0] swaps L/R, null a silent channel. |
Every op takes a trailing {at, duration, channel} range, except channel-changing remix and crossover, and every op that keeps the timeline a mix: how much of its output is heard, the rest its input, 0 to 1 or a curve {t, v} over time ({ mix: { t: [11.99, 12, 14, 14.01], v: [1, 0, 0, 1] } } turns it off from 12 s to 14 s, as RX's Restore Selection; an effect's own mix is its own). Times are seconds or strings ('1:30', '2m'); negative counts from the end. FFmpeg's short names work wherever the long ones do: d for duration, xfade for crossfade (a.remove({ at: 1, d: 0.5, xfade: 0.01 })).
a.trim(-30) // strip silence below -30dB
a.remove({ at: '2m', duration: 15 }) // delete 2:00–2:15, close gap
a.remove(12.3, 0.4, '10ms') // cut a breath, crossfaded: no click
a.insert(intro, { at: 0 }) // prepend; .insert(3) appends 3s silence
a.copy(60, 30).paste(120) // duplicate the chorus at 2:00
a.cut(2, 1).paste(5) // move 2s–3s to 5s of the shortened timeline
a.move({ at: 2, duration: 1, to: 5 }) // slide 2s–3s over 5s–6s, silence left at 2s–3s
let [pt1, pt2] = a.split('30m') // zero-copy parts
let hook = a.clip({ at: 60, duration: 30 }) // zero-copy excerpt
a.stretch(1.1) // 10% longer, same pitch
a.warp([[1, 1], [2, 2.4], [3, 3]]) // the hit at 2s lands at 2.4s; 1s–3s keeps its length
a.pitch(-2) // 2 semitones down, same tempo
a.pitch({ t: [1, 1.2, 2, 2.2], v: [0, 3, 3, 0] }, { voice: true }) // a note drawn 3 semitones up, formants kept
a.intonation(1.5, { at: 2, d: 3 }) // 2s–5s: every rise and fall half as wide again
a.center(0).drift(.5).modulation(.7) // each note on its semitone, half its drift and 70% of its vibrato left
a.formant(-2) // a larger, darker voice on the same notes
a.remix([0, 0]) // L→both; .remix(1) for mono
Audio MCP questions
How do I add Audio MCP to Claude Code?
Run claude mcp add audio -- npx -y audio in your terminal. Add --scope user to make it available in every project.
How do I add Audio MCP to Cursor?
Open ~/.cursor/mcp.json and add the server under mcpServers, using the Cursor config shown in the install section above.
Can I handle very large audio files?
Yes, the paged architecture allows you to open files larger than 10GB without hitting the 2GB memory limit.
Is the editing process destructive?
No, the server uses virtual edits and non-destructive processing, allowing for infinite undo and instant cloning.
What kind of audio analysis can be performed?
It can analyze loudness, spectrum, beats, pitch, chords, and key.