Piano notes book, powered by Astro and React.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

345 lines
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. };