Procházet zdrojové kódy

Merge branch 'master' of code.modal.sh:TheoryOfNekomata/piano-notes-book

master
TheoryOfNekomata před 6 měsíci
rodič
revize
07de9a2ca9
4 změnil soubory, kde provedl 540 přidání a 2 odebrání
  1. +8
    -1
      package.json
  2. +184
    -1
      pnpm-lock.yaml
  3. +340
    -0
      src/components/FrequenciesForm/index.tsx
  4. +8
    -0
      src/pages/appendix01-piano-key-frequencies.mdx

+ 8
- 1
package.json Zobrazit soubor

@@ -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"
}
}

+ 184
- 1
pnpm-lock.yaml Zobrazit soubor

@@ -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==}



+ 340
- 0
src/components/FrequenciesForm/index.tsx Zobrazit soubor

@@ -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>
);
};

+ 8
- 0
src/pages/appendix01-piano-key-frequencies.mdx Zobrazit soubor

@@ -0,0 +1,8 @@
---
title: Piano Key Frequencies
layout: ../layouts/Default.astro
---

import { FrequenciesForm } from '../components/FrequenciesForm';

<FrequenciesForm client:load />

Načítá se…
Zrušit
Uložit