Normalizing MIDI File Names

2. MIDI Metadata Normalizer

2. MIDI Metadata Normalizer

Transforming raw MIDI metadata into a clean, consistent, human‑editable dataset

After extracting the raw metadata from your MIDI library, the next step is to convert that unfiltered snapshot into a normalized, machine‑clean, human‑friendly spreadsheet. The Metadata Normalizer script takes the raw dump and produces a structured, consistent, encoding‑safe metadata file that is ready for human review.

This step is crucial because raw metadata often contains:

  • mojibake (UTF‑8/CP1252 corruption)
  • inconsistent punctuation
  • multiple composers or ambiguous composer fields
  • missing or duplicated titles
  • stray comments or text events
  • inconsistent capitalization
  • whitespace artifacts

The normalizer script does not attempt to interpret the music — that’s the human’s job in Step 3. Instead, it ensures that every row is clean, consistent, and ready for curation.


What This Script Does

✔ Repairs encoding issues

The script attempts to reverse common CP1252/UTF‑8 mojibake patterns so titles and composers become readable again.

✔ Normalizes whitespace and punctuation

Extra spaces, stray underscores, and odd punctuation are cleaned up.

✔ Extracts the primary composer

If multiple composers are listed (e.g., “Smith / Jones”), the script selects the first one as the canonical composer.

✔ Ensures consistent column structure

Every row contains the same fields, even if some are empty.

✔ Writes a clean, normalized XLSX and CSV

The output file is named something like:

normalized_metadata.xlsx
normalized_metadata.csv

This file becomes the authoritative source for human review in Step 3.


Why This Step Matters

  • It separates mechanical cleanup from human interpretation.
  • It ensures the renamer script receives clean, predictable input.
  • It prevents encoding issues from propagating into filenames.
  • It makes the human review step dramatically easier.
  • It creates a stable, reproducible metadata foundation for the entire library.

FULL METADATA NORMALIZER SCRIPT

import csv
import re
from pathlib import Path
from openpyxl import Workbook, load_workbook

RAW_METADATA_FILE = "raw_metadata"   # base name only; script auto-detects CSV or XLSX
OUTPUT_XLSX = "normalized_metadata.xlsx"
OUTPUT_CSV = "normalized_metadata.csv"


# ------------------------------------------------------------
# Encoding cleanup
# ------------------------------------------------------------

def fix_encoding(s):
    """Attempt to repair common CP1252/UTF‑8 mojibake."""
    if not isinstance(s, str):
        return ""
    try:
        return s.encode("cp1252").decode("utf-8")
    except:
        return s


def clean_text(s):
    """Normalize whitespace and punctuation."""
    s = fix_encoding(s)
    s = s.replace("\u2018", "'").replace("\u2019", "'")
    s = s.replace("\u201C", '"').replace("\u201D", '"')
    s = s.replace("\u2013", "-").replace("\u2014", "-")
    s = s.replace("\u2026", "...")
    s = re.sub(r"\s+", " ", s)
    return s.strip()


# ------------------------------------------------------------
# Composer normalization
# ------------------------------------------------------------

def extract_primary_composer(composer):
    """Use only the first composer if multiple are listed."""
    composer = clean_text(composer)
    parts = re.split(r"[\/,&]| and ", composer, flags=re.IGNORECASE)
    return parts[0].strip()


# ------------------------------------------------------------
# Load raw metadata
# ------------------------------------------------------------

def load_raw_metadata(base_name):
    csv_path = Path(base_name + ".csv")
    xlsx_path = Path(base_name + ".xlsx")

    if csv_path.exists():
        with open(csv_path, newline="", encoding="utf-8") as f:
            return list(csv.DictReader(f))

    if xlsx_path.exists():
        wb = load_workbook(xlsx_path)
        ws = wb.active
        rows = list(ws.iter_rows(values_only=True))
        headers = rows[0]
        return [dict(zip(headers, row)) for row in rows[1:]]

    raise FileNotFoundError("No raw metadata file found.")


# ------------------------------------------------------------
# Write output files
# ------------------------------------------------------------

def write_xlsx(rows, path):
    wb = Workbook()
    ws = wb.active
    ws.append(list(rows[0].keys()))
    for row in rows:
        ws.append(list(row.values()))
    wb.save(path)


def write_csv(rows, path):
    with open(path, "w", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=rows[0].keys())
        writer.writeheader()
        writer.writerows(rows)


# ------------------------------------------------------------
# Main normalization logic
# ------------------------------------------------------------

def main():
    rows = load_raw_metadata(RAW_METADATA_FILE)
    normalized = []

    for row in rows:
        filename = row.get("filename", "").strip()
        title_raw = row.get("title_raw", "")
        composer_raw = row.get("composer_raw", "")
        comments_raw = row.get("comments_raw", "")
        tempo = row.get("tempo", "")
        time_sig = row.get("time_signature", "")
        key_sig = row.get("key_signature", "")

        title = clean_text(title_raw)
        composer = clean_text(composer_raw)
        comments = clean_text(comments_raw)
        primary_composer = extract_primary_composer(composer)

        normalized.append({
            "filename": filename,
            "title": title,
            "composer": composer,
            "primary_composer": primary_composer,
            "comments": comments,
            "tempo": tempo,
            "time_signature": time_sig,
            "key_signature": key_sig,
        })

    write_xlsx(normalized, OUTPUT_XLSX)
    write_csv(normalized, OUTPUT_CSV)

    print("\nNormalized metadata written to:")
    print(f"  {OUTPUT_XLSX}")
    print(f"  {OUTPUT_CSV}")
    print("\nNormalization complete.")


if __name__ == "__main__":
    main()

Summary

The Metadata Normalizer is the bridge between raw extraction and human curation.
It produces a clean, consistent, encoding‑safe metadata file that is easy to review and ready for the renaming stage. This step ensures that the entire workflow remains deterministic, reversible, and archivally sound.

Project type