Initial commit

main
Charlotte Som 2023-03-06 02:11:34 +00:00
commit f39fb3214e
12 changed files with 2350 additions and 0 deletions

9
.editorconfig Normal file
View File

@ -0,0 +1,9 @@
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = crlf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = true

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

36
.gitignore vendored Normal file
View File

@ -0,0 +1,36 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts

3
README.md Normal file
View File

@ -0,0 +1,3 @@
# 12tet.umm.gay
two-way calculator for 12 tone equal temperament values

6
next.config.js Normal file
View File

@ -0,0 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
}
module.exports = nextConfig

22
package.json Normal file
View File

@ -0,0 +1,22 @@
{
"name": "12tet.umm.gay",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@types/node": "18.14.6",
"@types/react": "18.0.28",
"@types/react-dom": "18.0.11",
"eslint": "8.35.0",
"eslint-config-next": "13.2.3",
"next": "13.2.3",
"react": "18.2.0",
"react-dom": "18.2.0",
"typescript": "4.9.5"
}
}

6
pages/_app.tsx Normal file
View File

@ -0,0 +1,6 @@
import "@/styles/globals.css";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return <Component {...pageProps} />;
}

13
pages/_document.tsx Normal file
View File

@ -0,0 +1,13 @@
import { Html, Head, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en">
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
);
}

130
pages/index.tsx Normal file
View File

@ -0,0 +1,130 @@
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>
</>
);
}

50
styles/globals.css Normal file
View File

@ -0,0 +1,50 @@
:root {
font-family: sans-serif;
font-size: 1.25rem;
}
html {
background-color: #19151b;
color: #ffe3fb;
}
html,
body {
margin: 0;
padding: 0;
}
main {
max-width: 960px;
padding: 1em;
margin: 0 auto;
}
header {
margin-bottom: 1em;
}
a {
color: #b57fdc;
text-decoration: none;
border-bottom: 1px solid #b57fdc;
}
input[type="text"] {
padding: 0.25em;
margin: 0.125em;
font-size: 1.125rem;
outline: none;
border: none;
border-radius: 0.25em;
background-color: #ffffff;
border: 2px solid #ffffff;
color: black;
}
input[type="text"]:focus {
border-color: rgb(141, 255, 208);
}

23
tsconfig.json Normal file
View File

@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"paths": {
"@/*": ["./*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}

2049
yarn.lock Normal file

File diff suppressed because it is too large Load Diff