Effects for any player on your phone
Free and open source under the MIT license.
Website: effectdeck.nemut.ai
An independent project. EffectDeck is built by nemut.ai. It is not affiliated with, endorsed by, or supported by EffeTune or its author (Frieve-A / Yoshiyuki Kobayashi). It bundles EffeTune's DSP core under the MIT license and says so.
Send anything about this app to nemut.ai, not to EffeTune. Do not open issues about it on the EffeTune repository.
Effects for any player on your phone. Anything with a transport in Control Center, the Now Playing kind, goes through the chain: it takes that audio, runs it through EffeTune's effects, and plays it on whatever was the output before you picked EffectDeck: the speaker, wired headphones, AirPods. No virtual cable, no input device to configure. Pick EffectDeck as the output in Control Center and that is the whole setup.
Spotify / a podcast app / Safari
↓ pick EffectDeck as the output in Control Center
extension (Media Device Extension) ← receives
↓ 127.0.0.1:47101
app ← runs EffeTune's DSP
↓
the output you had before (speaker / headphones / AirPods)
Almost always Spotify with Canvas on. Canvas is the short looping video behind some tracks. While one plays, the session counts as video output, so iOS sends the route to AirPlay instead of here, finds no receiver, and puts it back where it was. A track with Canvas enabled cannot connect to EffectDeck, and Spotify does not have to be on screen for it. Turn Canvas off in Spotify's settings, then restart Spotify.
Spotify sometimes cannot connect even on a track without a Canvas: when Spotify has a video (such as a Canvas) loaded, iOS treats it as playing video, even while paused. Restart Spotify, then connect to EffectDeck again.
Playing a YouTube video may disconnect EffectDeck, depending on YouTube's playback state. Restart YouTube, then connect to EffectDeck again; the same video usually connects.
Otherwise, try these in order:
- Restart the player app
- Restart EffectDeck
- Restart the iPhone
The decision is made by iOS. None of the arguments MediaOutputDevice takes are read
when it is made, so there is nothing on this side to set.
iOS 27 added MediaDevice.framework, which lets an app present itself as an output
device the way an AirPlay speaker does. EffectDeck advertises itself that way, and
once it is picked the system hands it the audio as samples.
It runs as two processes. The extension advertises the device and receives the audio;
the app processes it and plays it. They are split because the extension's sandbox denies
files, shared memory and bind. Outbound connections are allowed, so the audio goes over
a single TCP connection to the app. It ships as one app, and the user installs one app.
Signing the app itself with com.apple.developer.media-device-extension stops it from
opening an AVAudioSession: every category fails with '!pla'. The check only looks at
whether the entitlement's array is empty, so the app carries an empty array and the
extension carries the protocol identifier. That also gets past App Store Connect's
ITMS-91183.
To build an app like this, start from ios27-media-device-passthrough: the same two processes in a handful of files, with a low-pass filter where EffectDeck has its effects. It is MIT-0, so you can copy it without keeping the copyright notice.
EffectDeck hosts single-file audio JSFX through the portable EEL2 interpreter.
JSFX.md is the contract: what is supported, what is
deliberately absent, the rules that reject a file outright, and the resource
limits. It is written to be handed to a language model as-is — the top section
states the requirements in the order they are usually violated, and there is a
checklist to run a generated script against before you try to import it.
In the app, Write JSFX with ChatGPT (Plugins, or the Import JSFX menu) opens
ChatGPT with a short request that names the EffectDeck and EffeTune DSP versions, points it at
JSFX.md, and asks what you want to build. Import the file it
returns with Import JSFX → From Files, or copy the script and use From Clipboard.
Use a paid ChatGPT plan: it reads the
linked file and reasons through the code, while the free tier may skip the link and
miss the rules.
The short version: one file, no import or include(), no filesystem, no MIDI,
desc: first, and no JIT — so keep @sample cheap.
It describes only the differences, not the language. For JSFX itself, read REAPER's JS: Programming Reference and JoepVanlier/ysfx, which is the interpreter embedded here.
Build with ChatGPT… (the top of the Effects list in Available Effects) opens ChatGPT the same way, for a chain of the
built-in effects instead of a script. CHAIN.md is its contract; the effect
names and keys it points to are generated for each EffeTune DSP version under
chain/. Bring the chain back with Import from clipboard in
Presets, or tap the link ChatGPT gives when it can run code.
git clone https://github.com/satomasahiro2005/EffectDeck
cd EffectDeck
git submodule update --init Vendor/effetune Vendor/ysfx
bash Scripts/build.sh # build and install on the attached deviceLeave out --recursive: ysfx's own submodules are not used. Keep the effetune submodule's
full history, not --depth 1, because Tools/gen_version.py reads its dsp-v* tag.
To open it in Xcode, generate first. The .xcodeproj is not tracked; project.yml is the
source.
bash Scripts/setup.sh
open EffeTuneLive.xcodeprojYou need:
- macOS with Xcode 27 or later
- A device running iOS 27 or later. The extension needs iOS 27, so no audio flows in the simulator
- Apple Developer Program membership (set
DEVELOPMENT_TEAMinproject.ymlto yours) xcodegenand python3 3.10+ from Homebrew
The script finds the attached device. With more than one, use
DEV_ID=<UDID> bash Scripts/build.sh. The log goes to build.log; look for
BUILD SUCCEEDED there.
Run Scripts/build.sh from Terminal in the Mac's own login session, not over SSH. Over SSH
codesign cannot reach the keychain, and signing the extension fails with
errSecInternalComponent. The compile still succeeds, so an unsigned out/EffectDeck.app
appears and the install then fails with "not a valid bundle".
Set DEVELOPMENT_TEAM in project.yml to your team, change the identifiers below to your
own, and create them in the Apple Developer portal.
| What | Today's value | Where it is written |
|---|---|---|
| App ID for the app | ai.nemut.effetune |
project.yml (EffeTuneLive target) |
| App ID for the Media Device extension | ai.nemut.effetune.extension |
project.yml (EffeTuneLiveExtension) |
| App ID for the share extension | ai.nemut.effetune.share |
project.yml (EffectDeckShare) |
| Media Device Sharing Extension identifier | media-device-protocol.ai.nemut.effetune |
Sources/Extension/Extension.entitlements, UTExportedTypeDeclarations in Sources/Extension/Info.plist, and kProtocolID in Sources/Extension/EffeTuneLiveExtension.swift |
| App Group | group.ai.nemut.effetune |
all three .entitlements files and ETShareInbox.group in Sources/EffeTuneLive/DSP/ETShareInbox.swift |
| iCloud key-value storage | follows the app's bundle ID | Sources/EffeTuneLive/EffeTuneLive.entitlements |
| Associated Domains | applinks:effectdeck.nemut.ai |
Sources/EffeTuneLive/EffeTuneLive.entitlements |
- The Media Device Sharing Extension identifier is made under Identifiers > new. There is
no review. The entitlement value must be an array with one element; a bare string stops
the extension from launching. Change all three places together: the extension offers
kProtocolIDas its protocol type, and it must match the entitlement. Tools/check_release_binary.pychecks a release archive against today's values (BUNDLES,APP_GROUPandAPPLINKSat the top of the file). Change them there too if you use it.- The app itself carries
com.apple.developer.media-device-extensionas an empty array. Leave it empty (see The iOS 27 Media Device Extension above). - Enable App Groups on all three App IDs, iCloud (key-value storage only) on the app, and Associated Domains on the app.
- Share links only open the app if the domain serves an
apple-app-site-associationthat names your app.site/serves it foreffectdeck.nemut.ai. Without a domain of your own, remove the Associated Domains key; links then open in the browser.
The app is EffectDeck. The project, the targets and many files still carry its first
name, EffeTune Live: EffeTuneLive.xcodeproj, the EffeTuneLive app target (product name
EffectDeck), EffeTuneLiveExtension, Sources/EffeTuneLive. They are the same app.
"EffeTune" alone means the upstream project in Vendor/effetune.
| Path | What it is |
|---|---|
Vendor/effetune |
EffeTune, pinned by the submodule. Its dsp/ is the audio engine |
Vendor/ysfx |
ysfx, the JSFX interpreter, pinned to 5c3452fe. Scripts/setup.sh refuses another revision |
Patches/abi-begin-ptr.diff |
Adds et_instance_asset_begin_ptr, a 64-bit staging pointer. Without it the seven effects that load data (IR Reverb and the ones designed in the app, such as Room EQ) pass audio through silently |
Patches/effetune-external-*.diff |
Let AUv3 and JSFX run as nodes inside the EffeTune chain (docs/external-processor.md) |
Patches/ysfx-effectdeck-ios.diff |
The iOS sandbox for ysfx and fixes taken from upstream WDL |
Patches/ysfx-effectdeck-ios.old.diff |
The previous ysfx patch. Never applied; setup.sh uses it to take the old version off a copied tree |
Scripts/setup.sh applies the patches to the submodule working trees, regenerates the
effect catalog and presets from Vendor/effetune, and runs xcodegen. The patched
submodule trees are never committed; the patches are. docs/ lists the
design documents and notes.
None of these need a device or a paid account. CONTRIBUTING.md has the exact commands.
| Suite | Runs on | Command |
|---|---|---|
Logic tests (all of Tests/Unit, JSFX included) |
Mac with Xcode, simulator | bash Scripts/test.sh, or the Logic scheme in Xcode |
| The Foundation-only part of the Logic tests | Linux or WSL with Swift | bash Tests/Linux/run.sh --name local |
Native C tests (Tests/Native) |
Linux, macOS or WSL with CMake | cd Tests/Native && cmake --preset asan && cmake --build --preset asan && ctest --preset asan |
Website (site/) |
Node 22 | cd site && npm ci && npm test |
Generators and checks (Tools/, Tests/Tools) |
Python 3.10+ and Node 22, any OS | python3 -m unittest discover -s Tests/Tools |
UI tests (Tests/UI) |
Mac with Xcode, simulator | bash Scripts/uitest.sh MenuProbe (one class; see CONTRIBUTING) |
GitHub Actions runs all of them but the UI tests, and checks that generated files are up to
date, on every push to main and every pull request (CI).
For the simulator and for debugging. They are read from UserDefaults, so they are passed
as -Name value to xcrun simctl launch, xcrun devicectl device process launch or an
XCUITest launchArguments. The icon on the home screen passes none.
| Argument | What it does |
|---|---|
-ETSeed <name> |
Starts with a prepared chain instead of the saved one, every card open. none, peq, compressor, saturation, meter, spectrum, peq-spectrum, chain, store, analyzers4; a factory preset by short name (vinyl, karaoke, analyzers, live, tube, bbe, fmradio); demo in Debug builds; anything else is read as plugin type names separated by commas |
-ETMock 1 |
Feeds a generated test signal, so meters and graphs move without the extension |
-ETWidth <pt> |
With -ETSeed, the width of the one-column chain, so screenshots taken on iPad look like a phone. Default 393; 0 keeps the device width |
-ETLayout wide |
With -ETSeed, keeps the two-column iPad layout instead of one column |
-ETCollapsed 1 |
With -ETSeed, starts with every card closed. A card with a graph still shows the graph |
-ETSheet <name> |
Opens a sheet at launch: picker, settings, routing, presets, ir, tips |
-ETAutoExpand 1, -ETAutoExpandIndex <n> |
Taps an effect once, 4 seconds after launch, to record the animation. A tap moves the card one step (open → graph only → folded → open), so it opens only a folded card, and a card that -ETSeed opened keeps only its graph. The first effect by default; n counts from 0 and skips Sections |
-ETDebugBlocks 1 |
Tints each Section block to check grouping |
-ETDiag 1 |
Adds a hidden diag text with the active node count and chain length, for UI tests |
-ETConsole 1 |
Also prints the diagnostic log to stdout, for devicectl ... --console |
-ETNowPlaying on|off|first |
Whether the app claims Now Playing (default off). Debug builds save it to UserDefaults and keep it on later launches until another value is passed; Release builds use it for that launch only |
-ETProbe 1 |
Shows the reorder probe screen instead of the app |
-pref.<key> <value> |
Overrides a setting for that launch, for example -pref.power balanced |
Free and open source under the MIT license. Everything bundled in the app is open source too; NOTICE.md lists each part and its license.


