Skip to content

Repository files navigation

Truedat - Music Mood Extractor & Fingerprinter

Truedat is a Windows .NET CLI that extracts per-track mood across a music library — the signal MBXHub's - AutoQ engine uses for mood-aware shuffle — and writes it (alongside identity, authenticity, and metadata) into mbxmoods.json that MBXHub reads directly.

Mood is the core job, and truedat captures it two independent ways:

  • Essentia — when Essentia is present, truedat calls it to extract the per-track acoustic feature set (see Extracted Features for the exact counts). The primary mood read. Truedat writes features, not an interpretation: the valence/arousal mapping is made downstream by the consumer (MBXHub/AutoQ) from these fields. truedat provides a custom x64 build (see Essentia Builds) that handles large files better, but doesn't bundle it — it's a separate AGPL tool, invoked if found.
  • SMFM (Sony 12 TONE / SensMe) — truedat doesn't add this; it reads Sony's own analysis when it's already embedded in a file — the SMFM block — yielding 10 STMO mood scores + BPM (the smfm* fields). MBXHub projects these to a second (valence, arousal) opinion on the same AutoQ mood map — an independent take alongside Essentia, most useful exactly where the two disagree.

Everything below is built on top of that mood signal — identity, authenticity, and library-scale plumbing:

  • Composite track identityfingerprint.v1 (file size + path tail + audio properties + 64 KB head MD5 + MP3 LAME tag block), audioStreamSha256 (SHA-256 over the audio region — frame-anchored for FLAC, TagLib's invariant region otherwise; cross-machine portable, tag-edit resilient for MP3/WMA/FLAC alike), and an optional whole-file MD5 (--file-md5).
  • Authenticity signalsbitUsage (ffmpeg-driven LSB walk that catches fake-hi-res 24-bit padding), hfEnergyRatio (bin-sharp energy above 22.05 kHz via managed FFT), hfSpectralStructure (flatness / peak-to-mean / mirror-imaging from the same FFT pass — catches ffmpeg-upsampled fakes the bit-level signals miss), MP3 LAME-tag parsing (pure-managed Xing/Info+LAME decoder for transcode detection).
  • truedat.* verdict block — multi-signal voted hi-res / lossy-transcode verdicts (yes / no / unknown / n/a) computed inline at write time, so threshold changes ship without rescanning.
  • Library-scale operations — incremental & cache-aware scanning across four cache tiers (path+mtime, head-64k quick, path+sha, cross-sha), --verify / --verify --backfill (identity + features tiers), --merge-moods, deterministic multi-machine --chunk M/N, --hash-only NDJSON manifest mode, opus auto-retry, and a standalone --transcode utility.

The output (mbxmoods.json) carries all of this in a single per-track object that MBXHub reads directly.

Minor utility modes (--synthesize, --seed-moods) cover synthetic-library generation and AcousticBrainz-driven mood seeding for scale-testing MBXHub — see Test Tooling at the bottom of this doc.

Output:

  • mbxmoods.json - mood coordinates and raw audio features for every track
  • mbxmoods-errors.csv - tracks that failed analysis, with the error reason, file size, duration and a FailedComponents column naming exactly what failed (e.g. decode-27%;fingerprint;sha). Failure is broader than "Essentia crashed": a track that analyzes but cannot be hashed or identified (corrupt header), whose tags are unreadable (per-file modes), whose decode covers less than 95% of its claimed duration, or whose applicable bitUsage/HF signal died is a fail — no catalog entry is written, and it is skipped on later scans until fixed (--retry-errors re-attempts). Legitimately-absent signals (bitUsage on lossy files or silence, HF at 44.1 kHz, no ffmpeg, no SMFM) are never failures.
  • mbxmoods-skipped.csv - every track dropped before analysis, with the reason: remote stream URLs (never scannable), DSD streams (.dsf/.dff/.dsd), video files, playlist/redirector entries, missing files (length called out when the path exceeds MAX_PATH), and any track matched by an exclude rule in mbxmoods-exclude.json (excluded (rule: …) — see Excluding files from analysis). Speech labels and embedded file markers are not a skip reason any more — they are review evidence on --preview's output; see the upgrade note below if you relied on the old auto-skip. Columns path,extension,reason,timestamp, appended per run; --audit also lists each drop on the console. Over-MAX_PATH files that do exist are analyzed through a \\?\ staged copy, not skipped.
  • mbxmoods-verify.csv - per-entry status from --verify / --verify --backfill (OK / DRIFT / MISSING / NO_HASH / BACKFILLED / REANALYZE_NEEDED / ERROR, plus the list of fields filled per entry)
  • truedat.log - full console output for diagnostics (when --audit is used)

Download

truedat.zip — full self-contained bundle (truedat.exe + Essentia + ffmpeg). Runs offline, no install. Unzip anywhere and run truedat.exe.

What's inside and what happens if you drop pieces: dependency-surface notes.

Source tree and tagged snapshots are on GitHub.

What It Does

Truedat reads an iTunes Music Library XML file (or a --folder, --file-list, or single --analyze-file) and runs each track through Essentia plus its own analyses. Per-track work is parallelized across cores; everything is cache-aware so re-runs only touch what changed.

Default scan (analysis + identity + authenticity)

A single default invocation produces the full per-track record:

  • Mood features (Essentia) — Valence (0-1, sad ↔ happy; 8 input features), Arousal (0-1, calm ↔ energetic; 7 input features), and 15 core + 40 extended Essentia features stored per track for runtime recomputation (extended set covers loudness envelope, silence profile, spectral shape, psychoacoustic bands, rhythm/tonal aggregates).
  • Identity (truedat-native, pure-managed)fingerprint.v1 composite, audioStreamSha256 (TagLib invariant audio region), and optionally fileMd5 (whole-file MD5, maintained only with --file-md5). All computed concurrently with the Essentia decode so they're effectively free.
  • Authenticity signals (truedat-native, ffmpeg-driven)bitUsage block (LSB walk for fake-hi-res detection on lossless 24-bit files), hfEnergyRatio + hfSpectralStructure (single managed-FFT pass over the same 30 s mid-track segment — bin-sharp HF energy ratio plus Wiener-entropy flatness, peak-to-mean, and mid↔HF mirror correlation; catches ffmpeg-upsampled fakes the bit-level signals miss), MP3 LAME-tag parsing (Xing/Info+LAME header decode for transcode detection).
  • truedat.* verdict block — multi-signal voted hi-res / lossy-transcode verdicts computed inline at write time from the signals above. Threshold changes ship without rescanning.

MBXHub consumes the whole record: mood features drive AutoQ vibe selection; identity drives cross-cache matching; authenticity drives the hi-res / transcode classifiers.

Verify & backfill (--verify, --verify --backfill)

Walks an existing mbxmoods.json and either reports drift (read-only) or repairs missing fields in place (no Essentia re-run). Two backfill tiers — identity (TagLib + cheap IO) and features (ffmpeg-driven bitUsage / hfEnergyRatio / hfSpectralStructure) — both gated by an SHA drift check so drifted entries are flagged for re-analysis rather than touched. See Verify & Backfill for tier scoping via --backfill-level.

Backups & snapshots (--snapshot, --restore, --keep-backups)

The mutating modes (--fixup, --remap, --merge-moods, --migrate) drop a timestamped backup before they rewrite the catalog. Those backups are now compressed ZIPs (mbxmoods.json.bak.<ts>.zip) — mbxmoods.json is repetitive text and shrinks ~8–10×, so keeping several is cheap — and they rotate: --keep-backups N keeps the newest N (default 5, 0 keeps everything; rotation never silently truncates).

--snapshot [path] is a deliberate, read-only capture: it bundles the current catalog, a manifest (track count + content SHA-256 for verification), and the mbxmoods-exclude.json policy file into one restorable, double-clickable mbxmoods-snapshot-<ts>.zip. --restore <archive> rebuilds mbxmoods.json from a snapshot, a compressed backup, a .gz, or a plain catalog — the format is auto-detected, so older plain-copy backups restore too — and it backs up whatever it's about to overwrite first, so a restore is itself reversible. (The live mbxmoods.json MBXHub reads stays plain JSON; only backups and snapshots are compressed.)

Filename Check (--check-filenames)

Scans your library for filenames with characters that cause Essentia tools to fail. Reports three tiers:

  • Errors — Fullwidth Unicode substitution characters (e.g. ) that are known to break Essentia's ANSI argv parsing. These files will always fail analysis.
  • Warnings — Other non-ASCII characters where 8.3 short path fallback is unavailable. These files may fail depending on system configuration.
  • Suspects — Audio files under 50 KB that may be corrupt or truncated.
  • Long paths — Paths at or over the Windows 260-char MAX_PATH. The scan handles these automatically (\\?\ long-path fallback through a staged copy), but other tools may not — consider shortening.

Quick Start

REM Mood analysis (default)
truedat.exe "iTunes Music Library.xml"

Output: mbxmoods.json (next to the XML file)

Auto-discovery: if you omit the positional XML arg, truedat searches for the library nearest-first and takes the first hit. In order: the exe's parent directory (the drop-in case — put the truedat folder under your library directory and it just works), the exe's own directory, the Library\ subfolder of each, then successive parent directories (bounded) and their Library\ subfolders, then the instance's AppData\ subfolders, and finally %APPDATA%\MusicBee for a non-portable install. The error message lists the probed locations so a no-hit failure is self-diagnosing.

truedat --paths tells you what it decided — every path in force (catalog, sidecar, exclusion file, playlist, logs, ledgers, staging), each present or absent, and which step resolved the catalog. It writes nothing. When something looks wrong, run that first: "which files is it actually using" is usually the answer.

In practice that means anywhere under a MusicBee instance is found — including running truedat from <root>\AppData\MBXHub\ — while <root>\Library always wins when several candidates exist, and the user's default library is only consulted when nothing instance-relative matched. It is a fixed set of known locations rather than a recursive scan, so it stays fast and predictable.

The catalog modes (--stats, --prune-excluded, --list-speech, --list-smfm, --list-missing-smfm, --list-formats, --snapshot, --compact / --prettify, --restore, --verify-coverage) find mbxmoods.json through the same ladder when you give them no path. The exclusion file follows the catalog, so mbxmoods-exclude.json is found beside it. The upshot is that a bare truedat --stats always means the same library a bare truedat scan does, from whichever directory you happen to run them.

Options

truedat --help shows the short everyday page; truedat --help all lists every option. Unknown flags fail with a "did you mean" suggestion instead of being ignored. The reference below covers the commonly used options; --help all is the complete and authoritative list.

truedat.exe <path-to-iTunes-Music-Library.xml> [options]

  -p, --parallel <N>      Number of parallel threads (default: all cores)
  --fixup                 Validate and remap paths in mbxmoods.json without re-analyzing.
                          With --remap, performs a pure prefix swap (see --remap below).
  --remap <old>=<new>     With --fixup: wholesale prefix swap on mbxmoods.json keys.
                          Pass the moods file as the positional arg (no iTunes XML
                          needed). Case-insensitive prefix match; entries that don't
                          start with the old prefix pass through unchanged. Writes a
                          compressed .bak.<timestamp>.zip before atomic replace. Example:
                          truedat --fixup --remap "D:\Music\=\\nas\share\Music\" mbxmoods.json
  --verify                Recompute audioStreamSha256 per entry, report drift / missing
                          (read-only; writes mbxmoods-verify.csv next to the moods file)
  --verify --backfill     Fill in missing fields for entries whose audio bytes are
                          unchanged. Drifted entries are flagged as REANALYZE_NEEDED,
                          never modified. No Essentia re-run. Idempotent. Two tiers,
                          both run by default — scope down with --backfill-level:
                            identity tier (TagLib + cheap file IO; fast):
                              audioStreamSha256, fileMd5 (with --file-md5), whole fingerprint.v1, sub-fields
                              (bitDepth, encoder, ...), and the MP3 LAME tag block for
                              codec=mp3 entries.
                            features tier (ffmpeg-driven; slow — ~30s of decode per
                              applicable track):
                              bitUsage (lossless ∧ bitDepth >= 24 only),
                              hfEnergyRatio + hfSpectralStructure (single FFT pass;
                              sourceSampleRate > 44.1k only). Silently skipped when
                              ffmpeg is absent.
  --backfill-level <name> With --verify --backfill: scope which tier runs.
                            all       (default) identity + features
                            identity  fast tier only (TagLib + cheap file IO)
                            features  ffmpeg tier only (bitUsage / hfEnergyRatio / hfSpectralStructure)
  --stats [path]          Read-only catalog summary: Essentia-analyzed count, hash coverage
                          per kind, and SMFM track count. Path defaults to ./mbxmoods.json.
                          Also printed at end of every scan. With --audit, written to the log.
                          Reports TWO disjoint speech counts, so each points at a surface that
                          can actually show what it counted: "Speech (acoustic)" is the
                          recomputed speechLikely == yes set (exactly what --list-speech lists)
                          and "Speech (genre label)" is the stored genre == "Podcast" remainder
                          that never reached an acoustic yes (invisible to --list-speech, so it
                          points at a genre rule instead). Both point at review + an exclusion
                          rule, never at --migrate, which doesn't touch entries either way.
                          Also reports the .mbxs sidecar's version, freshness and entry count —
                          the cheap way to see whether MBXHub can boot from it or is silently
                          falling back to parsing the whole JSON. Separately, a Recommended
                          section maps detected gaps (missing tonal/rhythm wave, missing
                          fingerprint.v1, stray fileMd5, entries an exclusion rule now covers,
                          an unusable sidecar) to the exact command that closes each — truedat
                          never prompts interactively.
  --stats-detail N        List per-file status when a catalog has < N tracks (default 5).
  --list-speech [path]    Read-only: list the entries whose verdict is speechLikely=yes —
                          candidates for an exclusion rule, not a --migrate prune (--migrate
                          never removes entries). Writes mbxmoods-speech.csv (path, artist,
                          title, album, genre, codec, confidence, method) next to the moods
                          file and prints a count + preview. The speech verdict is still
                          -untuned, so real music with talk-like shape (ambient, field
                          recordings, spoken intros) can appear here; review before excluding.
  --list-smfm [path]      Read-only: the full Sony SMFM (12-TONE) report. Every catalog
                          entry, classified into one of three states, with the decoded
                          values. Writes mbxmoods-smfm.csv (path, state, artist, title,
                          album, codec, smfmBpm, smfmChannel, topScore, scores) and prints
                          the three counts.
                            data     block present, at least one non-zero score
                            no-data  block present, every score zero — Sony wrote the
                                     block and scored nothing. NOT an untagged file:
                                     re-tagging is the fix for no-smfm and is exactly
                                     what has already been tried here
                            no-smfm  no block at all — never through Sony's tagger
                          This is the answer to "the scan reported +smfm on N tracks —
                          which ones?": that scan line is transient console output and
                          fires only when a track NEWLY gains SMFM, so it is a change
                          notice, not an inventory. The raw score vector rides along
                          because smfmChannel is the argmax SLOT, not a mood channel —
                          a consumer needs the vector.
  --list-missing-smfm [path]
                          Read-only: list catalog entries carrying no Sony SMFM (12-TONE)
                          data, with overall coverage (present / total / %). Writes
                          mbxmoods-smfm-missing.csv (path, artist, title, album, codec).
                          SMFM is embedded in the file by Sony tooling and only READ by
                          truedat — a rescan will not add it, so this is a "which files
                          still need Sony tagging" report, not a truedat gap. For that
                          reason it is deliberately absent from the --stats Recommended
                          block, which only ever names gaps a truedat command can close.
  --list-formats [path]   Read-only: what the library is MADE OF, by codec/container, and
                          which folders each format lives in. Prints a summary table
                          (count, share, lossy/lossless) then a per-format folder rollup;
                          writes mbxmoods-formats.csv (path, codec, codecRaw, bitrate,
                          sampleRate, bitDepth, fileSize, artist, album, title). Answers
                          "I still have WMA somewhere — where?", which no coverage report
                          does: --stats reports how COMPLETE the catalog is, never what is
                          in it. Bitrate and friends ride along as columns so "and by
                          bitrate?" is a pivot rather than another mode.
  --strip-smfm [path]     Remove the Sony SMFM (12-TONE) fields from every catalog entry.
                          Removes DATA, not entries: every track and every other field
                          survives. Both key generations go (smfm* and the legacy sensme*
                          the reader still falls back to), a compressed backup is written
                          first, the rewrite is atomic, and the .mbxs sidecar is
                          regenerated — the hub boots sidecar-first, so a hand edit of the
                          JSON would change nothing at runtime. Prints how many entries
                          carry SCORED 12-TONE data before acting: those drop to the
                          essentia-only mood head, which is the weaker one for valence,
                          and that count is wildly library-dependent (~2% of one library,
                          93% of another) so it is reported as a count, never a share you
                          have to remember. Add --dry-run to report and write nothing.
                          SMFM is READ from the file's Sony tag and never computed, so a
                          later analysis of a file still carrying the block puts it back —
                          scan with --no-smfm if the removal is meant to stay.
  --prune-excluded [path] Remove catalog entries that an exclusion rule now covers — the
                          ones scanned before the rule existed, which exclusions otherwise
                          only ever keep out of FUTURE scans. Rules are the sole authority:
                          include rules still win, and nothing is removed on a classification.
                          Needs no iTunes XML and reads no audio (genre comes from the entry),
                          so it runs on a metadata mirror. Writes a compressed backup first
                          and regenerates the .mbxs sidecar. Add --dry-run to print exactly
                          what would go and write nothing. Audio files are never touched.
  --duplicates [path]     Read-only duplicate-audio report over mbxmoods.json: exact groups
                          (byte-identical audioStreamSha256) plus probable cross-encode
                          candidates (quantized feature match), each with a recommended
                          keeper (lossless > bitDepth > sampleRate > bitrate > SMFM-tagged >
                          size > shortest path). Writes mbxmoods-duplicates.csv + .json;
                          per member: codec/bitrate/sampleRate/bitDepth/album + an smfm flag.
  --losers-m3u [path]     With --duplicates: write non-keeper members to an .m3u8 playlist
                          for review/removal inside MusicBee. Path must end in .m3u or .m3u8;
                          default is mbxmoods-duplicate-losers.m3u8 next to the moods file.
  --html [path]           With --duplicates: write a self-contained interactive review page
                          (offline, no server). Include duplicate groups in chunks, confirm
                          keepers, click Build losers playlist to download the .m3u8. Default
                          mbxmoods-duplicates.html next to the moods file.
  --manifest [path]       With --duplicates: emit the kind:dupes review-surface manifest that
                          MBXHub's review.html renders. No path auto-locates the running
                          MusicBee instance's <root>\AppData\MBXHub\review\dupes.json.
  --chunk M/N             Split scan across machines via deterministic hash-mod assignment
                          (output auto-suffixed: mbxmoods.<hostname>.json; combine via --merge-moods)
                          Also composes with --verify, where it serves a different purpose:
                          making a long verify RESTARTABLE. Each shard verifies its own slice
                          and writes mbxmoods-verify.<host>.<M>of<N>.csv, so a finished shard is
                          durably done and an interrupted pass costs one slice, not the whole
                          run. The shards partition the catalog exactly. With --backfill, run
                          shards ONE AT A TIME — each save rewrites the whole catalog, so
                          concurrent shards would overwrite each other's backfills.
  --retry-errors          Re-attempt all previously failed files (clears error log)
  --migrate               Clean up mbxmoods.json: strip legacy fields (valence/arousal,
                          audioMd5, chromaprint) and fileMd5 (kept with --file-md5), rename
                          SMFM keys (sensme*->smfm*) (creates a compressed backup). Field
                          cleanup only — never removes a catalog entry, on any verdict.
  --snapshot [path]       Read-only: bundle the current catalog into a restorable, double-
                          clickable ZIP — mbxmoods.json + a manifest (trackCount, content
                          SHA-256, byte size) + mbxmoods-exclude.json when present. Default
                          output mbxmoods-snapshot-<timestamp>.zip beside the catalog.
  --restore <archive>     Rebuild mbxmoods.json from a snapshot, a compressed backup, a .gz,
                          or a plain catalog (format is auto-detected). Backs up any existing
                          catalog first, then atomically swaps. Restores the catalog only — a
                          bundled exclusion file is left in the archive (extract it by hand to
                          roll policy back too).
  --compact [path]        Re-format an existing catalog to minimal JSON in place — the format
                          every save already writes. Lossless: a JSON-level round-trip that
                          preserves every field byte-for-byte in value, including keys truedat
                          does not model. Archives a compressed backup first; never re-analyzes.
  --prettify [path] [out] The inverse — indented, human-readable JSON. With no <out> the catalog
                          is rewritten in place (backup archived first); give <out> and the
                          source is left untouched. Read a catalog with this, --compact to
                          put it back.
                          NOTE: the modes that REWRITE a catalog (--compact, --prettify,
                          --restore, --prune-excluded) refuse to fall back to whatever
                          mbxmoods.json happens to sit in the current directory. They accept a
                          path, --moods, the library XML as the positional argument, or a
                          catalog auto-discovered beside the exe (see Auto-discovery); with
                          none of those they refuse and name the file they would have hit.
                          Read-only modes (--stats, --list-speech, --list-missing-smfm) still
                          fall back to the current directory.
  --keep-backups N        How many timestamped catalog backups to keep after --fixup / --remap
                          / --merge-moods / --migrate (rotation, never truncation; 0 = keep
                          all; default 5). Backups are compressed .zip — mbxmoods.json is
                          repetitive text and shrinks ~8-10x, so keeping several costs little.
  --output <path>         --hash-only mode: append identity envelopes as NDJSON to <path>
  --hash-only             Identity-only mode (no Essentia). Requires --level, --file-list, --output
  --level <name>          With --hash-only: 'fingerprint' (cheap composite) or 'stream' (durable SHA-256)
  --audit                 Write all console output to truedat.log (for debugging)
  --self-test             Run inline FFT sanity checks and exit (no library scan)
  --analyze-file <path> Analyze a single audio file with Essentia (no iTunes XML needed)
  --file-list <path>    Analyze files listed in a text file (one path per line, UTF-8, # comments)
                        Mutually exclusive with --analyze-file; -p sets parallelism
  --check-filenames       Scan for filenames with characters that break Essentia tools
  --transcode <input>     Standalone: ffmpeg-transcode <input> to uncompressed FLAC.
                          Requires --transcode-out. Mutually exclusive with all scan modes.
  --transcode-out <path>  Output FLAC path for --transcode mode.
  --sample-rate <hz>      With --transcode: override output sample rate (default: match source).
  --bit-depth <16|24>     With --transcode: override output bit depth (default: match source).
  --no-stage              Disable source staging (UNC, mapped network drives, non-ASCII paths);
                          workers read source directly.
  --stage-dir <path>      Override the scratch dir used for staged source copies and
                          multi-channel downmixes (default %TEMP%\.truedat-stage). Point it
                          somewhere your antivirus exclusion covers — see "Antivirus" below.
                          Also the isolation lever if you ever need two truedats on one
                          machine — see "Run one truedat at a time" below.
  --max-duration <secs>   Max track length for Essentia analysis (default 48000 = 800 min —
                          the large-buffer extractor's ChordsDetection ceiling; pass 12000
                          if running the old small-buffer .1 extractor). Longer tracks
                          are a structural skip: ledgered to mbxmoods-skipped.csv, NOT
                          counted as failures. (They used to land in the errors CSV, which
                          made the next run skip them as previous errors — a legitimate
                          "too long for this extractor" turned into a permanent silent skip
                          that no longer explained itself.) Raise only when running an
                          extractor built with the larger buffer.
  --no-quick-cache        Disable the tags-only quick cache tier (head-64k check);
                          mtime-drifted files always take the full audio-hash check.
  --no-bitusage           Suppress ComputeBitUsage (omits the bitUsage JSON block).
  --no-hf-analysis        Suppress ComputeHfAnalysis (omits hfEnergyRatio + hfSpectralStructure).
  --no-smfm               Suppress every Sony SMFM (12-TONE) read — the cache-miss fan-out
                          and the --refresh-smfm backfill alike (omits the smfm* fields).
                          Unlike its two siblings above this saves no meaningful time: SMFM
                          is read from the file's tag, never computed. It exists to make a
                          --strip-smfm durable, because without it the next cache MISS on
                          any file still carrying the Sony block puts the data straight
                          back. Refuses to run alongside --refresh-smfm: one forces the
                          re-read the other suppresses, and a silent no-op is worse than a
                          refusal.
  --file-md5              Maintain the whole-file fileMd5 field (default off). Off means it is
                          never written: fresh analysis omits it, cache-tier refreshes drop it,
                          the --backfill fileMd5 fill is skipped, and --migrate strips stored
                          values. The durable audio identity is audioStreamSha256 either way;
                          enable this only if you compare whole-file MD5s with external tools.
  --pause                 Hold the console open at exit (press any key). For double-click
                          launches; no effect on redirected/scheduled runs.
  --allow-sleep           Let the machine sleep during scans. By default, work-bearing
                          runs (scan/verify/transcode) hold the system awake while on AC
                          power — see "Power & sleep" below. Display sleep is unaffected
                          either way.
  --exclusions <path>     Use this exclusion file instead of mbxmoods-exclude.json beside
                          the moods file.
  --no-exclusions         Ignore the exclusion file for this run. Prints a warning; for
                          diagnosing whether a rule is what kept a file out.
  --apply-exclusions <path>
                          Merge a decisions delta into the exclusion file. Backs up the
                          previous version and reports added / removed / already-set /
                          not-present counts. Exits 1 without writing if the document or
                          the existing file cannot be parsed. The write is staged to a
                          .tmp sibling and swapped atomically, so an interrupted or
                          out-of-disk apply can never truncate your rules; the merge holds
                          a zero-byte mbxmoods-exclude.json.lock sidecar across read and
                          write, so two applies at once cannot lose one author's rules
                          (a text editor saving the file at the same moment is outside
                          any lock — that is what the .bak is for).
  --preview [path]        Analyzes nothing, writes no mbxmoods.json, and never touches the
                          exclusion file. It DOES write two files: the scan work plan and the
                          review-candidate list go to preview.json plus mbxmoods-preview.html
                          in the MBXHub review folder of the instance that owns the library you
                          pointed it at (creating that folder if needed) — so this is read-only
                          over your catalog, not over the disk. When no MusicBee/MBXHub instance
                          owns the library they land beside the moods file as
                          mbxmoods-preview.json instead; never in some other instance's folder.
                          Pass [path] to choose the destination yourself. Auto-discovers the
                          iTunes XML the same way a normal scan does (exe dir, its parent, then
                          the cwd), so it needs no positional argument when run from inside the
                          library folder. [path] is an OUTPUT and is only claimed when it looks
                          like one (a directory separator or a .json suffix), so the library
                          path can still be positional; and it refuses, with exit 1 and nothing
                          written, to overwrite an .xml or an existing mood catalog.
  --long-track-mins N     Duration that flags a track for review in --preview (default 30).
                          A review *prompt* only: it decides what a human is asked to look
                          at and never excludes anything by itself.

After a mass tag edit: rewriting tags across the library changes every file's mtime without touching audio. Truedat detects this per file at ~64 KB/track (a quick head-hash check) instead of re-reading each file in full, so a full-library rescan after a retag pass finishes in a fraction of the time and re-runs zero analysis. --no-quick-cache forces the full per-file audio-hash check instead, and --verify remains the full-integrity check against the durable audioStreamSha256.

Network libraries: when the source is on a UNC share (\\server\share\…), a mapped network drive (e.g. Z:\ mapped to \\server\share), or a local path with non-ASCII characters, truedat stages each file once to a local temp copy and runs the 8-9 concurrent per-track workers (and the cache hierarchy's tier-2/4 body reads) against the local copy. Net: 1× full network read per track instead of ~3× full + ≥3× partial. Cache tier-1 (path + mtime equality) doesn't stage — already-cached tracks stay free. Local-ASCII paths read directly. Use --no-stage to opt out, or --stage-dir to relocate the staging directory (e.g. to a fast scratch volume when %TEMP% is on a small SSD). Per-track stage failures fall back to direct read with a one-line warning — scans never abort over a staging hiccup. End-of-scan summary reports staging: N staged (or N staged, M direct-fallback), with a stderr warning when >5% of attempted stages fell back so a wedged stage-dir is visible.

Antivirus. A scan creates and deletes a lot of short-lived scratch files, and real-time protection scans every one of them — work that buys nothing, since the big ones are byte-for-byte copies of files already sitting in your library. Three separate scratch locations are involved, and they are not all relocatable:

  • Staged source copies, multi-channel downmixes, and Essentia's per-track output JSON — all follow --stage-dir <path>. The staged copies are whole audio files and by far the largest share. Point --stage-dir somewhere an exclusion covers rather than excepting all of %TEMP%.
  • Per-drive .truedat-tmp hardlinks — created at the root of the volume holding the audio, and not relocatable: a hardlink has to live on the same volume as the file it points at. These are links rather than copies, so no audio is duplicated for a scanner to read.

So one --stage-dir exclusion covers everything except the hardlinks. If you would rather cover the lot in one move, exclude the truedat / essentia / ffmpeg processes instead of (or as well as) a directory — that follows the work wherever it writes. Process exclusions are also the answer if your scanner is heuristic about unsigned binaries; Essentia decodes audio in tight loops and reads like something worth a second look. None of this is required — it is about not paying to scan the same audio twice.

Run one truedat at a time. Truedat is single-instance by design. At startup it sweeps leftover scratch files from previous runs — staged copies and downmix WAVs under the staging dir, and .truedat-tmp hardlinks on each drive — and it cannot tell a dead run's leftovers from a live sibling's working files. Two guards make that safe rather than merely documented: a second truedat process on the machine skips the sweep entirely with a warning, and any file touched in the last 60 minutes is left alone regardless. If you genuinely need two runs at once, give each its own --stage-dir (or pass --no-stage); note the per-drive .truedat-tmp hardlink area cannot be relocated, because a hardlink has to sit on the same volume as its source, so it leans on those two guards. To scale across machines, use --chunk M/N and --merge-moods rather than concurrent runs against one library.

Optional: Place ffmpeg.exe and ffprobe.exe alongside truedat.exe (or on PATH) to enable:

  • Auto-downmix of multi-channel (5.1+) audio files during scans (without ffmpeg, multi-channel files are skipped with a warning).
  • Auto-retry of files essentia can't decode natively — e.g. .opus, which this essentia build lacks. The file is transcoded to a stereo WAV and essentia is re-run against it, all transparently.
  • Standalone --transcode mode for converting opus/etc. to uncompressed FLAC.

Large Libraries

For large libraries (50K+ tracks), expect multi-day scans for mood analysis. The scan is designed for this:

  • Incremental - Skips tracks already processed (by file path + last-modified timestamp).
  • Tag-edit resilient - When mtime drifts but the audio bytes are unchanged (e.g. tag editor rewrote a frame), the cache reuses Essentia features by recomputing only audioStreamSha256 (~50ms managed SHA per file) and refreshing the tag-affected identity fields. No full re-extraction.
  • Cross-path resilient - File moved or renamed? The cross-SHA fallback re-keys the cached entry to the new path without re-analyzing.
  • Multi-machine chunking - Two boxes pointed at the same library run --chunk 1/2 and --chunk 2/2 and produce hostname-suffixed shards (mbxmoods.<host>.json); merge later with --merge-moods. Hash-mod assignment means the iTunes XMLs need not be identical between machines. Each shard's mbxmoods-skipped.<host>.csv holds only that shard's own tracks, so the ledgers add up to the library instead of repeating it N times; per-rule exclusion hit counts are the deliberate exception — they are library-wide and identical on every machine, which is what makes a 0 matched reading trustworthy, and the console says so under --chunk.
  • Resumable - Stop and restart anytime. Progress is saved every 5 minutes, so an interrupted scan loses minutes of work rather than an unpredictable number of tracks.
  • Verifiable - truedat --verify walks the moods file and confirms each entry's audioStreamSha256 still matches disk, writing mbxmoods-verify.csv and exiting 1 on any drift / missing / error (CI-friendly). FLAC entries hashed by pre-2026-07 builds are recognized via a legacy-region compare in the same read and upgraded in place under --backfill (audioStreamSha256-upgraded); FLACs whose tags were rewritten before upgrading are re-keyed automatically during a normal scan (props-gated, reported as FLAC re-key), or by --verify --backfill --accept-flac-tag-drift without a scan. --backfill repairs missing fields in place with no Essentia re-run: the identity tier (TagLib-driven, fast) and the features tier (ffmpeg-driven, ~30 s per applicable lossless 24-bit track) both run by default — narrow with --backfill-level identity|features. Every tier is gated by the drift check, so drifted entries are flagged REANALYZE_NEEDED rather than touched.
  • ETA tracking - The progress line shows estimated completion, running average per track, and live throughput as Nx realtime — audio-seconds analyzed per wall-second, with MB/s as a footnote. Duration is the honest unit: a FLAC and a low-bitrate MP3 of the same content cost the same to scan, and the numerator is what was actually decoded. The estimate is two-class aware — tracks new to the catalog are costed by audio duration at the measured real-time factor, already-cataloged ones at the measured cache-hit average — and nothing is shown until something has been measured. A track at or past --long-track-mins (default 30) carries its duration on its own progress line (... - II [1h47m]). The end-of-scan summary leads with the scan-wide realtime factor and breaks down count and average cost per outcome class.
  • Error resilience - Failed tracks logged to errors CSV, skipped on retry.
REM First run - analyzes everything
truedat.exe "iTunes Music Library.xml" -p 4

REM Resume after interruption - picks up where it left off
truedat.exe "iTunes Music Library.xml" -p 4

REM Fix path separators without re-analyzing (e.g., after moving files)
truedat.exe "iTunes Music Library.xml" --fixup

REM Re-key mbxmoods.json from one root to another (e.g., scanned local copy of
REM a NAS mirror, need entries keyed by the UNC path). No iTunes XML needed.
truedat.exe --fixup --remap "D:\Music\=\\nas\share\Music\" mbxmoods.json

REM Check for problematic filenames before scanning
truedat.exe "iTunes Music Library.xml" --check-filenames

REM Analyze a single file (no iTunes XML needed)
truedat.exe --analyze-file "C:\Music\song.mp3" --json-output

REM Batch analyze files from a list, write entries to a moods file
truedat.exe --file-list files.txt --moods C:\Music\mbxmoods.json -p 4

REM Hash-only: cheap composite fingerprint, append to NDJSON manifest (ms per file)
truedat.exe --hash-only --level fingerprint --file-list files.txt --output manifest.ndjson -p 32

REM Hash-only: durable audioStreamSha256 (disk-bound; emits fingerprint.v1 too)
truedat.exe --hash-only --level stream --file-list files.txt --output manifest.ndjson -p 8

REM Verify the cache against disk (read-only — recomputes audioStreamSha256 per entry)
truedat.exe --verify --moods C:\Music\mbxmoods.json

REM Backfill ALL missing fields — identity (TagLib, fast) + features (ffmpeg, slow).
REM No Essentia re-run; drifted entries are flagged, not modified.
truedat.exe --verify --backfill --moods C:\Music\mbxmoods.json

REM Identity tier only — fast first pass that doesn't need ffmpeg
truedat.exe --verify --backfill --backfill-level identity --moods C:\Music\mbxmoods.json

REM Features tier only — fill ffmpeg-driven bitUsage / hfEnergyRatio / hfSpectralStructure on a library
REM whose identity is already complete (e.g., after the identity-only pass above)
truedat.exe --verify --backfill --backfill-level features --moods C:\Music\mbxmoods.json

REM Transcode opus (or any ffmpeg-readable input) to uncompressed FLAC at source rate/depth
truedat.exe --transcode "C:\Music\track.opus" --transcode-out "C:\Music\track.flac"

REM Same, but force a specific output rate and bit depth
truedat.exe --transcode "C:\Music\track.opus" --transcode-out "C:\Music\track.flac" --sample-rate 44100 --bit-depth 16

REM Find and clean duplicate audio
truedat.exe --duplicates "D:\MusicBee\Library" --losers-m3u
REM Review the report (exact = byte-identical, safe calls; probable = feature-match
REM candidates, confirm before acting), then load mbxmoods-duplicate-losers.m3u8 in
REM MusicBee to review and delete the redundant copies from inside the player.

REM Two-machine same-library scan: each box does its own deterministic shard
truedat.exe "iTunes Music Library.xml" --chunk 1/2     REM machine A
truedat.exe "iTunes Music Library.xml" --chunk 2/2     REM machine B
truedat.exe --merge-moods --merge-source mbxmoods.machineA.json --merge-source mbxmoods.machineB.json --merge-output mbxmoods.json

Power & sleep

While a scan, verify, or transcode is running on AC power, truedat holds a Windows power request so the machine will not idle into sleep mid-run (the display still sleeps; closing a laptop lid still sleeps the machine). The hold is visible in powercfg /requests (admin console) as "truedat: library scan in progress" and is released automatically when the run ends — including on crashes, since Windows frees it with the process.

  • On battery, normal sleep always applies — plug in for overnight runs.
  • --allow-sleep disables the hold entirely for operators whose machine policy should win.
  • On Windows 11, truedat also opts itself and its analysis subprocesses out of efficiency-mode throttling (EcoQoS) so hybrid-CPU boxes do not silently park a scan on efficiency cores — except under --background/--cpu-limit, which exist to yield.

Two things no process can override, worth setting on a dedicated scan box: Windows Update restarts (set active hours) and the Windows 11 power-mode slider (prefer Balanced or better while scanning).

Excluding files from analysis

Truedat decides what to skip from one file: mbxmoods-exclude.json, beside mbxmoods.json. Nothing else excludes a track for policy reasons — metadata signals like genre are evidence you can act on, never an instruction the scanner infers on its own.

Exclusion playlists — the easy way to author excludes

The easiest way to build an exclusion list is in MusicBee itself: make a playlist named mbxmoods-exclude and put every track you don't want scanned into it. Every scan looks for mbxmoods-exclude.m3u8 (or .m3u) beside the library XML, then in its Playlists folder, and treats every entry as a file exclude rule for that run — edit the playlist, rescan, the exclusions follow. Nothing is written to the JSON; the playlist itself is the durable list. The scan header names the playlist in force. You can also name one explicitly with --exclude-playlist <path.m3u8> (wins over discovery), or make the entries permanent JSON rules with truedat --apply-exclusions <playlist.m3u8> (entries become file/exclude rules, with backup and apply-result.json as usual). include rules in mbxmoods-exclude.json still win over playlist excludes, and --no-exclusions bypasses the playlist along with the file. Stream URLs in the playlist are skipped (they're never scannable); comment lines are ignored; relative entries resolve against the playlist's folder. A playlist that exists but yields no usable entries stops the scan rather than silently excluding nothing.

Upgrading: speech (podcasts, talk) is no longer skipped automatically

Older builds guessed which files were speech — podcasts, talk — and skipped them. Nothing guesses now — the only thing that keeps a file out of analysis is a rule you wrote. But the order matters, and it is counter-intuitive: to keep talk out of what AutoQ plays, analyze once, then exclude — do not exclude first.

Here is why. AutoQ picks from your MusicBee library, not from mbxmoods.json, and it keeps talk out by reading each track's speechLikely verdict — which only exists once the track has been analyzed. So excluding speech from scanning before it has ever been analyzed removes the very signal AutoQ needs: no analysis → no verdict → AutoQ's speech gate can't fire → the track stays fully pickable. Excluding speech from scanning makes AutoQ more likely to play it, not less.

The recommended order:

  1. Scan once — let Essentia analyze everything, talk included, so every track gets its speechLikely verdict. AutoQ's speech gate (on by default) now keeps talk out of your queues.
  2. truedat --preview — see what a scan would do and which tracks are worth a look.
  3. truedat --list-speech — review the tracks the acoustic verdict flagged as talk.
  4. For the ones you never want re-analyzed, write a decisions delta and apply it: {"schemaVersion":1,"kind":"exclusion-decisions","add":[{"kind":"folder","action":"exclude","pattern":"\\Podcasts\\**"}],"remove":[]} then truedat --apply-exclusions decisions.json (a genre rule for Podcast works too). This saves Essentia time on every future scan, and the AutoQ gate keeps working because the verdict already exists.

Excluding a folder you genuinely never want analyzed — a bulk podcast archive you never play — is still legitimate and saves real scan time up front. Just know the trade: you skip the scan cost, but those tracks get no speechLikely signal, so AutoQ has nothing to keep them out of a queue. Skip-to-save only when you never want it played and never want it analyzed; analyze-first when you want AutoQ to keep it out for you.

--include-podcasts is gone; it was all-or-nothing, which is the problem the rule file solves. --migrate no longer removes speech-labelled or speech-likely entries either — pruning an entry for a file that is still in your library is self-undoing, because the next scan re-analyzes it.

{
  "schemaVersion": 1,
  "rules": [
    { "kind": "folder", "action": "exclude", "pattern": "\\Podcasts\\**" },
    { "kind": "folder", "action": "include", "pattern": "\\Podcasts\\KEXP Song of the Day\\**" },
    { "kind": "genre",  "action": "exclude", "value": "Podcast" },
    { "kind": "file",   "action": "exclude", "path": "D:\\Music\\setlist-2019.mp3",
      "note": "many songs in one file, so one average describes nothing" }
  ]
}

Three rule kinds, each exclude or include:

  • folderpattern must end in ** (the subtree). Write it as a fragment starting with a separator (\Podcasts\**) and it matches under any root, which is what you want when the same library is mounted on more than one drive. An absolute pattern (D:\Music\Podcasts\**) matches that root only. Fragments are boundary-aligned, so \Podcasts\** does not match \MyPodcastsBackup\.
  • genre — exact match, case-insensitive, trimmed. Not a substring match: Podcast does not match Comedy Podcast.
  • file — one exact path. Matching is always that exact path, nothing else. Optional audioStreamSha256 records the audio's durable content identity: when the path no longer exists, --preview reports the rule as moved or deleted rather than leaving it looking like a stale rule you should delete, and names the catalog paths that still hold that content so you can re-point the rule. It is a report and an offer — the rule never starts matching those paths on its own, because one audioStreamSha256 routinely covers several copies of the same audio (that is exactly what --duplicates groups on) and a rule must not quietly widen. Optional note records why you decided.

include always wins. If any include rule matches, the track is analyzed — regardless of rule order. That is the escape hatch for music a label or an embedded marker misclassifies.

include does not override structural skips — a missing file, a video, a stream URL, a DSD file or a track over --max-duration still can't be analyzed, and a rule can't change that.

--no-exclusions bypasses the whole exclusion file for this run, include rules and all — a track an include rule was rescuing reverts to whatever the remaining rules say (or nothing, if none apply) for the duration of that run. Either way, no existing mbxmoods.json entry is ever pruned by this — exclusion is purely a future-analysis switch (see below).

Speech signals are evidence, not a filter

Nothing in truedat decides a track is speech any more. The iTunes-XML labels (Podcast=true, Genre=Podcast) and an embedded file-marker sniff still run, but only to populate reasons on --preview's review candidates — a human reads them and, if warranted, writes an exclusion rule. The file-marker evidence is graded, because the markers don't all assert the same thing:

  • strong — ID3v2 PCST / MP4 pcst: an app asserting "this IS a podcast".
  • provenance — ID3v2 WFED / TGID, MP4 purl: "this came from a feed", which says nothing about content — music gets distributed by RSS too.
  • genre text — ID3v2 TCON exactly Podcast (trimmed, case-insensitive; not a substring match, so Comedy Podcast / Podcasts / Podcast Rock don't count).

An explicit PCST flag is stronger evidence than a WFED feed URL for exactly that reason.

There are three independent sources, and they answer different questions. The two above are declared evidence — someone wrote a label or a tag. The third is measured: the speechLikely verdict is computed from the audio's own features (danceability, chord strength, silence rate, zero-crossing rate, tempo-peak weight, key strength) and is the only one that can see a file nobody labelled. That matters because the two classes of talk content behave differently: a registered podcast — subscribed in MusicBee, carrying a feed URL and a genre — is already identified by its labels, while a downloaded or copied archive of the same show carries nothing at all and is invisible to every label and marker. The acoustic verdict is what finds the second kind.

What identification deliberately does not do. It classifies the genus — is this speech-dominant? — and stops there. Audiobook, comedy, lecture, news, interview and talk-dominant are one class, and truedat does not try to tell them apart, because the thing that separates them is provenance (was this distributed as an episodic feed?) and provenance is not in the audio. A file that lost its feed registration lost that information permanently; no amount of signal processing recovers it. Species-level questions are answered by things that do carry provenance — folder layout, genre, feed registration — which is to say, by rules you write.

Every exclusion is ledgered in mbxmoods-skipped.csv with the rule that caused it, on every scan mode (--analyze-file, --file-list/--folder, and the iTunes-XML library scan alike). The iTunes-XML library scan additionally reports per-rule hit counts, so a rule matching zero tracks is visible rather than silent — a single-file invocation has nothing to count, so the per-file modes skip that summary but still print a warning for any rule that failed to parse:

  Exclusions: 2 rule(s) from D:\MusicBee\Library\mbxmoods-exclude.json
  Excluded 374 track(s) by rule — listed in mbxmoods-skipped.csv (--audit lists each)
    genre=Podcast: 374 matched
    folder=\Old Shows\**: 0 matched   (stale rule?)

(stale rule?) is a prompt to delete the rule, so --preview never prints it for a file rule that has merely moved — deleting that rule would re-admit the file you rejected. Such a rule is reported instead as (moved — content now at: …) when it recorded an audioStreamSha256 the catalog still holds, (moved or deleted — no catalog entry holds this content) when it did not, and (file not reachable from here — still in the catalog, rule is live) when you are previewing from a machine that cannot see the audio (a metadata mirror, an unplugged drive, a share that is down). In preview.json these are the state and candidates[] fields on each rule.

Excluding a file does not remove an existing mbxmoods.json entry — it only stops future analysis, so the decision is reversible.

Retiring the entries a rule now covers

Because a rule only gates future scanning, anything analyzed before you wrote the rule (or before exclusions existed at all) keeps its catalog entry indefinitely — a talk archive you excluded months ago is still sitting in mbxmoods.json, stale and never refreshed. --prune-excluded closes that gap:

truedat --prune-excluded --dry-run      # list exactly what would go, write nothing
truedat --prune-excluded                # remove them (compressed backup written first)

You don't have to remember to check: truedat --stats counts the entries your rules now cover and names the command in its Recommended block, and that count is the same set the prune removes (pinned by a test, so the advisor can never quote a number its own command won't produce). A catalog with nothing to retire says nothing.

It reads the catalog and the rules and nothing else — no iTunes XML, no audio, genre taken from the entry itself — so it runs on a metadata mirror. include rules still win, so a track you rescued from a scan keeps its entry too, and a rule matching nothing reports 0 matched just as a scan reports it. The catalog is rewritten atomically after a rotated .zip backup, and the .mbxs sidecar is regenerated so it can't go stale behind the catalog.

Removal is decided only by rules you wrote. Nothing is pruned on a classification — not a speech verdict, not a genre heuristic, not an embedded marker — which is the same line the rest of the system holds (see Speech signals are evidence, not a filter). --no-exclusions is refused here rather than obeyed: it would bypass the very authority the mode acts on and report a completed run that pruned nothing.

Entries removed this way come back if the file is still in your library and you later drop the rule — the next scan re-analyzes it like any other track.

Removing one specific entry

When you know exactly which entry has to go, naming it is the direct route — you do not have to write a rule first:

truedat --prune-entry "D:\Music\Artist\Album\03 Track.flac" --dry-run   # report, write nothing
truedat --prune-entry "D:\Music\Artist\Album\03 Track.flac"             # remove it

Repeat the flag to remove several in one pass. Matching uses the catalog's own path comparer, so case and slash direction don't decide whether your path is the right one — d:/music/…/03 track.flac finds the same entry — and the report echoes the catalog's spelling so you can see exactly what matched. A path that matches nothing is listed by name and the run exits non-zero: a typo, a stale path or the wrong catalog must never read like a completed removal. Like the other catalog verbs it writes a rotated .zip backup first, swaps atomically, and regenerates the .mbxs sidecar.

It removes the entry, not the file, and it is not durable on its own. An entry is a scan result: the file is still in your library, so the next scan analyzes it again and the entry comes back. That is the right behaviour for a one-off — an entry written from a bad analysis, a duplicate you have since dealt with — and the wrong one if you meant "never again". For that, the authority is a rule: add a file / exclude rule through --apply-exclusions (then --prune-excluded retires the entry, or --prune-entry does it now). The run prints this caveat every time rather than leaving you to discover it at the next scan.

Ground truth here is you, not the filesystem. Unlike --fixup it never asks whether the file exists, so it works on a metadata mirror and on entries whose volume is offline — and for the same reason it needs no reachability probe, since it cannot mistake a downed share for a deletion when it never consults the share at all.

Edit the file by hand, or merge changes into it:

truedat --apply-exclusions decisions.json

where decisions.json is a delta — {"schemaVersion":1,"kind":"exclusion-decisions","add":[…],"remove":[…]}. Merging rather than overwriting is deliberate: the file has several legitimate authors, and a whole-file write would discard whichever one went second. The previous version is backed up before any change. If the file can't be parsed, truedat refuses to scan rather than quietly analyzing everything you thought was excluded — pass --no-exclusions if you want to bypass it on purpose.

Previewing a scan

truedat --preview answers "what would a scan do?" without doing it. Like a normal scan, it finds the iTunes library XML on its own — the exe's directory, its parent, then the current directory — so running it bare from inside the library folder works with no positional argument:

Scan preview (nothing was analyzed):
  Library:    71,520 tracks
  Analyzed:   62,187   New: 503
  Estimate:   11h27m (catalog-rtf)
  Skipped:     8,516  streamUrl  (structural — cannot be analyzed)
  Skipped:       789  video      (structural — cannot be analyzed)
  Skipped:        42  missing    (structural — cannot be analyzed)
  Skipped:        11  dsd        (structural — cannot be analyzed)
  Excluded:      374  by rule
    genre=Podcast: 374 matched
    folder=\Old Shows\**: 0 matched   (stale rule?)
  To review:  1,204  (listing the first 500)
  Ceiling:    800 min (default)   long-track prompt: 30 min

(Illustrative numbers, not a measurement — a real run reports your own library's counts, and the estimate is derived from your own catalog, never a canned figure.)

What "preview" does and does not touch. It analyzes nothing, writes no mbxmoods.json, and never modifies your exclusion file — those are the guarantees. It is not read-only over the disk: it writes preview.json (the same data as a machine-readable manifest, which is what MBXHub's review surface renders) and mbxmoods-preview.html (a self-contained review page you can open offline). Both go into the MBXHub review folder of the instance that owns the library you pointed it at — <library root>\AppData\MBXHub\review\, created if it does not exist, which is a live application's data directory. The destination is anchored to the scanned library, the same way --duplicates --manifest is, so several MusicBee instances on one machine cannot receive each other's plans. When no instance owns the library, both land beside the moods file (mbxmoods-preview.json) instead — never in some other instance's folder. Pass an explicit --preview <path> to choose the destination yourself.

New counts only the tracks a scan would actually hand to the analyser, and the estimate is built from those tracks alone — so the structural skips and the rule-excluded tracks are outside it. A speech label alone does not move a track out of New any more — only a matching exclusion rule does — so a speech-labelled track with no rule against it shows up inside New (and in --preview's review list, flagged speech-labelled, for you to judge). That is the point of the mode: the Excluded line shows what your rules save you, and it would be meaningless if the saving were still sitting inside the cost.

Two distinctions worth internalising:

  • Structural skips versus rules. A missing file, a video, a stream URL, a playlist file, a DSD file or a track over the duration ceiling cannot be analyzed, so they are counted and never offered for review — no decision changes them. Everything else is policy, and policy is reviewable.
  • The long-track threshold is a prompt, not a rule. It surfaces long files so you can judge them; it excludes nothing. That matters because long is not the same class as bad to analyze: an hours-long ambient piece or a DJ set is one coherent thing and analyzes fine, while a radio show or a setlist rip is many different pieces of music that would collapse into one meaningless average. Only a human can tell those apart from the metadata, which is the whole reason this surface exists.

The estimate comes from your own catalog: stored per-track analysis times against known track lengths, median-averaged (reported as catalog-rtf). With nothing analyzed yet there is nothing to learn from, so the estimate is omitted rather than guessed.

--apply-exclusions additionally writes apply-result.json beside the exclusion file, carrying the same counts as the console output plus any error — on failure as well as success — so a tool driving truedat reads a file instead of parsing console text.

Test Tooling

Two minor utility modes that exist to exercise MBXHub at scale or bootstrap mood data without running Essentia. Both depend on a catalog file built from AcousticBrainz + MusicBrainz data dumps (data/synthlib-catalog.jsonl.gz). Build it once via src/catalog-prep.py (~21 GB of one-time downloads; see the script's docstring for the exact invocation).

--synthesize — synthetic library

Generates stub MP3s (~12 KB each, 3 s of silence) with real ID3 metadata from MusicBrainz, organized as {output}/{Artist}/{Album}/{NN} {Title}.mp3. Used to scale-test MBXHub's AutoQ against 100k–500k tracks before real users hit those numbers. Every synthetic track is tagged with Grouping = Synthetic and Comment = SYNTH:{seed}:{mbid} for easy filtering. Add the output folder to MusicBee as a monitored library; remove it when done.

REM Preview
truedat.exe --synthesize --catalog data\synthlib-catalog.jsonl.gz --count 100 --dry-run

REM Generate 100k tracks and merge their mood data into an existing moods file
truedat.exe --synthesize --catalog data\synthlib-catalog.jsonl.gz --synth-output D:\synthlib --count 100000 --synth-moods C:\Music\mbxmoods.json

Flags: --synthesize, --catalog <path>, --synth-output <dir>, --count <n> (default 430000), --album-ratio <r> (default 0.5), --synth-moods <path>, --seed <n> (default 42), --dry-run.

--seed-moods — mood seeding from AcousticBrainz

Populates mbxmoods.json with pre-computed acoustic features matched by normalized artist+title — instant mood data for matched tracks without running Essentia. Seeded entries carry _confidence: 0.6 / _source: "ab-metadata"; local Essentia analysis (_confidence: 1.0) is never overwritten.

truedat.exe "iTunes Music Library.xml" --seed-moods --seed-catalog data\synthlib-catalog.jsonl.gz

Flags: --seed-moods, --seed-catalog <path>, --seed-target <path> (default: next to library XML).

Installation

Place truedat.exe and the required tools in the same folder. No additional runtime needed on Windows 10+.

Recommended layout for use alongside MusicBee/iTunes: drop the truedat folder under your library directory (e.g. <library>\truedat\). The exe will auto-discover the iTunes XML one level up — no need to pass the XML path or cd anywhere first. Output (mbxmoods.json etc.) lands next to the XML, and the catalog modes (--stats, --prune-excluded, …) find it there without a path too.

Dependencies

Truedat calls these tools as subprocesses. Place them alongside truedat.exe or on PATH.

Tool Enables License Source
Essentia essentia_streaming_extractor_music.exe Mood analysis (default mode) AGPL-3.0 Essentia / x64 build / dist
FFmpeg ffmpeg.exe Multi-channel downmix, opus decode retry, bitUsage / HF-analysis authenticity signals, --transcode GPL-3.0+ gyan.dev / deps
FFmpeg ffprobe.exe --transcode source-property matching GPL-3.0+ gyan.dev / deps

All tools are optional — truedat runs without them but the corresponding features are unavailable. The scan's identity fields (fingerprint.v1, audioStreamSha256, and fileMd5 with --file-md5) are computed in pure-managed code with no subprocess. Custom x64 Essentia builds are in essentia-build/, ready to use from dist/truedat/.

Essentia Builds

All Essentia tools are custom 64-bit builds from source. See essentia-build/ for build scripts and documentation. The x64 builds handle large files that exceed the 2 GB address space limit of 32-bit binaries.

Building from Source

build-truedat.cmd

Creates dist/truedat/truedat.exe (single file, ~1 MB). Requires .NET SDK 8.0+.

Extracted Features

Truedat writes features, not an interpretation. It extracts descriptors from Essentia's output and stores them; the valence/arousal mapping and any mood scoring are made downstream by the consumer (MBXHub/AutoQ) from these fields. Nothing in mbxmoods.json is a mood coordinate — valence and arousal keys written by much older builds are stripped by --migrate as legacy.

A fully analyzed track carries:

Group Count What
Core 15 Always present. 12 numeric, plus key and mode (strings) and mfcc[] (13 values).
Extended 40 Nullable, omit-when-missing. Loudness envelope, silence, spectral shape, Bark/ERB/Mel band statistics, rhythm/tonal.
Tonal/rhythm 16 Nullable, Essentia-derived. keyVotes (3 profiles), tempo-histogram peaks, chords, tuning, averageLoudness.
Harmonic texture 14 Nullable, Essentia-derived (2026-08-09). hpcp12[12] / thpcp12[12] chroma, dynamicComplexity, beat-interval summary, within-track frame stdevs, mfccStdev[13].
Timbre arrays 4 Nullable, Essentia-derived. spectralContrastCoeffs[6], spectralContrastValleys[6], beatsLoudnessBandRatio[6], gfcc[13].
Authenticity 3 blocks bitUsage, hfEnergyRatio + method, hfSpectralStructure. Populated only where the codec and sample rate make them meaningful.

Do not trust a total in this table over the file itself. Counts are measured from the JSON write surface, not maintained by hand: analyse one track and count the named feature keys in the result, minus the catalog/identity/housekeeping ones (analysisDuration, lastModified, and the fingerprint.v1 / identity fields). A hand-kept total has nothing checking it — an earlier figure of "55" survived here for months after the tonal/rhythm wave landed, and its replacement "70" went stale the same way when the two waves below it were added.

Emission is registry-driven: any field listed in mbxmoods-schema.json's excluded block beside the exe is not written and is stripped from existing catalogs by --fixup (bpmHistogram and trackId are cut this way today — so a current scan emits neither). Re-instating a cut Essentia field costs a full re-scan — the raw extractor JSON is deleted at parse, so there is no parse-only backfill.

The 40 extended descriptors are what MBXHub persists for richer downstream scoring — sub-genre profiling, loudness normalisation, fingerprint-free clustering.

Extended features are emitted as nullable JSON fields — absent/NaN Essentia paths become omitted keys, not zeros. Older mbxmoods.json entries that pre-date the extended set round-trip cleanly as nulls.

Core — Arousal-related (energy/intensity)

Feature Essentia Path What It Measures
BPM rhythm.bpm Tempo in beats per minute
Onset rate rhythm.onset_rate Percussive events per second
Loudness lowlevel.loudness_ebu128.integrated Perceived loudness (EBU R128, dB)
Spectral flux lowlevel.spectral_flux.mean Rate of spectral change
Spectral RMS lowlevel.spectral_rms.mean Raw energy level
Zero-crossing rate lowlevel.zerocrossingrate.mean Noise/distortion indicator
Danceability rhythm.danceability Rhythmic regularity (0-1)

Core — Valence-related (positivity/happiness)

Feature Essentia Path What It Measures
Key tonal.key_edma.key Musical key (C, D, E...)
Mode tonal.key_edma.scale Major (bright) vs minor (dark)
Spectral centroid lowlevel.spectral_centroid.mean Brightness/timbre (Hz)
Spectral flatness lowlevel.spectral_flatness_db.mean Tonal vs noise-like
Dissonance lowlevel.dissonance.mean Harmonic tension
Pitch salience lowlevel.pitch_salience.mean Harmonic clarity (HNR proxy)
Chord changes rate tonal.chords_changes_rate Rate of harmonic movement
MFCCs lowlevel.mfcc.mean 13-coefficient timbre fingerprint

Extended — Loudness envelope (EBU R128)

JSON key Essentia Path What It Measures
dynamicRange lowlevel.loudness_ebu128.loudness_range EBU R128 loudness range (LRA) in LU — quiet↔loud span
loudnessMomentary lowlevel.loudness_ebu128.momentary.mean Mean momentary loudness (400 ms gate), LU
loudnessShortTerm lowlevel.loudness_ebu128.short_term.mean Mean short-term loudness (3 s gate), LU
replayGain metadata.audio_properties.replay_gain ReplayGain adjustment for normalised playback, dB

Extended — Silence profile

Fraction of analysis frames whose RMS falls below the given threshold — higher = sparser / more silent content.

JSON key Essentia Path Threshold
silenceRate20dB lowlevel.silence_rate_20dB.mean -20 dB
silenceRate30dB lowlevel.silence_rate_30dB.mean -30 dB
silenceRate60dB lowlevel.silence_rate_60dB.mean -60 dB

Extended — Spectral shape

JSON key Essentia Path What It Measures
spectralRolloff lowlevel.spectral_rolloff.mean Hz below which 85% of spectral energy sits (bright↔dark)
spectralComplexity lowlevel.spectral_complexity.mean Count of significant spectral peaks per frame
spectralEntropy lowlevel.spectral_entropy.mean Shannon entropy of normalised spectrum (noise↔tone)
spectralKurtosis lowlevel.spectral_kurtosis.mean Peakedness of spectral distribution
spectralSkewness lowlevel.spectral_skewness.mean Asymmetry of spectral distribution
spectralSpread lowlevel.spectral_spread.mean 2nd-moment spread around centroid
spectralStrongPeak lowlevel.spectral_strongpeak.mean Prominence of dominant spectral peak
spectralDecrease lowlevel.spectral_decrease.mean Average slope; negative ⇒ energy concentrated at low freqs
spectralEnergy lowlevel.spectral_energy.mean Total spectral energy
spectralEnergyLow lowlevel.spectral_energyband_low.mean Energy in low band (Essentia split)
spectralEnergyMidLow lowlevel.spectral_energyband_middle_low.mean Energy in mid-low band
spectralEnergyMidHigh lowlevel.spectral_energyband_middle_high.mean Energy in mid-high band
spectralEnergyHigh lowlevel.spectral_energyband_high.mean Energy in high band
hfc lowlevel.hfc.mean High-frequency content — cymbals, sibilance, brightness

Extended — Psychoacoustic bands

Shape statistics over perceptually-spaced filterbanks. Same five statistics across three scales: Bark (27 critical bands), ERB (40 equivalent-rectangular-bandwidth bands), Mel (40 mel-scaled bands).

JSON key (per scale) Essentia Path What It Measures
{bark,erb,mel}Crest lowlevel.{bark,erb,mel}bands_crest.mean Peak-to-mean ratio — high ⇒ tonal/peaky within the band
{bark,erb,mel}Flatness lowlevel.{bark,erb,mel}bands_flatness_db.mean Geometric/arithmetic mean ratio in dB (noisy↔tonal)
{bark,erb,mel}Kurtosis lowlevel.{bark,erb,mel}bands_kurtosis.mean Peakedness of the band distribution
{bark,erb,mel}Skewness lowlevel.{bark,erb,mel}bands_skewness.mean Asymmetry of the band distribution
{bark,erb,mel}Spread lowlevel.{bark,erb,mel}bands_spread.mean Second-moment spread across the bands

Extended — Rhythm & tonal aggregates

JSON key Essentia Path What It Measures
beatsLoudness rhythm.beats_loudness.mean Mean loudness at beat positions — kick/snare intensity
chordsStrength tonal.chords_strength.mean Mean chord-detector confidence (0-1)
hpcpCrest tonal.hpcp_crest.mean Crest of 12-bin harmonic pitch class profile — tonal focus (dB)
hpcpEntropy tonal.hpcp_entropy.mean Entropy of HPCP — high ⇒ atonal/chromatic, low ⇒ diatonic

Extended — Tonal/rhythm wave (2026-07-22)

All nullable, populated on fresh analysis only (legacy entries lack them until re-analyzed). To fill an existing catalog, run scans with --refresh — entries missing these fields re-analyze, everything else stays cached; resumable (progress saves every 5 minutes), so run it in sessions until coverage is complete.

field Essentia source meaning
keyVotes tonal.key_{krumhansl,temperley,edma} Nested block: all three key-profile votes, each {key, scale, strength} — confidence + agreement for harmonic mixing (flat key/mode come from edma)
bpmFirstPeak / bpmFirstPeakWeight rhythm.bpm_histogram_first_peak_* Dominant tempo-histogram peak + its weight
bpmSecondPeak / bpmSecondPeakWeight / bpmSecondPeakSpread rhythm.bpm_histogram_second_peak_* Secondary peak — half/double-time ambiguity evidence
chordsKey / chordsScale tonal.chords_key / chords_scale Chord-level tonality (vs the key-profile view)
chordsHistogram tonal.chords_histogram 24-bin chord distribution
chordsNumberRate tonal.chords_number_rate Chord vocabulary richness
tuningFrequency tonal.tuning_frequency Reference tuning in Hz (~440)
tuningEqualTemperedDeviation / tuningDiatonicStrength / tuningNontemperedEnergyRatio tonal.tuning_* Temperament deviation / diatonic strength / non-tempered energy
averageLoudness lowlevel.average_loudness Simple 0..1 loudness (distinct from the LUFS envelope)

Output Format

mbxmoods.json:

{
  "version": "1.0",
  "generatedAt": "2026-02-06T...",
  "trackCount": 70000,
  "tracks": {
    "C:\\Music\\Artist\\Song.mp3": {
      "artist": "Artist",
      "title": "Song",
      "album": "Album",
      "genre": "Rock",
      "bpm": 128.0,
      "key": "C",
      "mode": "major",
      "spectralCentroid": 2456.3,
      "spectralFlux": 0.2134,
      "loudness": -8.52,
      "danceability": 0.7821,
      "onsetRate": 3.45,
      "zeroCrossingRate": 0.0892,
      "spectralRms": 0.1245,
      "spectralFlatness": 0.2341,
      "dissonance": 0.3456,
      "pitchSalience": 0.6789,
      "chordsChangesRate": 0.8901,
      "mfcc": [-234.5, 45.2, -12.3, 8.7, -3.1, 1.2, -0.8, 0.5, -0.3, 0.2, -0.1, 0.1, -0.05],
      "dynamicRange": 7.3,
      "dynamicRangeSource": "essentia-lra",
      "loudnessMomentary": -11.82, "loudnessShortTerm": -10.47, "replayGain": -8.4,
      "silenceRate20dB": 0.02, "silenceRate30dB": 0.07, "silenceRate60dB": 0.21,
      "spectralRolloff": 4211.5, "spectralComplexity": 14.2, "spectralEntropy": 7.86,
      "spectralKurtosis": 21.1, "spectralSkewness": 3.42, "spectralSpread": 2.18e6,
      "spectralStrongPeak": 1.7, "spectralDecrease": -1.8e-8,
      "spectralEnergy": 0.0041, "spectralEnergyLow": 0.0015, "spectralEnergyMidLow": 0.0012,
      "spectralEnergyMidHigh": 0.0009, "spectralEnergyHigh": 0.0005,
      "hfc": 112.4,
      "barkCrest": 9.4, "barkFlatness": -9.1, "barkKurtosis": 7.3, "barkSkewness": 2.1, "barkSpread": 21.3,
      "erbCrest": 11.2, "erbFlatness": -8.7, "erbKurtosis": 5.8, "erbSkewness": 1.9, "erbSpread": 18.2,
      "melCrest": 10.6, "melFlatness": -8.4, "melKurtosis": 6.1, "melSkewness": 2.0, "melSpread": 19.5,
      "beatsLoudness": -14.1, "chordsStrength": 0.61, "hpcpCrest": 6.2, "hpcpEntropy": 2.87,
      "keyVotes": {
        "krumhansl": { "key": "C", "scale": "major", "strength": 0.71 },
        "temperley": { "key": "C", "scale": "major", "strength": 0.68 },
        "edma":      { "key": "C", "scale": "major", "strength": 0.74 }
      },
      "bpmFirstPeak": 128.0, "bpmFirstPeakWeight": 0.62, "bpmFirstPeakSpread": 0.91,
      "bpmSecondPeak": 64.0, "bpmSecondPeakWeight": 0.21, "bpmSecondPeakSpread": 1.44,
      "chordsKey": "C", "chordsScale": "major", "chordsNumberRate": 0.021,
      "chordsHistogram": [12.4, 0.8, 3.1, "..."],
      "tuningFrequency": 440.2, "tuningEqualTemperedDeviation": 0.12,
      "tuningDiatonicStrength": 0.58, "tuningNontemperedEnergyRatio": 0.09,
      "averageLoudness": 0.87,
      "hpcp12": [1.0, 0.21, 0.44, "..."],
      "thpcp12": [1.0, 0.33, 0.18, "..."],
      "dynamicComplexity": 3.14,
      "beatsIntervalMean": 0.469, "beatsIntervalStdev": 0.011,
      "beatsIntervalMin": 0.44, "beatsIntervalMax": 0.51,
      "spectralCentroidStdev": 812.4, "spectralEnergyStdev": 0.0022,
      "dissonanceStdev": 0.041, "pitchSalienceStdev": 0.087, "hpcpEntropyStdev": 0.51,
      "zeroCrossingRateStdev": 0.031, "mfccStdev": [41.2, 18.6, "..."],
      "spectralContrastCoeffs": [-0.62, -0.71, "..."],
      "spectralContrastValleys": [-8.1, -8.6, "..."],
      "beatsLoudnessBandRatio": [0.41, 0.22, "..."],
      "gfcc": [-92.1, 41.3, "..."],
      "bitUsage": {
        "lowestNonZeroBit": 8,
        "bottomBitActivity": 0.4123,
        "effectiveBits": 19.47,
        "samplesAnalyzed": 1323000,
        "method": "ffmpeg-s32le-30s-mid-native"
      },
      "hfEnergyRatio": 0.000028,
      "hfEnergyMethod": "managed-fft-radix2-30s-mid-native",
      "hfSpectralStructure": {
        "flatness": 0.1326,
        "peakToMean": 41.8,
        "imagingSymmetry": -0.0408,
        "method": "managed-fft-radix2-30s-mid-native"
      },
      "lastModified": "2025-12-01T00:00:00.0000000Z",
      "analysisDuration": 4.2,
      "fileMd5": "d41d8cd98f00b204e9800998ecf8427e",
      "audioStreamSha256": "3a0be88f7ea54faa66f9e092ac40e0820197377d59a1b2c8c2d3a4b5c6d7e8f9",
      "audioStreamSha256Source": "flac-frames",
      "fingerprint.v1": {
        "fileSize": 8421376,
        "pathTail": "Artist/Album/Song.flac",
        "durationMs": 245123,
        "sampleRate": 96000,
        "channels": 2,
        "bitDepth": 24,
        "codec": "flac",
        "codecRaw": "audio/flac",
        "bitrate": 2745,
        "encoder": "reference libFLAC 1.3.2 20170101",
        "encoderRaw": "reference libFLAC 1.3.2 20170101",
        "audioHead64kMd5": "ab12cd34ef56789012abcdef34567890"
      },
      "truedat": {
        "hiresGenuine": "yes",
        "hiresConfidence": 1.0,
        "lossyTranscodeLikely": "n/a",
        "speechLikely": "no",
        "speechConfidence": 0.91,
        "speechMethod": "truedat-speech-v1.3-untuned-2026-08-13",
        "method": "truedat-v1-fft-corpus1-2026-05-18"
      },
      "smfmScores": [22, 41, 8, 130, 95, 12, 4, 7, 88, 33],
      "smfmChannel": 3,
      "smfmBpm": 128.004
    }
  }
}

For MP3 entries, fingerprint.v1 also carries a nested mp3LameTag block when the file has a Xing/Info+LAME header:

"fingerprint.v1": {
  "...": "...",
  "codec": "mp3",
  "encoder": "LAME3.100",
  "mp3LameTag": {
    "version": "LAME3.100",
    "vbrMethodCode": 4,
    "vbrMethod": "VBR Method 4 (Two Pass)",
    "lowpassHz": 19500,
    "encoderDelay": 576,
    "encoderPadding": 1656,
    "musicCrc": 12345,
    "infoTagRevision": 0
  }
}

Sony SMFM (12-TONE)

When the source file carries a Sony SMFM block (embedded by Sony Music Center) truedat reads it header-only and emits smfmScores (10 raw STMO slot scores, 0–255), smfmChannel (the dominant slot index) and smfmBpm. Slot names never appear: a 2026-06-27 device test refuted the slot→name mapping (Sony's mood channels are 2-D arousal×valence regions, not 1:1 with STMO slots), so smfmChannelName is kept as a back-compat property, held permanently null, and omitted from the JSON. Any mood label is derived downstream by MBXHub. Renamed from sensme* on 2026-06-28 — the reader still accepts the old keys and --migrate converts them.

truedat only ever reads SMFM, never writes it, so a missing block means the file has not been through Sony's tagger and no rescan can close that gap. Reporting is --list-smfm (every entry, one row, three states — data, no-data, no-smfm) and the narrower --list-missing-smfm; removal is --strip-smfm, made durable by --no-smfm. See Options for each, and smfm-tools/ for the format itself.

fingerprint.v1.codec — two buckets that refuse to guess

wma covers both WMA and WMA Lossless, and m4a covers both AAC and ALAC. The label comes from TagLib's MIME type, which cannot cheaply tell those pairs apart, so the catalog does not carry the distinction — and --list-formats printing a confident "lossy" over a folder of WMA Lossless would assert what it has no evidence for. Resolving it needs ffprobe on the bytes, which is what --convert-dir <folder> --wma-lossless-only does on the machine holding the audio. Entries predating fingerprint.v1 carry no codec at all and get a (not recorded) row rather than one inferred from the file extension — the extension is precisely what a mislabelled file lies about.

Identity fields

  • fileMd5 — MD5 of the file bytes. Written only under --file-md5; nothing downstream consumes it.
  • audioStreamSha256 — SHA-256 of the invariant audio region, and the identity MBXHub indexes on. It survives moves, renames and tag edits. audioStreamSha256Source names the anchor only when it is not the default: flac-frames for FLAC (whose metadata blocks sit inside TagLib's region, so an ordinary retag would otherwise drift the hash) or whole-file when invariant bounds were unavailable. Absent means the plain invariant region. Computed concurrently with Essentia in pure-managed code — per-track wall-clock is max(analysis, slowest-hash), not the sum.
  • fingerprint.v1 — one flat key whose name contains a dot; it is not a fingerprint object with a v1 child. Cheap composite (file size, path tail, audio properties, 64 KB head MD5) that drives the quick cache tier. bitDepth and encoder are what let a file's claimed format be cross-checked against its content — a 320 kbps MP3 whose encoder reads LAME3.100 and whose LAME tag lowpasses at 16 kHz is not what it claims. For MP3 the nested mp3LameTag block is populated when a Xing/Info+LAME header is present.

Authenticity blocks

Three independent measured signals feed the verdict below. Each is omitted where the codec or sample rate makes it meaningless, all three need ffmpeg, and all three backfill with --verify --backfill --backfill-level features.

  • bitUsage — lossless containers claiming ≥24-bit only. A 30 s mid-track ffmpeg PCM walk builds a trailing-zeros histogram of the s32le samples. lowestNonZeroBit is where signal actually starts: true 24-bit lands at ~7–8 after ffmpeg's alignment, 16-bit content padded to 24 lands at ~16. Plus bottomBitActivity (non-zero fraction at the resolution boundary), effectiveBits (continuous, clipped to [0, 32]), samplesAnalyzed and method. A ~5 ms TagLib peek gates applicability before the decode, so lossy files never cost 30 s to learn nothing.
  • hfEnergyRatio (+ hfEnergyMethod) — fraction of energy above 22.05 kHz, at the source's native rate, via a radix-2 FFT over 4096-sample Hann frames at 50 % overlap. Only when sourceSampleRate > 44100; CD-rate files have no headroom there. This catches what bitUsage cannot: an upsampler adding dither produces plausible LSB activity but cannot fabricate energy above the original Nyquist. Genuine 24/96 lands ~1e-5; upsampled-from-44.1 lands at literal 0.
  • hfSpectralStructureflatness, peakToMean, imagingSymmetry from that same FFT pass. Upsampled fakes show very low flatness and high peak-to-mean (80–180) — a few narrow imaging spikes in an otherwise empty band — while genuine HF is either broadband (orchestral, ~0.5) or peaky but uncorrelated with the mid band (cymbals). imagingSymmetry is emitted but currently unused in the vote.

For MP3, fingerprint.v1.mp3LameTag carries the Xing/Info+LAME header when present. lowpassHz is the strongest transcode tell — a "320 kbps" MP3 low-passed at 16 kHz was almost certainly built from a 128 kbps source. Also version, vbrMethod, encoderDelay / encoderPadding, musicCrc, infoTagRevision. Parsed pure-managed from the first ~8 KB. ffmpeg re-encodes (Lavc…) usually carry no LAME tag at all, and that absence is itself a soft signal.

Authenticity verdict (truedat.* block)

Each track in mbxmoods.json carries a nested truedat block with two authenticity verdicts, a talk-vs-music verdict, and per-question confidence:

"truedat": {
  "hiresGenuine":            "yes" | "no" | "unknown" | "n/a",
  "hiresConfidence":         0.85,
  "lossyTranscodeLikely":    "yes" | "no" | "unknown" | "n/a",
  "lossyTranscodeConfidence": 0.92,
  "speechLikely":            "yes" | "no" | "unknown" | "n/a",
  "speechConfidence":        0.78,
  "speechMethod":            "truedat-speech-v1.3-untuned-2026-08-13",
  "prov":                    "up44",           // optional: pad16 | tc | up44 | lossy, joined with +
  "content":                 "speech",         // optional: speech | silence | silence?
  "method":                  "truedat-v1-fft-corpus1-2026-05-18"
}

Four-string enum, not a bool — collapsing "unknown" into yes/no is exactly what produces false positives and negatives in the wild. "unknown" and "n/a" are first-class outcomes. "n/a" means the test wasn't applicable to this file (hi-res check on a 16-bit FLAC, transcode check on a FLAC, etc.); "unknown" means it was applicable but the signals are weak or disagreeing.

The block is omitted when no verdict decided "yes"/"no" and no prov/content code is set (legacy entries without fingerprint.v1, weird-codec files, entries lacking signal). The prov/content half matters: a mostly-silent track decides no yes/no axis, so an axes-only rule would suppress its own flag. Run --verify --backfill to populate the authenticity inputs; speechLikely needs no backfill — it is computed from features already present, so it fills in the next time each entry is saved.

Multi-signal weighted voting per question. Hi-res verdict combines four signals: bitUsage.lowestNonZeroBit (0.40), hfEnergyRatio (0.40), bitUsage.effectiveBits (0.20), and hfSpectralStructure (Phase 5 — Signal F, 0.35) — total available weight 1.35 when all signals vote. Transcode verdict (MP3 only) combines encoder string, MP3 LAME tag lowpass, and LAME tag presence with weights 0.30 / 0.35 / 0.20. (An earlier spectralRolloff signal was dropped: it produced false positives on naturally low-HF material.) ±0.7 normalized-score threshold (score / maxWeight) means signals must collectively cross 70% agreement for a yes/no verdict; one strong signal alone abstains as "unknown". Signal F intentionally abstains in the middle band (0.005 ≤ flatness ≤ 0.5 or peakToMean ≤ 50), reinforcing existing yes/no calls without driving them on its own — that discipline is what keeps it from flipping peaky-but-genuine cymbal content.

speechLikely classifies talk content vs. music, voting over danceability, chordsStrength, silenceRate30dB, zeroCrossingRate, bpmFirstPeakWeight and keyVotes strength. "yes" additionally requires two gates: the zero-crossing signal must fire on its own (a tone or ambient bed shares talk's shape on the other signals but sits low on zcr), and danceability < 0.50 (sparse, live and free-form instrumental music craters on every other signal exactly like speech; genuine speech measures 0.00, real-music false positives ran 0.66–1.10). Either gate demotes to "unknown", never "no". Review the "yes" set with --list-speech. speechMethod carries its own tag, independent of the authenticity method — the two verdict families tune on separate schedules.

Computed inline at write time, not persisted in cache. Threshold changes ship without a rescan; the method tags bump when thresholds change so consumers can detect algorithm drift. Per-signal vote+weight trace available via --audit for debugging.

Current method tag: truedat-v1-fft-corpus1-2026-05-18. The tag identifies the threshold generation, so a consumer can tell which algorithm produced a stored verdict; it bumps to truedat-v1-…-YYYY-MM-DD whenever the thresholds are retuned. Treat verdicts as high-confidence but not proof — see what each check misses, below.

What each check catches, and what it misses

Truedat measures the audio rather than trusting the file's label. Each check below votes; no single one decides, and when they disagree the verdict is "unknown".

Check Measures Catches Blind to
bitUsage.lowestNonZeroBit Which bit the signal actually starts on 16-bit content padded into a 24-bit container — a CD sold as hi-res An upsample with dither added, which refills the low bits
bitUsage.effectiveBits How many bits carry information A 24-bit label with ~13 bits behind it Same — dither raises this too
hfEnergyRatio Energy above 22.05 kHz A hi-res file with an empty top octave Music that never had ultrasonic content; never decides alone
hfSpectralStructure Shape of that band — flatness, peak-to-mean Narrow imaging spikes left by a cheap resampler A good resampler, which leaves no spikes to find
mp3LameTag.lowpassHz The cutoff the encoder chose "320 kbps" built from a 128 kbps source Anything not MP3; MP3s with no Xing/LAME header
encoder string What wrote the file ffmpeg re-encodes, which carry no LAME tag A second LAME pass, which rewrites the tag and hides the first

Applicability is gated, not guessed: bit checks run only on lossless files claiming 24-bit, high-frequency checks only above 44.1 kHz, and the transcode checks only on MP3. Outside those, the answer is "n/a" rather than a verdict from a measurement that means nothing there.

What none of them can do is tell you where a file has been. Anything uploaded to YouTube comes back at 48 kHz — the video audio baseline — whatever went in. Download it and you have a genuine 48 kHz file; the container is not lying. What is gone is that it was ever 44.1, and the lossy encode on the way through already removed the frequencies that would have shown the seam. The same holds for lossy audio re-wrapped in FLAC: the copy is perfect, so there is nothing left to catch it by. Sample rate, bit depth and spectral shape describe what a file is — not what it has been through.

How Mood Vectors Work

Based on Russell's circumplex model of emotion. Each track gets a 2D coordinate [valence, arousal]:

                    High Arousal
                         |
         Angry/Tense     |     Energetic/Happy
                         |
  Low Valence -----------+----------- High Valence
                         |
         Sad/Melancholy  |     Calm/Relaxed
                         |
                    Low Arousal

Truedat does not compute these coordinates. It writes features; MBXHub turns them into valence and arousal at runtime, which is why retuning the mapping never requires a rescan.

That mapping is documented, feature by feature and weight by weight, at The Audio Features on mbxhub.com — which is also where to look for what has since replaced it. AutoQ's estimates now come from a trained model rather than the weighted-sum formula, and the formula survives as the fallback path when the model is off or a track lacks the features it needs.

This README deliberately no longer restates those weights. It carried its own copy for a while and the copy drifted — differing from the live values in every weight and in which features feed which axis.

Visualization

python src/visualize.py mbxmoods.json

Scatter plot of your library's mood distribution.

iTunes Music Library XML

Truedat uses the iTunes Music Library XML format as its input. MusicBee can export your library in this format:

  1. In MusicBee, go to Edit > Preferences > Library
  2. Enable "iTunes Music Library.xml" export
  3. MusicBee writes iTunes Music Library.xml to your library folder, updating it automatically

This is a standard XML format originally from iTunes/Apple Music that many music players support as an export option.

Integration with MBXHub

Features - MBXHub

The Audio Features - MBXHub - what AutoQ does with every feature Truedat writes

Download - MBXHub

Truedat generates the mood data that MBXHub's AutoQ engine consumes. The workflow:

  1. Truedat scans your library (iTunes XML export, or a folder / file list) and produces mbxmoods.json
  2. MBXHub loads the file at startup and recomputes valence/arousal using its current weight settings
  3. AutoQ uses mood vectors for mood-aware shuffle, reactions, and influence scoring

Place mbxmoods.json in your MusicBee Library folder (sibling to AppData) or in %APPDATA%\MusicBee\MBXHub\. MBXHub searches both locations automatically.

License

  • truedat.exe: MIT - Copyright (c) 2026 Halrad LLC
  • System.Text.Json: MIT - Copyright (c) .NET Foundation (merged into exe)
  • TagLibSharp: LGPL-2.1 - TagLibSharp (merged into exe; used by fingerprint.v1, codec detection, identity backfill, bitUsage applicability peek, and synthetic-track metadata)
  • Essentia tools: AGPL-3.0 - Essentia by Music Technology Group, Universitat Pompeu Fabra
  • FFmpeg tools: GPL-3.0+ - FFmpeg (optional dependency)

See LICENSE for details.

Acknowledgments

This software uses Essentia, an open-source C++ library for audio analysis developed by the Music Technology Group at Universitat Pompeu Fabra.

If you use this in academic work, please cite:

Bogdanov, D., Wack N., Gomez E., Gulati S., Herrera P., Mayor O., et al. (2013). ESSENTIA: an Audio Analysis Library for Music Information Retrieval. International Society for Music Information Retrieval Conference (ISMIR'13).

About

Audio feature extraction tool for music libraries, built on Essentia. Extracts a rich per-track feature set — tonal, rhythmic, spectral and loudness — plus content identity and authenticity signals, into a single JSON catalog that downstream tools use for mood-aware playlisting.

Resources

Stars

4 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages