Sound analysis - track split

27 July 2025 - Written by ML

Highlights

  • 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.

Aim

  • 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.

Method

  • 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.

1. Convert MP3 to WAV

  • 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)

2. Split WAV to five clips

  • 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}")

3. WAV and Clip Statistics

3.1 Full WAV stats

  • Primary statistics generated for each track include:
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
  • And the python script used to compile and generate the statistics for the full WAV tracks is:
#!/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}")

3.2 Clip statistics

  • The script for the statistics is broadly unchanged. However extra rows are added for each clip.
#!/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}")

4. Merging Statistics

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}")

5. Analysis

  • 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

5.1 IBI Shift Histogram (ibi_shift_histogram.png)

  • Purpose: Shows the overall distribution of tempo differences across all clips
Ibi Shift Histogram
Ibi Shift Histogram
  • X-axis: IBI shift in seconds (positive = clip is slower than full track, negative = clip is faster)
  • Y-axis: Count of clips with that shift value
  • Insight: For most tracks there is little difference between the median reading in IBI calculated for clips compared to the full track. And clips for the most part move in synch.

5.2 Boxplot by Clip Position (ibi_shift_boxplot_by_clip.png)

  • Purpose: Shows how IBI shift varies by which clip position (1-5) within each track
Ibi Shift Boxplot
Ibi Shift Boxplot
  • X-axis: Clip index (1st, 2nd, 3rd, 4th, or 5th clip from each track)
  • Y-axis: IBI shift distribution for each position
  • Insight: Once more when considering each clip the inter beat interval between clips and the full track is close.

5.3 Scatter Plot: Clip vs Full Track IBI (slice_vs_full_scatter.png)

  • Purpose: Shows the correlation between clip and full-track median IBIs
Ibi Shift Scatterplot
Ibi Shift Scatterplot
  • X-axis: Full-track median IBI (seconds)
  • Y-axis: Clip median IBI (seconds)
  • Diagonal line: Perfect correlation reference
  • Insight: The close clustering of clip IBI to full track IBI shows close pattern.

5.4 Error Bar Plot by Clip Position (ibi_shift_errorbar_by_clip.png)

  • Purpose: Shows mean IBI shift with standard deviation for each clip position
Ibi Shift Error bar
Ibi Shift Error bar
  • X-axis: Clip index (1-5)
  • Y-axis: Mean IBI shift with error bars showing ±1 standard deviation
  • Horizontal line: Zero reference (no shift)
  • Insight: Quantifies the systematic bias and variability at each clip position.

Conclusion

  • 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.


  1. 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).↩︎