← Public packages
@kentcdodds/pocket-piano
A pocket piano for choir practice: your singing part's range on your phone, installable and offline.
src/parts.ts
76 lines · 2.8 KB · TypeScript// Singing parts. The selected part sets the highlighted range on the keyboard and where
// the keyboard opens. Chosen on the About page, stored in this browser's localStorage.
export type SingingPart = {
id: string
label: string
low: number // MIDI note number
high: number // MIDI note number
lowName: string
highName: string
}
export const PARTS: SingingPart[] = [
{ id: 'soprano', label: 'Soprano', low: 60, high: 81, lowName: 'C4', highName: 'A5' },
{ id: 'alto', label: 'Alto', low: 53, high: 74, lowName: 'F3', highName: 'D5' },
{ id: 'tenor', label: 'Tenor', low: 48, high: 69, lowName: 'C3', highName: 'A4' },
{ id: 'baritone', label: 'Baritone', low: 45, high: 65, lowName: 'A2', highName: 'F4' },
{ id: 'bass', label: 'Bass', low: 40, high: 64, lowName: 'E2', highName: 'E4' },
]
export const DEFAULT_PART_ID = 'tenor'
/** localStorage key. Same `pocket-piano:` prefix and JSON encoding as the sustain setting. */
export const PART_STORAGE_KEY = 'pocket-piano:part'
// Browser helpers shared by the piano and About pages (inlined before their scripts).
// Plain JS in a raw string: no backticks or template placeholders inside.
export const PARTS_JS =
'window.pocketPianoParts = (function () {\n' +
"\t'use strict'\n" +
'\tvar PARTS = ' +
JSON.stringify(PARTS) +
'\n\tvar DEFAULT_ID = ' +
JSON.stringify(DEFAULT_PART_ID) +
'\n\tvar KEY = ' +
JSON.stringify(PART_STORAGE_KEY) +
'\n' +
String.raw`
function byId(id) {
for (var i = 0; i < PARTS.length; i++) if (PARTS[i].id === id) return PARTS[i]
return null
}
function read() {
var id = null
try {
var raw = localStorage.getItem(KEY)
id = raw === null ? null : JSON.parse(raw)
} catch (e) {}
return byId(id) || byId(DEFAULT_ID)
}
function write(id) {
var part = byId(id)
if (!part) return read()
try { localStorage.setItem(KEY, JSON.stringify(part.id)) } catch (e) {}
return part
}
// Where the keyboard opens for a part: pos is the white-key index at the left edge,
// span is how many white keys are visible. Prefer a window that starts on a C and still
// shows the whole part (closest to centered), otherwise center the part.
function homePos(whites, part, span) {
var maxPos = Math.max(0, whites.length - span)
var lo = whites.indexOf(part.low)
var hi = whites.indexOf(part.high)
if (lo < 0) lo = whites.indexOf(part.low - 1)
if (hi < 0) hi = whites.indexOf(part.high + 1)
var centered = Math.round((lo + hi + 1) / 2 - span / 2)
var best = null
for (var p = Math.max(0, hi - span + 1); p <= Math.min(lo, maxPos); p++) {
if (whites[p] % 12 !== 0) continue
if (best === null || Math.abs(p - centered) < Math.abs(best - centered)) best = p
}
return Math.min(maxPos, Math.max(0, best === null ? centered : best))
}
return { list: PARTS, defaultId: DEFAULT_ID, key: KEY, byId: byId, read: read, write: write, homePos: homePos }
})()
`