diff --git a/package.json b/package.json index 70dc59d..d4e9299 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,15 @@ "dependencies": { "@astrojs/check": "^0.5.10", "@astrojs/mdx": "^2.2.4", + "@astrojs/react": "^3.3.1", "@astrojs/tailwind": "^5.1.0", + "@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.3.0", + "react-dom": "^18.3.0", "tailwindcss": "^3.4.3", "typescript": "^5.4.4", "verovio": "^4.1.0" @@ -27,6 +33,7 @@ "@types/verovio": "^3.13.4", "archiver": "^7.0.1", "astro-auto-import": "^0.4.2", - "tsx": "^4.7.2" + "tsx": "^4.7.2", + "prop-types": "^15.8.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2c5c570..6e50265 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,15 +11,33 @@ dependencies: '@astrojs/mdx': specifier: ^2.2.4 version: 2.2.4(astro@4.5.16) + '@astrojs/react': + specifier: ^3.3.1 + version: 3.3.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(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.1) + '@types/react': + specifier: ^18.3.0 + version: 18.3.1 + '@types/react-dom': + 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) jsdom: specifier: ^24.0.0 version: 24.0.0 + react: + specifier: ^18.3.0 + version: 18.3.1 + react-dom: + specifier: ^18.3.0 + version: 18.3.1(react@18.3.1) tailwindcss: specifier: ^3.4.3 version: 3.4.3 @@ -49,6 +67,9 @@ devDependencies: astro-auto-import: specifier: ^0.4.2 version: 0.4.2(astro@4.5.16) + prop-types: + specifier: ^15.8.1 + version: 15.8.1 tsx: specifier: ^4.7.2 version: 4.7.2 @@ -178,6 +199,26 @@ packages: dependencies: prismjs: 1.29.0 + /@astrojs/react@3.3.1(@types/react-dom@18.3.0)(@types/react@18.3.1)(react-dom@18.3.1)(react@18.3.1)(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.3.1 + '@types/react-dom': 18.3.0 + '@vitejs/plugin-react': 4.2.1(vite@5.2.8) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + ultrahtml: 1.5.3 + transitivePeerDependencies: + - supports-color + - vite + dev: false + /@astrojs/tailwind@5.1.0(astro@4.5.16)(tailwindcss@3.4.3): resolution: {integrity: sha512-BJoCDKuWhU9FT2qYg+fr6Nfb3qP4ShtyjXGHKA/4mHN94z7BGcmauQK23iy+YH5qWvTnhqkd6mQPQ1yTZTe9Ig==} peerDependencies: @@ -364,6 +405,26 @@ packages: '@babel/core': 7.24.4 '@babel/helper-plugin-utils': 7.24.0 + /@babel/plugin-transform-react-jsx-self@7.24.1(@babel/core@7.24.4): + resolution: {integrity: sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.24.0 + dev: false + + /@babel/plugin-transform-react-jsx-source@7.24.1(@babel/core@7.24.4): + resolution: {integrity: sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + dependencies: + '@babel/core': 7.24.4 + '@babel/helper-plugin-utils': 7.24.0 + dev: false + /@babel/plugin-transform-react-jsx@7.23.4(@babel/core@7.24.4): resolution: {integrity: sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==} engines: {node: '>=6.9.0'} @@ -992,6 +1053,17 @@ packages: /@shikijs/core@1.2.4: resolution: {integrity: sha512-ClaUWpt8oTzjcF0MM1P81AeWyzc1sNSJlAjMG80CbwqbFqXSNz+NpQVUC0icobt3sZn43Sn27M4pHD/Jmp3zHw==} + /@theoryofnekomata/react-musical-keyboard@1.0.13(mem@6.1.1)(react@18.3.1): + resolution: {integrity: sha512-YH3AV7SI5FyiY37QOwZCas8lE9qtUZr8Paso2kAm36JdAUP+GtSETtBXMGRU75dQHjRI2YcIAVlmqxoHPWcEvg==} + engines: {node: '>=10'} + peerDependencies: + mem: ^6.1.0 + react: '>=16' + dependencies: + mem: 6.1.1 + react: 18.3.1 + dev: false + /@types/acorn@4.0.6: resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} dependencies: @@ -1084,6 +1156,23 @@ packages: dependencies: undici-types: 5.26.5 + /@types/prop-types@15.7.12: + resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + dev: false + + /@types/react-dom@18.3.0: + resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} + dependencies: + '@types/react': 18.3.1 + dev: false + + /@types/react@18.3.1: + resolution: {integrity: sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==} + dependencies: + '@types/prop-types': 15.7.12 + csstype: 3.1.3 + dev: false + /@types/readdir-glob@1.1.5: resolution: {integrity: sha512-raiuEPUYqXu+nvtY2Pe8s8FEmZ3x5yAH4VkLdihcPdalvsHltomrRC9BzuStrJ9yk06470hS0Crw0f1pXqD+Hg==} dependencies: @@ -1107,6 +1196,22 @@ packages: /@ungap/structured-clone@1.2.0: resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} + /@vitejs/plugin-react@4.2.1(vite@5.2.8): + resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 + dependencies: + '@babel/core': 7.24.4 + '@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.4) + '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.4) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 5.2.8(@types/node@20.12.7) + transitivePeerDependencies: + - supports-color + dev: false + /@volar/kit@2.1.6(typescript@5.4.4): resolution: {integrity: sha512-dSuXChDGM0nSG/0fxqlNfadjpAeeo1P1SJPBQ+pDf8H1XrqeJq5gIhxRTEbiS+dyNIG69ATq1CArkbCif+oxJw==} peerDependencies: @@ -1751,6 +1856,10 @@ packages: rrweb-cssom: 0.6.0 dev: false + /csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + dev: false + /data-urls@5.0.0: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} @@ -2672,6 +2781,12 @@ packages: /longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + /loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + dependencies: + js-tokens: 4.0.0 + /lru-cache@10.2.0: resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} engines: {node: 14 || >=16.14} @@ -2693,6 +2808,13 @@ packages: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + /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'} @@ -2891,6 +3013,14 @@ packages: dependencies: '@types/mdast': 4.0.3 + /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==} @@ -3232,6 +3362,11 @@ packages: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} + /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'} @@ -3337,7 +3472,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==} @@ -3377,6 +3511,11 @@ packages: string-width: 6.1.0 strip-ansi: 7.1.0 + /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'} @@ -3629,6 +3768,14 @@ packages: kleur: 3.0.3 sisteransi: 1.0.5 + /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==} @@ -3671,6 +3818,32 @@ packages: strip-json-comments: 2.0.1 optional: true + /react-dom@18.3.1(react@18.3.1): + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + dev: false + + /react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + dev: true + + /react-refresh@0.14.2: + resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} + engines: {node: '>=0.10.0'} + dev: false + + /react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + dependencies: + loose-envify: 1.4.0 + dev: false + /read-cache@1.0.0: resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} dependencies: @@ -3922,6 +4095,12 @@ packages: xmlchars: 2.2.0 dev: false + /scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + dependencies: + loose-envify: 1.4.0 + dev: false + /section-matter@1.0.0: resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} engines: {node: '>=4'} @@ -4321,6 +4500,10 @@ packages: engines: {node: '>=14.17'} hasBin: true + /ultrahtml@1.5.3: + resolution: {integrity: sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==} + dev: false + /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} diff --git a/src/components/FrequenciesForm/index.tsx b/src/components/FrequenciesForm/index.tsx new file mode 100644 index 0000000..d5646ea --- /dev/null +++ b/src/components/FrequenciesForm/index.tsx @@ -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(); + const [keyChannels, setKeyChannels] = React.useState([] as { key: number, velocity: number, channel: number, oscillator: OscillatorNode }[]); + const [gain, setGain] = React.useState(); + const [equalDivisionOfTheOctave] = React.useState(12); + + const handleBaseKeyChange: React.ChangeEventHandler = (e) => { + setBaseKey(e.currentTarget.valueAsNumber); + }; + + const handleBaseFrequencyChange: React.ChangeEventHandler = (e) => { + setBaseFrequency(e.currentTarget.valueAsNumber); + }; + + const handleStretchFactorFineChange: React.ChangeEventHandler = (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 = (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 ( +
+ {showForm && ( + <> +
+ + + + +
+
+ + + +
+ + )} + + + + + + + + + + {PITCH_CLASSES.map((c) => ( + + ))} + + + + {new Array(11).fill(0).map((_, octave) => { + return ( + + + {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 ( + + ); + })} + + ); + })} + +
+ MIDI note frequencies and their stretched counterparts + (base frequency={baseFrequency} Hz for key #{baseKey}, factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}) +
+ Octave + + Pitch Class +
+ {c} +
{octave} + {nonStretched.toFixed(2)} +
{' '} + {stretched.toFixed(2)} +
{' '} + + ({difference.toFixed(2)}) + +
+ {false && ( + + + + + + + + + + + + + {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 ( + + + + + + + + ); + })} + +
+ MIDI note frequencies and their stretched counterparts + (base frequency={baseFrequency} Hz for key #{baseKey}, factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}) +
+ MIDI Note Number + + Key + + Frequency +
+ {' '} + (non-stretched, in Hz) +
+ Frequency +
+ {' '} + (stretched, in Hz) +
+ Difference (in Hz) +
+ {i} + + {PITCH_CLASSES[i % PITCH_CLASSES.length]} + {Math.floor(i / PITCH_CLASSES.length)} + + {nonStretched.toFixed(5)} + + {stretched.toFixed(5)} + + {difference.toFixed(5)} +
+ )} +
+ ); +}; diff --git a/src/pages/appendix01-piano-key-frequencies.mdx b/src/pages/appendix01-piano-key-frequencies.mdx new file mode 100644 index 0000000..441b484 --- /dev/null +++ b/src/pages/appendix01-piano-key-frequencies.mdx @@ -0,0 +1,8 @@ +--- +title: Piano Key Frequencies +layout: ../layouts/Default.astro +--- + +import { FrequenciesForm } from '../components/FrequenciesForm'; + +