A Kotlin tool that analyses a FLAC file and determines whether the audio is genuinely lossless or a fake - a lossy file (MP3, AAC, Ogg Vorbis) that has been re-encoded into a FLAC container. It also detects upsampling fraud, where a standard CD-quality file has had its sample rate inflated to appear as hi-res.
| Label | Meaning |
|---|---|
TRUE_HIRES |
Genuine 24-bit hi-res with verified high-frequency content above 22 kHz |
TRUE_CD |
Genuine 16-bit / 44.1 kHz CD-quality lossless |
UPSAMPLED_FAKE |
Sample rate or bit depth has been inflated; no genuine content to back it up |
MP3_TRANSCODE |
Brick-wall spectral cutoff consistent with MP3 encoding |
AAC_TRANSCODE |
Gradual high-frequency rolloff consistent with AAC encoding |
OGG_TRANSCODE |
Psychoacoustic gentle rolloff consistent with Ogg Vorbis encoding |
LOSSY_UNKNOWN |
Lossy transcode detected; codec could not be identified |
LIKELY_LOSSLESS |
No lossy codec signature detected, but authenticity cannot be proven |
UNCERTAIN |
Insufficient evidence to classify |
Build the fat JAR once, then run it:
./gradlew shadowJar
java -jar build/libs/flac-classifier-1.0.0-SNAPSHOT-all.jar <file> [options]For convenience you can alias it:
alias flac-classifier='java -jar /path/to/flac-classifier-1.0.0-SNAPSHOT-all.jar'flac-classifier [-hV] [-v] [--json] <file>
| Argument | Description |
|---|---|
<file> |
Path to the FLAC file to analyse. Required. |
| Option | Description |
|---|---|
--json |
Print results as a machine-readable JSON object instead of the default table. |
-v, --verbose |
Include the detailed confidence notes in the table output. Has no effect when combined with --json (notes are always included in JSON). |
-h, --help |
Print usage help and exit. |
-V, --version |
Print version (flac-classifier 1.0.0) and exit. |
| Code | Meaning |
|---|---|
0 |
Analysis completed successfully. |
1 |
File not found. |
2 |
FLAC decoding error (corrupt or non-FLAC file). |
┌──────────────────────────────────────────────────────────┐
│ flac-classifier - Audio Authenticity Report │
├──────────────────────────────────────────────────────────┤
│ File : /music/track01.flac │
│ Sample Rate : 44100 Hz │
│ Bit Depth : 16-bit (declared) │
│ Channels : 2 (Stereo) │
│ Duration : 4:32 │
├──────────────────────────────────────────────────────────┤
│ Spectral Cutoff : 16021 Hz │
│ Rolloff Shape : BRICK_WALL │
│ Content Above 22kHz: N/A (44.1 kHz file) │
│ Effective Bit Depth: 16 bits │
│ LSB Entropy : 7.82 bits │
├──────────────────────────────────────────────────────────┤
│ CLASSIFICATION: MP3_TRANSCODE │
│ Brick-wall cutoff consistent with MP3 encoding │
└──────────────────────────────────────────────────────────┘
With --verbose an additional section appears below the classification showing each confidence note (e.g. the measured cutoff window that matched an MP3 bitrate fingerprint).
{
"filePath": "/music/track01.flac",
"sampleRate": 44100,
"bitsPerSample": 16,
"channels": 2,
"durationSeconds": 272.4,
"spectralCutoffHz": 16021.0,
"rolloffShape": "BRICK_WALL",
"hasContentAbove22kHz": null,
"effectiveBitDepth": 16,
"lsbEntropyBits": 7.82,
"classification": "MP3_TRANSCODE",
"confidenceNotes": [
"Brick-wall rolloff at 16021 Hz matches MP3 128 kbps fingerprint (window 15500–16500 Hz)"
]
}hasContentAbove22kHz is null for 44.1 kHz files (the check is only meaningful for higher sample rates).
Import from GitHub Packages or Maven Local (see docs/library-api.md):
import dev.niessen.flacclassifier.FlacClassifier
import dev.niessen.flacclassifier.model.Classification
import java.io.File
// From a file
val result = FlacClassifier.analyze(File("track.flac"))
// From a stream (e.g. Spring multipart upload)
val result = FlacClassifier.analyze(multipartFile.inputStream, multipartFile.originalFilename ?: "upload")
println(result.classification) // e.g. Classification.Mp3Transcode
println(result.spectralCutoffHz) // e.g. 16021.0
println(result.confidenceNotes) // human-readable reasoningAll classifier thresholds and codec fingerprints are defined in src/main/resources/classifier.properties, which is bundled inside the JAR and serves as the single source of defaults. No values are hardcoded — if you want to tune behaviour, override any subset of properties in a file outside the JAR.
The tool looks for an override file at the following paths in order, and uses the first one found:
~/.config/flac-classifier/config.properties— user-level overrides./flac-classifier.properties— working-directory overrides
Only the properties you want to change need to appear in your override file; everything else falls back to the bundled defaults.
| Property | Default | Description |
|---|---|---|
spectral.cutoffThresholdDb |
-60.0 |
Noise-floor threshold (dB re. peak) for detecting a frequency cutoff |
spectral.slopeBrickWallDb |
-40.0 |
Slope below which a rolloff is classified as brick-wall (MP3-like) |
spectral.slopeGradualDb |
-15.0 |
Slope below which a rolloff is classified as gradual (AAC-like) |
spectral.psychoacousticEnergyCliffDb |
30.0 |
Minimum energy cliff (dB) between the 2–4 kHz reference band below the cutoff and the near-cutoff window to classify a gentle rolloff as psychoacoustic (codec-induced). A codec boundary leaves active content in the reference band; natural attenuation declines gradually. Lower to catch more Ogg; raise to reduce false positives |
spectral.above22kRatioThreshold |
0.001 |
Minimum power ratio above 22 kHz to count as genuine hi-res content |
bitdepth.maxSampleCount |
500000 |
Maximum samples analysed for bit-depth detection (performance knob) |
bitdepth.zeroPaddingThreshold |
0.99 |
LSB zero-fraction above which a 24-bit file is flagged as zero-padded |
bitdepth.lsbEntropyThreshold |
4.0 |
Minimum LSB Shannon entropy (bits) to classify a file as genuine 24-bit |
classification.effectiveDepthThreshold |
20 |
Minimum effective bit depth to classify a file as true hi-res |
classification.fullBandwidthCutoffHz |
20000.0 |
Cutoff at or above this frequency (Hz) is treated as full bandwidth; psychoacoustic rolloff below it is flagged as Ogg Vorbis |
classification.naturalAttenuationCutoffHz |
18000.0 |
Lower boundary (Hz) for treating an unidentified (UNKNOWN) rolloff as natural attenuation rather than an unresolved codec. Files whose cutoff is below fullBandwidthCutoffHz but at or above this threshold proceed to the sample-rate and bit-depth authenticity checks. |
classification.mp3CutoffRanges |
15500-16500;18500-19500;19500-21000 |
MP3 encoder cutoff fingerprints (Hz), semicolon-separated low-high ranges |
classification.mp3BitrateLabels |
~128 kbps;~192 kbps;~256-320 kbps |
Labels for each MP3 range (semicolon-separated, same order) |
classification.aacCutoffRanges |
14900-16500;17800-19400 |
AAC encoder cutoff fingerprints (Hz), semicolon-separated low-high ranges |
classification.aacBitrateLabels |
~128 kbps;~192 kbps |
Labels for each AAC range (semicolon-separated, same order) |
When using flac-classifier as a library you can construct and pass a ClassifierConfig directly, bypassing the properties file entirely:
val config = ClassifierConfig.load() // reads bundled defaults + user file
val result = FlacClassifier.analyze(File("track.flac"), config)A Makefile wraps the common Gradle commands:
make # build the fat CLI JAR (default)
make test # run unit tests
make clean # remove build artefacts
make install # publish library JAR to ~/.m2
make run FILE=/path/to/track.flac # build (if needed) then analyse
make run FILE=/path/to/track.flac OPTS=--json # same with JSON output
make help # list all targetsRaw Gradle equivalents:
./gradlew test # run unit tests
./gradlew shadowJar # build CLI fat JAR → build/libs/*-all.jar
./gradlew jar # build library JAR → build/libs/*.jar (no -all suffix)
./gradlew publishToMavenLocal # install to ~/.m2 for local useSee docs/how-it-works.md for the full technical explanation of the detection pipeline.
src/main/kotlin/dev/niessen/flacclassifier/
├── FlacClassifier.kt Public API facade
├── FlacDecoder.kt JFLAC wrapper - FLAC → PCM samples
├── SpectralAnalyzer.kt FFT pipeline - cutoff frequency + rolloff shape
├── BitDepthAnalyzer.kt LSB entropy - effective bit depth
├── ClassificationEngine.kt Heuristic decision tree → Classification
├── ClassifierConfig.kt Configuration loader (reads classifier.properties)
├── Main.kt CLI entry point (picocli)
└── model/
├── Classification.kt Sealed class with 9 subtypes
└── AnalysisResult.kt Output data class
src/main/resources/
└── classifier.properties Bundled defaults for all thresholds and fingerprints
Push a v* tag to cut a release. The tag name becomes the published version.
git tag v1.2.0
git push origin v1.2.0The .github/workflows/publish.yml pipeline runs tests first; if they pass it publishes dev.niessen:flac-classifier:<version> to GitHub Packages. See docs/library-api.md for how to consume it.