Skip to content
← Public packages

@kentcdodds/pocket-piano

A pocket piano for choir practice: your singing part's range on your phone, installable and offline.

FollowForkOpen app

src/parts.ts

76 lines · 2.8 KB · TypeScript
Raw
// 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 }
})()
`