Piano notes book, powered by Astro and React.
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.
 
 

345 lignes
8.8 KiB

  1. import * as React from 'react';
  2. import MusicalKeyboard, { StyledAccidentalKey, StyledNaturalKey, KeyboardMap } from '@theoryofnekomata/react-musical-keyboard';
  3. const useFrequenciesForm = () => {
  4. const [baseFrequency, setBaseFrequency] = React.useState(440);
  5. const [baseKey, setBaseKey] = React.useState(69);
  6. const [stretchFactorNumerator, setStretchFactorNumerator] = React.useState(12.125);
  7. const [audioContext, setAudioContext] = React.useState<AudioContext>();
  8. const [keyChannels, setKeyChannels] = React.useState([] as { key: number, velocity: number, channel: number, oscillator: OscillatorNode }[]);
  9. const [gain, setGain] = React.useState<GainNode>();
  10. const [equalDivisionOfTheOctave] = React.useState(12);
  11. const handleBaseKeyChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
  12. setBaseKey(e.currentTarget.valueAsNumber);
  13. };
  14. const handleBaseFrequencyChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
  15. setBaseFrequency(e.currentTarget.valueAsNumber);
  16. };
  17. const handleStretchFactorFineChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
  18. const { form, valueAsNumber } = e.currentTarget;
  19. setStretchFactorNumerator(valueAsNumber);
  20. if (!form) {
  21. return;
  22. }
  23. const coarse = form.elements.namedItem('stretchFactorNumeratorCoarse');
  24. if (!coarse) {
  25. return;
  26. }
  27. if (!('value' in coarse)) {
  28. return;
  29. }
  30. coarse.value = valueAsNumber.toString();
  31. };
  32. const handleStretchFactorCoarseChange: React.ChangeEventHandler<HTMLElementTagNameMap['input']> = (e) => {
  33. const { form, valueAsNumber } = e.currentTarget;
  34. setStretchFactorNumerator(valueAsNumber);
  35. if (!form) {
  36. return;
  37. }
  38. const fine = form.elements.namedItem('stretchFactorNumeratorFine');
  39. if (!fine) {
  40. return;
  41. }
  42. if (!('value' in fine)) {
  43. return;
  44. }
  45. fine.value = valueAsNumber.toString();
  46. };
  47. React.useEffect(() => {
  48. const audioContext = new AudioContext();
  49. setAudioContext(audioContext);
  50. const gainNode = audioContext.createGain();
  51. gainNode?.gain.setValueAtTime(0.05, audioContext.currentTime);
  52. setGain(gainNode);
  53. gainNode.connect(audioContext.destination);
  54. }, []);
  55. const playSound = (keys: { velocity: number, channel: number, key: number }[]) => {
  56. if (!audioContext) {
  57. return;
  58. }
  59. if (!gain) {
  60. return;
  61. }
  62. setKeyChannels((oldOscillators) => {
  63. const activeKeys = keys.map(k => `${k.channel}:${k.key}`);
  64. const oscillatorsToCancel = oldOscillators.filter((k) => !activeKeys.includes(`${k.channel}:${k.key}`));
  65. for (let i = 0; i < oscillatorsToCancel.length; i += 1) {
  66. oldOscillators[i]?.oscillator?.stop();
  67. oldOscillators[i]?.oscillator?.disconnect();
  68. }
  69. return keys.map((k) => {
  70. const existingOscillator = oldOscillators.find((o) => o.key === k.key && o.channel === k.channel);
  71. if (existingOscillator) {
  72. return existingOscillator;
  73. }
  74. const oscillator = audioContext.createOscillator();
  75. const f = (
  76. baseFrequency * (
  77. 2 ** (
  78. 1 / equalDivisionOfTheOctave
  79. )
  80. ) ** (
  81. (
  82. k.key - baseKey
  83. ) * (
  84. stretchFactorNumerator / equalDivisionOfTheOctave
  85. )
  86. )
  87. );
  88. oscillator?.frequency.setValueAtTime(f, audioContext.currentTime);
  89. oscillator.type = 'square';
  90. oscillator.connect(gain);
  91. oscillator.start();
  92. return {
  93. ...k,
  94. oscillator,
  95. };
  96. });
  97. });
  98. };
  99. return {
  100. baseFrequency,
  101. baseKey,
  102. stretchFactorNumerator,
  103. handleBaseFrequencyChange,
  104. handleBaseKeyChange,
  105. handleStretchFactorCoarseChange,
  106. handleStretchFactorFineChange,
  107. equalDivisionOfTheOctave,
  108. playSound,
  109. keyChannels,
  110. };
  111. };
  112. const PITCH_CLASSES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'] as const;
  113. export const FrequenciesForm = () => {
  114. const [showForm, setShowForm] = React.useState(false);
  115. const {
  116. handleStretchFactorCoarseChange,
  117. handleStretchFactorFineChange,
  118. baseKey,
  119. baseFrequency,
  120. handleBaseFrequencyChange,
  121. handleBaseKeyChange,
  122. stretchFactorNumerator,
  123. equalDivisionOfTheOctave,
  124. playSound,
  125. keyChannels,
  126. } = useFrequenciesForm();
  127. React.useEffect(() => {
  128. setShowForm(true);
  129. }, []);
  130. React.useEffect(() => {
  131. }, []);
  132. return (
  133. <div>
  134. {showForm && (
  135. <div className="print-hidden">
  136. <form>
  137. <input
  138. type="number"
  139. defaultValue={baseFrequency}
  140. name="baseFrequency"
  141. onChange={handleBaseFrequencyChange}
  142. />
  143. <input
  144. type="number"
  145. defaultValue={baseKey}
  146. name="baseKey"
  147. onChange={handleBaseKeyChange}
  148. />
  149. <input
  150. type="range"
  151. min={equalDivisionOfTheOctave * 0.95}
  152. max={equalDivisionOfTheOctave * 1.05}
  153. defaultValue={stretchFactorNumerator}
  154. name="stretchFactorNumeratorFine"
  155. onChange={handleStretchFactorFineChange}
  156. step="any"
  157. />
  158. <input
  159. type="number"
  160. min={equalDivisionOfTheOctave * 0.95}
  161. max={equalDivisionOfTheOctave * 1.05}
  162. defaultValue={stretchFactorNumerator}
  163. name="stretchFactorNumeratorCoarse"
  164. onChange={handleStretchFactorCoarseChange}
  165. step="any"
  166. />
  167. </form>
  168. <div style={{ position: 'relative', backgroundColor: 'black', }}>
  169. {/* @ts-ignore */}
  170. <MusicalKeyboard
  171. hasMap
  172. startKey={0}
  173. endKey={127}
  174. height={50}
  175. keyComponents={{
  176. accidental: StyledAccidentalKey,
  177. natural: StyledNaturalKey,
  178. }}
  179. keyChannels={keyChannels}
  180. >
  181. <KeyboardMap
  182. channel={0}
  183. onChange={playSound}
  184. />
  185. </MusicalKeyboard>
  186. </div>
  187. </div>
  188. )}
  189. <table
  190. className="alternate-rows"
  191. style={{
  192. fontSize: '0.75em',
  193. }}
  194. >
  195. <caption>
  196. MIDI note frequencies and their stretched counterparts
  197. (base frequency={baseFrequency} Hz for key #{baseKey}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}).
  198. First figures indicate non-stretched frequencies, second figures indicate stretched frequencies. Parenthesized figures represent difference
  199. between the frequencies.
  200. </caption>
  201. <thead>
  202. <tr>
  203. <th rowSpan={2}>
  204. Octave
  205. </th>
  206. <th colSpan={12}>
  207. Pitch Class
  208. </th>
  209. </tr>
  210. <tr>
  211. {PITCH_CLASSES.map((c) => (
  212. <th
  213. key={c}
  214. style={{ textAlign: 'right' }}
  215. >
  216. {c}
  217. </th>
  218. ))}
  219. </tr>
  220. </thead>
  221. <tbody>
  222. {new Array(11).fill(0).map((_, octave) => {
  223. return (
  224. <tr>
  225. <th>{octave}</th>
  226. {PITCH_CLASSES.map((_, pitchClassIndex) => {
  227. const i = (octave * PITCH_CLASSES.length) + pitchClassIndex;
  228. const nonStretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** (i - baseKey));
  229. const stretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** ((i - baseKey) * (stretchFactorNumerator / equalDivisionOfTheOctave)));
  230. const difference = (stretched - nonStretched);
  231. return (
  232. <td
  233. style={{ textAlign: 'right' }}
  234. >
  235. {nonStretched.toFixed(2)}
  236. <br />{' '}
  237. {stretched.toFixed(2)}
  238. <br />{' '}
  239. <small>
  240. ({difference.toFixed(2)})
  241. </small>
  242. </td>
  243. );
  244. })}
  245. </tr>
  246. );
  247. })}
  248. </tbody>
  249. </table>
  250. {false && (
  251. <table>
  252. <caption>
  253. MIDI note frequencies and their stretched counterparts
  254. (base frequency={baseFrequency} Hz for key #{baseKey}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)})
  255. </caption>
  256. <thead>
  257. <tr>
  258. <th>
  259. MIDI Note Number
  260. </th>
  261. <th>
  262. Key
  263. </th>
  264. <th>
  265. Frequency
  266. <br />
  267. {' '}
  268. (non-stretched, in Hz)
  269. </th>
  270. <th>
  271. Frequency
  272. <br />
  273. {' '}
  274. (stretched, in Hz)
  275. </th>
  276. <th>
  277. Difference (in Hz)
  278. </th>
  279. </tr>
  280. </thead>
  281. <tbody>
  282. {new Array(128).fill(0).map((_, i) => {
  283. const nonStretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** (i - baseKey));
  284. const stretched = (baseFrequency * (2 ** (1 / equalDivisionOfTheOctave)) ** ((i - baseKey) * (stretchFactorNumerator / equalDivisionOfTheOctave)));
  285. const difference = (stretched - nonStretched);
  286. return (
  287. <tr key={i}>
  288. <th>
  289. {i}
  290. </th>
  291. <th>
  292. {PITCH_CLASSES[i % PITCH_CLASSES.length]}
  293. {Math.floor(i / PITCH_CLASSES.length)}
  294. </th>
  295. <td style={{ textAlign: 'right' }}>
  296. {nonStretched.toFixed(5)}
  297. </td>
  298. <td style={{ textAlign: 'right' }}>
  299. {stretched.toFixed(5)}
  300. </td>
  301. <td style={{ textAlign: 'right' }}>
  302. {difference.toFixed(5)}
  303. </td>
  304. </tr>
  305. );
  306. })}
  307. </tbody>
  308. </table>
  309. )}
  310. </div>
  311. );
  312. };