import useEvent from '@src/useEvent.mjs'; import useFrame from '@src/useFrame.mjs'; import { getAudioContext } from '@strudel/webaudio'; import { midi2note } from '@strudel/core'; import { useState, useRef, useEffect } from 'react'; import Claviature from '@components/Claviature'; let Button = (props) => ; function plotValues(ctx, values, min, max, color) { let { width, height } = ctx.canvas; ctx.strokeStyle = color; const thickness = 8; ctx.lineWidth = thickness; ctx.beginPath(); let x = (f) => ((f - min) / (max - min)) * width; let y = (i) => (1 - i / values.length) * height; values.forEach((f, i, a) => { ctx.lineTo(x(f), y(i)); }); ctx.stroke(); } function getColor(cssVariable) { if (typeof document === 'undefined') { return 'white'; } const dummyElement = document.createElement('div'); dummyElement.style.color = cssVariable; // Append the dummy element to the document body document.body.appendChild(dummyElement); // Get the computed style of the dummy element const styles = getComputedStyle(dummyElement); // Get the value of the CSS variable const color = styles.getPropertyValue(cssVariable); document.body.removeChild(dummyElement); return color; } let pitchColor = '#eab308'; let frequencyColor = '#3b82f6'; export function PitchSlider({ buttons = [], animatable = false, plot = false, showPitchSlider = false, showFrequencySlider = false, pitchStep = '0.001', min = 55, max = 7040, initial = 220, baseFrequency = min, zeroOffset = 0, claviature, }) { const oscRef = useRef(); const activeRef = useRef(); const freqRef = useRef(initial); const historyRef = useRef([freqRef.current]); const frameRef = useRef(); const canvasRef = useRef(); const [hz, setHz] = useState(freqRef.current); useEffect(() => { freqRef.current = hz; }, [hz]); useEvent('mouseup', () => { oscRef.current?.stop(); activeRef.current = false; }); let freqSlider2freq = (progress) => min + progress * (max - min); let pitchSlider2freq = (progress) => min * 2 ** (progress * Math.log2(max / min)); let freq2freqSlider = (freq) => (freq - min) / (max - min); let freq2pitchSlider = (freq) => { const [minOct, maxOct] = [Math.log2(min), Math.log2(max)]; return (Math.log2(freq) - minOct) / (maxOct - minOct); }; const freqSlider = freq2freqSlider(hz); const pitchSlider = freq2pitchSlider(hz); let startOsc = (hz) => { if (oscRef.current) { oscRef.current.stop(); } oscRef.current = getAudioContext().createOscillator(); oscRef.current.frequency.value = hz; oscRef.current.connect(getAudioContext().destination); oscRef.current.start(); activeRef.current = true; setHz(hz); }; let startSweep = (exp = false) => { let f = min; startOsc(f); const frame = () => { if (f < max) { if (!exp) { f += 10; } else { f *= 1.01; } oscRef.current.frequency.value = f; frameRef.current = requestAnimationFrame(frame); } else { oscRef.current.stop(); cancelAnimationFrame(frameRef.current); } setHz(f); }; requestAnimationFrame(frame); }; useFrame(() => { historyRef.current.push(freqRef.current); historyRef.current = historyRef.current.slice(-1000); if (canvasRef.current) { let ctx = canvasRef.current.getContext('2d'); ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); if (showFrequencySlider) { plotValues(ctx, historyRef.current, min, max, frequencyColor); } if (showPitchSlider) { const [minOct, maxOct] = [Math.log2(min), Math.log2(max)]; let perceptual = historyRef.current.map((v) => Math.log2(v)); plotValues(ctx, perceptual, minOct, maxOct, pitchColor); } } }, plot); let handleChangeFrequency = (f) => { setHz(f); if (oscRef.current) { oscRef.current.frequency.value = f; } }; let handleMouseDown = () => { cancelAnimationFrame(frameRef.current); startOsc(hz); }; let exponent, activeNote, activeNoteLabel; if (showPitchSlider) { const expOffset = baseFrequency ? Math.log2(baseFrequency / min) : 0; exponent = freq2pitchSlider(hz) * Math.log2(max / min) - expOffset; let semitones = parseFloat((exponent * 12).toFixed(2)); if (zeroOffset) { semitones = semitones + zeroOffset; const isWhole = Math.round(semitones) === semitones; activeNote = midi2note(Math.round(semitones)); activeNoteLabel = (!isWhole ? '~' : '') + activeNote; semitones = !isWhole ? semitones.toFixed(2) : semitones; exponent = ( <> ({semitones} - {zeroOffset})/12 > ); } else if (semitones % 12 === 0) { exponent = {semitones / 12}; } else if (semitones % 1 === 0) { exponent = ( <> {semitones}/12 > ); } else { exponent = {exponent.toFixed(2)}; } } return ( <> {showFrequencySlider && {hz.toFixed(0)}Hz} {showFrequencySlider && showPitchSlider && <> = >} {showPitchSlider && ( <> {baseFrequency || min}Hz * 2{exponent} > )} {claviature && ( <> {' '} = {activeNoteLabel} > )}