A four channel music tracker plays a chiptune, note rows scrolling up past the play line while the meters jump, the channel scopes wiggle and the pattern counter steps.
▀TRACKER▀▀NIGHT DRIVE.MOD▀▀▀▀▀BPM 125 SPD 6 POS 00/03 PAT 00 ROW 03▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀00:00▀▀▀ ┌─ Samples ──────────────┐ ┌─ CH1 ─────────┐ ┌─ CH2 ─────────┐ ┌─ CH3 ─────────┐ ┌─ CH4 ─────────┐ │ 01 kick │ │░░ │ │░░ │ │░░ │ │░░ │ │ 02 snare │ │░░ │ │██ • • │ │░░ │ │░░ │ │ 03 closed hat │ │░░ │ │██ • │ │░░ • • • ••│ │░░ •• ••│ │ 04 bass saw │ │██ • ••• ••│ │██ • • • │ │██ │ │░░ • • │ │ 05 chord arp │ │██ ••• ••• │ │██ • • •│ │██ │ │██ • • │ │ 06 lead pulse │ │██ │ │██ • • │ │██ •• •• •• • │ │██ •• •• │ │ 07 pad │ │██ │ │██ • │ │██ │ │██ │ │ 08 noise sweep │ │██ │ │██ │ │██ │ │██ │ └────────────────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ └───────────────┘ ╔══════════════════════════════════════════════════════════════════════════════════════════════════╗ ║ 58 │ --- .. ... ... │ G-3 04 v64 ... │ --- .. ... ... │ --- .. ... ... ║ ║ 59 │ F#4 03 v64 C10 │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... ║ ║ 60 │ D-3 02 v64 ... │ G-2 04 v64 ... │ G-4 05 v64 047 │ F-6 06 v48 ... ║ ║ 61 │ F#4 03 v64 C20 │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... ║ ║ 62 │ --- .. ... ... │ G-3 04 v64 E91 │ --- .. ... ... │ A#5 06 v48 ... ║ ║ 63 │ F#4 03 v64 C10 │ --- .. ... ... │ --- .. ... ... │ C-6 08 v48 F06 ║ ║ 00 │ C-3 01 v64 ... │ A-2 04 v64 ... │ A-4 05 v64 037 │ --- .. ... ... ║ ║ 01 │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... ║ ║ 02 │ --- .. ... ... │ A-3 04 v64 ... │ --- .. ... ... │ --- .. ... ... ║ ║►03▒│▒---▒..▒...▒▒...▒▒▒▒▒▒│▒---▒..▒...▒▒...▒▒▒▒▒▒│▒---▒..▒...▒▒...▒▒▒▒▒▒│▒---▒..▒...▒▒...▒▒▒▒▒▒▒◄║ ║ 04 │ D-3 02 v64 ... │ A-2 04 v64 ... │ A-4 05 v64 037 │ --- .. ... ... ║ ║ 05 │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... ║ ║ 06 │ --- .. ... ... │ A-3 04 v64 ... │ --- .. ... ... │ --- .. ... ... ║ ║ 07 │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... ║ ║ 08 │ C-3 01 v64 ... │ A-2 04 v64 ... │ A-4 05 v64 037 │ --- .. ... ... ║ ║ 09 │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... ║ ║ 10 │ --- .. ... ... │ A-3 04 v64 ... │ --- .. ... ... │ --- .. ... ... ║ ║ 11 │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... │ --- .. ... ... ║ ║ 12 │ D-3 02 v64 ... │ A-2 04 v64 ... │ A-4 05 v64 037 │ --- .. ... ... ║ ╚══════════════════════════════════════════════════════════════════════════════════════════════════╝
Paste two lines into any page, play it in a terminal, or put a GIF in a README. The code follows the look you pick above.
<script type="module" src="https://www.ascii-magic.com/ascii/ascii.js"></script>
<ascii-art piece="chiptune-tracker"></ascii-art>// copy chiptune-tracker.js and kit.js into your project, then:
import { useEffect, useRef } from "react";
import create from "./chiptune-tracker.js";
export function Piece() {
const ref = useRef(null);
useEffect(() => {
const draw = create(), t0 = performance.now();
let raf;
const tick = () => {
const f = draw((performance.now() - t0) / 1000);
ref.current.textContent = f.text ?? f;
raf = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(raf);
}, []);
return <pre ref={ref} style={{ lineHeight: 1, fontFamily: "Menlo, Consolas, monospace" }} />;
}npx --yes https://www.ascii-magic.com/ascii/cli.tgz chiptune-tracker<!-- Download GIF below, commit it, then: -->
<p align="center">
<img src="assets/chiptune-tracker.gif" alt="Chiptune Tracker, animated ASCII art" width="720">
</p>Pause on the frame you like, pick a look, then download it at an exact size.
MIT licensed. It imports one small helper file, kit.js.
/* chiptune-tracker: a four channel music tracker playing a song, pattern rows scrolling past the play line with meters and scopes. */
import { Grid, palette, rng } from "../lib/kit.js";
export const meta = {
slug: "chiptune-tracker",
name: "Chiptune Tracker",
category: "retro",
note: "A four channel music tracker plays a chiptune, note rows scrolling up past the play line while the meters jump, the channel scopes wiggle and the pattern counter steps.",
tags: ["retro", "music", "tracker", "chiptune", "demoscene", "dashboard"],
cols: 100, rows: 32, fps: 24,
palette: palette(["#e6edf3", "#8b949e", "#484f58", "#30363d", "#79c0ff", "#d2a8ff", "#ffa657", "#7ee787", "#f2cc60", "#ff7b72", "#56d4dd", "#1f6feb", "#0d1f3a", "#161b22"]),
};
const NN = ["C-", "C#", "D-", "D#", "E-", "F-", "F#", "G-", "G#", "A-", "A#", "B-"];
const SAMPLES = ["kick", "snare", "closed hat", "bass saw", "chord arp", "lead pulse", "pad", "noise sweep"];
const ROOTS = [9, 5, 0, 7], MINOR = [true, false, false, false];
const PENTA = [0, 3, 5, 7, 10];
const RPS = 6.5, ROWS = 64, PATS = 4;
export default function create() {
const W = meta.cols, H = meta.rows;
const g = new Grid(W, H);
// song data: [pattern][row][channel] = {n, i, e} or null
const song = [];
for (let p = 0; p < PATS; p++) {
const R = rng(31 + p * 5);
const pat = [];
for (let r = 0; r < ROWS; r++) {
const ch = [null, null, null, null];
const bar = Math.floor(r / 16), root = ROOTS[bar];
if (r % 8 === 0) ch[0] = { n: 36, i: 1, e: "..." };
else if (r % 8 === 4) ch[0] = { n: 38, i: 2, e: "..." };
else if (r % 2 === 1 && p > 0) ch[0] = { n: 54, i: 3, e: "C" + (r % 4 === 1 ? "20" : "10") };
if (r % 2 === 0) ch[1] = { n: 24 + root + (r % 4 === 2 ? 12 : 0), i: 4, e: r % 16 === 14 ? "E91" : "..." };
if (r % 4 === 0) ch[2] = { n: 48 + root, i: 5, e: MINOR[bar] ? "037" : "047" };
if (p > 1 || r >= 32) {
if (R() < 0.42 && r % 2 === 0) ch[3] = { n: 60 + PENTA[Math.floor(R() * 5)] + (R() < 0.25 ? 12 : 0), i: 6, e: R() < 0.3 ? "4" + "48"[Math.floor(R() * 2)] + "3" : "..." };
}
if (r === 63 && p === 3) ch[3] = { n: 72, i: 8, e: "F06" };
pat.push(ch);
}
song.push(pat);
}
const ORDER = [0, 1, 2, 3];
const PERIOD = (ROWS * PATS) / RPS;
const noteStr = (c) => (c ? NN[c.n % 12] + Math.floor(c.n / 12) : "---");
const lastHit = (pi, r, k) => {
for (let a = 0; a < 16; a++) {
let rr = r - a, pp = pi;
while (rr < 0) { rr += ROWS; pp = (pp + PATS - 1) % PATS; }
if (song[ORDER[pp]][rr][k]) return [a, song[ORDER[pp]][rr][k]];
}
return [99, null];
};
return (t) => {
g.clear();
const lt = t % PERIOD;
const abs = lt * RPS;
const row = Math.floor(abs) % ROWS, pos = Math.floor(abs / ROWS) % PATS;
const frac = abs % 1;
// title bar
g.fill(0, 0, W, 1, "▀", "c");
g.text(1, 0, "TRACKER", "8");
g.text(10, 0, "NIGHT DRIVE.MOD", "0");
g.text(30, 0, "BPM 125 SPD 6 POS " + String(pos).padStart(2, "0") + "/" + String(PATS - 1).padStart(2, "0") + " PAT " + String(ORDER[pos]).padStart(2, "0") + " ROW " + String(row).padStart(2, "0"), "1");
const secs = Math.floor(lt);
g.text(W - 8, 0, String(Math.floor(secs / 60)).padStart(2, "0") + ":" + String(secs % 60).padStart(2, "0"), "7");
// samples list
g.box(0, 1, 26, 10, "light", "3");
g.text(2, 1, " Samples ", "1");
const hits = [0, 1, 2, 3].map((k) => lastHit(pos, row, k));
SAMPLES.forEach((s, i) => {
const active = hits.some(([a, c]) => c && c.i === i + 1 && a < 2);
g.text(2, 2 + i, String(i + 1).padStart(2, "0") + " " + s, active ? "8" : "1");
});
// meters and scopes per channel
const CH_CO = ["9", "4", "5", "7"];
for (let k = 0; k < 4; k++) {
const x0 = 27 + k * 18;
g.box(x0, 1, 17, 10, "light", "3");
g.text(x0 + 2, 1, " CH" + (k + 1) + " ", CH_CO[k]);
const [age, c] = hits[k];
const lvl = Math.max(0, 1 - (age + frac) * 0.14);
// vertical meter
for (let y = 0; y < 8; y++) {
const on = (7 - y) / 8 < lvl;
g.text(x0 + 1, 2 + y, on ? "██" : "░░", on ? (y < 2 ? "9" : y < 4 ? "8" : "7") : "3");
}
// scope
for (let x = 0; x < 12; x++) {
let v = 0;
if (c) {
const ph = (x / 12) * (2 + (c.n % 12) / 4) + t * 3;
v = k === 0 ? Math.sin(ph * 6.28) * (age < 1 ? 1 : 0.4) : k === 1 ? ((ph % 1) * 2 - 1) : k === 2 ? (ph % 1 < 0.5 ? 1 : -1) : Math.sin(ph * 6.28) * 0.9;
v *= lvl;
}
const y = Math.round(5.5 - v * 3.2);
g.set(x0 + 4 + x, Math.max(2, Math.min(9, y)), "•", CH_CO[k]);
}
}
// pattern view
const PY = 12, PH = H - PY - 1, mid = PY + Math.floor(PH / 2);
g.box(0, PY - 1, W, PH + 2, "double", "3");
for (let y = PY; y < PY + PH; y++) {
const r = row + (y - mid);
let pp = pos, rr = r;
if (rr < 0) { rr += ROWS; pp = (pos + PATS - 1) % PATS; }
if (rr >= ROWS) { rr -= ROWS; pp = (pos + 1) % PATS; }
const cur = y === mid;
if (cur) g.fill(1, y, W - 2, 1, "▒", "c");
g.text(2, y, String(rr).padStart(2, "0"), cur ? "8" : rr % 16 === 0 ? "6" : rr % 4 === 0 ? "1" : "2");
for (let k = 0; k < 4; k++) {
const x = 6 + k * 23;
g.set(x - 1, y, "│", "3");
const c = song[ORDER[pp]][rr][k];
const dim = pp !== pos;
g.text(x + 1, y, noteStr(c), c ? (cur ? "0" : dim ? "1" : "0") : "2");
g.text(x + 5, y, c ? String(c.i).padStart(2, "0") : "..", c ? "a" : "2");
g.text(x + 8, y, c ? "v" + (k === 3 ? "48" : "64") : "...", c ? "7" : "2");
g.text(x + 13, y, c ? c.e : "...", c && c.e !== "..." ? "5" : "2");
}
}
g.set(1, mid, "►", "8"); g.set(W - 2, mid, "◄", "8");
return g.frame();
};
}
/YOUR OWN
The pieces here are hand-built animations. To turn your own photo, logo or video into animated ASCII, use the ASCII Magic editor.