Find and remove unused FL Studio audio takes
Finds FL Studio recorded takes that no .flp project references, and moves them to a
quarantine folder. FL's Purge unused audio clips only drops references inside the
project — the .wav files stay on disk forever. This cleans up the disk side.
Nothing is deleted by default. Nothing is even moved by default.
py -m pip install -r requirements.txtFLJunk/fljunk.py the tool - a single self-contained script
tests/test_fljunk.py the suite
tests/conftest.py puts FLJunk/ on sys.path
tests/fixtures/ArquivosFl/ a real FL Studio 20 save
tests/fixtures/Recorded/ the takes it refers to
Run it with no arguments:
py FLJunk/fljunk.pyIt asks three things — which projects, where your takes are, and what counts as junk — shows you the report, and only then asks whether to move anything:
Which projects should I check?
[1] My projects folder - I'll search it for .flp files
(safest: it does not need to contain only projects, and any
project left out is a project whose takes get moved)
[2] Specific .flp file(s) I pick myself
choice [1]:
Point it at your ordinary projects folder. It does not need to contain only .flp
files — renders, artwork, stems, zips and Backup/ subfolders are all fine. Every
subfolder is searched, .flp files are picked out by extension, and everything else is
ignored. It then shows you what it found and asks you to confirm the list looks complete,
because a project left out is a project whose takes get moved.
Selection opens a normal file dialog; with no GUI available it falls back to typing the path. The takes folder is remembered for next time if you say so.
Everything the wizard does is also available as flags, for scripting and repeat runs.
Dry run (the default — lists candidates, touches nothing):
py FLJunk/fljunk.py --flp "C:\Users\you\Desktop\FL Projetos" --takes-dir "C:\Users\you\Documents\Image-Line\FL Studio\Audio\Recorded"Actually quarantine them:
py FLJunk/fljunk.py --flp "C:\Users\you\Desktop\FL Projetos" --takes-dir "...\Audio\Recorded" --applyUndo, using the manifest written into the quarantine folder:
py FLJunk/fljunk.py --takes-dir "...\Audio\Recorded" --restore --applySave the takes folder so you stop retyping it (~/.fljunk/config.json; any CLI flag
still overrides it):
py FLJunk/fljunk.py --takes-dir "...\Audio\Recorded" --save-config--flp accepts .flp files and folders, which are searched recursively.
Pass every project that shares the takes folder. A file is kept if any of the projects you passed references it — so a project you forget to pass is a project whose samples get quarantined. Pointing
--flpat your whole projects directory is the safe habit.
Two defensible definitions, and they disagree. Pick with --unused-means:
| Mode | A file is kept when… | Risk |
|---|---|---|
not-in-use (default) |
a project places it on a playlist or feeds it to a plugin | Catches the most junk: what it drops is the sample that only occupies a channel rack slot and is never heard. Those projects reopen with it missing. |
unreferenced |
any project mentions it at all | Conservative. Quarantining a candidate can never break a project. |
not-in-use is what you want if the goal is "remove the takes I loaded and never actually
used". It counts three kinds of use separately, and the report breaks them down:
- playlist — the sample is placed as a clip (read from the playlist events)
- plugin — the path appears inside plugin state: Slicex, DirectWave, Edison, Fruity Convolver, and any VST that stores its sample path as plain text in its chunk
- channel rack only — loaded in a slot, never placed, never fed to anything → junk
Plugin detection is a heuristic: the raw event stream is scanned for path-looking strings
in both Latin-1 and UTF-16, including misaligned runs inside otherwise-binary chunks. It
is validated against a real Slicex project in this repo (tests/fixtures/ArquivosFl/teste_with_plugin.flp),
whose sample sits inside a 3.6 MB plugin blob. A third-party VST that stores its paths
compressed or in a proprietary encoding would not be seen, and its sample would look
rack-only. That is the one case where not-in-use can move a file you still need — which
is why the default action stays quarantine, and why --unused-means=unreferenced exists.
One FL behaviour worth knowing before you trust the numbers: loading a take into Slicex
does not necessarily keep the original in use. In the fixture, Slicex holds a new take
that FL wrote into %FLStudioUserData%\Audio\Recorded\, and the file that was dragged in
is left behind as an ordinary channel rack slot. So the original really is junk, even
though a plugin was involved. Check the quarantine folder before emptying it.
Slicex, DirectWave and Fruity Convolver do not merely point at a sample — they copy the
whole thing into their state. The fixture's 3.8 MB .flp is 3.6 MB of Slicex blob with a
complete RIFF/WAVE inside it, and that project opens and plays with the .wav deleted.
So a plugin-held take is usually not a dependency at all — it is a second copy of audio the
.flp already carries. FLJunk detects the container header inside the blob and tags such a
take with an extra reason, plugin:embedded, plus a DUPLICATED line in the report:
DUPLICATED: 1 kept file(s), 3.5 MB, are also stored *inside* a project.
It is still kept, deliberately: the file on disk is your editable source, and a plugin that stores only a path genuinely does need it. The flag is information, never a decision — it can qualify a keep but can never rescue a take the tiers already called a candidate. If all you need is for the projects to open, those files are the biggest easy win on disk.
Attribution is per blob, not per byte range: FLJunk can tell that a given plugin state carries audio and names certain samples, not which sample the audio is. A blob naming two samples while inlining one would flag both — which only ever over-reports a duplicate.
not-in-playlist is still accepted as an old name for not-in-use.
Because the default can move files a project still points at, --apply stops and shows
you exactly which ones before doing it:
2 of the 5 file(s) about to be moved are still referenced by a
project (loaded in the channel rack, just never placed on the playlist).
Those projects will reopen with missing samples:
1.4 MB teste_with_plugin_notinplaylist.wav
Use --unused-means=unreferenced to move only true orphans.
Type 'yes' to continue:
Candidates that are true orphans never trigger that prompt. --yes skips it.
This is also why the default action is quarantine, not delete: if a project does open
with a missing sample, --restore --apply puts everything back.
FL does not store absolute paths. It writes placeholders:
%FLStudioUserData%\Audio\Recorded\take_2026-07-30 21-12-39_Insert 1.wav
%USERPROFILE%\Desktop\Projetos\FLJunk\Recorded\take_...wav
If you compare those strings against real filenames, nothing matches and every file
looks unused. FLJunk expands them: %FLStudioUserData% is auto-detected (override with
--fl-user-data), other %VAR% come from the environment, and relative paths resolve
against the .flp's own folder. A placeholder that cannot be expanded is reported, never
guessed.
Comparisons fold to NFC before case-folding. "coração" has two valid Unicode
spellings — precomposed, or c + combining cedilla — and Windows stores whichever it was
handed rather than normalising, so the same take can be spelled one way on disk and the
other in the project. Compared raw, it looks unreferenced and gets moved.
Because that expansion can still fail — a project copied from another machine, a
redirected Documents folder — matching defaults to resolved path OR filename
(--match both). --match path is stricter and will quarantine the samples of any
project that has moved. --match name ignores location entirely.
References are read two ways and unioned: via pyflp's object model, and via an
independent byte-level scan of the raw event stream that also looks inside plugin state
blobs — places Channel.sample_path does not reach. Over-collecting references only ever
keeps a file. The scan keeps the two sources apart, which is what makes not-in-use
able to tell "fed to a plugin" from "sitting in a rack slot".
A project that cannot be parsed never counts as "references nothing" — that would quarantine every sample it uses. A real projects folder accumulates half-written autosaves and files from much older FL versions, so there are two lines of defence:
- If
pyflprefuses a file, the byte-level scanner tries it directly. It is far more permissive and usually recovers the sample paths. Playlist data is not recoverable that way, so all of that project's samples are treated as in use. - If even that fails, the run stops and lists the offending files.
--skip-unreadablecontinues without them; guided mode asks you first, defaulting to no.
If a project references a path that resolves into the takes folder but the file is not there, the report says so:
MISSING: 2 reference(s) point into this folder at a file that is not there.
shaker.wav <- sitting in the quarantine folder
wanted by beat.flp
2 of those are in the quarantine folder - a project needs them.
Put them back with: --restore --apply
Two cases, and the second is the one that matters:
- Not in quarantine — the take was renamed, moved or deleted after the project was saved. That project already opens with a missing sample; FLJunk cannot match a file that no longer carries that name. Re-save the project in FL to update the reference.
- In quarantine — FLJunk moved a file a project still needs. Restore it.
Samples that live somewhere else entirely — factory packs, another drive — are normal and never reported, so this stays quiet unless something is genuinely wrong.
FL writes edited or split regions next to the original as <take> - Part_N.wav. If a
project references take - Part_1.wav but not take.wav, the original is by default
kept, tagged part-origin, since it is the source material for a file you are using.
Disable with --no-protect-part-origins.
Caveat, stated plainly: this convention was confirmed from FL's own registry history on
this machine (Shared\Plugins\Edison\MRU), not from a purpose-built Edison fixture. Other
Edison flows — Send to playlist as audio clip, rendering to a new name — produce files
whose link to the source take is not recoverable from the filename. For those, the
original take genuinely looks unreferenced. That is the reason quarantine, not deletion,
is the default action: check the folder before you empty it.
| Flag | Effect |
|---|---|
(no arguments) / -i |
guided mode |
--flp PATH... |
.flp files and/or folders (recursive) |
--takes-dir DIR |
folder FL records into; scanned recursively |
--apply |
actually move (default is dry run) |
--delete |
delete instead of quarantining; asks for confirmation |
--restore |
undo a quarantine run from its manifest |
--quarantine-dir DIR |
default <takes-dir>/_fljunk_unused |
--unused-means |
not-in-use (default) or unreferenced |
--match |
both (default), path, name |
--skip-unreadable |
continue past .flp files that cannot be read at all |
--no-protect-part-origins |
stop protecting - Part_N source takes |
--ext .wav .mp3 … |
extensions to consider |
--fl-user-data DIR |
value for %FLStudioUserData% |
--json FILE |
full machine-readable report (- for stdout) |
--list-all |
list every candidate, not just the 25 largest |
--save-config |
persist --takes-dir / --fl-user-data / --quarantine-dir |
--yes |
skip confirmation prompts |
Subfolder structure under the takes folder is preserved inside the quarantine folder, and the quarantine folder is excluded from scanning, so re-runs are idempotent.
py -m pytest -q117 tests, anchored on a real FL Studio 20 save in this repo rather than a
pyflp.save() round-trip — tests/fixtures/ArquivosFl/teste_with_plugin.flp, which
carries all four cases in one project:
| Channel | Reference recovered from | Playlist |
|---|---|---|
teste_with_plugin_inplaylist.wav |
sampler sample_path |
yes |
teste_with_plugin_notinplaylist.wav |
sampler sample_path |
no |
teste_with_plugin_inplugin_notinplaylist.wav |
sampler sample_path |
no |
teste_with_plugin_2026-07-30 22-30-39_Insert 1.wav |
Slicex plugin state blob | no |
The first three ship as .wav files under tests/fixtures/Recorded/. The fourth does not:
its audio is already inside the .flp (that is the point of the Slicex case), so tests
needing it on disk write a stub into a tmp_path rather than the repo carrying a second
copy.
tests/conftest.py puts FLJunk/ on sys.path so import fljunk resolves from the tests
folder. Run pytest from the repo root.
Tests that need a genuinely orphaned take synthesise one in a tmp_path copy, so the
fixtures are never mutated. The guided mode is tested by replacing its four prompt
functions, not by driving a real dialog.
Path-resolution tests assert that %USERPROFILE% expands to the current user's profile,
not that it lands on this checkout — the fixture was saved at ~/Desktop/Projetos/FLJunk,
so asserting the latter would fail for anyone who clones the repo elsewhere.
pyflp 2.2.1 crashes on import-time enum lookup under Python 3.12+ (EnumType.__call__
stopped falling through to _missing_ for a memberless enum). FLJunk/fljunk.py patches this at
runtime in _patch_pyflp_enum(); no changes to the installed package are needed. Remove
it if pyflp ships a fix.