// 定义音高对照表的类型,每个八度(number)映射到一个音符(string)和其频率(number)的对象 type FrequencyTable = { [octave: number]: { [note: string]: number; }; }; const noteNames = ["c", "♯c", "d", "♯d", "e", "f", "♯f", "g", "♯g", "a", "♯a", "b"]; /** * 根据给定音名、八度和基准 A4 频率计算音符频率 * @param note - 音名(例如 "C", "C#", "D", ..., "B") * @param octave - 八度,数字(例如 4 表示第 4 八度) * @param baseA4 - A4 的基准频率(Hz),默认为 442 * @returns 计算得到的频率(Hz) */ function getFrequency(note: string, octave: number, baseA4: number = 442): number { const noteIndex = noteNames.indexOf(note); if (noteIndex === -1) { throw new Error("Invalid note: " + note); } // A4 的序号:4*12 + 9 = 57 (因为 A 的索引为 9) const A4Index = 4 * 12 + 9; const currentIndex = octave * 12 + noteIndex; const semitoneOffset = currentIndex - A4Index; return baseA4 * Math.pow(2, semitoneOffset / 12); } /** * 生成一个从 C0 到 B9 的完整音名与频率对照表 * @param baseA4 - A4 的基准频率(Hz),例如 442 或 440 * @returns 一个 FrequencyTable 对象,结构如下: * { * 0: { C: 16.352, "C#": 17.324, ..., B: 30.868 }, * 1: { C: 32.703, "C#": 34.648, ..., B: 61.735 }, * ... * 9: { ... } * } */ export function buildFreqTable(baseA4: number = 442): FrequencyTable { const freqTable: FrequencyTable = {}; // 遍历八度范围 0 ~ 9 for (let octave = 0; octave <= 9; octave++) { freqTable[octave] = {}; noteNames.forEach(note => { const freq = getFrequency(note, octave, baseA4); // 保留3位小数 freqTable[octave][note] = parseFloat(freq.toFixed(3)); }); } return freqTable; } /** * 根据给定频率值和基准 A4 频率,计算该频率对应的音名和八度 * @param freq 待计算的频率(Hz) * @param baseA4 A4 的基准频率(Hz),默认值为 442 * @returns 返回一个对象,包含音名(note)和八度(octave) */ export function frequencyToNote(freq: number, baseA4: number = 442): { note: string; octave: number } { // A4 对应的绝对半音索引:A 在 noteNames 中索引为 9,加上 4 个八度(每个八度 12 个半音),所以 A4Index = 4 * 12 + 9 = 57 const A4Index = 57; // 计算待测频率相对于基准 A4 的半音偏移值(可能是小数) const semitoneOffset = 12 * Math.log2(freq / baseA4); // 四舍五入到最近的半音 const nearestSemitone = Math.round(semitoneOffset); // 计算绝对音高索引:以 C0 为起点 const absoluteIndex = nearestSemitone + A4Index; // 计算所属八度:整数除以 12 const octave = Math.floor(absoluteIndex / 12); // 计算音符在该八度内的索引(0~11) const noteIndex = absoluteIndex % 12; const note = noteNames[noteIndex]; return { note, octave }; } /** * 根据给定频率、基准 A4 频率计算该频率对应的绝对半音索引 * @param freq - 待计算的频率(Hz) * @param baseA4 - A4 的基准频率(Hz),默认为 442Hz * @returns 绝对半音索引(C0 对应 0, B0 对应 11, C1 对应 12,依此类推) */ function frequencyToAbsoluteIndex(freq: number, baseA4: number = 442): number { const A4Index = 57; // A4 在十二平均律中的绝对索引 (4 * 12 + 9) const semitoneOffset = 12 * Math.log2(freq / baseA4); const nearestSemitone = Math.round(semitoneOffset); return A4Index + nearestSemitone; } /** * 根据传入的最低频率和最高频率,计算出对应的 bottomNoteIndex 和 topNoteIndex * @param lowFreq - 最低频率(Hz) * @param highFreq - 最高频率(Hz) * @param baseA4 - A4 的基准频率(Hz),默认为 442Hz * @returns 对象 { bottomNoteIndex, topNoteIndex } * - frequencyToAbsoluteIndex 会将频率转换为一个绝对半音索引。 例如:如果 lowFreq 对应的音符是 C3,那么绝对索引大约为 3 * 12 + 0 = 36; 同理,highFreq 对应的音符可能为 B4,即 4 * 12 + 11 = 59; - 实际数值会因基准频率不同而略有变化。 */ export function calculateNoteRange(lowFreq: number, highFreq: number, baseA4: number = 442): { bottomNoteIndex: number; topNoteIndex: number } { const bottomNoteIndex = frequencyToAbsoluteIndex(lowFreq, baseA4); const topNoteIndex = frequencyToAbsoluteIndex(highFreq, baseA4); return { bottomNoteIndex, topNoteIndex }; } // 计算音符索引 export function getAbsoluteIndex(note: string, octave: number): number { // const noteNames = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]; const noteIndex = noteNames.indexOf(note); return octave * 12 + noteIndex; }