Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

FLJunk logo

FLJunk

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.

Install

py -m pip install -r requirements.txt

Layout

FLJunk/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

Use — guided mode

Run it with no arguments:

py FLJunk/fljunk.py

It 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.

Use — command line

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" --apply

Undo, using the manifest written into the quarantine folder:

py FLJunk/fljunk.py --takes-dir "...\Audio\Recorded" --restore --apply

Save 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 --flp at your whole projects directory is the safe habit.

What "unused" means

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.

Samples the project already contains

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.

How it decides

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:

  1. If pyflp refuses 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.
  2. If even that fails, the run stops and lists the offending files. --skip-unreadable continues without them; guided mode asks you first, defaulting to no.

References to files that are not there

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.

Edison / edited takes

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.

Options

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.

Tests

py -m pytest -q

117 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.

Note on pyflp

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.

About

FLJunk finds and removes unused FL Studio audio takes to free up disk space. Safe by default: files are quarantined instead of permanently deleted.

Topics

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages