Initial commit
This commit is contained in:
commit
f39fb3214e
12 changed files with 2350 additions and 0 deletions
9
.editorconfig
Normal file
9
.editorconfig
Normal 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
3
.eslintrc.json
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"extends": "next/core-web-vitals"
|
||||||
|
}
|
36
.gitignore
vendored
Normal file
36
.gitignore
vendored
Normal 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
3
README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# 12tet.umm.gay
|
||||||
|
|
||||||
|
two-way calculator for 12 tone equal temperament values
|
6
next.config.js
Normal file
6
next.config.js
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
22
package.json
Normal file
22
package.json
Normal 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
6
pages/_app.tsx
Normal 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
13
pages/_document.tsx
Normal 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
130
pages/index.tsx
Normal 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
50
styles/globals.css
Normal 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
23
tsconfig.json
Normal 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"]
|
||||||
|
}
|
Loading…
Reference in a new issue