{"spec_id":"piano-roll-midi","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\npiano-roll-midi: MIDI Piano Roll Visualization\nLibrary: matplotlib 3.10.9 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport matplotlib.colors as mcolors\nimport matplotlib.patches as mpatches\nimport matplotlib.patheffects as pe\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.colors import LinearSegmentedColormap\n\n\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\n\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\"\n\n# Imprint sequential colormap for velocity (single-polarity, soft→loud)\nimprint_seq = LinearSegmentedColormap.from_list(\"imprint_seq\", [\"#009E73\", \"#4467A3\"])\n\n# Key row shading: black keys get a slightly offset background\nBLACK_KEY_BG = \"#EBE9E2\" if THEME == \"light\" else \"#242420\"\n\n# Piano black-key semitone positions: C#, D#, F#, G#, A#\nBLACK_KEYS = {1, 3, 6, 8, 10}\n\n# Data: a musical phrase with melody, bass, and chords across 8 measures\nnp.random.seed(42)\n\nnotes = [\n    # Measure 1 - Opening melody\n    (0.0, 1.0, 72, 100),\n    (1.0, 0.5, 74, 90),\n    (1.5, 0.5, 76, 85),\n    (2.0, 1.5, 79, 110),\n    (3.5, 0.5, 76, 70),\n    # Bass measure 1\n    (0.0, 2.0, 60, 80),\n    (2.0, 2.0, 64, 75),\n    # Measure 2 - Continuation\n    (4.0, 1.0, 74, 95),\n    (5.0, 1.0, 72, 88),\n    (6.0, 2.0, 76, 105),\n    # Bass measure 2\n    (4.0, 2.0, 65, 78),\n    (6.0, 2.0, 67, 82),\n    # Measure 3 - Chords + melody\n    (8.0, 2.0, 79, 115),\n    (8.0, 2.0, 76, 90),\n    (8.0, 2.0, 72, 85),\n    (10.0, 1.0, 81, 120),\n    (11.0, 1.0, 79, 100),\n    # Bass measure 3\n    (8.0, 4.0, 60, 85),\n    # Measure 4 - Descending\n    (12.0, 1.0, 79, 95),\n    (13.0, 1.0, 76, 88),\n    (14.0, 1.0, 74, 80),\n    (15.0, 1.0, 72, 75),\n    # Bass measure 4\n    (12.0, 2.0, 64, 78),\n    (14.0, 2.0, 62, 72),\n    # Measure 5 - New phrase, louder\n    (16.0, 0.5, 72, 105),\n    (16.5, 0.5, 74, 100),\n    (17.0, 0.5, 76, 110),\n    (17.5, 0.5, 79, 115),\n    (18.0, 2.0, 81, 125),\n    # Bass measure 5\n    (16.0, 2.0, 60, 90),\n    (18.0, 2.0, 67, 88),\n    # Measure 6 - Sustained\n    (20.0, 3.0, 79, 108),\n    (23.0, 1.0, 76, 85),\n    # Chord measure 6\n    (20.0, 2.0, 72, 80),\n    (20.0, 2.0, 67, 75),\n    # Bass measure 6\n    (20.0, 4.0, 60, 82),\n    # Measure 7 - Climax\n    (24.0, 1.0, 76, 100),\n    (25.0, 1.0, 79, 110),\n    (26.0, 2.0, 81, 127),\n    (26.0, 2.0, 76, 100),\n    (26.0, 2.0, 72, 95),\n    # Bass measure 7\n    (24.0, 2.0, 65, 90),\n    (26.0, 2.0, 64, 85),\n    # Measure 8 - Resolution\n    (28.0, 2.0, 79, 90),\n    (30.0, 2.0, 72, 70),\n    (28.0, 2.0, 76, 80),\n    (30.0, 2.0, 67, 65),\n    # Bass measure 8\n    (28.0, 4.0, 60, 75),\n]\n\npitches = np.array([n[2] for n in notes])\npitch_min = int(pitches.min()) - 1\npitch_max = int(pitches.max()) + 1\n\nnorm = mcolors.Normalize(vmin=40, vmax=127)\n\n# Canvas: landscape 3200×1800 px (figsize × dpi = 8×400, 4.5×400)\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Alternating row shading to distinguish black vs white piano keys\nfor pitch in range(pitch_min, pitch_max + 1):\n    row_color = BLACK_KEY_BG if (pitch % 12) in BLACK_KEYS else PAGE_BG\n    ax.axhspan(pitch - 0.5, pitch + 0.5, color=row_color, zorder=0)\n\n# Vertical grid: subtle beats with stronger measure boundaries\ntotal_beats = 32\nfor beat in range(total_beats + 1):\n    if beat % 4 == 0:\n        ax.axvline(beat, color=INK_SOFT, linewidth=0.7, alpha=0.5, zorder=1)\n    else:\n        ax.axvline(beat, color=INK_MUTED, linewidth=0.35, alpha=0.35, zorder=1)\n\n# Note rectangles: rounded corners with subtle depth shadow\nfor start, dur, pitch, vel in notes:\n    color = imprint_seq(norm(vel))\n    rect = mpatches.FancyBboxPatch(\n        (start, pitch - 0.4),\n        dur,\n        0.8,\n        boxstyle=\"round,pad=0.05\",\n        facecolor=color,\n        edgecolor=PAGE_BG,\n        linewidth=0.8,\n        zorder=2,\n        path_effects=[pe.withStroke(linewidth=2.5, foreground=\"#00000018\"), pe.Normal()],\n    )\n    ax.add_patch(rect)\n\n# Y-axis: MIDI note names (C4, D#4, etc.) in monospace for alignment\nvisible_pitches = list(range(pitch_min, pitch_max + 1))\npitch_labels = []\nfor p in visible_pitches:\n    octave = p // 12 - 1\n    semitone = p % 12\n    name = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"][semitone]\n    pitch_labels.append(f\"{name}{octave}\")\n\nax.set_yticks(visible_pitches)\nax.set_yticklabels(pitch_labels, fontsize=8, fontfamily=\"monospace\")\n\n# X-axis: measure numbers (1-indexed)\nbeat_ticks = np.arange(0, total_beats + 1, 4)\nax.set_xticks(beat_ticks)\nax.set_xticklabels([str(int(b // 4) + 1) for b in beat_ticks], fontsize=8)\n\n# Axes limits and labels\nax.set_xlim(-0.2, total_beats + 0.2)\nax.set_ylim(pitch_min - 0.5, pitch_max + 0.5)\nax.set_xlabel(\"Measure\", fontsize=10, labelpad=8, color=INK)\nax.set_ylabel(\"Pitch\", fontsize=10, labelpad=8, color=INK)\n\ntitle = \"piano-roll-midi · python · matplotlib · anyplot.ai\"\nax.set_title(title, fontsize=12, fontweight=\"medium\", pad=12, color=INK)\n\n# Chrome: remove all spines, hide tick marks, apply theme colors\nfor spine in ax.spines.values():\n    spine.set_visible(False)\nax.tick_params(axis=\"both\", length=0, labelcolor=INK_SOFT)\n\n# Velocity colorbar using Imprint sequential colormap\nsm = plt.cm.ScalarMappable(cmap=imprint_seq, norm=norm)\nsm.set_array([])\ncbar = plt.colorbar(sm, ax=ax, pad=0.02, aspect=25, shrink=0.85)\ncbar.set_label(\"Velocity (MIDI)\", fontsize=10, color=INK)\ncbar.ax.tick_params(labelsize=8, colors=INK_SOFT)\ncbar.outline.set_visible(False)\nplt.setp(cbar.ax.yaxis.get_ticklabels(), color=INK_SOFT)\n\nplt.tight_layout()\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}