(() => { "use strict"; const OCTAVES = [0, 1, 2, 3, 4, 5, 6, 7, 8]; const SPEED_OF_SOUND_METERS_PER_SECOND = 343; const INTERVALS_FROM_C = [ "Unison", "Minor second", "Major second", "Minor third", "Major third", "Perfect fourth", "Tritone", "Perfect fifth", "Minor sixth", "Major sixth", "Minor seventh", "Major seventh" ]; const NOTE_DEFINITIONS = [ { id: "c", label: "C", pitchClass: 0, family: "Natural", keyColor: "White key", aliases: ["Do"], description: "C is the anchor note for scientific pitch notation, and middle C is the common visual landmark many players use to orient the keyboard.", usage: "In basic theory it often feels like a starting point because C major uses only natural notes." }, { id: "c-sharp-d-flat", label: "C#/Db", pitchClass: 1, family: "Accidental", keyColor: "Black key", aliases: ["C sharp", "D flat"], description: "C sharp and D flat are the same sounding pitch in equal temperament, sitting one semitone above C and one semitone below D.", usage: "It is a common chromatic color tone that adds immediate tension or forward motion." }, { id: "d", label: "D", pitchClass: 2, family: "Natural", keyColor: "White key", aliases: ["Re"], description: "D is a whole step above C and often acts like a clear, open support tone in many scales and melodies.", usage: "In C major it functions as the supertonic, which often points onward rather than feeling final." }, { id: "d-sharp-e-flat", label: "D#/Eb", pitchClass: 3, family: "Accidental", keyColor: "Black key", aliases: ["D sharp", "E flat"], description: "D sharp and E flat are enharmonic spellings of the same pitch class, positioned three semitones above C.", usage: "It is common in minor colors and in flat-oriented keys such as E-flat major." }, { id: "e", label: "E", pitchClass: 4, family: "Natural", keyColor: "White key", aliases: ["Mi"], description: "E is the major third above C, so it strongly helps define the sound of major harmony.", usage: "When E is present over C, the chord immediately feels brighter and more settled." }, { id: "f", label: "F", pitchClass: 5, family: "Natural", keyColor: "White key", aliases: ["Fa"], description: "F is the perfect fourth above C and often feels stable while still wanting to move or resolve in tonal music.", usage: "It frequently appears as a structural tone in melodies and as the root of flat-side key centers." }, { id: "f-sharp-g-flat", label: "F#/Gb", pitchClass: 6, family: "Accidental", keyColor: "Black key", aliases: ["F sharp", "G flat"], description: "F sharp and G flat form the tritone above C, the octave's most evenly divided and tense pitch relationship.", usage: "Because it sits at the midpoint of the octave, it is often used to create instability or dramatic pull." }, { id: "g", label: "G", pitchClass: 7, family: "Natural", keyColor: "White key", aliases: ["Sol"], description: "G is the perfect fifth above C and one of the most stable interval relationships in Western tonal music.", usage: "It reinforces harmony strongly and often feels supportive, open, and structurally important." }, { id: "g-sharp-a-flat", label: "G#/Ab", pitchClass: 8, family: "Accidental", keyColor: "Black key", aliases: ["G sharp", "A flat"], description: "G sharp and A flat are enharmonic spellings used in different key contexts for the same sounding pitch.", usage: "This pitch often acts as a vivid chromatic color that leans upward toward A or downward toward G." }, { id: "a", label: "A", pitchClass: 9, family: "Natural", keyColor: "White key", aliases: ["La"], description: "A is the modern concert-pitch reference note, with A4 standardized at 440 Hz for most contemporary instruments and tuning devices.", usage: "It is the main calibration point for ensembles, tuners, and synthesized reference tones." }, { id: "a-sharp-b-flat", label: "A#/Bb", pitchClass: 10, family: "Accidental", keyColor: "Black key", aliases: ["A sharp", "B flat"], description: "A sharp and B flat are the same equal-tempered pitch, frequently spelled as B flat in ensemble and band-oriented music.", usage: "It shows up often in brass, wind, and flat-key writing, where B-flat-centered harmony is common." }, { id: "b", label: "B", pitchClass: 11, family: "Natural", keyColor: "White key", aliases: ["Ti"], description: "B is the major seventh above C, so it often behaves like a leading tone that wants to resolve upward.", usage: "That strong pull makes it useful for creating expectation and directional movement toward C." } ]; const state = { initialized: false, entries: [], filteredEntries: [], selectedId: "", searchQuery: "", audioContext: null, oscillatorNode: null, gainNode: null, stopTimerId: 0, activePlaybackKey: "" }; function getElements() { return { countEl: document.getElementById("audio-note-count"), listEl: document.getElementById("audio-note-list"), searchEl: document.getElementById("audio-note-search-input"), searchClearEl: document.getElementById("audio-note-search-clear"), detailNameEl: document.getElementById("audio-note-detail-name"), detailSubEl: document.getElementById("audio-note-detail-sub"), detailSummaryEl: document.getElementById("audio-note-detail-summary"), detailBodyEl: document.getElementById("audio-note-detail-body") }; } function normalize(value) { return String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9#]+/g, " ") .replace(/\s+/g, " "); } function getMidiNumber(pitchClass, octave) { return ((octave + 1) * 12) + pitchClass; } function getFrequencyHz(pitchClass, octave) { const midiNumber = getMidiNumber(pitchClass, octave); return 440 * Math.pow(2, (midiNumber - 69) / 12); } function formatFrequencyHz(value) { if (value >= 100) { return `${value.toFixed(2)} Hz`; } if (value >= 10) { return `${value.toFixed(3)} Hz`; } return `${value.toFixed(4)} Hz`; } function formatMeters(value) { if (value >= 1) { return `${value.toFixed(2)} m`; } return `${value.toFixed(3)} m`; } function formatMilliseconds(value) { return `${value.toFixed(2)} ms`; } function formatSignedSemitoneOffset(value) { if (value === 0) { return "Concert pitch reference"; } const distance = Math.abs(value); const direction = value > 0 ? "above" : "below"; return `${distance} semitone${distance === 1 ? "" : "s"} ${direction} A4`; } function buildEntries() { return NOTE_DEFINITIONS.map((definition) => { const referenceOctave = 4; const referenceFrequencyHz = getFrequencyHz(definition.pitchClass, referenceOctave); const referenceMidiNumber = getMidiNumber(definition.pitchClass, referenceOctave); const octaveSeries = OCTAVES.map((octave) => { const frequencyHz = getFrequencyHz(definition.pitchClass, octave); return { octave, label: `${definition.label}${octave}`, frequencyHz, midiNumber: getMidiNumber(definition.pitchClass, octave) }; }); return { ...definition, intervalFromC: INTERVALS_FROM_C[definition.pitchClass], referenceOctave, referenceLabel: `${definition.label}${referenceOctave}`, referenceFrequencyHz, referenceMidiNumber, wavelengthMeters: SPEED_OF_SOUND_METERS_PER_SECOND / referenceFrequencyHz, periodMilliseconds: 1000 / referenceFrequencyHz, semitoneOffsetFromA4: referenceMidiNumber - 69, octaveSeries, searchText: normalize([ definition.label, definition.family, definition.keyColor, definition.description, definition.usage, INTERVALS_FROM_C[definition.pitchClass], ...(definition.aliases || []) ].join(" ")) }; }); } function findEntryById(id) { return state.entries.find((entry) => entry.id === id) || null; } function getAudioContext() { if (state.audioContext) { return state.audioContext; } const AudioContextCtor = window.AudioContext || window.webkitAudioContext; if (typeof AudioContextCtor !== "function") { return null; } state.audioContext = new AudioContextCtor(); return state.audioContext; } function clearStopTimer() { if (state.stopTimerId) { window.clearTimeout(state.stopTimerId); state.stopTimerId = 0; } } function clearActivePlayback(shouldRender = true) { state.oscillatorNode = null; state.gainNode = null; state.activePlaybackKey = ""; clearStopTimer(); if (shouldRender) { render(); } } function stopPlayback(shouldRender = true) { clearStopTimer(); const oscillatorNode = state.oscillatorNode; const gainNode = state.gainNode; const audioContext = state.audioContext; state.oscillatorNode = null; state.gainNode = null; state.activePlaybackKey = ""; if (oscillatorNode && gainNode && audioContext) { const now = audioContext.currentTime; gainNode.gain.cancelScheduledValues(now); gainNode.gain.setValueAtTime(Math.max(gainNode.gain.value, 0.0001), now); gainNode.gain.exponentialRampToValueAtTime(0.0001, now + 0.06); try { oscillatorNode.stop(now + 0.07); } catch (_error) { } } if (shouldRender) { render(); } } async function playFrequency(frequencyHz, playbackKey) { const audioContext = getAudioContext(); if (!audioContext) { return; } if (state.activePlaybackKey === playbackKey) { stopPlayback(true); return; } if (audioContext.state === "suspended") { await audioContext.resume(); } stopPlayback(false); const oscillatorNode = audioContext.createOscillator(); const gainNode = audioContext.createGain(); const now = audioContext.currentTime; oscillatorNode.type = "sine"; oscillatorNode.frequency.setValueAtTime(frequencyHz, now); gainNode.gain.setValueAtTime(0.0001, now); gainNode.gain.exponentialRampToValueAtTime(0.16, now + 0.03); gainNode.gain.exponentialRampToValueAtTime(0.0001, now + 1.1); oscillatorNode.connect(gainNode); gainNode.connect(audioContext.destination); state.oscillatorNode = oscillatorNode; state.gainNode = gainNode; state.activePlaybackKey = playbackKey; oscillatorNode.onended = () => { if (state.oscillatorNode === oscillatorNode || state.activePlaybackKey === playbackKey) { clearActivePlayback(true); } }; oscillatorNode.start(now); oscillatorNode.stop(now + 1.12); state.stopTimerId = window.setTimeout(() => { clearActivePlayback(true); }, 1250); render(); } function createMetaCard(title) { const card = document.createElement("div"); card.className = "planet-meta-card"; const heading = document.createElement("strong"); heading.textContent = title; card.appendChild(heading); return card; } function createFrequencyGrid(entry) { const grid = document.createElement("div"); grid.className = "audio-note-frequency-grid"; entry.octaveSeries.forEach((seriesEntry) => { const playbackKey = `${entry.id}:${seriesEntry.octave}`; const isPlaying = state.activePlaybackKey === playbackKey; const cell = document.createElement("button"); cell.type = "button"; cell.className = `audio-note-frequency-cell audio-note-frequency-cell--button${isPlaying ? " is-playing" : ""}`; cell.dataset.action = "play-frequency"; cell.dataset.playbackKey = playbackKey; cell.dataset.frequencyHz = String(seriesEntry.frequencyHz); cell.setAttribute("aria-pressed", isPlaying ? "true" : "false"); cell.setAttribute("aria-label", `${isPlaying ? "Stop" : "Play"} ${seriesEntry.label} at ${formatFrequencyHz(seriesEntry.frequencyHz)}`); const label = document.createElement("div"); label.className = "audio-note-frequency-label"; label.textContent = seriesEntry.label; const value = document.createElement("div"); value.className = "audio-note-frequency-value"; value.textContent = formatFrequencyHz(seriesEntry.frequencyHz); const meta = document.createElement("div"); meta.className = "audio-note-frequency-meta"; meta.textContent = `MIDI ${seriesEntry.midiNumber}`; const status = document.createElement("div"); status.className = "audio-note-frequency-status"; status.textContent = isPlaying ? "Playing now" : "Tap to hear"; cell.append(label, value, meta, status); grid.appendChild(cell); }); return grid; } function renderList(elements) { if (!elements.listEl) { return; } const fragment = document.createDocumentFragment(); state.filteredEntries.forEach((entry) => { const button = document.createElement("button"); button.type = "button"; button.className = "planet-list-item audio-note-list-item"; button.dataset.noteId = entry.id; button.setAttribute("role", "option"); const isSelected = entry.id === state.selectedId; button.classList.toggle("is-selected", isSelected); button.setAttribute("aria-selected", isSelected ? "true" : "false"); const name = document.createElement("span"); name.className = "planet-list-name"; name.textContent = entry.label; const meta = document.createElement("span"); meta.className = "planet-list-meta"; meta.textContent = `${entry.referenceLabel} = ${formatFrequencyHz(entry.referenceFrequencyHz)} · ${entry.intervalFromC}`; button.append(name, meta); fragment.appendChild(button); }); elements.listEl.replaceChildren(fragment); if (!state.filteredEntries.length) { const empty = document.createElement("div"); empty.className = "planet-text"; empty.style.padding = "16px"; empty.style.color = "#71717a"; empty.textContent = "No notes match your search."; elements.listEl.appendChild(empty); } if (elements.countEl) { elements.countEl.textContent = `${state.filteredEntries.length} notes`; } } function renderDetail(elements) { const entry = findEntryById(state.selectedId); if (!elements.detailNameEl || !elements.detailSubEl || !elements.detailSummaryEl || !elements.detailBodyEl) { return; } elements.detailBodyEl.replaceChildren(); if (!entry) { elements.detailNameEl.textContent = "--"; elements.detailSubEl.textContent = "Select a note to explore"; elements.detailSummaryEl.textContent = "--"; return; } elements.detailNameEl.textContent = entry.label; elements.detailSubEl.textContent = `${entry.family} note · ${entry.intervalFromC}`; elements.detailSummaryEl.textContent = entry.description; const grid = document.createElement("div"); grid.className = "planet-meta-grid"; const profileCard = createMetaCard("Note Profile"); profileCard.innerHTML += `
Reference pitch
${entry.referenceLabel}
Reference frequency
${formatFrequencyHz(entry.referenceFrequencyHz)}
Pitch class
${entry.pitchClass}
Interval above C
${entry.intervalFromC}
Keyboard key
${entry.keyColor}
Relation to A4
${formatSignedSemitoneOffset(entry.semitoneOffsetFromA4)}
Wavelength
${formatMeters(entry.wavelengthMeters)}
Period
${formatMilliseconds(entry.periodMilliseconds)}
`; const aliasRow = document.createElement("div"); aliasRow.className = "audio-note-chip-row"; const familyChip = document.createElement("span"); familyChip.className = `audio-note-chip audio-note-chip--${entry.family.toLowerCase()}`; familyChip.textContent = entry.family; aliasRow.appendChild(familyChip); (entry.aliases || []).forEach((alias) => { const chip = document.createElement("span"); chip.className = "audio-note-chip"; chip.textContent = alias; aliasRow.appendChild(chip); }); profileCard.appendChild(aliasRow); const explanationCard = createMetaCard("Explanation"); const explanation = document.createElement("div"); explanation.className = "audio-note-copy"; explanation.textContent = `${entry.description} ${entry.usage}`; explanationCard.appendChild(explanation); const playbackCard = createMetaCard("Tone Preview"); const playbackCopy = document.createElement("div"); playbackCopy.className = "audio-note-playback-copy"; playbackCopy.textContent = `Play ${entry.referenceLabel} as a quick sine-wave reference, or use any octave button below to hear that exact frequency.`; const playbackActions = document.createElement("div"); playbackActions.className = "audio-note-playback-actions"; const playReferenceButton = document.createElement("button"); playReferenceButton.type = "button"; playReferenceButton.className = "alpha-nav-btn audio-note-playback-btn"; playReferenceButton.dataset.action = "play-frequency"; playReferenceButton.dataset.playbackKey = `${entry.id}:${entry.referenceOctave}`; playReferenceButton.dataset.frequencyHz = String(entry.referenceFrequencyHz); playReferenceButton.textContent = `Play ${entry.referenceLabel}`; const stopButton = document.createElement("button"); stopButton.type = "button"; stopButton.className = "alpha-nav-btn alpha-nav-btn--ghost audio-note-playback-btn"; stopButton.dataset.action = "stop-playback"; stopButton.textContent = "Stop"; playbackActions.append(playReferenceButton, stopButton); playbackCard.append(playbackCopy, playbackActions); const frequencyCard = createMetaCard("Frequencies By Octave"); frequencyCard.appendChild(createFrequencyGrid(entry)); const formulaCard = createMetaCard("Equal-Temperament Rule"); const formula = document.createElement("div"); formula.className = "audio-note-formula"; formula.textContent = "Reference frequencies here use twelve-tone equal temperament with A4 = 440 Hz. Every octave doubles the frequency, and each semitone step multiplies the pitch by the twelfth root of 2."; formulaCard.appendChild(formula); grid.append(profileCard, explanationCard, playbackCard, frequencyCard, formulaCard); elements.detailBodyEl.appendChild(grid); } function render() { const elements = getElements(); renderList(elements); renderDetail(elements); } function applyFilter() { const query = normalize(state.searchQuery); state.filteredEntries = state.entries.filter((entry) => !query || entry.searchText.includes(query)); if (!state.filteredEntries.some((entry) => entry.id === state.selectedId)) { state.selectedId = state.filteredEntries[0]?.id || ""; } render(); const elements = getElements(); if (elements.searchClearEl) { elements.searchClearEl.disabled = !state.searchQuery; } } function selectNote(id) { if (!findEntryById(id)) { return; } stopPlayback(false); state.selectedId = id; render(); } function bindEvents() { const elements = getElements(); if (!elements.listEl || !elements.searchEl || !elements.searchClearEl || !elements.detailBodyEl) { return; } elements.listEl.addEventListener("click", (event) => { const button = event.target instanceof Element ? event.target.closest("button[data-note-id]") : null; if (!button) { return; } selectNote(button.dataset.noteId || ""); }); elements.searchEl.addEventListener("input", () => { state.searchQuery = elements.searchEl.value || ""; applyFilter(); }); elements.searchClearEl.addEventListener("click", () => { state.searchQuery = ""; elements.searchEl.value = ""; applyFilter(); elements.searchEl.focus(); }); elements.detailBodyEl.addEventListener("click", (event) => { const control = event.target instanceof Element ? event.target.closest("button[data-action]") : null; if (!control) { return; } const action = control.dataset.action || ""; if (action === "stop-playback") { stopPlayback(true); return; } if (action === "play-frequency") { const frequencyHz = Number(control.dataset.frequencyHz); const playbackKey = String(control.dataset.playbackKey || "").trim(); if (!Number.isFinite(frequencyHz) || !playbackKey) { return; } void playFrequency(frequencyHz, playbackKey); } }); window.addEventListener("beforeunload", () => { stopPlayback(false); }); } function init() { if (state.initialized) { render(); return; } state.entries = buildEntries(); state.filteredEntries = [...state.entries]; state.selectedId = state.entries[0]?.id || ""; bindEvents(); render(); state.initialized = true; } function ensureAudioNotesSection() { init(); } window.AudioNotesUi = { ...(window.AudioNotesUi || {}), init, ensureAudioNotesSection, selectNote }; })();