almost stable
This commit is contained in:
264
src/App.svelte
264
src/App.svelte
@ -1,25 +1,31 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import WaveformDisplay from './lib/components/WaveformDisplay.svelte';
|
||||
import VUMeter from './lib/components/VUMeter.svelte';
|
||||
import { TwoOpFM, type TwoOpFMParams } from './lib/audio/engines/TwoOpFM';
|
||||
import { AudioService } from './lib/audio/services/AudioService';
|
||||
import { downloadWAV } from './lib/audio/utils/WAVEncoder';
|
||||
import { loadVolume, saveVolume, loadDuration, saveDuration } from './lib/utils/settings';
|
||||
import { generateRandomColor } from './lib/utils/colors';
|
||||
import { onMount } from "svelte";
|
||||
import WaveformDisplay from "./lib/components/WaveformDisplay.svelte";
|
||||
import VUMeter from "./lib/components/VUMeter.svelte";
|
||||
import { engines } from "./lib/audio/engines/registry";
|
||||
import type { SynthEngine } from "./lib/audio/engines/SynthEngine";
|
||||
import { AudioService } from "./lib/audio/services/AudioService";
|
||||
import { downloadWAV } from "./lib/audio/utils/WAVEncoder";
|
||||
import {
|
||||
loadVolume,
|
||||
saveVolume,
|
||||
loadDuration,
|
||||
saveDuration,
|
||||
} from "./lib/utils/settings";
|
||||
import { generateRandomColor } from "./lib/utils/colors";
|
||||
|
||||
let currentMode = 'Mode 1';
|
||||
const modes = ['Mode 1', 'Mode 2', 'Mode 3'];
|
||||
let currentEngineIndex = 0;
|
||||
let engine = engines[currentEngineIndex];
|
||||
|
||||
const engine = new TwoOpFM();
|
||||
const audioService = new AudioService();
|
||||
|
||||
let currentParams: TwoOpFMParams | null = null;
|
||||
let currentParams: any = null;
|
||||
let currentBuffer: AudioBuffer | null = null;
|
||||
let duration = loadDuration();
|
||||
let volume = loadVolume();
|
||||
let playbackPosition = 0;
|
||||
let playbackPosition = -1;
|
||||
let waveformColor = generateRandomColor();
|
||||
let showModal = true;
|
||||
|
||||
onMount(() => {
|
||||
audioService.setVolume(volume);
|
||||
@ -62,7 +68,7 @@
|
||||
|
||||
function download() {
|
||||
if (!currentBuffer) return;
|
||||
downloadWAV(currentBuffer, 'synth-sound.wav');
|
||||
downloadWAV(currentBuffer, "synth-sound.wav");
|
||||
}
|
||||
|
||||
function handleVolumeChange(event: Event) {
|
||||
@ -77,17 +83,83 @@
|
||||
duration = parseFloat(target.value);
|
||||
saveDuration(duration);
|
||||
}
|
||||
|
||||
function switchEngine(index: number) {
|
||||
currentEngineIndex = index;
|
||||
engine = engines[index];
|
||||
generateRandom();
|
||||
}
|
||||
|
||||
async function closeModal() {
|
||||
showModal = false;
|
||||
await audioService.initialize();
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
// Ignore if typing in an input
|
||||
if (event.target instanceof HTMLInputElement) return;
|
||||
|
||||
const key = event.key.toLowerCase();
|
||||
|
||||
// Close modal with Escape key
|
||||
if (key === "escape" && showModal) {
|
||||
closeModal();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (key) {
|
||||
case "m":
|
||||
mutate();
|
||||
break;
|
||||
case "r":
|
||||
generateRandom();
|
||||
break;
|
||||
case "s":
|
||||
download();
|
||||
break;
|
||||
case "arrowleft":
|
||||
event.preventDefault();
|
||||
const durationDecrement = event.shiftKey ? 1 : 0.05;
|
||||
duration = Math.max(0.05, duration - durationDecrement);
|
||||
saveDuration(duration);
|
||||
break;
|
||||
case "arrowright":
|
||||
event.preventDefault();
|
||||
const durationIncrement = event.shiftKey ? 1 : 0.05;
|
||||
duration = Math.min(8, duration + durationIncrement);
|
||||
saveDuration(duration);
|
||||
break;
|
||||
case "arrowdown":
|
||||
event.preventDefault();
|
||||
const volumeDecrement = event.shiftKey ? 0.2 : 0.05;
|
||||
volume = Math.max(0, volume - volumeDecrement);
|
||||
audioService.setVolume(volume);
|
||||
saveVolume(volume);
|
||||
break;
|
||||
case "arrowup":
|
||||
event.preventDefault();
|
||||
const volumeIncrement = event.shiftKey ? 0.2 : 0.05;
|
||||
volume = Math.min(1, volume + volumeIncrement);
|
||||
audioService.setVolume(volume);
|
||||
saveVolume(volume);
|
||||
break;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
<div class="container">
|
||||
<div class="top-bar">
|
||||
<div class="mode-buttons">
|
||||
{#each modes as mode}
|
||||
{#each engines as engine, index}
|
||||
<button
|
||||
class:active={currentMode === mode}
|
||||
onclick={() => currentMode = mode}
|
||||
class="engine-button"
|
||||
class:active={currentEngineIndex === index}
|
||||
data-description={engine.getDescription()}
|
||||
onclick={() => switchEngine(index)}
|
||||
>
|
||||
{mode}
|
||||
{engine.getName()}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@ -124,22 +196,62 @@
|
||||
<WaveformDisplay
|
||||
buffer={currentBuffer}
|
||||
color={waveformColor}
|
||||
playbackPosition={playbackPosition}
|
||||
{playbackPosition}
|
||||
onclick={replaySound}
|
||||
/>
|
||||
<div class="bottom-controls">
|
||||
<button onclick={generateRandom}>Random</button>
|
||||
<button onclick={mutate}>Mutate</button>
|
||||
<button onclick={download}>Download</button>
|
||||
<button onclick={generateRandom}>Random (R)</button>
|
||||
<button onclick={mutate}>Mutate (M)</button>
|
||||
<button onclick={download}>Download (D)</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vu-meter-container">
|
||||
<VUMeter
|
||||
buffer={currentBuffer}
|
||||
playbackPosition={playbackPosition}
|
||||
/>
|
||||
<VUMeter buffer={currentBuffer} {playbackPosition} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showModal}
|
||||
<div
|
||||
class="modal-overlay"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onclick={closeModal}
|
||||
onkeydown={(e) => e.key === "Enter" && closeModal()}
|
||||
>
|
||||
<div
|
||||
class="modal-content"
|
||||
role="dialog"
|
||||
aria-labelledby="modal-title"
|
||||
tabindex="-1"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h1 id="modal-title">Vending Machine</h1>
|
||||
<p class="description">
|
||||
Oh, looks like you found a sound vending machine. This one seems
|
||||
slightly broken and it seems that you can get sounds for free... Have
|
||||
fun!
|
||||
</p>
|
||||
<div class="modal-links">
|
||||
<p>
|
||||
Created by <a
|
||||
href="https://raphaelforment.fr"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">Raphaël Forment (BuboBubo)</a
|
||||
>
|
||||
</p>
|
||||
<p>
|
||||
Licensed under <a
|
||||
href="https://www.gnu.org/licenses/gpl-3.0.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">GPL 3.0</a
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<button class="modal-close" onclick={closeModal}>Start</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@ -165,15 +277,39 @@
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mode-buttons button {
|
||||
.engine-button {
|
||||
opacity: 0.7;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mode-buttons button.active {
|
||||
.engine-button.active {
|
||||
opacity: 1;
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
.engine-button::after {
|
||||
content: attr(data-description);
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
left: 0;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background-color: #0a0a0a;
|
||||
border: 1px solid #444;
|
||||
color: #ccc;
|
||||
font-size: 0.85rem;
|
||||
width: 30vw;
|
||||
white-space: normal;
|
||||
word-wrap: break-word;
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.engine-button:hover::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.controls-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
@ -282,4 +418,76 @@
|
||||
input[type="range"]::-moz-range-thumb:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: #000;
|
||||
border: 2px solid #fff;
|
||||
padding: 2rem;
|
||||
max-width: 500px;
|
||||
width: 90%;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.modal-content h1 {
|
||||
margin: 0 0 1rem 0;
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal-content .description {
|
||||
margin: 0 0 1.5rem 0;
|
||||
line-height: 1.6;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.modal-links {
|
||||
margin: 1.5rem 0;
|
||||
padding: 1rem 0;
|
||||
border-top: 1px solid #333;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.modal-links p {
|
||||
margin: 0.5rem 0;
|
||||
font-size: 0.9rem;
|
||||
color: #ccc;
|
||||
}
|
||||
|
||||
.modal-links a {
|
||||
color: #646cff;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.modal-links a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
margin-top: 1rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
font-size: 1.1rem;
|
||||
background-color: #fff;
|
||||
color: #000;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.modal-close:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user