improve pitch page

This commit is contained in:
Felix Roos 2023-07-21 17:46:45 +02:00
parent 2d8176f213
commit 7373138e3d
3 changed files with 248 additions and 30 deletions

View file

@ -0,0 +1,216 @@
import useEvent from '@strudel.cycles/react/src/hooks/useEvent.mjs';
import useFrame from '@strudel.cycles/react/src/hooks/useFrame.mjs';
import { getAudioContext } from '@strudel.cycles/webaudio';
import { useState, useRef, useEffect } from 'react';
let Button = (props) => <button {...props} className="bg-lineHighlight p-2 rounded-md color-foreground" />;
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 = true,
pitchStep = '0.001',
min = 55,
max = 7040,
}) {
const oscRef = useRef();
const activeRef = useRef();
const freqRef = useRef(220);
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 = () => {
console.log('sweep');
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);
};
return (
<>
<span className="font-mono">
{showFrequencySlider && <span className="text-blue-500">{hz.toFixed(0)}Hz</span>}
{showFrequencySlider && showPitchSlider && <> = </>}
{showPitchSlider && (
<>
{min}Hz * 2
<sup>
<span className="text-yellow-500">{(freq2pitchSlider(hz) * Math.log2(max / min)).toFixed(2)}</span>
</sup>
</>
)}
</span>
<span></span>
<div>
{showFrequencySlider && (
<div className="flex space-x-1 items-center">
<input
type="range"
value={freqSlider}
min={0}
max={1}
step={0.001}
onMouseDown={handleMouseDown}
className={`block w-full max-w-[600px] accent-blue-500 `}
onChange={(e) => {
const f = freqSlider2freq(parseFloat(e.target.value));
handleChangeFrequency(f);
}}
/>
</div>
)}
{showPitchSlider && (
<div>
<input
type="range"
value={pitchSlider}
min={0}
max={1}
//step=".001"
step={pitchStep}
onMouseDown={handleMouseDown}
className={`block w-full max-w-[600px] accent-yellow-500`}
onChange={(e) => {
const f = pitchSlider2freq(parseFloat(e.target.value));
handleChangeFrequency(f);
}}
/>
</div>
)}
</div>
<div className="px-2">
{plot && <canvas ref={canvasRef} className="w-full max-w-[584px] h-[300px]" height="600" width={800} />}
</div>
<div className="space-x-2">
{animatable && (
<Button onClick={() => startSweep()}>
<span style={{ color: '#3b82f6' }}>Frequency Sweep</span>
</Button>
)}
{animatable && (
<Button onClick={() => startSweep(true)}>
<span style={{ color: '#eab308' }}>Pitch Sweep</span>
</Button>
)}
{buttons.map((f, i) => (
<Button key={(f, i)} onMouseDown={() => startOsc(f)}>
{f}Hz
</Button>
))}
</div>
</>
);
}

View file

@ -89,6 +89,9 @@ export const SIDEBAR: Sidebar = {
{ text: 'Accumulation', link: 'learn/accumulation' }, { text: 'Accumulation', link: 'learn/accumulation' },
{ text: 'Tonal Functions', link: 'learn/tonal' }, { text: 'Tonal Functions', link: 'learn/tonal' },
], ],
Understand: [
{ text: 'Pitch', link: 'understand/pitch' },
],
Development: [ Development: [
{ text: 'REPL', link: 'technical-manual/repl' }, { text: 'REPL', link: 'technical-manual/repl' },
{ text: 'Sounds', link: 'technical-manual/sounds' }, { text: 'Sounds', link: 'technical-manual/sounds' },

View file

@ -8,64 +8,63 @@ import { PitchSlider } from '../../components/PitchSlider';
# Understanding Pitch # Understanding Pitch
Let's learn how pitch works! But first, let's experience pitch in its rawest form: Let's learn how pitch works! The slider below controls the <span style="color:#3b82f6;">frequency</span> of an oscillator, producing a pitch:
<PitchSlider client:load /> {/* <PitchSlider client:load showFrequencySlider plot /> */}
<PitchSlider client:load showFrequencySlider />
- Drag the slider to hear a pitch - Drag the slider to hear a pitch
- Move the slider to change the pitch - Move the slider to change the pitch
- Observe how the number on the right changes - Observe how the Hz number changes
The number on the right is the **frequency** of the pitch you're hearing. The Hz number is the frequency of the pitch you're hearing.
The higher the frequency, the higher the pitch and vice versa. The higher the frequency, the higher the pitch and vice versa.
A pitch occurs whenever something is vibrating / oscillating at a frequency, in this case it's your speaker. A pitch occurs whenever something is vibrating / oscillating at a frequency, in this case it's your speaker.
The unit **Hz** describes how many times that oscillation happens per second. The unit **Hz** describes how many times that oscillation happens per second.
Our eyes are too slow to actually see the oscillation on the speaker, but we can [see it in slow motion](https://www.youtube.com/watch?v=CDMBWw7OuJQ&t=5s). Our eyes are too slow to actually see the oscillation on the speaker, but we can [see it in slow motion](https://www.youtube.com/watch?v=CDMBWw7OuJQ&t=5s).
## Pitch Perception ## Frequency vs Pitch Perception
Maybe you have already noticed that the pitch slider is "lopsided". To make that more obvious, let's automate the slider! Maybe you have already noticed that the <span style="color:#3b82f6;">frequency slider</span> is "lopsided",
Below are 2 buttons for automation, try them out: meaning the pitch changes more in the left region and less in the right region.
To make that more obvious, let's add a <span style="color:#eab308">pitch slider</span>
that controls the frequency on a different scale:
<PitchSlider animatable plot client:load /> <PitchSlider animatable plot showFrequencySlider showPitchSlider client:load />
There are 2 different colored lines: Try out the buttons above to sweep through the frequency range in 2 different ways:
- <span style="color:#3b82f6;">blue</span>: the frequency value - Frequency Sweep: <span style="color:#3b82f6;">frequency rises linear</span> , <span style="color:#eab308">pitch rises logarithmic</span>
- <span style="color:#eab308">yellow</span>: the pitch value or how you perceive the frequency - Pitch Sweep: <span style="color:#3b82f6;">frequency rises exponential</span> , <span style="color:#eab308">pitch rises linear</span>
Depending on the type of sweep, the lines behave differently:
- Frequency Sweep: <span style="color:#3b82f6;">frequency is linear</span> , <span style="color:#eab308">pitch is logarithmic</span>
- Pitch Sweep: <span style="color:#3b82f6;">frequency is exponential</span> , <span style="color:#eab308">pitch is linear</span>
Don't be scared of these mathematical terms: Don't be scared of these mathematical terms:
- "logarithmic" is just a fancy way of saying "it starts fast and slows down" - "logarithmic" is just a fancy way of saying "it starts fast and slows down"
- "exponential" is just a fancy way of saying "it starts slow and gets faster" - "exponential" is just a fancy way of saying "it starts slow and gets faster"
## A Pitch Slider Most of the time, we might want to control pitch in a way that matches our perception,
which is what the <span style="color:#eab308">pitch slider</span> does.
Most of the time, we might want to control pitch in a way that matches our perception. ## From Hz to Semitones
Now that we know that frequency in Hz does not match our perception,
let's make the slider exponential (and yellow):
<PitchSlider logarithmic plot animatable client:load /> Because Hz does not match our perception, let's try to find a unit for pitch that matches.
- Do you see how the slider is now linked to the yellow line?
- Try out the buttons again and compare it to the frequency slider above
## A unit for Pitch
Let's try to find a linear unit for pitch, as frequency won't cut it.
To approach that unit of pitch, let's look at how frequency behaves when it is doubled: To approach that unit of pitch, let's look at how frequency behaves when it is doubled:
<PitchSlider logarithmic client:load buttons={[50, 100, 200, 400, 800, 1600]} /> <PitchSlider client:load showPitchSlider showFrequencySlider pitchStep={1 / 7} />
- Use the now stepped pitch slider above
- Can you hear how these pitches seem related to each other? - Can you hear how these pitches seem related to each other?
<PitchSlider logarithmic client:load buttons={[50, 100, 200, 400, 800, 1600]} /> In mathematical terms, the frequency of button `n` would be `50Hz * 2^n` (n starting from 0).
We could already use that `n` as a pitch unit! So a value of 0 would relate to a certain base frequency (50Hz),
and each whole number step would be an additional doubling of 2.
In musical terms, a pitch with double the frequency of another is an `octave` higher.
Because octaves are pretty far apart, octaves are typically divided into 12 equal parts:
<PitchSlider client:load showPitchSlider showFrequencySlider pitchStep={1 / 12} min={440} max={880} />
## Definition ## Definition