131 lines
3.4 KiB
TypeScript
131 lines
3.4 KiB
TypeScript
|
import Head from "next/head";
|
||
|
import { ChangeEventHandler, useState } from "react";
|
||
|
|
||
|
const calculateFrequency = (root: number, noteDescriptor: string) => {
|
||
|
const match = noteDescriptor.match(
|
||
|
/([a-gA-G])\s*(#?)(b?)\s*(-?\d+)([+-]\d+)?/
|
||
|
);
|
||
|
if (match == null) return undefined;
|
||
|
|
||
|
const [value, note, sharp, flat, octave, cents] = match;
|
||
|
const dists: { [key: string]: number | undefined } = {
|
||
|
C: -9,
|
||
|
D: -7,
|
||
|
E: -5,
|
||
|
F: -4,
|
||
|
G: -2,
|
||
|
A: 0,
|
||
|
B: 2,
|
||
|
};
|
||
|
let distanceFromA = dists[note.toUpperCase()];
|
||
|
if (distanceFromA == null) return;
|
||
|
if (sharp) distanceFromA += 1;
|
||
|
if (flat) distanceFromA -= 1;
|
||
|
if (cents) distanceFromA += parseInt(cents) / 100;
|
||
|
|
||
|
let octaveDistance = parseInt(octave) - 4;
|
||
|
return root * Math.pow(2, octaveDistance + distanceFromA / 12);
|
||
|
};
|
||
|
|
||
|
const calculateNote = (root: number, frequency: number) => {
|
||
|
const distanceFromA = 12 * Math.log2(frequency / root);
|
||
|
const semis = Math.round(distanceFromA);
|
||
|
const cents = Math.round((distanceFromA - semis) * 100);
|
||
|
let octave = 4 + Math.floor(semis / 12);
|
||
|
if (semis % 12 >= 3) octave += 1;
|
||
|
const notes = [
|
||
|
"A",
|
||
|
"A#",
|
||
|
"B",
|
||
|
"C",
|
||
|
"C#",
|
||
|
"D",
|
||
|
"D#",
|
||
|
"E",
|
||
|
"F",
|
||
|
"F#",
|
||
|
"G",
|
||
|
"G#",
|
||
|
];
|
||
|
const note = notes[semis % 12];
|
||
|
|
||
|
let centsStr = "";
|
||
|
if (cents <= -1) {
|
||
|
centsStr = cents.toString().padStart(2, "0");
|
||
|
} else if (cents >= 1) {
|
||
|
centsStr = "+" + cents.toString().padStart(2, "0");
|
||
|
}
|
||
|
|
||
|
return `${note}${octave}${centsStr}`;
|
||
|
};
|
||
|
|
||
|
export default function Main() {
|
||
|
const [root, setRoot] = useState(440);
|
||
|
const [note, setNote] = useState("C5");
|
||
|
const [frequency, setFrequency] = useState("523.251");
|
||
|
|
||
|
const setRootValue: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||
|
const newRoot = parseInt(e.target.value);
|
||
|
if (!isNaN(newRoot)) setRoot(newRoot);
|
||
|
};
|
||
|
|
||
|
const setNoteValue: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||
|
setNote(e.target.value);
|
||
|
const frequency = calculateFrequency(root, e.target.value);
|
||
|
if (frequency != null) setFrequency(frequency.toFixed(3));
|
||
|
};
|
||
|
|
||
|
const setFrequencyValue: ChangeEventHandler<HTMLInputElement> = (e) => {
|
||
|
setFrequency(e.target.value);
|
||
|
const newFrequency = parseFloat(e.target.value);
|
||
|
if (!isNaN(newFrequency)) {
|
||
|
const noteDescriptor = calculateNote(root, newFrequency);
|
||
|
setNote(noteDescriptor);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<>
|
||
|
<Head>
|
||
|
<title>12 tone equal temperament :)</title>
|
||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
|
</Head>
|
||
|
<main>
|
||
|
<header>
|
||
|
(by <a href="https://som.codes/">charlotte som</a>)
|
||
|
</header>
|
||
|
|
||
|
<section>
|
||
|
{"if A4 is "}
|
||
|
<input
|
||
|
type="text"
|
||
|
pattern="\d+"
|
||
|
value={root}
|
||
|
placeholder="440"
|
||
|
onChange={setRootValue}
|
||
|
/>
|
||
|
{" Hz"}, then
|
||
|
</section>
|
||
|
<section>
|
||
|
<input
|
||
|
type="text"
|
||
|
pattern=""
|
||
|
value={note}
|
||
|
placeholder="C5"
|
||
|
onChange={setNoteValue}
|
||
|
/>
|
||
|
{" is "}
|
||
|
<input
|
||
|
type="text"
|
||
|
pattern="\d+"
|
||
|
value={frequency}
|
||
|
placeholder="523.251"
|
||
|
onChange={setFrequencyValue}
|
||
|
/>
|
||
|
{" Hz"}
|
||
|
</section>
|
||
|
</main>
|
||
|
</>
|
||
|
);
|
||
|
}
|