Concerts & long recordings
Everything else in musiscape assumes one file is one track. A concert is the other shape: one long recording holding a sequence of songs, often split across several files because the camera stopped at a size limit partway through the evening.
Analysed as it arrives, such a recording averages a whole evening into a single key and a single
tempo, which is to say into nothing. segment finds the songs inside it first.
musiscape segment ~/video/concert -o ~/video/concert/analysis
musiscape report ~/video/concert/analysis/songs
The first command writes one audio file per song plus a manifest. The second treats that folder as
an ordinary collection, and so does every other verb: thumbnails --style keyscape for tonality,
figures for a labelled tempogram, pdf for the whole set in one document.
What separates a song from the space around it
Not level. An enthusiastic room is as loud as the band, and a hall between songs is not silent.
Spectral flatness is what separates them. Applause is broadband noise and measures flat; played music is tonal and measures roughly an order of magnitude lower. On a concert recorded to a camera's built-in microphone:
| level | spectral flatness | |
|---|---|---|
| songs | ≈ −20 dB | 0.001 to 0.006 |
| applause, talk, room | −27 to −40 dB | 0.02 to 0.07 |
The two form distinct modes, so the threshold between them is taken from each recording's own distribution by Otsu's method on log-flatness rather than from a fixed number. The ratio survives a change of room and microphone; the absolute values do not.
Two conditions keep that split honest. Otsu returns a threshold for any distribution, including one
with a single mode, so the flatness test is used only when the two classes are both separated
(BIMODAL_MIN_DECADES) and populated (BIMODAL_MIN_SHARE). A rehearsal tape that is music
throughout has one mode and falls back to a level test instead of being cut in half. The threshold
itself is the winning histogram bin's upper edge rather than its centre, because a sharp spike puts
many frames at one value and a centre-valued cut would divide the spike.
A level floor, set relative to the recording's own loud frames, drops silence and distant room tone underneath all of this.
Turning a mask into songs
Runs of music separated by less than --min-gap seconds are one song: a quiet bar or a held breath
does not end a piece. Spans shorter than --min-song are not songs at all, which keeps tuning, a
false start, and a spoken introduction over a held chord out of the listing.
The gap default is worth understanding before changing it. Near the flatness threshold the
per-second decision is unstable, so a passage of continuous music can produce a run of several
non-music seconds that is measurement noise rather than a break. Vary --min-gap and watch the
song count: a value on a plateau is a real answer, one on a cliff is an artefact. On the reference
concert the count is nine at 8 s and eight anywhere from 10 to 15, which is why the default is 12.
| option | default | meaning |
|---|---|---|
--min-song |
60 s | shortest span counted as a song |
--min-gap |
12 s | shortest break that ends a song |
Several files, one concert
Files are read in name order, which is playing order, since cameras number sequentially. A span that runs to the end of one file and resumes at the start of the next is one song, because the camera split at a size limit rather than at a musical boundary. That join is applied before the minimum-length test, so a song cut ten seconds before its end survives as a song instead of being discarded as a fragment. Its clip holds both halves, back to back.
Times are reported on a concert clock that runs from the start of the first file and treats the
files as butted together. A camera that stops and restarts loses a few seconds at each join, and
that loss is not recoverable from the audio, so the clock drifts behind wall time by however long
the changeovers took. Within a song, the manifest's parts carry the true offsets into each source
file, which is what the clips are cut from.
What it writes
analysis/
songs.json the manifest
songs/
01-MAH08537-0254.flac
02-MAH08537-0729.flac
...
Clip names carry the running order, the source file, and the concert timecode of the song's start. Each manifest entry looks like:
{
"start_s": 2712.0, "end_s": 2947.0, "duration_s": 235.0,
"parts": [{"file": "MAH08538.MP4", "start_s": 1309.0, "end_s": 1403.0},
{"file": "MAH08539.MP4", "start_s": 0.0, "end_s": 141.0}],
"index": 8, "file": "08-MAH08538-4512.flac"
}
Detection runs at 22.05 kHz; the clips are written at 44.1 kHz so they stay worth listening to when you check a boundary by ear. Check them. The segmenter reports where the music was, not what it was called, so matching songs to a setlist is yours to do, and it is the moment a wrong boundary becomes obvious.
The timeline
segment also labels every second of the recording and draws it as one waveform, coloured by what
was happening, so a whole evening fits on one strip:
analysis/
regions.json every span, labelled
timeline.png the coloured waveform, at --width pixels
Five labels are used. music comes from the setlist, so the ribbon and the song listing always
agree: find_songs bridges a quiet bar mid-song and a frame classifier does not, and without this
one song is drawn as three. The rest is decided frame by frame:
| label | what it is | how it is told apart |
|---|---|---|
music |
a piece being played | taken from the setlist |
applause |
the room clapping | the flattest and brightest thing in the hall |
voices |
someone talking | flatness that swings, as speech alternates voiced and unvoiced |
quiet |
room tone | below the level floor |
other |
audible, but nothing above fits | the classifier declining to guess |
other is worth reading as a real answer rather than a leftover. It marks the places where the
material is genuinely ambiguous, which on a concert recording is usually a song ending, or a stage
being reset.
The colour sits on the waveform rather than in a separate band above it, so one lane carries both what happened and how loud it was. An applause swell dying away looks different from a block saying only that applause occurred.
These thresholds were calibrated on one 53-minute concert recorded to a camera's built-in microphone; the measurements behind them are in the source, and they are module constants so they can be moved.
Handing the rest to a soundscape toolbox
What happens between the songs is a soundscape question rather than a music one, and
ambiscape is the toolbox for those. The two meet at the file
boundary rather than by importing one another, so segment writes every non-music span into a
folder that ambiscape analyze reads as one session:
musiscape segment ~/video/concert -o ~/video/concert/analysis
ambiscape analyze ~/video/concert/analysis/other
Two things make that handover work. The files are FLAC, which is lossless, about half the size of
WAV, and read natively on both sides. And each name leads with a YYYYMMDD_HHMMSS stamp taken from
the recording's own start time, which is the convention recorders use and the one ambiscape reads;
without it every span lands at the same second and the session has no clock.
The start time comes from the container's creation_time where there is one, else from a stamp in
the source filename. Video containers store that tag in UTC and it is converted to local time, since
what makes a session clock readable is the wall time of the room. Where neither exists the names
fall back to concert timecodes, which at least keep the folder in playing order.
Video files
segment accepts video containers (.mp4, .mov, .mkv, .m4v, .avi, .mts, .m2ts),
because concerts usually arrive as video. They are decoded by ffmpeg, which is not a package
dependency: it is asked for only when a video file is actually handed over, and its absence is
reported as a missing program rather than as a decode failure.
Collections stay audio-only. A folder of films is not an album, and letting open_collection pick
video up would change what every other verb sees.
Reading a concert's key and tempo
The two gates described in Features need care here rather than obedience. Both are computed on whole-song statistics, and a four-minute live take is the case they were not calibrated on.
pulse_R folds a whole song at one global period. A live band drifts, so the resultant collapses
even where the beat is steady, and a concert can read below the 0.1 threshold song after song.
chroma_entropy is computed on a whole-song mean chroma. A full band in a reverberant room flattens
that average far more than the solo instrumental material the threshold was calibrated on, so a
concert can sit near the log2(12) ceiling throughout.
musiscape.stability measures both quantities per 20-second window instead, and key_agreement and
tempo_agreement travel beside the gated numbers in features.json. They answer the narrower
question that can be answered: not whether the music is tonal or pulsed, but whether the estimate
holds still across the track. The keyscape card shows the same thing visually, since a genuine tonal
centre paints one colour across the whole triangle where an artefact paints a patchwork.
There is no pulse gate
No descriptor here reports whether a track has a beat, because on live material no statistic of
periodicity can tell one. Applause is rhythmic: a room clapping in near-unison has a periodic onset
envelope, and measures of beat strength or tempogram peak prominence put it in the same range as the
songs it sits between. That is why segment separates music from applause by spectral flatness
instead.
For a single track, read the tempogram. musiscape figures --width 1920 draws it with a labelled
BPM axis, where a bright band holding level across the width is a steady tempo and one that bends is
a band speeding up or slowing down.
Segmenting from AudioSet tags (--method panns)
The flatness classifier is cheap and was calibrated on one camera-mic concert; it
hears loud rock as applause and noise music as "other". With ambiscape[ml]
installed, musiscape segment FOLDER --method panns --device auto labels the
evening from PANNs posteriors instead (musiscape.tagging.segment_concert), keeps
the song finder for snapping the edges, and moves each piece's start back to
where the sound begins. In Python:
from musiscape import concert, tagging
from musiscape.io import load_recording
y, sr = load_recording("concert.mp4")
songs = concert.find_songs(["concert.mp4"])
res = tagging.segment_concert(y, sr, songs=songs, device="auto")
res["spans"] # music / applause / voices / quiet / other, contiguous
Matching the setlist
musiscape segment FOLDER --setlist kjoreplan.docx reads the running order
(JSON list, or the first table of a .docx with Nr. / Innslag / Låt /
Medvirkende columns) and writes setlist.json. With only the audio the match is
by running order. Give musiscape.setlist.align_setlist the transcribed
introduction before each piece ({"id": ..., "intro": text}) and it matches the
names the host says, ignores the ones the host thanks, and reports the acts that
never happened.