@@ -942,7 +826,7 @@
- 079 · whistle · octave 0
Play your Mac instead of opening a workstation. Menu Band puts a two-octave instrument in the menu bar: type notes, connect a MIDI keyboard, tune live radio, or send notes to your DAW. Hundreds of built-in sounds are ready when nothing else is connected.
Get Menu Band$4.99 once · macOS 11+
@@ -951,35 +835,7 @@
-
-
-
-
-
-
- Menu Band in motion
-Two tiny films about making music without leaving the menu bar.
-
-
-
-
- Meet Menu Band
- The menu-bar instrument in under a minute.
-
-
-
-
-
- Sing the scale
- C D E F G… but weird.
-
-
-
- TrackDrum
+Turn your Mac trackpad into Menu Band’s percussion surface.
@@ -1013,176 +869,6 @@
let voiceName = "whistle";
let digitBuffer = "";
let lastDigitAt = 0;
- const lesson = { kind: "chord", root: 0, mode: "idle", timers: [], step: 0, matched: new Set(), voices: new Map(), narration: null };
- const ROOTS = ["C", "C♯", "D", "E♭", "E", "F", "F♯", "G", "A♭", "A", "B♭", "B"];
- const KEY_FOR_MIDI = new Map(keys.map((key) => [Number(key.dataset.midi), key]));
- const SCALE_STEPS = [0, 2, 4, 5, 7, 9, 11, 12];
- const noteName = (midi) => ROOTS[((midi % 12) + 12) % 12];
- const currentLesson = () => {
- const rootMidi = 60 + lesson.root;
- const notes = lesson.kind === "chord"
- ? [rootMidi, rootMidi + 4, rootMidi + 7]
- : SCALE_STEPS.map((step) => rootMidi + step);
- return { rootMidi, notes, title: `${ROOTS[lesson.root]} major ${lesson.kind}` };
- };
-
- const lessonTab = document.getElementById("learn-tab");
- const lessonDrawer = document.getElementById("learn-drawer");
- const lessonGrid = document.getElementById("lesson-grid");
- const lessonTitle = document.getElementById("lesson-title");
- const lessonKeys = document.getElementById("lesson-keys");
- const lessonCaption = document.getElementById("lesson-caption");
- const lessonProgress = document.getElementById("lesson-progress");
- const lessonFeedback = document.getElementById("lesson-feedback");
- const demoButton = document.getElementById("lesson-demo");
- const practiceButton = document.getElementById("lesson-practice");
-
- const clearLessonTimers = () => {
- lesson.timers.forEach(clearTimeout);
- lesson.timers.length = 0;
- keys.forEach((key) => key.classList.remove("is-taught", "is-correct"));
- [...Array(24)].forEach((_, i) => instrument.release(`lesson:${i}`));
- globalThis.speechSynthesis?.cancel?.();
- if (lesson.narration) { lesson.narration.pause(); lesson.narration.currentTime = 0; lesson.narration = null; }
- };
- const later = (fn, ms) => lesson.timers.push(setTimeout(fn, ms));
- const showLessonNotes = (notes, className = "is-taught") => {
- keys.forEach((key) => key.classList.remove("is-taught", "is-correct"));
- notes.forEach((midi) => KEY_FOR_MIDI.get(midi)?.classList.add(className));
- };
- const say = (text) => {
- if (!("speechSynthesis" in window)) return;
- speechSynthesis.cancel();
- const utterance = new SpeechSynthesisUtterance(text);
- utterance.rate = 0.92; utterance.pitch = 0.96; utterance.volume = 0.82;
- const preferred = speechSynthesis.getVoices().find((voice) => /jeffrey/i.test(voice.name))
- || speechSynthesis.getVoices().find((voice) => /^en[-_ ]?US/i.test(voice.lang));
- if (preferred) utterance.voice = preferred;
- speechSynthesis.speak(utterance);
- };
- const narrate = (text) => {
- const id = `${lesson.kind}-${["c", "c-sharp", "d", "e-flat", "e", "f", "f-sharp", "g", "a-flat", "a", "b-flat", "b"][lesson.root]}`;
- const src = lesson.voices.get(id);
- if (!src) { say(text); return; }
- lesson.narration = new Audio(src);
- lesson.narration.play().catch(() => say(text));
- };
- const playLessonNote = (midi, slot, duration = 420) => {
- const id = `lesson:${slot}`;
- instrument.pressMidi(midi, id);
- later(() => instrument.release(id), duration);
- };
- const renderLesson = () => {
- clearLessonTimers(); lesson.mode = "idle"; lesson.step = 0; lesson.matched.clear();
- const spec = currentLesson();
- lessonTitle.textContent = spec.title;
- lessonKeys.textContent = spec.notes.map((midi) => KEY_FOR_MIDI.get(midi)?.dataset.key).join(lesson.kind === "chord" ? " + " : " ");
- lessonCaption.textContent = "Watch the keys, then play it yourself.";
- lessonFeedback.textContent = "choose watch & hear or practice";
- lessonProgress.value = 0;
- [...lessonGrid.children].forEach((button, index) => button.setAttribute("aria-current", String(index === lesson.root)));
- };
- const finishDemo = () => {
- lesson.mode = "idle"; demoButton.textContent = "↻ watch again";
- lessonProgress.value = 1;
- lessonCaption.textContent = "Now you play it.";
- lessonFeedback.textContent = "press practice and follow the glowing keys";
- showLessonNotes([]);
- };
- const startDemo = async () => {
- clearLessonTimers(); lesson.mode = "demo"; lessonProgress.value = 0;
- demoButton.textContent = "■ stop"; lessonFeedback.textContent = "listen and watch";
- const spec = currentLesson();
- const patch = await instrument.ready().catch(() => null);
- await Promise.all(spec.notes.map((midi) => patch?.prefetch?.(midi))).catch(() => null);
- if (lesson.kind === "chord") {
- const names = spec.notes.map(noteName);
- const intro = `Here’s ${ROOTS[lesson.root]} major. ${names.join(", ")}. Three notes, one chord.`;
- lessonCaption.textContent = `Here’s ${ROOTS[lesson.root]} major.`; narrate(intro);
- spec.notes.forEach((midi, i) => later(() => {
- showLessonNotes([midi]); playLessonNote(midi, i, 560);
- lessonCaption.textContent = `${names[i]} — key ${KEY_FOR_MIDI.get(midi)?.dataset.key}`;
- lessonProgress.value = (i + 1) / 5;
- }, 850 + i * 720));
- later(() => {
- showLessonNotes(spec.notes);
- spec.notes.forEach((midi, i) => playLessonNote(midi, 8 + i, 1150));
- lessonCaption.textContent = `${names.join(" + ")} — ${ROOTS[lesson.root]} major.`;
- lessonProgress.value = .82;
- }, 3150);
- later(finishDemo, 4550);
- } else {
- const intro = lesson.root === 0
- ? "Here’s how to type out the C scale. Watch it climb, then play it back."
- : `Here’s the ${ROOTS[lesson.root]} major scale. Watch it climb, then play it back.`;
- lessonCaption.textContent = `Here’s the ${ROOTS[lesson.root]} major scale.`; narrate(intro);
- spec.notes.forEach((midi, i) => later(() => {
- showLessonNotes([midi]); playLessonNote(midi, i, 330);
- lessonCaption.textContent = `${noteName(midi)} — key ${KEY_FOR_MIDI.get(midi)?.dataset.key}`;
- lessonProgress.value = (i + 1) / (spec.notes.length + 1);
- }, 1000 + i * 390));
- later(finishDemo, 1000 + spec.notes.length * 390 + 500);
- }
- };
- const startPractice = () => {
- clearLessonTimers(); lesson.mode = "practice"; lesson.step = 0; lesson.matched.clear();
- const spec = currentLesson();
- practiceButton.textContent = "reset"; demoButton.textContent = "▶ watch & hear";
- lessonProgress.value = 0;
- lessonCaption.textContent = lesson.kind === "chord" ? "Find all three glowing notes." : "Play the glowing keys in order.";
- lessonFeedback.textContent = "your turn";
- showLessonNotes(lesson.kind === "chord" ? spec.notes : [spec.notes[0]]);
- };
- const hearPracticeNote = (midi) => {
- if (lesson.mode !== "practice") return;
- const spec = currentLesson();
- if (lesson.kind === "chord") {
- if (!spec.notes.includes(midi)) { lessonFeedback.textContent = `${noteName(midi)} isn’t in this chord — keep going`; return; }
- lesson.matched.add(midi); KEY_FOR_MIDI.get(midi)?.classList.add("is-correct");
- lessonProgress.value = lesson.matched.size / spec.notes.length;
- lessonFeedback.textContent = `${lesson.matched.size} of ${spec.notes.length}`;
- if (lesson.matched.size === spec.notes.length) {
- lessonCaption.textContent = `${spec.title}. You got it!`; lessonFeedback.textContent = "✓ chord complete";
- say(`${spec.title}. You got it!`); lesson.mode = "complete";
- }
- } else {
- const expected = spec.notes[lesson.step];
- if (midi !== expected) { lessonFeedback.textContent = `try ${KEY_FOR_MIDI.get(expected)?.dataset.key} next`; return; }
- KEY_FOR_MIDI.get(midi)?.classList.add("is-correct"); lesson.step += 1;
- lessonProgress.value = lesson.step / spec.notes.length;
- if (lesson.step === spec.notes.length) {
- lessonCaption.textContent = `${spec.title}. You played it!`; lessonFeedback.textContent = "✓ scale complete";
- say(`${spec.title}. You played it!`); lesson.mode = "complete"; showLessonNotes(spec.notes, "is-correct");
- } else {
- lessonCaption.textContent = `${noteName(midi)}. Next: ${noteName(spec.notes[lesson.step])}.`;
- showLessonNotes([spec.notes[lesson.step]]);
- lessonFeedback.textContent = `${lesson.step} of ${spec.notes.length}`;
- }
- }
- };
- const setLearnOpen = (open) => {
- lessonDrawer.classList.toggle("is-open", open); lessonDrawer.setAttribute("aria-hidden", String(!open));
- lessonTab.setAttribute("aria-expanded", String(open)); document.body.classList.toggle("learn-open", open);
- if (!open) { clearLessonTimers(); lesson.mode = "idle"; }
- };
- ROOTS.forEach((root, index) => {
- const button = document.createElement("button"); button.type = "button"; button.textContent = root;
- button.addEventListener("click", () => { lesson.root = index; renderLesson(); }); lessonGrid.append(button);
- });
- document.querySelectorAll(".learn-kinds button").forEach((button) => button.addEventListener("click", () => {
- lesson.kind = button.dataset.kind;
- document.querySelectorAll(".learn-kinds button").forEach((item) => item.setAttribute("aria-selected", String(item === button)));
- renderLesson();
- }));
- lessonTab.addEventListener("click", () => setLearnOpen(!lessonDrawer.classList.contains("is-open")));
- document.getElementById("learn-close").addEventListener("click", () => setLearnOpen(false));
- demoButton.addEventListener("click", () => lesson.mode === "demo" ? renderLesson() : startDemo());
- practiceButton.addEventListener("click", startPractice);
- fetch("/menuband/audio/learn/manifest.json", { cache: "force-cache" }).then((response) => response.ok ? response.json() : null).then((manifest) => {
- manifest?.lessons?.forEach((voice) => lesson.voices.set(voice.id, voice.src));
- }).catch(() => {});
- renderLesson();
-
const label = () => {
const down = keys.filter((key) => key.classList.contains("is-down")).map((key) => key.dataset.key);
const octave = instrument.octave > 0 ? `+${instrument.octave}` : String(instrument.octave);
@@ -1199,7 +885,6 @@
const id = `pointer:${pointerId}:${key.dataset.midi}`;
pointers.set(pointerId, { id, key });
light(key, true);
- hearPracticeNote(Number(key.dataset.midi));
instrument.pressMidi(Number(key.dataset.midi), id).catch(() => { status.textContent = "tap again to wake audio"; });
};
const endPointer = (pointerId) => {
@@ -1228,7 +913,6 @@
if (event.detail !== 0) return; // Pointer gestures already sounded above.
const id = `activate:${key.dataset.midi}`;
light(key, true);
- hearPracticeNote(Number(key.dataset.midi));
instrument.pressMidi(Number(key.dataset.midi), id);
setTimeout(() => { instrument.release(id); light(key, false); }, 220);
}));
@@ -1274,7 +958,6 @@
digitBuffer = "";
heldKeyboard.add(letter);
light(keyByLetter.get(letter), true);
- hearPracticeNote(instrument.midiForKey(letter));
instrument.pressKey(letter).catch(() => { status.textContent = "press again to wake audio"; });
});
addEventListener("keyup", (event) => {
@@ -1475,12 +1158,6 @@
purchaseNote: "The complete instrument. No subscription or in-app unlocks.",
advanced: "advanced",
viewSource: "view source",
- filmsTitle: "Menu Band in motion",
- filmsIntro: "Two tiny films about making music without leaving the menu bar.",
- filmMeet: "Meet Menu Band",
- filmMeetBody: "The menu-bar instrument in under a minute.",
- filmScale: "Sing the scale",
- filmScaleBody: "C D E F G… but weird.",
by: "by",
lede: "Play your Mac instead of opening a workstation. Menu Band puts a two-octave instrument in the menu bar: type notes, connect a MIDI keyboard, tune live radio, or send notes to your DAW. Hundreds of built-in sounds are ready when nothing else is connected.",
trackdrumClaim: "Turn your Mac trackpad into Menu Band’s percussion surface.",
@@ -1514,12 +1191,6 @@
purchaseNote: "El instrumento completo. Sin suscripción ni desbloqueos dentro de la app.",
advanced: "avanzado",
viewSource: "ver código",
- filmsTitle: "Menu Band en movimiento",
- filmsIntro: "Dos minipelículas para hacer música sin salir de la barra de menús.",
- filmMeet: "Conoce Menu Band",
- filmMeetBody: "El instrumento de la barra de menús en menos de un minuto.",
- filmScale: "Canta la escala",
- filmScaleBody: "Do re mi fa sol… pero raro.",
by: "por",
lede: "Toca tu Mac sin abrir una estación de trabajo. Menu Band pone un instrumento de dos octavas en la barra de menús: escribe notas, conecta un teclado MIDI, sintoniza radio en directo o envía notas a tu DAW. Cientos de sonidos integrados están listos cuando no hay nada más conectado.",
trackdrumClaim: "Convierte el trackpad de tu Mac en la superficie de percusión de Menu Band.",