|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344 |
- 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 && (
- <div className="print-hidden">
- <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', }}>
- {/* @ts-ignore */}
- <MusicalKeyboard
- hasMap
- startKey={0}
- endKey={127}
- height={50}
- keyComponents={{
- accidental: StyledAccidentalKey,
- natural: StyledNaturalKey,
- }}
- keyChannels={keyChannels}
- >
- <KeyboardMap
- channel={0}
- onChange={playSound}
- />
- </MusicalKeyboard>
- </div>
- </div>
- )}
-
- <table
- className="alternate-rows"
- style={{
- fontSize: '0.75em',
- }}
- >
- <caption>
- MIDI note frequencies and their stretched counterparts
- (base frequency={baseFrequency} Hz for key #{baseKey}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}).
- First figures indicate non-stretched frequencies, second figures indicate stretched frequencies. Parenthesized figures represent difference
- between the frequencies.
- </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}, stretch 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>
- );
- };
|