Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fine tune design details #3

Merged
merged 1 commit into from
Dec 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

- `npm i`
- `npm run dev`
- [localhost:8080](http://localhost:8080)

## Building and running in production mode

Expand All @@ -17,13 +16,3 @@ To create an optimised version of the app:
```bash
npm run build
```

## Single-page app mode

By default, sirv will only respond to requests that match files in `public`. This is to maximise compatibility with static fileservers, allowing you to deploy your app anywhere.

If you're building a single-page app (SPA) with multiple routes, sirv needs to be able to respond to requests for *any* path. You can make it so by editing the `"start"` command in package.json:

```js
"start": "sirv public --single"
```
34 changes: 22 additions & 12 deletions src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@
return best;
};

/**
* @param {number} number
* @param {number} precision
*/
const round = (number, precision) => Math.round(number * 10 ** precision) / 10 ** precision;

let fareySequenceOrder = $state(15);
const fareySequence = $derived(getFareySequence(fareySequenceOrder));

Expand All @@ -50,7 +56,7 @@

let frequency = $derived((n) => 2 ** (n / semitones));

let frequencies = $derived(scaleBinary.padEnd(semitones + 1, '0').split('').map((v, i) => [v === '1' || v === 'x', frequency(i)]));
let frequencies = $derived((scaleBinary.padEnd(semitones , '0') + (scaleBinary[0] ?? '0')).split('').map((v, i) => [v === '1' || v === 'x', frequency(i)]));

let frequencyInfo = $derived(([selected, freq]) => {
const base = {
Expand All @@ -59,7 +65,7 @@
freq: freq * frequencyRoot,
farey: getClosestFromFareySequence(fareySequence, freq),
};
base.fareyError = 1 - (1/ freq * base.farey[0] / base.farey[1]);
base.fareyError = 1 - (1 / freq * base.farey[0] / base.farey[1]);
base.fareyErrorAbs = freq * frequencyRoot - (frequencyRoot * base.farey[0] / base.farey[1]);

return base;
Expand All @@ -84,12 +90,13 @@

</script>
<h1>Equal temperament playground</h1>
<span class="source"><a href="https://github.com/BafS/equal-temperament-playground">source</a></span>

<WavePlot frequencyRoot={frequencyRoot} semitones={semitones} scaleBinary={scaleBinary} width={rowWidth} />

<div class="row main-inputs">
<span>Scale: <input type="text" maxlength={semitones} bind:value={scaleBinary}></span>
<span>Fundamental frequency (<em>hz</em>): <input type="number" bind:value={frequencyRoot}></span>
<span title="Use 0/1 to define which semi-tones are part of the scale">Scale: <input type="text" maxlength={semitones} bind:value={scaleBinary}></span>
<span><a href="https://en.wikipedia.org/wiki/Fundamental_frequency">Fundamental frequency</a> (<em>hz</em>): <input type="number" bind:value={frequencyRoot}></span>
<span>Semi-tones: <input type="number" bind:value={semitones}></span>
<span>Farey sequence order: <input type="number" min=2 max=200 bind:value={fareySequenceOrder}></span>
</div>
Expand Down Expand Up @@ -117,7 +124,7 @@
<td>{i}</td>
<td>{freq ? Math.round(freq * 100) / 100 : ''} <small>Hz</small> <button class="btn-play" onclick={() => playTone(freq, 1)}>▶</button></td>
<!-- <td></td> -->
<td>{farey.join('/')}</td>
<td>{farey.join(':')}</td>
<td>{Math.round(fareyError * 10000) / 100}<small>%</small> <small>({Math.round(fareyErrorAbs * 100) / 100} Hz)</small></td>
</tr>
{/each}
Expand All @@ -142,12 +149,11 @@
<ToneCircle
width={rowWidth > 1000 ? (rowWidth / 2) : 280}
height={rowWidth > 1000 ? (rowWidth / 3) : 280}
frequencyInfos={frequencyInfos}
semitones={semitones}
generator={toneCircleGenerator}
frequencyRoot={frequencyRoot}
/><br>
Generator: <input class="input-small" type="number" max="199" bind:value={toneCircleGenerator}>
Generator: <input class="input-small" type="number" min="1" max="199" bind:value={toneCircleGenerator}>
</div>

<div id="farey-sequence-pattern">
Expand All @@ -163,9 +169,9 @@

<div>
<p>
Perfect fifth: {frequencyRoot * 3/2} Hz<br>
Perfect fourth: {frequencyRoot * 4/3} Hz<br>
Major third: {frequencyRoot * 5/4} Hz<br>
<a href="https://en.wikipedia.org/wiki/Perfect_fifth">Perfect fifth</a>: {round(frequencyRoot * 3/2, 4)} Hz<br>
<a href="https://en.wikipedia.org/wiki/Perfect_fourth">Perfect fourth</a>: {round(frequencyRoot * 4/3, 4)} Hz<br>
<a href="https://en.wikipedia.org/wiki/Major_third">Major third</a>: {round(frequencyRoot * 5/4, 4)} Hz<br>
</p>
</div>
</div>
Expand All @@ -190,7 +196,7 @@
background: #eee;
}
table td, table th {
padding: .5rem 1.5rem;
padding: .4rem 1.5rem;
vertical-align: top;
}
table td {
Expand Down Expand Up @@ -222,6 +228,10 @@
margin: 0;
margin-bottom: 6px;
}
.source {
font-size: 80%;
float: right;
}

.main-inputs {
margin-top: 4px;
Expand All @@ -235,6 +245,6 @@
.btn-play {
background-color: rgba(200, 200, 200, .075);
font-size: 90%;
padding: 2px 10px;
padding: 3px 10px;
}
</style>
16 changes: 14 additions & 2 deletions src/FareySequencePattern.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,17 @@

let cellHeight = 12;
let height = $derived((fareySequenceOrder + 1.5) * cellHeight);

/**
* @param {number} i
* @param {number} freq
* @return {number}
*/
const alignX = (i, freq) => {
const base = (freq - frequencyRoot) * (width - 5) / frequencyRoot;
const numWidth = 4 * (1 + Math.log10(i) | 0);
return Math.min(width - numWidth * 1.5, base - numWidth / 2);
};
</script>

<svg width={width} height={height}>
Expand All @@ -29,14 +40,15 @@
{#each frequencyInfos as {selected, freq}, i}
<rect opacity="{selected ? .75 : .25}" fill="#FF5722" width="2" height="{(fareySequenceOrder) * cellHeight}" y="0" x={((freq - frequencyRoot) * (width - 5) / frequencyRoot) + 1} tabindex="-1"></rect>
<text x=0
y="-9"
y={i === frequencyInfos.length - 1 ? 9 : -9}
font-family="monospace"
transform="translate({((freq - frequencyRoot) * (width - 5) / frequencyRoot) - 1}, 0) rotate(90)"
font-size="10">
{Math.round(freq)} hz
</text>

<text x={((freq - frequencyRoot) * (width - 5) / frequencyRoot) - 1} y={((fareySequenceOrder + 1.2) * cellHeight)}
<text x={alignX(i, freq)}
y={(fareySequenceOrder + 1.2) * cellHeight}
opacity="{selected ? 1 : 0.5}"
transform="translate(, 0) rotate(90)"
font-family="monospace"
Expand Down
47 changes: 29 additions & 18 deletions src/WavePlot.svelte
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
<script>
import {onMount, afterUpdate} from 'svelte';

let canvas;
export let scaleBinary;
export let frequencyRoot = 440;
export let semitones = 12;
export let width = 1000;
export let height = 300;
import {onMount} from 'svelte';

let canvas = $state();

/**
* @typedef {Object} Props
* @property {string} scaleBinary
* @property {number} [frequencyRoot]
* @property {number} [semitones]
* @property {number} [width]
* @property {number} [height]
*/

/** @type {Props} */
let {
scaleBinary,
frequencyRoot = 440,
semitones = 12,
width = 1000,
height = 290
} = $props();
const colors = ['FF8A80', 'B388FF', '80D8FF', 'B9F6CA', 'FFFF8D', 'FF9E80', '8D6E63'];

$: frequency = (n) => 2 ** (n / semitones);
let frequency = $derived((n) => 2 ** (n / semitones));

$: frequencies = scaleBinary.padEnd(semitones + 1, '0').split('').map((v, i) => [v === '1' || v === 'x', frequency(i)]);
let frequencies = $derived(scaleBinary.padEnd(semitones + 1, '0').split('').map((v, i) => [v === '1' || v === 'x', frequency(i)]));

const frequencyFunctionCreator = (hz) => (t) => Math.sin(t * hz * Math.PI * 2);

Expand All @@ -24,7 +37,7 @@
const y = fun(i / compression) * canvas.height / 2.2;
let rel = canvas.height / 2;

ctx.lineWidth = "1.5";
ctx.lineWidth = '1.5';
ctx.strokeStyle = color; // Green path
ctx.moveTo(i, rel - previous);
ctx.lineTo(i + 1, rel - y);
Expand All @@ -33,7 +46,7 @@
ctx.stroke();
};

$: canvasFn = (() => {
let canvasFn = () => {
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);

Expand All @@ -43,7 +56,7 @@
let x = 0;
while (x < canvas.width) {
ctx.beginPath();
ctx.lineWidth = "2";
ctx.lineWidth = '2';
ctx.strokeStyle = '#' + colors[idx % colors.length];
x = Math.round(i * compression / frequencyRoot / 2 * n);
ctx.moveTo(x + .5, Math.round(canvas.height / 2) - 20);
Expand All @@ -70,17 +83,15 @@
var t2 = new Date();
var dt = t2 - t1;

console.log('elapsed time = ' + dt + ' ms');
});
console.debug('elapsed time = ' + dt + ' ms');
};

onMount(() => {
canvas.width = width * 2;
canvas.height = height * 2;
});

afterUpdate(() => {
canvasFn();
});
$effect(() => canvasFn());
</script>

<canvas
Expand Down
11 changes: 8 additions & 3 deletions src/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ body {
}

a {
color: rgb(0,100,200);
color: rgb(0,100,210);
text-decoration: none;
}

Expand All @@ -38,8 +38,8 @@ label {
}

input, button, select, textarea {
font-family: inherit;
font-size: inherit;
font-family: monospace;
font-size: 110%;
-webkit-padding: 0.4em 0;
padding: 0.4em;
margin: 0 0 0.5em 0;
Expand All @@ -56,6 +56,7 @@ button {
color: #333;
background-color: #f4f4f4;
outline: none;
margin: 0 4px;
}

button:disabled {
Expand All @@ -69,3 +70,7 @@ button:not(:disabled):active {
button:focus {
border-color: #666;
}

button:hover {
border-color: #88c;
}
4 changes: 2 additions & 2 deletions src/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
/**
* @param {number} frequency
* @param {number} duration
* @param {{gain: number, onended: () => any}|null} options
* @param {{gain: number, onended: () => void}|null} options
*/
export const playTone = (frequency, duration, options = null) => {
const gainNode = audioCtx.createGain();
gainNode.gain.value = options?.gain ?? 0.75;
gainNode.gain.value = options?.gain ?? 0.5;
gainNode.connect(audioCtx.destination);

const oscillator = audioCtx.createOscillator();
Expand Down
Loading