{"spec_id":"piano-roll-midi","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\npiano-roll-midi: MIDI Piano Roll Visualization\nLibrary: plotnine 0.15.5 | Python 3.13.13\nQuality: 91/100 | Updated: 2026-06-03\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    annotate,\n    coord_cartesian,\n    element_line,\n    element_rect,\n    element_text,\n    geom_rect,\n    geom_segment,\n    geom_text,\n    geom_vline,\n    ggplot,\n    guide_colorbar,\n    labs,\n    scale_fill_gradient,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_void,\n)\n\n\n# Theme tokens (Imprint palette, theme-adaptive chrome)\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\"\n\n# Piano key shading (theme-adaptive) — dark values boosted for visible contrast over #1A1A17\nWHITE_KEY_BG = \"#EDE9DE\" if THEME == \"light\" else \"#272720\"\nBLACK_KEY_BG = \"#D8D4C8\" if THEME == \"light\" else \"#0D0D0B\"\nBEAT_LINE = \"#C4C0B4\" if THEME == \"light\" else \"#2C2C29\"\nMEASURE_LINE = \"#8A8780\" if THEME == \"light\" else \"#4A4A46\"\nOCTAVE_LINE = \"#ABA89C\" if THEME == \"light\" else \"#363632\"\n\n# Data — C major → F major → G major → C major chord progression with melody\nnp.random.seed(42)\n\nnotes = [\n    # Measure 1: C major chord + melody (mf)\n    (0.0, 2.0, 48, 80),  # C3 bass\n    (0.0, 2.0, 52, 70),  # E3\n    (0.0, 2.0, 55, 70),  # G3\n    (0.0, 1.0, 60, 100),  # C4 melody\n    (1.0, 0.5, 62, 90),  # D4\n    (1.5, 0.5, 64, 95),  # E4\n    (2.0, 1.0, 65, 105),  # F4\n    (3.0, 0.5, 64, 85),  # E4\n    (3.5, 0.5, 62, 80),  # D4\n    # Measure 2: F major chord + melody (f to ff)\n    (4.0, 2.0, 53, 75),  # F3 bass\n    (4.0, 2.0, 57, 65),  # A3\n    (4.0, 2.0, 60, 65),  # C4\n    (4.0, 1.0, 65, 110),  # F4 melody\n    (5.0, 0.5, 67, 95),  # G4\n    (5.5, 0.5, 69, 100),  # A4\n    (6.0, 1.5, 72, 115),  # C5 — CLIMAX\n    (7.5, 0.5, 69, 80),  # A4\n    # Measure 3: G major chord + descending melody (diminuendo)\n    (8.0, 2.0, 50, 85),  # D3 bass (G/D inversion)\n    (8.0, 2.0, 55, 70),  # G3\n    (8.0, 2.0, 59, 70),  # B3\n    (8.0, 1.0, 71, 105),  # B4 melody\n    (9.0, 0.5, 69, 90),  # A4\n    (9.5, 0.5, 67, 85),  # G4\n    (10.0, 1.0, 65, 95),  # F4\n    (11.0, 0.5, 64, 80),  # E4\n    (11.5, 0.5, 62, 75),  # D4\n    # Measure 4: C major resolution (p, fading)\n    (12.0, 2.0, 48, 90),  # C3 bass\n    (12.0, 2.0, 52, 75),  # E3\n    (12.0, 2.0, 55, 75),  # G3\n    (12.0, 3.0, 60, 110),  # C4 — long resolution\n    (14.0, 1.0, 64, 70),  # E4\n    (15.0, 1.0, 60, 60),  # C4 — soft fade\n]\n\ndf = pd.DataFrame(notes, columns=[\"start\", \"duration\", \"pitch\", \"velocity\"])\ndf[\"end\"] = df[\"start\"] + df[\"duration\"]\ndf[\"ymin\"] = df[\"pitch\"] - 0.4\ndf[\"ymax\"] = df[\"pitch\"] + 0.4\n\nnote_names = [\"C\", \"C♯\", \"D\", \"D♯\", \"E\", \"F\", \"F♯\", \"G\", \"G♯\", \"A\", \"A♯\", \"B\"]\n\npitch_min = int(df[\"pitch\"].min()) - 1  # 47\npitch_max = int(df[\"pitch\"].max()) + 1  # 73\n\n# Background rows — theme-adaptive black/white key shading\nblack_key_semitones = {1, 3, 6, 8, 10}\nbg_rows = [\n    {\"ymin\": p - 0.5, \"ymax\": p + 0.5, \"fill_color\": BLACK_KEY_BG if p % 12 in black_key_semitones else WHITE_KEY_BG}\n    for p in range(pitch_min, pitch_max + 1)\n]\nbg_df = pd.DataFrame(bg_rows)\n\n# Y-axis: C (octave markers) and G (dominant) only — avoids adjacent-label crowding\nlabel_pitches = sorted(p for p in range(pitch_min, pitch_max + 1) if p % 12 in {0, 7})\nlabel_names = [f\"{note_names[p % 12]}{p // 12 - 1}\" for p in label_pitches]\n\n# Measure structure\ntotal_beats = 16\nmeasure_lines = [0, 4, 8, 12, 16]\nbeat_lines = [b for b in range(total_beats + 1) if b not in measure_lines]\n\n# Chord labels at measure tops\nmeasure_labels = pd.DataFrame(\n    {\"x\": [2, 6, 10, 14], \"label\": [\"I  (C)\", \"IV  (F)\", \"V  (G)\", \"I  (C)\"], \"y\": [pitch_max + 1.8] * 4}\n)\n\n# Dynamic markings below piano roll\ndynamic_labels = pd.DataFrame({\"x\": [2, 6.5, 10, 14.5], \"label\": [\"mf\", \"ff\", \"dim.\", \"p\"], \"y\": [pitch_min - 0.6] * 4})\n\n# Octave boundary lines at each C note\noctave_cs = [p for p in range(pitch_min, pitch_max + 1) if p % 12 == 0]\noctave_lines = pd.DataFrame(\n    {\"y\": [c - 0.5 for c in octave_cs], \"xstart\": [-0.3] * len(octave_cs), \"xend\": [total_beats + 0.3] * len(octave_cs)}\n)\n\ntitle = \"piano-roll-midi · python · plotnine · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot()\n    # Background rows — black/white key distinction\n    + geom_rect(\n        bg_df,\n        aes(xmin=-0.3, xmax=total_beats + 0.3, ymin=\"ymin\", ymax=\"ymax\"),\n        fill=bg_df[\"fill_color\"].tolist(),\n        color=None,\n        show_legend=False,\n    )\n    # Beat grid (subtle dotted)\n    + geom_vline(xintercept=beat_lines, color=BEAT_LINE, size=0.25, linetype=\"dotted\")\n    # Measure boundaries (solid)\n    + geom_vline(xintercept=measure_lines, color=MEASURE_LINE, size=0.5, linetype=\"solid\")\n    # Octave boundary lines (dashed)\n    + geom_segment(\n        octave_lines, aes(x=\"xstart\", xend=\"xend\", y=\"y\", yend=\"y\"), color=OCTAVE_LINE, size=0.35, linetype=\"dashed\"\n    )\n    # Note rectangles — Imprint sequential colormap (green=soft → blue=loud)\n    + geom_rect(df, aes(xmin=\"start\", xmax=\"end\", ymin=\"ymin\", ymax=\"ymax\", fill=\"velocity\"), color=INK, size=0.3)\n    # Climax annotation\n    + annotate(\"text\", x=7.6, y=72 + 1.0, label=\"← climax\", size=3.0, color=\"#AE3030\", fontstyle=\"italic\", ha=\"left\")\n    # Chord labels at top of each measure\n    + geom_text(measure_labels, aes(x=\"x\", y=\"y\", label=\"label\"), size=4.0, color=INK_SOFT, fontstyle=\"italic\")\n    # Dynamic markings below\n    + geom_text(dynamic_labels, aes(x=\"x\", y=\"y\", label=\"label\"), size=3.5, color=INK_MUTED, fontstyle=\"italic\")\n    # Imprint sequential cmap: #009E73 (soft/piano) → #4467A3 (loud/forte)\n    + scale_fill_gradient(\n        low=\"#009E73\", high=\"#4467A3\", limits=(55, 120), name=\"Velocity\", guide=guide_colorbar(nbin=200)\n    )\n    + scale_y_continuous(breaks=label_pitches, labels=label_names, expand=(0.02, 0.02))\n    + scale_x_continuous(breaks=measure_lines, labels=[\"0\", \"4\", \"8\", \"12\", \"16\"], expand=(0.01, 0.01))\n    + coord_cartesian(xlim=(-0.3, total_beats + 0.3), ylim=(pitch_min - 1.5, pitch_max + 2.5))\n    + labs(x=\"Time (beats)\", y=\"Pitch\", title=title)\n    + theme_void()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, weight=\"bold\", color=INK, margin={\"b\": 8}),\n        axis_title_x=element_text(size=10, color=INK, margin={\"t\": 6}),\n        axis_title_y=element_text(size=10, color=INK, margin={\"r\": 6}),\n        axis_text_x=element_text(size=8, color=INK_SOFT),\n        axis_text_y=element_text(size=8, color=INK_SOFT),\n        axis_ticks_major=element_line(color=INK_SOFT, size=0.4),\n        axis_ticks_length=3,\n        legend_position=\"right\",\n        legend_title=element_text(size=8, color=INK),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_background=element_rect(fill=ELEVATED_BG, color=None),\n        legend_key_height=30,\n        legend_key_width=10,\n        panel_background=element_rect(fill=PAGE_BG, color=None),\n        plot_background=element_rect(fill=PAGE_BG, color=None),\n        plot_margin=0.02,\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\", verbose=False)\n"}