{"spec_id":"piano-roll-midi","library":"muix","language":"javascript","code":"// anyplot.ai\n// piano-roll-midi: MIDI Piano Roll Visualization\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 94/100 | Created: 2026-08-20\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ContinuousColorLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useXScale, useYScale, useZColorScale, useDrawingArea } from \"@mui/x-charts/hooks\";\nimport { Box, Typography } from \"@mui/material\";\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// A four-bar chord loop (I - vi - IV - V in C major) played twice: a whole-note\n// bass root, a two-hit comping pattern per measure, and a diatonic melodic line\n// on top. Velocities climb measure by measure to build a single crescendo arc\n// (soft comping early -> loud melody peaks late), so the color scale alone\n// tells the dynamics story without any text callouts.\nconst BEATS_PER_MEASURE = 4;\nconst MEASURES = 8;\nconst TOTAL_BEATS = MEASURES * BEATS_PER_MEASURE;\n\nconst CHORDS = [\n  { root: 60, tones: [60, 64, 67], melody: [72, 74, 72, 71, 69] }, // C major\n  { root: 57, tones: [57, 60, 64], melody: [69, 71, 69, 67, 65] }, // A minor\n  { root: 65, tones: [65, 69, 72], melody: [74, 76, 74, 74, 71] }, // F major\n  { root: 67, tones: [67, 71, 74], melody: [76, 77, 76, 69, 72] }, // G major\n];\n\nconst MELODY_OFFSETS = [0, 1, 2, 2.5, 3];\nconst MELODY_DURATIONS = [1, 1, 0.5, 0.5, 1];\n\nconst notes = [];\nfor (let measure = 0; measure < MEASURES; measure += 1) {\n  const chord = CHORDS[Math.floor(measure / 2) % CHORDS.length];\n  const measureStart = measure * BEATS_PER_MEASURE;\n\n  // Bass: one whole note holding the chord root down an octave.\n  notes.push({\n    start: measureStart,\n    duration: BEATS_PER_MEASURE,\n    pitch: chord.root - 12,\n    velocity: 78 + measure * 3,\n  });\n\n  // Comping: the triad struck on beat 1 and beat 3 of the measure.\n  for (let hit = 0; hit < 2; hit += 1) {\n    const hitStart = measureStart + hit * 2;\n    const hitVelocity = 58 + measure * 2 + (hit === 1 ? 6 : 0);\n    chord.tones.forEach((pitch) => {\n      notes.push({ start: hitStart, duration: 2, pitch, velocity: hitVelocity });\n    });\n  }\n\n  // Melody: a five-note diatonic cell riding on top of the harmony.\n  MELODY_OFFSETS.forEach((offset, i) => {\n    notes.push({\n      start: measureStart + offset,\n      duration: MELODY_DURATIONS[i],\n      pitch: chord.melody[i],\n      velocity: 82 + measure * 3 + (i % 2 === 0 ? 6 : 0),\n    });\n  });\n}\n\nconst pitches = notes.map((n) => n.pitch);\nconst velocities = notes.map((n) => n.velocity);\nconst PITCH_MARGIN = 2; // auto-fit the visible range to the data, not all 128 MIDI notes\nconst pitchAxisMin = Math.min(...pitches) - PITCH_MARGIN;\nconst pitchAxisMax = Math.max(...pitches) + PITCH_MARGIN;\nconst velocityMin = Math.min(...velocities);\nconst velocityMax = Math.max(...velocities);\n\nconst NOTE_NAMES = [\"C\", \"C#\", \"D\", \"D#\", \"E\", \"F\", \"F#\", \"G\", \"G#\", \"A\", \"A#\", \"B\"];\nconst midiToNoteName = (midi) => `${NOTE_NAMES[((midi % 12) + 12) % 12]}${Math.floor(midi / 12) - 1}`;\nconst isBlackKey = (midi) => [1, 3, 6, 8, 10].includes(((midi % 12) + 12) % 12);\n\nconst measureTicks = Array.from({ length: MEASURES }, (_, i) => i * BEATS_PER_MEASURE);\nconst octaveTicks = [];\nfor (let p = Math.ceil(pitchAxisMin); p <= Math.floor(pitchAxisMax); p += 1) {\n  if (p % 12 === 0) octaveTicks.push(p);\n}\n\nconst hexToRgba = (hex, alpha) => {\n  const n = parseInt(hex.replace(\"#\", \"\"), 16);\n  return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;\n};\nconst blackKeyFill = hexToRgba(t.ink, 0.05);\nconst measureLineColor = hexToRgba(t.ink, 0.28);\n\nconst TITLE = \"piano-roll-midi · javascript · muix · anyplot.ai\";\n\n// --- Piano-key row shading (white/black key bands behind the notes) ---------\nfunction KeyboardRows() {\n  const yScale = useYScale(\"pitch\");\n  const drawingArea = useDrawingArea();\n  const rows = [];\n  for (let p = Math.ceil(pitchAxisMin); p <= Math.floor(pitchAxisMax); p += 1) {\n    if (isBlackKey(p)) rows.push(p);\n  }\n  return (\n    <g>\n      {rows.map((p) => (\n        <rect\n          key={p}\n          x={drawingArea.left}\n          y={yScale(p + 0.5)}\n          width={drawingArea.width}\n          height={yScale(p - 0.5) - yScale(p + 0.5)}\n          fill={blackKeyFill}\n        />\n      ))}\n    </g>\n  );\n}\n\n// --- Beat / measure grid lines (stronger at measure boundaries) ------------\nfunction BeatGrid() {\n  const xScale = useXScale(\"beats\");\n  const drawingArea = useDrawingArea();\n  const beats = Array.from({ length: TOTAL_BEATS + 1 }, (_, b) => b);\n  return (\n    <g>\n      {beats.map((b) => {\n        const isMeasure = b % BEATS_PER_MEASURE === 0;\n        return (\n          <line\n            key={b}\n            x1={xScale(b)}\n            x2={xScale(b)}\n            y1={drawingArea.top}\n            y2={drawingArea.top + drawingArea.height}\n            stroke={isMeasure ? measureLineColor : t.grid}\n            strokeWidth={isMeasure ? 1.5 : 1}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Notes: one rounded rect per (start, duration, pitch), fill by velocity -\nfunction NoteBars() {\n  const xScale = useXScale(\"beats\");\n  const yScale = useYScale(\"pitch\");\n  const colorScale = useZColorScale(\"velocity\");\n  return (\n    <g>\n      {notes.map((n, i) => {\n        const x0 = xScale(n.start);\n        const x1 = xScale(n.start + n.duration);\n        const rowTop = yScale(n.pitch + 0.5);\n        const rowBottom = yScale(n.pitch - 0.5);\n        const pad = (rowBottom - rowTop) * 0.14;\n        return (\n          <rect\n            key={i}\n            x={x0 + 1}\n            y={rowTop + pad}\n            width={Math.max(x1 - x0 - 2, 3)}\n            height={Math.max(rowBottom - rowTop - pad * 2, 2)}\n            rx={3}\n            fill={colorScale ? colorScale(n.velocity) : t.palette[0]}\n            stroke={t.pageBg}\n            strokeWidth={1}\n          />\n        );\n      })}\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) ------------\nexport default function Chart() {\n  const size = window.ANYPLOT_SIZE;\n  const padding = { top: 24, right: 28, bottom: 20, left: 28 };\n  const titleBlockHeight = 44;\n  const chartWidth = size.width - padding.left - padding.right;\n  const chartHeight = size.height - padding.top - padding.bottom - titleBlockHeight;\n\n  return (\n    <Box\n      sx={{\n        width: size.width,\n        height: size.height,\n        boxSizing: \"border-box\",\n        padding: `${padding.top}px ${padding.right}px ${padding.bottom}px ${padding.left}px`,\n        display: \"flex\",\n        flexDirection: \"column\",\n      }}\n    >\n      <Typography sx={{ fontSize: 22, fontWeight: 600, color: \"text.primary\", mb: \"18px\", lineHeight: 1 }}>\n        {TITLE}\n      </Typography>\n      <ChartContainer\n        width={chartWidth}\n        height={chartHeight}\n        series={[]}\n        margin={{ left: 64, right: 24, top: 52, bottom: 56 }}\n        xAxis={[\n          {\n            id: \"beats\",\n            scaleType: \"linear\",\n            min: -0.4,\n            max: TOTAL_BEATS + 0.4,\n            tickInterval: measureTicks,\n            valueFormatter: (beat) => `M${beat / BEATS_PER_MEASURE + 1}`,\n            label: \"Time (measures)\",\n            tickLabelStyle: { fontSize: 14 },\n            labelStyle: { fontSize: 15 },\n          },\n        ]}\n        yAxis={[\n          {\n            id: \"pitch\",\n            scaleType: \"linear\",\n            min: pitchAxisMin,\n            max: pitchAxisMax,\n            tickInterval: octaveTicks,\n            valueFormatter: (pitch) => midiToNoteName(pitch),\n            label: \"Pitch\",\n            tickLabelStyle: { fontSize: 14 },\n            labelStyle: { fontSize: 15 },\n          },\n        ]}\n        zAxis={[\n          {\n            id: \"velocity\",\n            min: velocityMin,\n            max: velocityMax,\n            colorMap: { type: \"continuous\", color: [t.seq[0], t.seq[1]] },\n          },\n        ]}\n        disableAxisListener\n      >\n        <KeyboardRows />\n        <BeatGrid />\n        <ChartsYAxis />\n        <ChartsXAxis />\n        <NoteBars />\n        <ContinuousColorLegend\n          axisDirection=\"z\"\n          axisId=\"velocity\"\n          minLabel=\"Soft\"\n          maxLabel=\"Loud\"\n          position={{ horizontal: \"right\", vertical: \"top\" }}\n          direction=\"row\"\n          length=\"22%\"\n          thickness={10}\n          labelStyle={{ fontSize: 14, fill: t.inkSoft }}\n        />\n      </ChartContainer>\n    </Box>\n  );\n}\n"}