{"spec_id":"piano-roll-midi","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\npiano-roll-midi: MIDI Piano Roll Visualization\nLibrary: seaborn 0.13.2 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport matplotlib.colors as mcolors\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\n# Theme setup\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\nPP_COLOR = \"#4467A3\" if THEME == \"light\" else \"#8AAAD6\"\n\n# Imprint sequential colormap for MIDI velocity (single-polarity continuous data)\nimprint_seq = mcolors.LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\n\nsns.set_theme(\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# MIDI helpers\nNOTE_NAMES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"]\nBLACK_KEY_INDICES = {1, 3, 6, 8, 10}\nWHITE_KEY_NAMES = {\"C\", \"D\", \"E\", \"F\", \"G\", \"A\", \"B\"}\n\n# Jazz ii-V-I-turnaround: Dm7 - G7 - Cmaj7 - A7\nnp.random.seed(42)\n\nnotes_data = []\n\nchords = {\n    \"Dm7\": [50, 53, 57, 60, 65],  # D3, F3, A3, C4, F4\n    \"G7\": [47, 50, 55, 59, 65],  # B2, D3, G3, B3, F4 (tritone)\n    \"Cmaj7\": [48, 52, 55, 59, 64],  # C3, E3, G3, B3, E4\n    \"A7\": [45, 52, 57, 61, 64],  # A2, E3, A3, C#4, E4\n}\nprogression = [\"Dm7\", \"G7\", \"Cmaj7\", \"A7\"]\n\n# Dynamic arc: tension → resolution → relaxation → turnaround\nchord_velocity_contour = [58, 62, 55, 50, 45, 55, 62, 48]\n\nfor measure in range(8):\n    chord_name = progression[measure % 4]\n    beat_offset = measure * 4\n    base_vel = chord_velocity_contour[measure]\n\n    for pitch in chords[chord_name]:\n        velocity = base_vel + np.random.randint(-4, 7)\n        velocity = np.clip(velocity, 35, 72)\n        notes_data.append({\"start\": beat_offset, \"duration\": 4.0, \"pitch\": pitch, \"velocity\": int(velocity)})\n\n# Melody with jazz phrasing — swung feel, chromatic approach notes, wider intervals\nmelody = [\n    # M1 (Dm7): rising, jazz phrasing\n    (0, 0.5, 69, 95),  # A4\n    (0.5, 0.5, 65, 85),  # F4\n    (1.0, 1.0, 69, 108),  # A4 accent\n    (2.0, 0.5, 72, 90),  # C5\n    (2.5, 0.5, 71, 80),  # B4 (chromatic approach)\n    (3.0, 1.0, 69, 88),  # A4\n    # M2 (G7): tension — tritone leap\n    (4, 0.5, 67, 100),  # G4\n    (4.5, 0.25, 66, 90),  # F#4 (chromatic)\n    (5.0, 1.0, 65, 118),  # F4 (tritone of G7) — peak\n    (6.0, 0.5, 62, 80),  # D4\n    (6.5, 0.5, 64, 78),  # E4\n    (7.0, 1.0, 67, 92),  # G4\n    # M3 (Cmaj7): resolution, flowing\n    (8, 1.5, 72, 115),  # C5 — landed\n    (9.5, 0.5, 71, 90),  # B4 (maj7)\n    (10.0, 1.0, 69, 100),  # A4\n    (11.0, 1.0, 67, 85),  # G4\n    # M4 (A7): darker, blues-tinged\n    (12, 0.5, 64, 90),  # E4\n    (12.5, 0.5, 61, 78),  # C#4 (3rd of A7)\n    (13.0, 1.5, 64, 85),  # E4\n    (14.5, 0.5, 67, 70),  # G4 (7th of A7)\n    (15.0, 1.0, 69, 75),  # A4\n    # M5 (Dm7 repeat): fortissimo climax — blue note peak\n    (16, 0.5, 72, 105),  # C5\n    (16.5, 0.5, 74, 112),  # D5\n    (17.0, 2.0, 75, 127),  # Eb5 — blue note fortissimo\n    (19.0, 1.0, 72, 95),  # C5\n    # M6 (G7): softer, reflective\n    (20, 1.5, 71, 72),  # B4\n    (21.5, 0.5, 67, 60),  # G4\n    (22.0, 1.5, 65, 65),  # F4\n    (23.5, 0.5, 64, 58),  # E4\n    # M7 (Cmaj7): crescendo to second climax\n    (24, 0.5, 67, 88),  # G4\n    (24.5, 0.5, 69, 98),  # A4\n    (25.0, 1.0, 72, 115),  # C5\n    (26.0, 0.5, 75, 125),  # Eb5 (blue note)\n    (26.5, 0.5, 77, 112),  # F5\n    (27.0, 1.0, 72, 100),  # C5\n    # M8 (A7 turnaround): ritardando diminuendo\n    (28, 2.0, 71, 90),  # B4\n    (30.0, 1.0, 67, 65),  # G4\n    (31.0, 1.0, 57, 45),  # A3 — resolves to low A, very soft\n]\n\nfor start, dur, pitch, vel in melody:\n    notes_data.append({\"start\": start, \"duration\": dur, \"pitch\": pitch, \"velocity\": vel})\n\ndf = pd.DataFrame(notes_data)\n\n# Pitch range with margin\npitch_min = df[\"pitch\"].min() - 1\npitch_max = df[\"pitch\"].max() + 1\npitches = list(range(pitch_min, pitch_max + 1))\nn_pitches = len(pitches)\n\n# Time-pitch matrix (sixteenth-note resolution)\nresolution = 0.25\ntotal_beats = 32\nn_steps = int(total_beats / resolution)\nmatrix = np.full((n_pitches, n_steps), np.nan)\n\nfor _, row in df.iterrows():\n    pitch_idx = int(row[\"pitch\"]) - pitch_min\n    start_step = int(row[\"start\"] / resolution)\n    end_step = int((row[\"start\"] + row[\"duration\"]) / resolution)\n    end_step = min(end_step, n_steps)\n    for t in range(start_step, end_step):\n        existing = matrix[pitch_idx, t]\n        if np.isnan(existing) or row[\"velocity\"] > existing:\n            matrix[pitch_idx, t] = row[\"velocity\"]\n\n# Flip so highest pitch is at the top\nmatrix_flipped = matrix[::-1]\npitches_flipped = pitches[::-1]\n\n# Pitch labels: white keys labeled, black keys empty\npitch_labels = []\nfor p in pitches_flipped:\n    name = NOTE_NAMES[p % 12]\n    octave = p // 12 - 1\n    pitch_labels.append(f\"{name}{octave}\" if name in WHITE_KEY_NAMES else \"\")\n\nheatmap_df = pd.DataFrame(matrix_flipped, index=pitch_labels)\n\n# Black key mask for background shading\nblack_key_mask = np.array([p % 12 in BLACK_KEY_INDICES for p in pitches_flipped])\nBLACK_KEY_BG = \"#E8E3DB\" if THEME == \"light\" else \"#252522\"\nBLACK_KEY_EDGE = \"#D8D2C8\" if THEME == \"light\" else \"#2E2E2A\"\n\n# Canvas: landscape 3200 × 1800 (figsize=(8, 4.5) × dpi=400)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400)\nfig.patch.set_facecolor(PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Background shading for black key rows — slightly darker edge gives subtle depth\nfor i, is_black in enumerate(black_key_mask):\n    if is_black:\n        ax.axhspan(i, i + 1, color=BLACK_KEY_BG, zorder=0)\n        ax.axhspan(i, i + 0.08, color=BLACK_KEY_EDGE, zorder=0, alpha=0.6)\n        ax.axhspan(i + 0.92, i + 1, color=BLACK_KEY_EDGE, zorder=0, alpha=0.6)\n\n# Seaborn heatmap with Imprint sequential colormap\nsns.heatmap(\n    heatmap_df,\n    ax=ax,\n    cmap=imprint_seq,\n    vmin=35,\n    vmax=127,\n    cbar_kws={\"label\": \"Velocity (MIDI 0–127)\", \"shrink\": 0.72, \"aspect\": 22, \"pad\": 0.02},\n    xticklabels=False,\n    yticklabels=1,\n    linewidths=0,\n    mask=np.isnan(matrix_flipped),\n    square=False,\n)\n\n# Hierarchical beat grid lines\nfor beat in range(total_beats + 1):\n    x_pos = beat / resolution\n    if beat % 4 == 0:\n        ax.axvline(x_pos, color=INK_SOFT, linewidth=1.0, alpha=0.5, zorder=3)\n    elif beat % 2 == 0:\n        ax.axvline(x_pos, color=INK_SOFT, linewidth=0.5, alpha=0.2, zorder=3)\n    else:\n        ax.axvline(x_pos, color=INK_SOFT, linewidth=0.25, alpha=0.12, zorder=3)\n\n# Octave boundary lines at C notes\nfor i, p in enumerate(pitches_flipped):\n    if p % 12 == 0:\n        ax.axhline(i, color=INK_SOFT, linewidth=0.7, alpha=0.3, zorder=3)\n\n# X-axis measure labels\nmeasure_positions = [int(b / resolution) for b in range(0, total_beats, 4)]\nax.set_xticks([pos + 2 / resolution for pos in measure_positions])\nax.set_xticklabels([f\"M{i + 1}\" for i in range(len(measure_positions))], fontsize=8, color=INK_SOFT)\n\n# Chord name labels just above the heatmap top (y < 0 in heatmap data coords)\nchord_sequence = [\"Dm⁷\", \"G⁷\", \"Cmaj⁷\", \"A⁷\", \"Dm⁷\", \"G⁷\", \"Cmaj⁷\", \"A⁷\"]\nfor measure_idx, chord_label in enumerate(chord_sequence):\n    x_center = measure_idx * 4 / resolution + 2 / resolution\n    ax.text(x_center, -0.7, chord_label, ha=\"center\", va=\"bottom\", fontsize=8, color=INK_MUTED, clip_on=False)\n\nax.tick_params(axis=\"y\", labelsize=8, length=0, pad=3, colors=INK_SOFT)\nax.tick_params(axis=\"x\", length=0, pad=5, colors=INK_SOFT)\n\n# Colorbar styling\ncbar = ax.collections[0].colorbar\ncbar.ax.tick_params(labelsize=8, colors=INK_SOFT)\ncbar.set_label(\"Velocity (MIDI 0–127)\", fontsize=9, labelpad=8, color=INK)\ncbar.outline.set_visible(False)\n\n# Dynamic annotations — ff at fortissimo peak, pp at soft passage\nff_pitch = 75  # Eb5 peak\npp_pitch = 65  # F4 soft passage\nff_y = next((i for i, p in enumerate(pitches_flipped) if p == ff_pitch), None)\npp_y = next((i for i, p in enumerate(pitches_flipped) if p == pp_pitch), None)\nif ff_y is not None:\n    ax.annotate(\n        \"ff\",\n        xy=(17.5 / resolution, ff_y - 0.7),\n        fontsize=8,\n        fontweight=\"bold\",\n        color=\"#AE3030\",\n        ha=\"center\",\n        va=\"bottom\",\n        alpha=0.9,\n    )\nif pp_y is not None:\n    ax.annotate(\n        \"pp\",\n        xy=(22 / resolution, pp_y - 0.7),\n        fontsize=8,\n        fontstyle=\"italic\",\n        color=PP_COLOR,\n        ha=\"center\",\n        va=\"bottom\",\n        alpha=0.9,\n    )\n\nax.set_xlabel(\"Measure (4/4 time, jazz ii–V–I)\", fontsize=10, labelpad=14, color=INK)\nax.set_ylabel(\"Pitch\", fontsize=10, labelpad=8, color=INK)\nax.set_title(\"piano-roll-midi · python · seaborn · anyplot.ai\", fontsize=12, fontweight=\"medium\", pad=20, color=INK)\n\nsns.despine(ax=ax, left=True, bottom=True)\nfor spine in ax.spines.values():\n    spine.set_visible(False)\n\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}