Skip to content

Commit

Permalink
Add Synth experiment
Browse files Browse the repository at this point in the history
  • Loading branch information
jverneaut committed Oct 7, 2024
1 parent 05c1021 commit 7c9be8a
Show file tree
Hide file tree
Showing 4 changed files with 248 additions and 0 deletions.
29 changes: 29 additions & 0 deletions src/synth/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />

<title>Synth</title>
<meta name="category" content="Misc" />
<meta name="date" content="1727987764376" />
<%= htmlWebpackPlugin.options.headContent %>
</head>
<body>
<div class="info">
<p>Click and drag on the canvas to play notes.</p>
<p>
This innovative method allows you to modulate the notes freely while
ensuring they always return to the correct frequency.
</p>
<p>
Each white bar represents a note in the scale, while each gray line
indicates the modulation zone for that note.
</p>
<p>
The X-axis controls the pitch, and the Y-axis controls the filter
cutoff.
</p>
</div>
</body>
</html>
171 changes: 171 additions & 0 deletions src/synth/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import './main.scss';

const mousePos = { x: 0, y: 0 };
const interpolated = { x: 0, y: 0 };

const distance = { x: 0, y: 0 };

window.addEventListener('mousemove', (e) => {
mousePos.x = e.clientX;
mousePos.y = e.clientY;
});

const mouse = document.createElement('div');
const size = 40;
Object.assign(mouse.style, {
background: 'red',
position: 'absolute',
transform: 'translate(-50%, -50%)',
width: `${size}px`,
height: `${size}px`,
borderRadius: `${size}px`,
});

document.body.appendChild(mouse);

const closestMultiple = (value, multiple) => {
return Math.round(value / multiple) * multiple;
};

function calculateFrequency(baseFrequency, semitonesAway) {
const semitoneRatio = Math.pow(2, 1 / 12);
return baseFrequency * Math.pow(semitoneRatio, semitonesAway);
}

const range = 12;
const baseFreq = calculateFrequency(440, -18);
const ratio1 = 0.24;
const ratio2 = 0.25 * ratio1;

document.documentElement.style.backgroundSize = `calc(100% / ${range})`;

const audioContext = new AudioContext();

const chord = [-24, 3, 7 - 12, 10, 14 - 12];

const oscillators = chord.map((note) => ({
note,
osc: audioContext.createOscillator(),
connected: false,
started: false,
}));

const mix = 0.6;

const gainNodeDry = audioContext.createGain();
gainNodeDry.gain.setValueAtTime(
((1 - mix) * (0.8 * 1)) / oscillators.length,
audioContext.currentTime
);
gainNodeDry.connect(audioContext.destination);

const gainNodeWet = audioContext.createGain();
gainNodeWet.gain.setValueAtTime(
(mix * (0.8 * 1)) / oscillators.length,
audioContext.currentTime
);
gainNodeWet.connect(audioContext.destination);

const convolver = audioContext.createConvolver();
const createImpulseResponse = (duration, decay) => {
const sampleRate = audioContext.sampleRate;
const length = sampleRate * duration;
const impulse = audioContext.createBuffer(2, length, sampleRate);
for (let i = 0; i < 2; i++) {
const channelData = impulse.getChannelData(i);
for (let j = 0; j < length; j++) {
channelData[j] =
(Math.random() * 2 - 1) * Math.pow(1 - j / length, decay);
}
}
return impulse;
};

convolver.buffer = createImpulseResponse(1.0, 1.0);

const lp = audioContext.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.setValueAtTime(400, audioContext.currentTime);
lp.connect(convolver);

lp.connect(gainNodeDry);
convolver.connect(gainNodeWet);

oscillators.forEach(({ osc }) => {
osc.type = 'sawtooth';
osc.connect(lp);
});

const adjust = (x, y) => {
const ratio = y / window.innerHeight;
lp.frequency.setValueAtTime(
200 + 4400 * Math.pow(1 - ratio, 2),
audioContext.currentTime
);

oscillators.forEach((osc) => {
osc.osc.frequency.setValueAtTime(
calculateFrequency(baseFreq, osc.note + (range * x) / window.innerWidth),
audioContext.currentTime
);
});

const value = mix * (0.8 * 1);
gainNodeWet.gain.setValueAtTime(
(value + (1.5 - value) * 1.5 * Math.pow(1 - ratio, 2)) / oscillators.length,
audioContext.currentTime
);
};

let isPlaying = false;

const interpolate = () => {
const targetX = closestMultiple(mousePos.x, window.innerWidth / range);
const targetY = closestMultiple(mousePos.y, window.innerWidth / range);

interpolated.x += ratio1 * (mousePos.x - interpolated.x);
interpolated.y += ratio1 * (mousePos.y - interpolated.y);

distance.x += ratio2 * (targetX - interpolated.x - distance.x);

mouse.style.left = distance.x + interpolated.x + 'px';
mouse.style.top = interpolated.y + 'px';

const x = distance.x + interpolated.x;
const y = interpolated.y;

adjust(x, y);

requestAnimationFrame(interpolate);
};

document.addEventListener('mousedown', () => {
if (!isPlaying) {
oscillators.forEach((osc) => {
if (!osc.started) {
osc.osc.start();
osc.started = true;
}

osc.osc.connect(lp);
osc.connected = true;
});

isPlaying = true;
}
});

document.addEventListener('mouseup', () => {
if (isPlaying) {
oscillators.forEach((osc) => {
if (osc.connected) {
osc.osc.disconnect(lp);
osc.connected = false;
}
});

isPlaying = false;
}
});

requestAnimationFrame(interpolate);
48 changes: 48 additions & 0 deletions src/synth/main.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
@import '../../src/reset.css';

html {
$bg: rgb(32, 35, 37);
$mid: rgb(56, 58, 58);
$fg: rgb(145, 181, 201);

background: repeating-linear-gradient(
to right,
$bg 0%,
$bg calc(50% - 1px),
$mid calc(50% - 1px),
$mid calc(50%),
$bg calc(50%),
$bg calc(100% - 2px),
$fg calc(100% - 2px),
$fg 100%
);

height: 100%;
background-size: calc(100% / 24);
}

.info {
user-select: none;
pointer-events: none;
position: absolute;
bottom: 16px;
left: 50%;
width: 400px;
max-width: 100%;
background: black;
color: white;
padding: 16px;
transform: translate(-50%, 0);
font-size: 12px;
font-family: system-ui;
line-height: 1.25;
display: flex;
flex-direction: column;
gap: 8px;
margin: 12px 0;

@media screen and (max-width: 600px) {
font-size: 9px;
gap: 6px;
}
}
Binary file added src/synth/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 7c9be8a

Please sign in to comment.