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 && (
{/* @ts-ignore */}
)} {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}, stretch factor={(stretchFactorNumerator/equalDivisionOfTheOctave).toFixed(3)}). First figures indicate non-stretched frequencies, second figures indicate stretched frequencies. Parenthesized figures represent difference between the frequencies.
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}, stretch 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)}
)}
); };