Looking for structure in "Red is a Slow Colour"
This is an experiment in signal processing, visual analysis (and even building a small tool) to explore the internal structure of a song.
How detailed can we measure the structure of a song?
I decided to take a closer look at one of the tracks on How to Measure a Planet?: Red Is a Slow Colour.
Some years ago, during the last decade, I was listening too closely to this album. I noticed a subtle echoing overlay in this song. When Anneke sings "the picture on the wall is chaotic", the word "chaotic" seems to repeat a second later, almost like an echo that wasn't meant to be heard.
So, if this song (too) contains patterns within patterns, can we visualize them as we did before? Could we actually see the repeated "chaotic"?
I wanted to explore its internal structure using signal processing, again.
The experiment would combine three things:
- Signal processing to extract measurable features from the audio.
- Visualizations to expose patterns.
- But also listening to interpret what those patterns might mean.
For the initial experiment, I averaged the stereo channels into a mono signal. This simplified the analysis by focusing on the combined audio rather than spatial differences between the channels.
But, as we've seen above, a waveform alone isn't particularly useful for identifying recurring musical textures.
We needed a representation that described how the sound changes over time.
The short-time Fourier transform (STFT) provides a time-frequency representation of the signal. Instead of looking only at the amplitude of the waveform, we examine the distribution of spectral energy across successive time windows.
Window sizes
The duration of the STFT window affects the representation.
Short windows provide better temporal resolution, while longer windows provide better frequency resolution.
I decided to experiment with five window sizes:
| Window size | Approximate duration |
|---|---|
| 256 samples | 5.8 ms |
| 1,024 samples | 23.2 ms |
| 4,096 samples | 92.9 ms |
| 8,192 samples | 185.8 ms |
| 16,384 samples | 371.5 ms |
The intention was to compare the resulting representations rather than assume that one resolution would reveal everything.
Once the audio has been converted into feature vectors, we can compare every frame with every other frame, and the result is a self-similarity matrix (SSM).
Each axis represents time. Every point in the matrix indicates how similar two moments are according to the selected features and similarity measure as we used cosine similarity to compare the spectral feature vectors.
The diagonal represents each frame compared with itself (that's why the similarity = 1). Away from the diagonal, repeated or similar passages can produce additional patterns.
For example, if a musical passage repeats later, we might see a corresponding structure away from the main diagonal.
The similarity matrix tells us that certain moments are similar under a particular representation.
The first problem: the matrix was larger than the machine could handle
The first implementation attempted to calculate a dense self-similarity matrix for the entire track.
It failed miserably at the smallest window size.
With a hop length of 64 samples, the STFT produced approximately 266,000 frames. Comparing every frame against every other frame required a matrix with roughly 70.7 billion elements 😸
At eight bytes per element, the matrix alone would occupy approximately... 527 GiB of RAM.
So, the Linux kernel eventually killed the process. This was a useful engineering lesson.
Halving the number of frames reduces the matrix to one-quarter of its original size. Reducing the frame count to one-quarter reduces the matrix to one-sixteenth.
We could have increased the STFT window size to reduce the number of frames, but that would have changed the temporal resolution of the experiment.
I wanted to retain the ability to inspect finer details without requiring a ridiculous amount of memory.
Changing the approach: analyze the song in smaller pieces
Instead of processing the entire track as one matrix, I decided to divide it into four equal blocks, each approximately 96.5 seconds long.
This reduced the size of each matrix substantially. With the same frame density, each block's matrix would be approximately one-sixteenth the size of the original whole-track matrix.
I also changed the matrix data type from 64-bit to 32-bit floating-point values (float32), halving its storage requirements.
Finally, I adjusted the hop length according to the window size to balance temporal coverage against computational cost.
The resulting implementation would calculate the matrices independently and save each visualization separately.
Four blocks multiplied by five window sizes meant 20 plots.
facundo@HP-245-G8:~/Documents/htmap_analysis$ ls self_similarity_output/*.png
self_similarity_output/block_01_start_0000s_window_1024.png
self_similarity_output/block_01_start_0000s_window_16384.png
self_similarity_output/block_01_start_0000s_window_256.png
self_similarity_output/block_01_start_0000s_window_4096.png
self_similarity_output/block_01_start_0000s_window_8192.png
self_similarity_output/block_02_start_0096s_window_1024.png
self_similarity_output/block_02_start_0096s_window_16384.png
self_similarity_output/block_02_start_0096s_window_256.png
self_similarity_output/block_02_start_0096s_window_4096.png
self_similarity_output/block_02_start_0096s_window_8192.png
self_similarity_output/block_03_start_0192s_window_1024.png
self_similarity_output/block_03_start_0192s_window_16384.png
self_similarity_output/block_03_start_0192s_window_256.png
self_similarity_output/block_03_start_0192s_window_4096.png
self_similarity_output/block_03_start_0192s_window_8192.png
self_similarity_output/block_04_start_0289s_window_1024.png
self_similarity_output/block_04_start_0289s_window_16384.png
self_similarity_output/block_04_start_0289s_window_256.png
self_similarity_output/block_04_start_0289s_window_4096.png
self_similarity_output/block_04_start_0289s_window_8192.png
This will allow to retain several analysis resolutions while keeping the experiment manageable on my laptop's hardware.
It also offered a practical advantage. I could inspect individual parts of the song without committing to an automatic segmentation algorithm. At some point I was thinking about auto-detect chapters (as I did at some point with some audio transcriptions) but it would add an extra layer of complexity, more things that could break and I just wanted that the thing to run within my humble laptop constraints.
Something we can explore
I had the matrices but I wanted to be able to listen to the song while examining the corresponding matrix, moving between sections without opening and switching between files.
This led to a second part of the project: a small, local web applet for exploring the precomputed results.
Build an interface around the existing analysis.
The applet would reuse the data and visualizations already produced by the previous analysis.
Defining regions of the song
To make navigation easier, I manually (listening and writing the time in a notepad) divided the track into sections.
The initial segmentation was:
| Time | Working label |
|---|---|
| 00:00–00:16 | Intro |
| 00:16–00:43 | Verse A |
| 00:43–01:15 | Verse B |
| 01:15–01:22 | Bridge |
| 01:22–01:50 | Chorus |
| 01:50–02:06 | Bridge |
| 02:06–02:35 | Verse B, repeated |
| 02:35–02:43 | Bridge |
| 02:43–03:12 | Chorus, repeated |
| 03:12–04:20 | Bridge (provisional) |
| 04:20–05:40 | Outro |
| 05:40–06:26 | Fade-out |
These labels were working hypotheses, not definitive musical classifications. In particular, the long passage between 03:12 and 04:20 was provisionally classified as a "bridge".
The purpose of segmentation was practical: to make it easier to inspect individual passages and compare repetitions.
Each region had its own PNG visualization and NumPy array. The PNG provided a convenient visual representation, while the .npy file preserved the numerical matrix for potential future analysis.
Designing the web applet
The applet was built around a small set of requirements:
- Everything should run locally.
- Existing analysis results should be reused.
- The interface should have minimal dependencies.
- Visual patterns should be easy to correlate with the audio.
The resulting architecture used Python's standard library for the HTTP server and plain HTML, CSS, and JavaScript for the frontend.
The main components
Audio player: The browser plays the original audio file using its native controls.
Timeline and regions: The manually defined sections provide an initial map of the song and a way to navigate to particular passages.
Self-similarity matrix viewer: The relevant regional visualization is displayed alongside the audio player.
Playback-synchronized cursor: A cursor moves through the matrix as playback advances, connecting the current audio position to its corresponding position in the visualization.
Image controls: Rotation, flipping, zooming, and panning make it easier to inspect patterns from different orientations and scales.
Annotations: Observations can be recorded with timestamps and saved as JSON. This provides a lightweight way to document interesting patterns without introducing a database.
This is where the experiment becomes more interesting than simply generating weird plots.
A self-similarity matrix shows how similar different moments are under the chosen signal representation.
A visible pattern might correspond to a repeated verse, a recurring instrumental texture, or another form of spectral similarity. Establishing which interpretation is appropriate requires listening and further investigation.
Questions worth exploring include:
- Do the two choruses produce similar patterns?
- How similar are the repeated verses?
- Are there smaller repetitions within the longer sections?
- Does a visually distinct passage correspond to an audible change?
- Could the long section provisionally labeled as a bridge be divided into smaller, meaningful regions?
- How much do the patterns change when the STFT window size changes?
Explore hierarchical analysis
Instead of calculating high-resolution matrices everywhere, a future version could analyze the song at several levels:
- Use inexpensive features to characterize the whole track.
- Identify candidate regions of interest.
- Analyze selected regions at higher resolution.
- Subdivide particularly interesting passages and repeat.
RMS energy, spectral flux, and frequency-band energy could provide initial features for identifying candidate changes.
However, a strong spectral change does not necessarily indicate a meaningful musical boundary.
Any automatic method would need to be evaluated against the actual recording and listening observations.
What we learned
The main outcome of this experiment was a small, reproducible workflow for exploring that structure.
The process evolved through a series of practical decisions:
- Represent the audio using STFT magnitude spectra.
- Compare time frames using cosine similarity.
- Explore multiple analysis resolutions.
- Address memory constraints by dividing the audio into blocks.
- Preserve the numerical results separately from their visualizations.
- Define initial regions manually.
- Build a lightweight viewer around the existing artifacts.
- Synchronize the visualization with playback.
- Record observations without modifying the underlying data.
Each decision addressed a specific problem while keeping the experiment manageable.
The resulting tool is modest, but it connects numerical analysis with direct listening in a way that makes further investigation easier.
The broader principle is straightforward:
Use computation to produce evidence, visualization to inspect it, and listening to interpret it.