@@ -12,14 +12,15 @@ | |||
"dependencies": { | |||
"@astrojs/check": "^0.5.10", | |||
"@astrojs/mdx": "^2.2.4", | |||
"@astrojs/react": "^3.1.1", | |||
"@astrojs/react": "^3.3.1", | |||
"@astrojs/tailwind": "^5.1.0", | |||
"@types/react": "^18.2.74", | |||
"@types/react-dom": "^18.2.24", | |||
"@theoryofnekomata/react-musical-keyboard": "1.0.13", | |||
"@types/react": "^18.3.0", | |||
"@types/react-dom": "^18.3.0", | |||
"astro": "^4.5.16", | |||
"jsdom": "^24.0.0", | |||
"react": "^18.2.0", | |||
"react-dom": "^18.2.0", | |||
"react": "^18.3.0", | |||
"react-dom": "^18.3.0", | |||
"tailwindcss": "^3.4.3", | |||
"typescript": "^5.4.4", | |||
"verovio": "^4.1.0" | |||
@@ -27,6 +28,7 @@ | |||
"devDependencies": { | |||
"@types/jsdom": "^21.1.6", | |||
"@types/node": "^20.12.7", | |||
"@types/verovio": "^3.13.4" | |||
"@types/verovio": "^3.13.4", | |||
"prop-types": "^15.8.1" | |||
} | |||
} |
@@ -12,17 +12,20 @@ dependencies: | |||
specifier: ^2.2.4 | |||
version: 2.2.4(astro@4.5.16) | |||
'@astrojs/react': | |||
specifier: ^3.1.1 | |||
version: 3.1.1(@types/react-dom@18.2.24)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0)(vite@5.2.8) | |||
specifier: ^3.3.1 | |||
version: 3.3.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0)(vite@5.2.8) | |||
'@astrojs/tailwind': | |||
specifier: ^5.1.0 | |||
version: 5.1.0(astro@4.5.16)(tailwindcss@3.4.3) | |||
'@theoryofnekomata/react-musical-keyboard': | |||
specifier: 1.0.13 | |||
version: 1.0.13(mem@6.1.1)(react@18.3.0) | |||
'@types/react': | |||
specifier: ^18.2.74 | |||
version: 18.2.74 | |||
specifier: ^18.3.0 | |||
version: 18.3.0 | |||
'@types/react-dom': | |||
specifier: ^18.2.24 | |||
version: 18.2.24 | |||
specifier: ^18.3.0 | |||
version: 18.3.0 | |||
astro: | |||
specifier: ^4.5.16 | |||
version: 4.5.16(@types/node@20.12.7)(typescript@5.4.4) | |||
@@ -30,11 +33,11 @@ dependencies: | |||
specifier: ^24.0.0 | |||
version: 24.0.0 | |||
react: | |||
specifier: ^18.2.0 | |||
version: 18.2.0 | |||
specifier: ^18.3.0 | |||
version: 18.3.0 | |||
react-dom: | |||
specifier: ^18.2.0 | |||
version: 18.2.0(react@18.2.0) | |||
specifier: ^18.3.0 | |||
version: 18.3.0(react@18.3.0) | |||
tailwindcss: | |||
specifier: ^3.4.3 | |||
version: 3.4.3 | |||
@@ -55,6 +58,9 @@ devDependencies: | |||
'@types/verovio': | |||
specifier: ^3.13.4 | |||
version: 3.13.4 | |||
prop-types: | |||
specifier: ^15.8.1 | |||
version: 15.8.1 | |||
packages: | |||
@@ -186,20 +192,20 @@ packages: | |||
prismjs: 1.29.0 | |||
dev: false | |||
/@astrojs/react@3.1.1(@types/react-dom@18.2.24)(@types/react@18.2.74)(react-dom@18.2.0)(react@18.2.0)(vite@5.2.8): | |||
resolution: {integrity: sha512-Uc4zY8UxkZrSKmiFGPyy+0uUKGgVETJSra5c/65Z2ZckJEHtgLYW0ZqGUoItGr0wJFMv+h1g3Z4OJGapGgcUyA==} | |||
engines: {node: '>=18.14.1'} | |||
/@astrojs/react@3.3.1(@types/react-dom@18.3.0)(@types/react@18.3.0)(react-dom@18.3.0)(react@18.3.0)(vite@5.2.8): | |||
resolution: {integrity: sha512-dwN6B+C0hQ1jpeQ5EckICpqv76t6RxEslldCP0UQ/ospolNk8GjqL11X/xQSbftiQvGWRMT0Tj5f0Xk17k5qJQ==} | |||
engines: {node: ^18.17.1 || ^20.3.0 || >=21.0.0} | |||
peerDependencies: | |||
'@types/react': ^17.0.50 || ^18.0.21 | |||
'@types/react-dom': ^17.0.17 || ^18.0.6 | |||
react: ^17.0.2 || ^18.0.0 | |||
react-dom: ^17.0.2 || ^18.0.0 | |||
dependencies: | |||
'@types/react': 18.2.74 | |||
'@types/react-dom': 18.2.24 | |||
'@types/react': 18.3.0 | |||
'@types/react-dom': 18.3.0 | |||
'@vitejs/plugin-react': 4.2.1(vite@5.2.8) | |||
react: 18.2.0 | |||
react-dom: 18.2.0(react@18.2.0) | |||
react: 18.3.0 | |||
react-dom: 18.3.0(react@18.3.0) | |||
ultrahtml: 1.5.3 | |||
transitivePeerDependencies: | |||
- supports-color | |||
@@ -1138,6 +1144,17 @@ packages: | |||
resolution: {integrity: sha512-ClaUWpt8oTzjcF0MM1P81AeWyzc1sNSJlAjMG80CbwqbFqXSNz+NpQVUC0icobt3sZn43Sn27M4pHD/Jmp3zHw==} | |||
dev: false | |||
/@theoryofnekomata/react-musical-keyboard@1.0.13(mem@6.1.1)(react@18.3.0): | |||
resolution: {integrity: sha512-YH3AV7SI5FyiY37QOwZCas8lE9qtUZr8Paso2kAm36JdAUP+GtSETtBXMGRU75dQHjRI2YcIAVlmqxoHPWcEvg==} | |||
engines: {node: '>=10'} | |||
peerDependencies: | |||
mem: ^6.1.0 | |||
react: '>=16' | |||
dependencies: | |||
mem: 6.1.1 | |||
react: 18.3.0 | |||
dev: false | |||
/@types/acorn@4.0.6: | |||
resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} | |||
dependencies: | |||
@@ -1232,14 +1249,14 @@ packages: | |||
resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} | |||
dev: false | |||
/@types/react-dom@18.2.24: | |||
resolution: {integrity: sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg==} | |||
/@types/react-dom@18.3.0: | |||
resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} | |||
dependencies: | |||
'@types/react': 18.2.74 | |||
'@types/react': 18.3.0 | |||
dev: false | |||
/@types/react@18.2.74: | |||
resolution: {integrity: sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw==} | |||
/@types/react@18.3.0: | |||
resolution: {integrity: sha512-DiUcKjzE6soLyln8NNZmyhcQjVv+WsUIFSqetMN0p8927OztKT4VTfFTqsbAi5oAGIcgOmOajlfBqyptDDjZRw==} | |||
dependencies: | |||
'@types/prop-types': 15.7.12 | |||
csstype: 3.1.3 | |||
@@ -2764,7 +2781,6 @@ packages: | |||
/js-tokens@4.0.0: | |||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} | |||
dev: false | |||
/js-yaml@3.14.1: | |||
resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} | |||
@@ -2903,7 +2919,6 @@ packages: | |||
hasBin: true | |||
dependencies: | |||
js-tokens: 4.0.0 | |||
dev: false | |||
/lru-cache@10.2.0: | |||
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} | |||
@@ -2930,6 +2945,13 @@ packages: | |||
'@jridgewell/sourcemap-codec': 1.4.15 | |||
dev: false | |||
/map-age-cleaner@0.1.3: | |||
resolution: {integrity: sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==} | |||
engines: {node: '>=6'} | |||
dependencies: | |||
p-defer: 1.0.0 | |||
dev: false | |||
/markdown-extensions@2.0.0: | |||
resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} | |||
engines: {node: '>=16'} | |||
@@ -3142,6 +3164,14 @@ packages: | |||
'@types/mdast': 4.0.3 | |||
dev: false | |||
/mem@6.1.1: | |||
resolution: {integrity: sha512-Ci6bIfq/UgcxPTYa8dQQ5FY3BzKkT894bwXWXxC/zqs0XgMO2cT20CGkOqda7gZNkmK5VP4x89IGZ6K7hfbn3Q==} | |||
engines: {node: '>=8'} | |||
dependencies: | |||
map-age-cleaner: 0.1.3 | |||
mimic-fn: 3.1.0 | |||
dev: false | |||
/merge-stream@2.0.0: | |||
resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} | |||
dev: false | |||
@@ -3516,6 +3546,11 @@ packages: | |||
engines: {node: '>=6'} | |||
dev: false | |||
/mimic-fn@3.1.0: | |||
resolution: {integrity: sha512-Ysbi9uYW9hFyfrThdDEQuykN4Ey6BuwPD2kpI5ES/nFTDn/98yxYNLZJcgUAKPT/mcrLLKaGzJR9YVxJrIdASQ==} | |||
engines: {node: '>=8'} | |||
dev: false | |||
/mimic-fn@4.0.0: | |||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==} | |||
engines: {node: '>=12'} | |||
@@ -3629,7 +3664,6 @@ packages: | |||
/object-assign@4.1.1: | |||
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} | |||
engines: {node: '>=0.10.0'} | |||
dev: false | |||
/object-hash@3.0.0: | |||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} | |||
@@ -3673,6 +3707,11 @@ packages: | |||
strip-ansi: 7.1.0 | |||
dev: false | |||
/p-defer@1.0.0: | |||
resolution: {integrity: sha512-wB3wfAxZpk2AzOfUMJNL+d36xothRSyj8EXOa4f6GMqYDN9BJaaSISbsk+wS9abmnebVw95C2Kb5t85UmpCxuw==} | |||
engines: {node: '>=4'} | |||
dev: false | |||
/p-limit@2.3.0: | |||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} | |||
engines: {node: '>=6'} | |||
@@ -3940,6 +3979,14 @@ packages: | |||
sisteransi: 1.0.5 | |||
dev: false | |||
/prop-types@15.8.1: | |||
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} | |||
dependencies: | |||
loose-envify: 1.4.0 | |||
object-assign: 4.1.1 | |||
react-is: 16.13.1 | |||
dev: true | |||
/property-information@6.5.0: | |||
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} | |||
dev: false | |||
@@ -3988,23 +4035,27 @@ packages: | |||
dev: false | |||
optional: true | |||
/react-dom@18.2.0(react@18.2.0): | |||
resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} | |||
/react-dom@18.3.0(react@18.3.0): | |||
resolution: {integrity: sha512-zaKdLBftQJnvb7FtDIpZtsAIb2MZU087RM8bRDZU8LVCCFYjPTsDZJNFUWPcVz3HFSN1n/caxi0ca4B/aaVQGQ==} | |||
peerDependencies: | |||
react: ^18.2.0 | |||
react: ^18.3.0 | |||
dependencies: | |||
loose-envify: 1.4.0 | |||
react: 18.2.0 | |||
scheduler: 0.23.0 | |||
react: 18.3.0 | |||
scheduler: 0.23.1 | |||
dev: false | |||
/react-is@16.13.1: | |||
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} | |||
dev: true | |||
/react-refresh@0.14.0: | |||
resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} | |||
engines: {node: '>=0.10.0'} | |||
dev: false | |||
/react@18.2.0: | |||
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} | |||
/react@18.3.0: | |||
resolution: {integrity: sha512-RPutkJftSAldDibyrjuku7q11d3oy6wKOyPe5K1HA/HwwrXcEqBdHsLypkC2FFYjP7bPUa6gbzSBhw4sY2JcDg==} | |||
engines: {node: '>=0.10.0'} | |||
dependencies: | |||
loose-envify: 1.4.0 | |||
@@ -4245,8 +4296,8 @@ packages: | |||
xmlchars: 2.2.0 | |||
dev: false | |||
/scheduler@0.23.0: | |||
resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} | |||
/scheduler@0.23.1: | |||
resolution: {integrity: sha512-5GKS5JGfiah1O38Vfa9srZE4s3wdHbwjlCrvIookrg2FO9aIwKLOJXuJQFlEfNcVSOXuaL2hzDeY20uVXcUtrw==} | |||
dependencies: | |||
loose-envify: 1.4.0 | |||
dev: false | |||
@@ -0,0 +1,340 @@ | |||
import * as React from 'react'; | |||
import MusicalKeyboard, { StyledAccidentalKey, StyledNaturalKey, KeyboardMap } from '@theoryofnekomata/react-musical-keyboard'; | |||
const useFrequenciesForm = () => { | |||
const [baseFrequency, setBaseFrequency] = React.useState(440); | |||
const [baseKey, setBaseKey] = React.useState(69); | |||
const [stretchFactorNumerator, setStretchFactorNumerator] = React.useState(12.125); | |||
const [audioContext, setAudioContext] = React.useState<AudioContext>(); | |||
const [keyChannels, setKeyChannels] = React.useState([] as { key: number, velocity: number, channel: number, oscillator: OscillatorNode }[]); | |||
const [gain, setGain] = React.useState<GainNode>(); | |||
const [equalDivisionOfTheOctave] = React.useState(12); | |||
const handleBaseKeyChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => { | |||
setBaseKey(e.currentTarget.valueAsNumber); | |||
}; | |||
const handleBaseFrequencyChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => { | |||
setBaseFrequency(e.currentTarget.valueAsNumber); | |||
}; | |||
const handleStretchFactorFineChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => { | |||
const { form, valueAsNumber } = e.currentTarget; | |||
setStretchFactorNumerator(valueAsNumber); | |||
if (!form) { | |||
return; | |||
} | |||
const coarse = form.elements.namedItem('stretchFactorNumeratorCoarse'); | |||
if (!coarse) { | |||
return; | |||
} | |||
if (!('value' in coarse)) { | |||
return; | |||
} | |||
coarse.value = valueAsNumber.toString(); | |||
}; | |||
const handleStretchFactorCoarseChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => { | |||
const { form, valueAsNumber } = e.currentTarget; | |||
setStretchFactorNumerator(valueAsNumber); | |||
if (!form) { | |||
return; | |||
} | |||
const fine = form.elements.namedItem('stretchFactorNumeratorFine'); | |||
if (!fine) { | |||
return; | |||
} | |||
if (!('value' in fine)) { | |||
return; | |||
} | |||
fine.value = valueAsNumber.toString(); | |||
}; | |||
React.useEffect(() => { | |||
const audioContext = new AudioContext(); | |||
setAudioContext(audioContext); | |||
const gainNode = audioContext.createGain(); | |||
gainNode?.gain.setValueAtTime(0.05, audioContext.currentTime); | |||
setGain(gainNode); | |||
gainNode.connect(audioContext.destination); | |||
}, []); | |||
const playSound = (keys: { velocity: number, channel: number, key: number }[]) => { | |||
if (!audioContext) { | |||
return; | |||
} | |||
if (!gain) { | |||
return; | |||
} | |||
setKeyChannels((oldOscillators) => { | |||
const activeKeys = keys.map(k => `${k.channel}:${k.key}`); | |||
const oscillatorsToCancel = oldOscillators.filter((k) => !activeKeys.includes(`${k.channel}:${k.key}`)); | |||
for (let i = 0; i < oscillatorsToCancel.length; i += 1) { | |||
oldOscillators[i]?.oscillator?.stop(); | |||
oldOscillators[i]?.oscillator?.disconnect(); | |||
} | |||
return keys.map((k) => { | |||
const existingOscillator = oldOscillators.find((o) => o.key === k.key && o.channel === k.channel); | |||
if (existingOscillator) { | |||
return existingOscillator; | |||
} | |||
const oscillator = audioContext.createOscillator(); | |||
const f = ( | |||
baseFrequency * ( | |||
2 ** ( | |||
1 / equalDivisionOfTheOctave | |||
) | |||
) ** ( | |||
( | |||
k.key - baseKey | |||
) * ( | |||
stretchFactorNumerator / equalDivisionOfTheOctave | |||
) | |||
) | |||
); | |||
oscillator?.frequency.setValueAtTime(f, audioContext.currentTime); | |||
oscillator.type = 'square'; | |||
oscillator.connect(gain); | |||
oscillator.start(); | |||
return { | |||
...k, | |||
oscillator, | |||
}; | |||
}); | |||
}); | |||
}; | |||
return { | |||
baseFrequency, | |||
baseKey, | |||
stretchFactorNumerator, | |||
handleBaseFrequencyChange, | |||
handleBaseKeyChange, | |||
handleStretchFactorCoarseChange, | |||
handleStretchFactorFineChange, | |||
equalDivisionOfTheOctave, | |||
playSound, | |||
keyChannels, | |||
}; | |||
}; | |||
const PITCH_CLASSES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] as const; | |||
export const FrequenciesForm = () => { | |||
const [showForm, setShowForm] = React.useState(false); | |||
const { | |||
handleStretchFactorCoarseChange, | |||
handleStretchFactorFineChange, | |||
baseKey, | |||
baseFrequency, | |||
handleBaseFrequencyChange, | |||
handleBaseKeyChange, | |||
stretchFactorNumerator, | |||
equalDivisionOfTheOctave, | |||
playSound, | |||
keyChannels, | |||
} = useFrequenciesForm(); | |||
React.useEffect(() => { | |||
setShowForm(true); | |||
}, []); | |||
React.useEffect(() => { | |||
}, []); | |||
return ( | |||
<div> | |||
{showForm && ( | |||
<> | |||
<form> | |||
<input | |||
type="number" | |||
defaultValue={baseFrequency} | |||
name="baseFrequency" | |||
onChange={handleBaseFrequencyChange} | |||
/> | |||
<input | |||
type="number" | |||
defaultValue={baseKey} | |||
name="baseKey" | |||
onChange={handleBaseKeyChange} | |||
/> | |||
<input | |||
type="range" | |||
min={equalDivisionOfTheOctave * 0.95} | |||
max={equalDivisionOfTheOctave * 1.05} | |||
defaultValue={stretchFactorNumerator} | |||
name="stretchFactorNumeratorFine" | |||
onChange={handleStretchFactorFineChange} | |||
step="any" | |||
/> | |||
<input | |||
type="number" | |||
min={equalDivisionOfTheOctave * 0.95} | |||
max={equalDivisionOfTheOctave * 1.05} | |||
defaultValue={stretchFactorNumerator} | |||
name="stretchFactorNumeratorCoarse" | |||
onChange={handleStretchFactorCoarseChange} | |||
step="any" | |||
/> | |||
</form> | |||
<div style={{ position: 'relative', backgroundColor: 'black', }}> | |||
<MusicalKeyboard | |||
hasMap | |||
startKey={0} | |||
endKey={127} | |||
height={50} | |||
keyComponents={{ | |||
accidental: StyledAccidentalKey, | |||
natural: StyledNaturalKey, | |||
}} | |||
keyChannels={keyChannels} | |||
> | |||
<KeyboardMap | |||
channel={0} | |||
onChange={playSound} | |||
/> | |||
</MusicalKeyboard> | |||
</div> | |||
</> | |||
)} | |||
<table | |||
style={{ | |||
fontSize: '0.875em', | |||
}} | |||
> | |||
<caption> | |||
MIDI note frequencies and their stretched counterparts | |||
(base frequency={baseFrequency} Hz for key #{baseKey}, factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}) | |||
</caption> | |||
<thead> | |||
<tr> | |||
<th rowSpan={2}> | |||
Octave | |||
</th> | |||
<th colSpan={12}> | |||
Pitch Class | |||
</th> | |||
</tr> | |||
<tr> | |||
{PITCH_CLASSES.map((c) => ( | |||
<th | |||
key={c} | |||
style={{ textAlign: 'right' }} | |||
> | |||
{c} | |||
</th> | |||
))} | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{new Array(11).fill(0).map((_, octave) => { | |||
return ( | |||
<tr> | |||
<th>{octave}</th> | |||
{PITCH_CLASSES.map((_, pitchClassIndex) => { | |||
const i = (octave * PITCH_CLASSES.length) + pitchClassIndex; | |||
const nonStretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** (i - baseKey)); | |||
const stretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** ((i - baseKey) * (stretchFactorNumerator / equalDivisionOfTheOctave))); | |||
const difference = (stretched - nonStretched); | |||
return ( | |||
<td | |||
style={{ textAlign: 'right' }} | |||
> | |||
{nonStretched.toFixed(2)} | |||
<br />{' '} | |||
{stretched.toFixed(2)} | |||
<br />{' '} | |||
<small> | |||
({difference.toFixed(2)}) | |||
</small> | |||
</td> | |||
); | |||
})} | |||
</tr> | |||
); | |||
})} | |||
</tbody> | |||
</table> | |||
{false && ( | |||
<table> | |||
<caption> | |||
MIDI note frequencies and their stretched counterparts | |||
(base frequency={baseFrequency} Hz for key #{baseKey}, factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}) | |||
</caption> | |||
<thead> | |||
<tr> | |||
<th> | |||
MIDI Note Number | |||
</th> | |||
<th> | |||
Key | |||
</th> | |||
<th> | |||
Frequency | |||
<br /> | |||
{' '} | |||
(non-stretched, in Hz) | |||
</th> | |||
<th> | |||
Frequency | |||
<br /> | |||
{' '} | |||
(stretched, in Hz) | |||
</th> | |||
<th> | |||
Difference (in Hz) | |||
</th> | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{new Array(128).fill(0).map((_, i) => { | |||
const nonStretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** (i - baseKey)); | |||
const stretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** ((i - baseKey) * (stretchFactorNumerator / equalDivisionOfTheOctave))); | |||
const difference = (stretched - nonStretched); | |||
return ( | |||
<tr key={i}> | |||
<th> | |||
{i} | |||
</th> | |||
<th> | |||
{PITCH_CLASSES[i % PITCH_CLASSES.length]} | |||
{Math.floor(i / PITCH_CLASSES.length)} | |||
</th> | |||
<td style={{ textAlign: 'right' }}> | |||
{nonStretched.toFixed(5)} | |||
</td> | |||
<td style={{ textAlign: 'right' }}> | |||
{stretched.toFixed(5)} | |||
</td> | |||
<td style={{ textAlign: 'right' }}> | |||
{difference.toFixed(5)} | |||
</td> | |||
</tr> | |||
); | |||
})} | |||
</tbody> | |||
</table> | |||
)} | |||
</div> | |||
); | |||
}; |
@@ -0,0 +1,8 @@ | |||
--- | |||
title: Piano Key Frequencies | |||
layout: ../layouts/Default.astro | |||
--- | |||
import { FrequenciesForm } from '../components/FrequenciesForm'; | |||
<FrequenciesForm client:load /> |