Sound analysis - track split
27 July 2025 - Written by ML
This report provides work flow and code to split WAV audio files into five clips of ten seconds each.
It carries on from the prior report Jazz insights (2025-07-24) that explained the anonymization of the tracks for analysis purpose.
Using python we split a sound track into five clips and compare how the statistical properties of the sound from these clips compares to the sound for the full undivided track.
In so doing we hope to make audio track analysis and tempo recognition faster and more efficient through a reduction in processing.
Code has been generated with considerable support from various ai including Chatgpt, Grok, Deep Seek, and Mistral.
The start point is a collection of 100 mp3 tracks and their labels. After renaming the mp3 files to WAV format the code extracts clips from the full WAV files. Statistics for the full WAV files are compiled and compared to aggregated clip statistics.
That comparison identifies how closely the aggregated data from the clips (using median/mean) compare to that of the full track data.
The rationale for making the conversion to WAV form is for ease of processing and workability1.
The code for making the conversion from mp3 to WAV was:
#!/usr/bin/env python3
import os
import subprocess
from pathlib import Path
# 1. Configuration
SRC_DIR = Path("/Users/ml/beats/data/raw/playlist_28056479")
DST_DIR = Path("/Users/ml/beats/data/raw/playlist_28056479_wav")
# 2. Make sure dst exists
DST_DIR.mkdir(parents=True, exist_ok=True)
# 3. Process each .mp3
for mp3_path in SRC_DIR.glob("*.mp3"):
wav_name = mp3_path.stem + ".wav"
wav_path = DST_DIR / wav_name
# Build ffmpeg command
cmd = [
"ffmpeg",
"-y", # overwrite without asking
"-i", str(mp3_path),# input file
str(wav_path) # output file
]
print(f"Converting {mp3_path.name} → {wav_name} …")
try:
subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
except subprocess.CalledProcessError as e:
print(f" ERROR converting {mp3_path.name}:")
print(e.stderr.decode("utf-8"))
else:
print(" Done.")
print("\nAll done! WAVs are in:", DST_DIR)
For our research we divided the full length WAV track into five equal spaced ten second clips. For each WAV file the script generates 5 separate 10-second clips (10,000 milliseconds each) with even distributed start points across the track length.
So for example if there is a file called song.wav
that’s 300 seconds long the script generates:
song_clip1_50s-60s.wav (starts at 50
seconds)
song_clip2_100s-110s.wav (starts at 100
seconds)
song_clip3_150s-160s.wav (starts at 150
seconds)
song_clip4_200s-210s.wav (starts at 200
seconds)
song_clip5_250s-260s.wav (starts at 250
seconds)
The clipping process ensures clips do not extend beyond the original track’s end and use fractions 1/6, 2/6, 3/6, 4/6, 5/6 of the track length for start points
import os
from pydub import AudioSegment
SRC_DIR = "/Users/ml/beats/data/working/playlist_28056479_wav"
OUT_DIR = os.path.join(SRC_DIR, "tensecs_dynamic")
os.makedirs(OUT_DIR, exist_ok=True)
NUM_CLIPS = 5
DURATION_MS = 10 * 1000 # 10 seconds
for fname in os.listdir(SRC_DIR):
if not fname.lower().endswith(".wav"):
continue
path_in = os.path.join(SRC_DIR, fname)
audio = AudioSegment.from_wav(path_in)
total_ms = len(audio)
# Compute the N evenly‐spaced start points:
# at fractions 1/(N+1), 2/(N+1), …, N/(N+1) of the track
starts = [
int(i * total_ms / (NUM_CLIPS + 1))
for i in range(1, NUM_CLIPS + 1)
]
# Clamp so we never exceed the track’s end
starts = [
min(start, max(0, total_ms - DURATION_MS))
for start in starts
]
name, _ = os.path.splitext(fname)
for idx, start_ms in enumerate(starts, start=1):
end_ms = start_ms + DURATION_MS
snippet = audio[start_ms:end_ms]
out_fn = f"{name}_clip{idx}_{start_ms//1000}s-{end_ms//1000}s.wav"
snippet.export(os.path.join(OUT_DIR, out_fn), format="wav")
print(f"Exported {NUM_CLIPS} clips for {fname}")
| Jazz Audio Rhythm Analysis Metrics | ||
| Feature extraction for musicological and computational analysis | ||
| Metric | Description | Primary Applications |
|---|---|---|
| Basic Audio Properties | ||
| duration_s | Track length in seconds - for understanding track scale | Musicological scale analysis, playlist management |
| sample_rate | Audio sampling rate - for processing consistency | Audio processing pipeline validation |
| n_samples | Total number of audio samples - raw data size metric | Data size estimation, processing requirements |
| amp_min/amp_max | Amplitude range - indicates dynamic range and recording quality | Recording quality assessment, dynamic range analysis |
| Onset Detection Features | ||
| onset_env_len | Length of onset envelope - indicates rhythmic complexity | Rhythmic complexity measurement across jazz styles |
| onset_std1 | Standard deviation of onset strength - measures rhythmic regularity vs. irregularity | Quantifying swing feel vs straight rhythm patterns |
| Beat Tracking Analysis | ||
| tempo_bpm | Global tempo estimate - describes rhythmic characteristic | Genre classification, performance practice studies |
| n_beats | Total number of detected beats - rhythmic density measure | Rhythmic intensity comparison between tracks |
| beat_times_s | Precise timing of each beat - detailed rhythmic structure | Detailed performance analysis, groove quantification |
| Inter-Beat Interval (IBI) Analysis | ||
| ibi_s2 | Time intervals between consecutive beats - micro-timing analysis | Micro-timing analysis of jazz musicians' feel |
| ibi_bpm | Instantaneous tempo at each interval - tempo variation tracking | Rubato and tempo fluctuation measurement |
| median_ibi_s | Central tendency of beat spacing - typical rhythmic feel | Establishing baseline rhythmic feel of performance |
| median_ibi_bpm | Central tendency of instantaneous tempo - typical playing speed | Comparing typical playing speeds between artists |
| Analysis designed for playlist 28056479 | Project: Computational Analysis of Jazz Rhythm | ||
| 1 For analyzing swing vs straight eighth note patterns | ||
| 2 For quantifying 'groove' in jazz performance | ||
#!/usr/bin/env python3
import os
import numpy as np
import pandas as pd
import librosa
import matplotlib.pyplot as plt
# Directory containing WAV audio files
input_dir = r"/Users/ml/beats/data/working/playlist_28056479_wav"
# Output CSV file
output_csv = r"wav_features_summary_with_ibi_fulltracks.csv"
# Only process WAV files
extensions = {'.wav'}
features_list = []
for fname in sorted(os.listdir(input_dir)):
file_path = os.path.join(input_dir, fname)
ext = os.path.splitext(fname)[1].lower()
if not os.path.isfile(file_path) or ext not in extensions:
continue
print(f"Processing: {fname}")
try:
# Load audio
y, sr = librosa.load(file_path, sr=None)
# Basic stats
duration = len(y) / sr
n_samples = len(y)
amp_min = float(np.min(y))
amp_max = float(np.max(y))
# Trim silence
y_trimmed, _ = librosa.effects.trim(y, top_db=20)
# Onset envelope
oenv = librosa.onset.onset_strength(
y=y_trimmed, sr=sr,
hop_length=512,
aggregate=np.median
)
onset_env_len = len(oenv)
onset_std = float(np.std(oenv))
# Beat tracking
tempo_arr, beat_frames = librosa.beat.beat_track(
onset_envelope=oenv,
sr=sr,
hop_length=512,
start_bpm=120,
tightness=100
)
# Primary tempo estimate (global)
tempo_bpm = float(tempo_arr) if isinstance(tempo_arr, np.ndarray) else tempo_arr
# Beat times
beat_times = librosa.frames_to_time(beat_frames, sr=sr, hop_length=512)
n_beats = len(beat_frames)
# --- NEW: Inter‑beat intervals and instantaneous tempo ---
ibi_s = np.diff(beat_times) # inter‑beat intervals (s)
ibi_bpm = 60.0 / ibi_s # instantaneous BPM per interval
median_ibi = float(np.median(ibi_s)) if ibi_s.size else np.nan
median_ibi_bpm = float(np.median(ibi_bpm)) if ibi_bpm.size else np.nan
# Collect features
features_list.append({
'filename' : fname,
'duration_s' : duration,
'sample_rate' : sr,
'n_samples' : n_samples,
'amp_min' : amp_min,
'amp_max' : amp_max,
'onset_env_len' : onset_env_len,
'onset_std' : onset_std,
'tempo_bpm' : tempo_bpm,
'n_beats' : n_beats,
'beat_times_s' : beat_times.tolist(),
'ibi_s' : ibi_s.tolist(),
'ibi_bpm' : ibi_bpm.tolist(),
'median_ibi_s' : median_ibi,
'median_ibi_bpm' : median_ibi_bpm
})
except Exception as e:
print(f"Error processing {fname}: {e}")
# Save to CSV
df = pd.DataFrame(features_list)
df.to_csv(output_csv, index=False)
print(f"Saved summary (with IBI) to {output_csv}")
#!/usr/bin/env python3
import warnings
warnings.filterwarnings('ignore', category=DeprecationWarning, message='.*np\.complex.*')
import numpy as np
# ─── Patch for deprecated np.complex ──────────────────────────────────────────
if not hasattr(np, 'complex'):
np.complex = complex
# ─────────────────────────────────────────────────────────────────────────────
import os
import pandas as pd
import librosa
INPUT_DIR = "/Users/ml/beats/data/working/playlist_28056479_wav/tensecs_dynamic"
OUTPUT_CSV = "wav_features_all_with_aggregates_multclip3.csv"
extensions = {'.wav'}
features = []
def get_base(fname):
name, _ = os.path.splitext(fname)
return name.split("_clip")[0] + ".wav"
for fname in sorted(os.listdir(INPUT_DIR)):
ext = os.path.splitext(fname)[1].lower()
if ext not in extensions:
continue
path = os.path.join(INPUT_DIR, fname)
try:
y, sr = librosa.load(path, sr=None)
# Basic stats
duration = len(y) / sr
amp_min = float(y.min())
amp_max = float(y.max())
# Trim silence
y_trim, _ = librosa.effects.trim(y, top_db=20)
oenv = librosa.onset.onset_strength(y=y_trim, sr=sr, hop_length=512, aggregate=np.median)
onset_std = float(oenv.std())
# Beat tracking
tempo, beats = librosa.beat.beat_track(onset_envelope=oenv, sr=sr, hop_length=512)
tempo_bpm = float(tempo) if np.isscalar(tempo) else float(tempo[0])
beat_times = librosa.frames_to_time(beats, sr=sr, hop_length=512)
ibi = np.diff(beat_times)
ibi_bpm = 60.0 / ibi if ibi.size else np.array([])
median_ibi_s = float(np.median(ibi)) if ibi.size else np.nan
median_ibi_bpm = float(np.median(ibi_bpm)) if ibi_bpm.size else np.nan
base = get_base(fname)
clip_idx = int(fname.split("_clip")[1].split("_")[0])
features.append({
'base_track' : base,
'clip_idx' : clip_idx,
'filename' : fname,
'duration_s' : duration,
'amp_min' : amp_min,
'amp_max' : amp_max,
'onset_std' : onset_std,
'tempo_bpm' : tempo_bpm,
'n_beats' : len(beats),
'beat_times_s' : beat_times.tolist(),
'ibi_s' : ibi.tolist() if ibi.size else [],
'ibi_bpm' : ibi_bpm.tolist() if ibi_bpm.size else [],
'median_ibi_s' : median_ibi_s,
'median_ibi_bpm' : median_ibi_bpm
})
except Exception as e:
print(f"Error processing {fname}: {e}")
# Build DataFrame
df = pd.DataFrame(features)
# Per‑track aggregates
agg = df.groupby('base_track').agg({
'duration_s': ['mean', 'median'],
'amp_min': ['mean', 'median'],
'amp_max': ['mean', 'median'],
'onset_std': ['mean', 'median'],
'tempo_bpm': ['mean', 'median'],
'n_beats': ['mean', 'median'],
'median_ibi_s': ['mean', 'median'],
'median_ibi_bpm': ['mean', 'median']
}).reset_index()
# Flatten multi‑index columns
agg.columns = ['_'.join(filter(None, col)).rstrip('_') for col in agg.columns.values]
# Combine per‑clip and per‑track
# per‑clip: df, per‑track-mean: agg where suffix _mean, per‑track-median: _median
combined = pd.concat([
df,
agg.rename(columns=lambda c: c.replace('_mean','')).assign(clip_idx=np.nan, filename=agg.base_track + "_mean"),
agg.rename(columns=lambda c: c.replace('_median','')).assign(clip_idx=np.nan, filename=agg.base_track + "_median")
], ignore_index=True, sort=False)
# Save
combined.to_csv(OUTPUT_CSV, index=False)
print(f"Saved all features and aggregates to {OUTPUT_CSV}")
In the following python script the statistics generated for the full WAV track and the five clips are merged and joined. Analysis is then made to examine whether short audio clips (10-second segments) can reliably represent the tempo characteristics of longer jazz recordings.
#!/usr/bin/env python3
import os
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# ——— Configuration ———
CLIP_CSV = "/Users/ml/beats/data/outputs/wav_features_all_with_aggregates_multclip3.csv"
FULL_CSV = "/Users/ml/beats/data/outputs/wav_features_summary_with_ibi_fulltracks.csv"
OUTPUT_DIR = "/Users/ml/beats/data/working/images/aggregate_summary2"
# ————————————————————
os.makedirs(OUTPUT_DIR, exist_ok=True)
# 1) Load clip‑level aggregates (5 slices per track)
clips = pd.read_csv(CLIP_CSV)
# Debug: Check dimensions and info
print(f"Clips shape: {clips.shape}") # Check dimensions
print("Clips info:")
clips.info() # Check column types and non-null counts
# 2) Load full‑track summary (already has median_ibi_s / median_ibi_bpm)
full = (
pd.read_csv(FULL_CSV)
.rename(columns={
"filename": "base_track",
"median_ibi_s": "full_median_ibi_s",
"median_ibi_bpm": "full_median_ibi_bpm"
})
[["base_track", "full_median_ibi_s", "full_median_ibi_bpm"]]
)
print(f"Full tracks shape: {full.shape}")
print("Full tracks info:")
full.info()
# 3) Merge on base_track
df = clips.merge(full, on="base_track", how="left")
print(f"Merged dataframe shape: {df.shape}")
print("Merged dataframe info:")
df.info()
# 4) Compute shift: slice median_ibi_s − full median_ibi_s
df["ibi_shift_s"] = df["median_ibi_s"] - df["full_median_ibi_s"]
print(f"After adding ibi_shift_s, shape: {df.shape}")
# — A) Histogram of all shifts —
plt.figure(figsize=(8,4))
plt.hist(df["ibi_shift_s"].dropna(), bins=30, edgecolor="black")
plt.xlabel("Clip median_ibi_s − Full median_ibi_s (s)")
plt.ylabel("Count")
plt.title("Distribution of IBI Shifts Across All Clips")
plt.tight_layout()
outA = os.path.join(OUTPUT_DIR, "ibi_shift_histogram.png")
plt.savefig(outA, dpi=150)
plt.close()
print(f"Saved IBI shift histogram to {outA}")
# — B) Boxplot of shifts by clip index —
plt.figure(figsize=(8,5))
boxdf = df.dropna(subset=["clip_idx","ibi_shift_s"]).copy()
boxdf["clip_idx"] = boxdf["clip_idx"].astype(int)
boxdf.boxplot(column="ibi_shift_s", by="clip_idx", grid=False)
plt.xlabel("Clip Index")
plt.ylabel("IBI Shift (s)")
plt.title("IBI Shift by Clip Position")
plt.suptitle("") # drop the auto subtitle
plt.tight_layout()
outB = os.path.join(OUTPUT_DIR, "ibi_shift_boxplot_by_clip.png")
plt.savefig(outB, dpi=150)
plt.close()
print(f"Saved boxplot by clip to {outB}")
# — C) Scatter: slice vs full median IBI —
plt.figure(figsize=(6,6))
plt.scatter(df["full_median_ibi_s"], df["median_ibi_s"], alpha=0.6)
minv = min(df["full_median_ibi_s"].min(), df["median_ibi_s"].min())
maxv = max(df["full_median_ibi_s"].max(), df["median_ibi_s"].max())
plt.plot([minv, maxv], [minv, maxv], ls="--", color="gray")
plt.xlabel("Full‑Track Median IBI (s)")
plt.ylabel("Slice Median IBI (s)")
plt.title("Slice vs. Full‑Track Median IBI")
plt.tight_layout()
outC = os.path.join(OUTPUT_DIR, "slice_vs_full_scatter.png")
plt.savefig(outC, dpi=150)
plt.close()
print(f"Saved slice vs full scatter to {outC}")
# Summary statistics by clip index
summary = (
df
.dropna(subset=["clip_idx","ibi_shift_s"])
.assign(clip_idx = lambda d: d.clip_idx.astype(int))
.groupby("clip_idx")
.ibi_shift_s
.agg(['mean','median','std','count'])
.reset_index()
)
print("Summary statistics by clip index:")
print(summary)
# Extract as numpy arrays
x = summary['clip_idx'].to_numpy()
y = summary['mean'].to_numpy()
yerr = summary['std'].to_numpy()
plt.figure(figsize=(8,5))
plt.errorbar(
x,
y,
yerr=yerr,
fmt='-o',
capsize=4,
color='C0',
ecolor='C1'
)
plt.axhline(0, color='gray', linestyle='--', linewidth=1)
plt.xlabel("Clip Index")
plt.ylabel("Mean IBI Shift (s)")
plt.title("Mean ± SD of IBI Shift by Clip Position")
plt.xticks(x) # ensure we label 1–5
plt.tight_layout()
outD = os.path.join(OUTPUT_DIR, "ibi_shift_errorbar_by_clip.png")
plt.savefig(outD, dpi=150)
plt.close()
print(f"Saved Mean±SD error‐bar plot to {outD}")
# Final summary
print("\n=== FINAL SUMMARY ===")
print(f"Total clips processed: {len(df)}")
print(f"Clips with valid clip_idx and ibi_shift_s: {len(df.dropna(subset=['clip_idx','ibi_shift_s']))}")
print(f"Unique base tracks: {df['base_track'].nunique()}")
print(f"IBI shift range: {df['ibi_shift_s'].min():.4f} to {df['ibi_shift_s'].max():.4f}")
print(f"IBI shift mean: {df['ibi_shift_s'].mean():.4f}")
print(f"IBI shift std: {df['ibi_shift_s'].std():.4f}")
The analysis examines the inter beat interval (IBI) from five different ten second clips per track and the inter beat interval computed for the complete track.
It Calculates “IBI shift” - the difference
between a clip’s median IBI and the full track’s median IBI
(clip_median_ibi_s - full_median_ibi_s).
And the resulting visualization charts convey information about how tempo varies between clips and full tracks
ibi_shift_histogram.png)ibi_shift_boxplot_by_clip.png)slice_vs_full_scatter.png)ibi_shift_errorbar_by_clip.png)In this research we show that the process to extract several evenly spaced ten second clips in an audio track reliably reflects the tempo for the entire track and especially so at the start and the end of the track.
For our purpose we believe the close visual relationship provides solid grounds on which to base further analysis. And will therefore be using aggregated clip data for examining audio track characteristics.
WAV is a loss-less audio format and contains data without compression. It is also a standard format for audio applications (Chatgpt, 2025-07-26).↩︎