README
¶
gocue
gocue is a Go audio analysis tool for professional playout workflows. It detects cue-in, cue-out, and overlay points and measures EBU R128 loudness, then prints JSON on stdout for Liquidsoap’s autocue: protocol.
gocue is a stand-alone analyzer: it reads existing liq_* / ReplayGain tags when they are complete (to skip a full loudness scan) and does not write tags back to files. Tag write-back, fades, and playlist wiring belong in a Liquidsoap .liq script. This repository ships a ready-to-use one — integration/scripts/gocue.liq, documented under Liquidsoap script (gocue.liq) — adapted from Moonbase59’s autocue; it is also published standalone as autocue.gocue.liq. See the Liquidsoap autocue docs for details.
For algorithm background, see Moonbase59’s autocue presentation.
Features
- Audio analysis — cue-in / cue-out and crossfade overlay points
- EBU R128 — integrated loudness, loudness range, and true peak from FFmpeg's
ebur128filter, run in-process through libav - Tag cache (read-only) — reuses existing file tags when complete; recomputes amplify for a new target LUFS without re-decoding
- Formats — WAV, OGG, MP3, FLAC, M4A, WMA, ASF, AIFF, and more (anything FFmpeg supports)
- Liquidsoap-oriented JSON — unit-suffixed loudness/gain fields matching the autocue protocol
- Configurable thresholds — silence, overlay, longtail, blankskip, clipping prevention
Quick Start
Prerequisites
- Go 1.27 or higher, a C compiler,
make,curl,xzandpkg-config(gocue uses cgo) nasmon x86, for FFmpeg's assembly optimisations- An audio file to analyze
gocue links FFmpeg 9.0.1 statically. make ffmpeg downloads it and builds the libraries into .ffmpeg/ (under a minute on a fast machine), and every make target then uses them. That build holds FFmpeg's own demuxers, decoders and parsers and the few filters gocue needs, and no external libraries, so the binary starts without loading any FFmpeg or codec library: a run on an already-tagged file takes about 4 ms. A shared FFmpeg found through pkg-config also works, for example Homebrew's, but a full build loads dozens of libraries at every start; on Linux, also link it with CGO_LDFLAGS="-Wl,-rpath,<dir>/lib -Wl,--disable-new-dtags".
Installation
From source
git clone https://github.com/iSerganov/gocue.git
cd gocue
make ffmpeg # once: static FFmpeg 9.0.1 in .ffmpeg/
make build
# binary: ./dist/gocue (version injected via ldflags from Makefile VERSION)
Using Go
go install github.com/iSerganov/gocue@latest
This links whatever FFmpeg pkg-config finds, so build the static one first and point PKG_CONFIG_PATH at its lib/pkgconfig for the fast start-up described above.
Basic usage
# Analyze with defaults (JSON on stdout; diagnostics on stderr)
./gocue audio_file.wav
# Pretty-print JSON
./gocue -n audio_file.wav
# Custom loudness target (-16 LUFS instead of -18)
./gocue -t -16 audio_file.wav
Usage
Command-line options
| Flag | Short | Default | Description |
|---|---|---|---|
--target |
-t |
-18.0 |
LUFS reference target (-23.0 to 0.0) |
--silence |
-s |
-42.0 |
LU below integrated loudness for cue-in & cue-out |
--overlay |
-o |
-8.0 |
LU below integrated loudness to start the next track |
--longtail |
-l |
15.0 |
Overlay duration (s) that triggers long-tail recalculation (0–60) |
--extra |
-x |
-12.0 |
Extra LU below overlay for long-tail / sustained endings |
--drop |
-d |
40.0 |
Max % loudness drop still counted as sustained ending (0–100; 0 disables) |
--noclip |
-k |
false |
Lower gain if needed so true peak stays at or below −1 dBFS |
--nice |
-n |
false |
Pretty-print JSON |
--blankskip |
-b |
0.0 |
Early cue-out on in-track silence longer than this many seconds (0 = off) |
--exec_timeout |
-e |
20s |
Timeout for the tag probe and the loudness scan |
--print_flags |
-p |
false |
Log flag values to stderr (stdout stays JSON-only) |
Parameter ranges
- Target LUFS: −23.0 to 0.0
- Silence / overlay / extra: −96.0 to 0.0
- Longtail / blankskip: 0.0 to 60.0 seconds
- Sustained drop: 0.0 to 100.0 percent
Output format
JSON is written to stdout. Progress/analysis lines go to stderr, so pipes and Liquidsoap stay machine-readable.
Loudness and gain fields are unit-suffixed strings (as required by the autocue protocol). Time fields and true peak (linear) are JSON numbers; booleans are JSON booleans.
{
"duration": 180.5,
"liq_cue_duration": 175.2,
"liq_cue_in": 2.8,
"liq_cue_out": 178.0,
"liq_cross_start_next": 173.5,
"liq_longtail": false,
"liq_sustained_ending": true,
"liq_loudness": "-14.200 LUFS",
"liq_loudness_range": "8.500 LU",
"liq_amplify": "3.800 dB",
"liq_amplify_adjustment": "0.000 dB",
"liq_reference_loudness": "-18.000 LUFS",
"liq_blankskip": 0.0,
"liq_blank_skipped": false,
"liq_true_peak": 0.95,
"liq_true_peak_db": "-0.400 dBFS"
}
Output fields
| Field | Meaning |
|---|---|
duration |
File duration in seconds (from the container when available) |
liq_cue_duration |
Playout length (cue_out − cue_in), seconds |
liq_cue_in / liq_cue_out |
Cue points from start of file, seconds |
liq_cross_start_next |
Suggested next-track overlay point, seconds |
liq_longtail |
Long fade-out handling was applied |
liq_sustained_ending |
Sustained ending handling was applied |
liq_loudness |
Integrated loudness ("… LUFS") |
liq_loudness_range |
Loudness range ("… LU") |
liq_amplify |
Gain to reach target ("… dB") |
liq_amplify_adjustment |
Extra reduction from --noclip ("… dB") |
liq_reference_loudness |
Target used ("… LUFS") |
liq_blankskip |
Blank-skip setting used, seconds |
liq_blank_skipped |
Early cue-out due to in-track silence |
liq_true_peak |
Linear true peak |
liq_true_peak_db |
True peak ("… dBFS") |
Note: gocue picks the latest of the normal / sustained / longtail overlay candidates so special endings stay intact.
Real-world examples
Hidden track
The Nirvana track Something in the Way / Endless, Nameless (Nevermind, 1991) has ~3:48 of song, ~10 minutes of silence, then the hidden track Endless, Nameless:
Normal mode (blankskip off, default):
$ gocue "Nirvana - Something in the Way _ Endless, Nameless.mp3"
# diagnostics on stderr, e.g.:
# Overlay: -18.47 LUFS, Longtail: -33.47 LUFS, Measured end avg: -41.05 LUFS, Drop: 38.91%
# Overlay times: 1222.30/1228.10/0.00 s (normal/sustained/longtail), using: 1228.10s.
# Cue out time: 1232.20 s
{"duration":1235.1,"liq_cue_duration":1232.2,"liq_cue_in":0,"liq_cue_out":1232.2,"liq_cross_start_next":1228.1,"liq_longtail":false,"liq_sustained_ending":true,"liq_loudness":"-10.470 LUFS","liq_loudness_range":"7.900 LU","liq_amplify":"-7.530 dB","liq_amplify_adjustment":"0.000 dB","liq_reference_loudness":"-18.000 LUFS","liq_blankskip":0,"liq_blank_skipped":false,"liq_true_peak":1.632,"liq_true_peak_db":"4.250 dBFS"}
With blank detection (cue-out at start of silence):
$ gocue -b 5 -- "Nirvana - Something in the Way _ Endless, Nameless.mp3"
# Cue out near the start of the long silence (~227.5 s) instead of the end of the file
{"duration":1235.1,"liq_cue_duration":227.5,"liq_cue_in":0,"liq_cue_out":227.5,"liq_cross_start_next":224.1,"liq_longtail":false,"liq_sustained_ending":false,"liq_loudness":"-10.470 LUFS","liq_loudness_range":"7.900 LU","liq_amplify":"-7.530 dB","liq_amplify_adjustment":"0.000 dB","liq_reference_loudness":"-18.000 LUFS","liq_blankskip":5,"liq_blank_skipped":true,"liq_true_peak":1.632,"liq_true_peak_db":"4.250 dBFS"}
Long tail handling
Bohemian Rhapsody has a long ending that should not be cut by an early overlay:
$ gocue "Queen - Bohemian Rhapsody.flac"
# Overlay times: 336.50/348.50/348.50 s (normal/sustained/longtail), using: 348.50s.
{"duration":355.1,"liq_cue_duration":353,"liq_cue_in":0,"liq_cue_out":353,"liq_cross_start_next":348.5,"liq_longtail":true,"liq_sustained_ending":true,"liq_loudness":"-15.500 LUFS","liq_loudness_range":"15.960 LU","liq_amplify":"-2.500 dB","liq_amplify_adjustment":"0.000 dB","liq_reference_loudness":"-18.000 LUFS","liq_blankskip":0,"liq_blank_skipped":false,"liq_true_peak":0.99,"liq_true_peak_db":"-0.090 dBFS"}
How that result is chosen:
- Cue-out — scan backwards from the end for momentary loudness above
loudness + --silence(default −42 LU → ~−60 LU noise floor at −18 LUFS target). - Normal overlay — scan backwards from cue-out for loudness above
loudness + --overlay(default −8 LU). Here that yields ~336.5 s (too early). - Long tail — if
cue_out − overlayexceeds--longtail(default 15 s), recalculate with--extra(default −12 LU). Sustained endings use a similar path via--drop. - Final overlay — the latest of normal / sustained / longtail is used (
348.5s here,liq_longtail: true).
Fade length after overlay is configured in Liquidsoap (not by gocue), e.g.:
settings.gocue.fade_out := 2.5 # seconds
Blank (silence) detection
Use --blankskip for “hidden track” gaps. It is off by default (0.0) in gocue—enable it only when you want early cue-outs on long in-track silence. Avoid it for spoken word, jingles, ads, DJ sets, or podcasts.
./gocue -b 5 audio_file.mp3 # cue-out after ≥5 s of silence
./gocue -b 0 audio_file.mp3 # explicit disable (same as default)
./gocue -b 10 audio_file.mp3 # require longer silence
Liquidsoap protocol
Requires Liquidsoap 2.3.0+. Note that Ubuntu 24.04 packages 2.2.4, which is too old — install an official release build.
Prefix a playlist or request with autocue::
radio = playlist(prefix="autocue:", "/path/to/playlist.m3u")
Or use enable_autocue_metadata() for all files—use one approach, not both. For video streams, exclude media with liq_cue_file=false (full video analysis is expensive).
Typical settings (defaults shown, as declared by the bundled gocue.liq):
settings.gocue.path := "gocue"
settings.gocue.fade_in := 0.1 # seconds
settings.gocue.fade_out := 2.5 # seconds
settings.gocue.timeout := 60.0 # seconds
settings.gocue.target := -18.0 # LUFS
settings.gocue.silence := -42.0 # LU below track loudness
settings.gocue.overlay := -8.0 # LU below track loudness
settings.gocue.longtail := 15.0 # seconds
settings.gocue.overlay_longtail := -12.0 # extra LU
settings.gocue.sustained_loudness_drop := 40.0
settings.gocue.noclip := false
settings.gocue.blankskip := 0.0
settings.gocue.unify_loudness_correction := true
settings.gocue.write_tags := false # the .liq script writes liq_* tags (not gocue itself)
settings.gocue.write_replaygain := false
settings.gocue.ignored_overrides := ["duration"]
settings.gocue.max_initial_duration_to_skip := 600.0 # seconds
settings.gocue.nice := false
settings.gocue.force_analysis := false # no gocue equivalent; logs a warning if set
settings.gocue.use_json_metadata := false # no gocue equivalent; logs a warning if set
Liquidsoap script (gocue.liq)
integration/scripts/gocue.liq is a complete, self-contained Liquidsoap autocue provider that shells out to the gocue binary. Despite living under integration/, it is not test scaffolding: the integration suite exercises the same file you are meant to deploy, so you can drop it straight into a production configuration as a custom autocue implementation.
It is a port of Moonbase59’s autocue.cue_file.liq with the settings namespace moved to settings.gocue.* and the CLI call adapted to gocue’s flags.
What including it does
%include-ing the file declares the settings.gocue.* tree, defines the provider, and registers it:
settings.autocue.metadata.priority := 10 # annotations (priority 5) can still override
settings.autocue.preferred := "gocue"
settings.autocue.amplify_behavior := "keep"
settings.fade.out.duration := settings.gocue.fade_out() # avoids dead air on reconcile
autocue.register(name="gocue", gocue)
That means autocue: prefixes and enable_autocue_metadata() route through gocue with no further wiring. Tracks resolved by it report liq_autocue="gocue".
The script also exports check_autocue_setup(~do_print=false), which re-applies those settings and logs the active script version and binary path. Calling it is optional; it always returns true (there is no version handshake with the binary).
What it does per track
- Merges metadata — request metadata (
annotate:) takes precedence over file tags. - Honours
liq_cue_file—falseskips analysis entirely and keeps existing metadata;truemakes gocue’s results win over existing metadata; absent (the default) lets existing metadata and annotations override gocue, apart from the keys insettings.gocue.ignored_overrides. - Resolves blank skipping — from
settings.gocue.blankskip, forced off forjingle_modetracks and for SAM Broadcastersongtype != "S", and ultimately overridable per track vialiq_blankskip(accepts the pre-3.0.0 boolean form). - Runs the binary —
-t -s -o -l -x -dfrom the settings, plus-kfornoclip,-bfor blankskip, and-ecarryingsettings.gocue.timeout; parses the one-line JSON from stdout. - Recomputes gain — derives
liq_amplify,liq_amplify_adjustment, andliq_reference_loudnessfromliq_loudnessandliq_true_peak_dbagainst your target, applying clipping prevention whennoclipis set. - Unifies ReplayGain — with
unify_loudness_correction, bringsreplaygain_track_gainandliq_amplifyinto line (target-aware, and handling the old RG1/mp3gain"89 dB"reference), so a script can amplify on either value without loudness jumps. - Validates the crossfade window — corrects
liq_cross_start_nextwhen only a cue-out was annotated, shrinksliq_cue_outwhen the requested fade-out is shorter than the overlay, clamps an over-longliq_fade_in, and recomputesliq_cue_duration. - Playlist sync (optional) — an
enforce_start_timeannotation shiftsliq_cue_into catch up to a wall-clock schedule, bounded bysettings.gocue.max_initial_duration_to_skip. - Writes tags (optional) — with
write_tags, remuxes the file via ffmpeg to store theliq_*(and, withwrite_replaygain,replaygain_*) tags, so later runs hit gocue’s read-only tag cache instead of a full scan. gocue itself never mutates files. - Filters output — emits only autocue-relevant keys, so decoder and annotation metadata are left intact.
Requirements and caveats
gocuereachable viasettings.gocue.path;ffmpegonPATHonly if you enablewrite_tags.- Liquidsoap 2.3.0+ (see Liquidsoap protocol above); developed and tested against 2.4.5.
liq_cross_start_nextis handed to Liquidsoap through the autocue record rather than as metadata, so it does not appear in track metadata — Liquidsoap reconciles it intoliq_cross_end_durationandliq_cross_max_start_duration. It is stored as a tag whenwrite_tagsis on.force_analysisanduse_json_metadataexist for configuration compatibility withautocue.cue_file.liqonly. gocue has no-for-j, so enabling either logs a warning and changes nothing.niceruns the binary throughnice(1)rather than passing a flag, because gocue’s-nmeans “pretty-print JSON”.
Minimum working example
Copy gocue.liq next to your configuration (%include resolves relative to the including script; an absolute path such as %include "/etc/liquidsoap/gocue.liq" also works):
# radio.liq
%include "gocue.liq"
settings.gocue.path := "/usr/local/bin/gocue"
settings.gocue.target := -18.0 # LUFS
settings.gocue.fade_out := 2.5 # seconds
# Optional: log the active script version and binary path at startup.
ignore(check_autocue_setup(do_print=true))
# The "autocue:" prefix sends every request through the registered provider.
radio = playlist(prefix="autocue:", "/var/music/playlist.m3u")
# Apply the loudness correction gocue calculated, then crossfade using its cue points.
radio = amplify(1.0, override="liq_amplify", radio)
radio = crossfade(radio)
output.icecast(
%mp3,
host="localhost",
port=8000,
password="hackme",
mount="/radio",
fallible=true,
radio
)
For a fuller configuration — harbor endpoints, metadata reporting, and a silence fallback — see integration/scripts/station.liq.
Advanced configuration
# Long tail: longer threshold, deeper extra LU
./gocue -l 20 -x -15 audio_file.wav
# Sustained ending sensitivity
./gocue -d 30 -x -10 audio_file.wav
# Clipping prevention (true peak ≤ −1 dBFS)
./gocue -k audio_file.wav
# Blank skip (hidden tracks)
./gocue -b 5 audio_file.wav
Use cases
Radio automation
Wire gocue through your Liquidsoap autocue script (see links above). A minimal sketch:
radio = playlist(prefix="autocue:", "~/music/")
Batch analysis
for file in *.wav; do
./gocue -t -16 -s -45 -o -6 "$file" > "${file%.wav}.json"
done
Strict EBU R128 target
./gocue -t -23 -s -60 -o -10 content.wav
Architecture
| Piece | Role |
|---|---|
cmd/cue |
Cobra CLI: flags, validation, JSON on stdout |
pkg/cue.Calculator |
Tag probe and ebur128 scan through libav (cgo) |
pkg/cue.Result |
Numeric result; unit suffixes only at JSON/YAML marshal |
pkg/cue.Frame |
Per-frame momentary loudness (~100 ms) |
Dependencies: FFmpeg libraries through cgo (external), Cobra, yaml.v3 (library marshal helper).
Performance notes:
- Decodes and measures the file in-process, one packet at a time, without starting ffmpeg or ffprobe and without holding the whole file in memory
- Reuses complete tags from the file when possible (read-only cache)
- Configurable
--exec_timeoutfor probe/scan
1.3.0 against 1.2.0, on the three longest test files (748–948 s of stereo Ogg Vorbis), Apple M4 Max, FFmpeg 9.0.1 linked statically. Go figures are medians of 10 make bench samples, and command wall times medians of 10 hyperfine runs; these time and memory changes are significant at p < 0.01 by benchstat. The cached-path command time is the mean of 50 hyperfine runs on a fully tagged copy of the longest file. CPU and kernel time are hyperfine means; peak RSS is one /usr/bin/time -l run per file.
| Measure | 1.2.0 | 1.3.0 | Change |
|---|---|---|---|
gocue command on an already-tagged file (the cached path) |
19.3 ms | 4.2 ms | 4.6× faster |
| Tag probe inside gocue | 22.8–23.0 ms | 0.18–0.31 ms | 75× to 125× faster |
Full analysis (Calc) |
2.730–3.370 s | 1.518–1.899 s | 44% faster |
gocue command, full analysis, wall time |
2.687–3.377 s | 1.517–1.916 s | 43% faster |
gocue command, full analysis, CPU time (user + sys) |
3.16–3.98 s | 3.68–4.61 s | 16% to 17% more |
gocue command, full analysis, kernel (sys) time |
0.24–0.30 s | 0.05–0.07 s | 77% less |
| Go memory allocated per analysis | 1.69–2.20 MiB | 0.23–0.29 MiB | 86% less |
| Peak RSS, cached path | 17.0 MiB | 13.3 MiB | 22% less |
| Peak RSS, full analysis | 24.7–25.1 MiB, most likely the ffmpeg child | 26.1–27.0 MiB | one process instead of two |
Three changes produce these numbers. gocue no longer starts ffprobe or ffmpeg, so there is no second process, no pipe, and no printing and parsing of every frame as text. It links a minimal static FFmpeg, so a start loads no FFmpeg or codec library; that is what makes the cached path fast. And it measures true peak, about three quarters of a full analysis, per channel on separate threads. That last step is why a full analysis takes more CPU time while finishing 43% sooner: ebur128 cannot measure the peak alone, so each channel's thread repeats the cheap loudness part. gocue.liq runs gocue under nice -n 18, so the extra work never competes with Liquidsoap's own audio thread. Mono files get the startup and parsing gains but not the parallel peak.
Testing
go test ./...
go test -race -count=1 ./...
go test ./pkg/cue -v
# Liquidsoap end-to-end (needs liquidsoap + ffmpeg on PATH)
go test -tags=integration ./integration/ -count=1 -timeout 10m
# or: make test-integration
Requires FFmpeg 9.0.1: the libraries from make ffmpeg (or pkg-config) for the build, and ffmpeg / ffprobe on PATH for preparing tagged test files and for the integration harness. CI installs that version through .github/actions/setup-ffmpeg and fails if a different one is found. Integration tests also need liquidsoap and use fixtures under integration/testdata/ with scripts in integration/scripts/.
Project structure
gocue/
├── cmd/cue/ # CLI
├── pkg/cue/ # Library
│ ├── calculator.go
│ ├── scan.go
│ ├── result.go
│ ├── frame.go
│ ├── error.go
│ └── test_data/ # Audio fixtures
├── main.go
├── Makefile
└── go.mod
Contributing
Contributions are welcome. For larger changes, open an issue first.
- Fork and create a feature branch
- Add tests where practical
- Run
go test -race ./... - Open a pull request
License
Apache 2.0 — see LICENSE.
Acknowledgments
Inspired by Moonbase59/autocue (Python). That project is no longer maintained and drifted from current Liquidsoap; gocue is a Go port with a focus on performance and a cleaner library/CLI split.
- FFmpeg — awesome AV processing framework
- Liquidsoap — powerful audio streaming language/toolkit to build radio automation systems
Documentation
¶
There is no documentation for this package.