init
This commit is contained in:
285
src/App.svelte
Normal file
285
src/App.svelte
Normal file
@ -0,0 +1,285 @@
|
||||
<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';
|
||||
|
||||
let currentMode = 'Mode 1';
|
||||
const modes = ['Mode 1', 'Mode 2', 'Mode 3'];
|
||||
|
||||
const engine = new TwoOpFM();
|
||||
const audioService = new AudioService();
|
||||
|
||||
let currentParams: TwoOpFMParams | null = null;
|
||||
let currentBuffer: AudioBuffer | null = null;
|
||||
let duration = loadDuration();
|
||||
let volume = loadVolume();
|
||||
let playbackPosition = 0;
|
||||
let waveformColor = generateRandomColor();
|
||||
|
||||
onMount(() => {
|
||||
audioService.setVolume(volume);
|
||||
audioService.setPlaybackUpdateCallback((position) => {
|
||||
playbackPosition = position;
|
||||
});
|
||||
generateRandom();
|
||||
});
|
||||
|
||||
function generateRandom() {
|
||||
currentParams = engine.randomParams();
|
||||
waveformColor = generateRandomColor();
|
||||
regenerateBuffer();
|
||||
}
|
||||
|
||||
function mutate() {
|
||||
if (!currentParams) {
|
||||
generateRandom();
|
||||
return;
|
||||
}
|
||||
currentParams = engine.mutateParams(currentParams);
|
||||
waveformColor = generateRandomColor();
|
||||
regenerateBuffer();
|
||||
}
|
||||
|
||||
function regenerateBuffer() {
|
||||
if (!currentParams) return;
|
||||
|
||||
const sampleRate = audioService.getSampleRate();
|
||||
const data = engine.generate(currentParams, sampleRate, duration);
|
||||
currentBuffer = audioService.createAudioBuffer(data);
|
||||
audioService.play(currentBuffer);
|
||||
}
|
||||
|
||||
function replaySound() {
|
||||
if (currentBuffer) {
|
||||
audioService.play(currentBuffer);
|
||||
}
|
||||
}
|
||||
|
||||
function download() {
|
||||
if (!currentBuffer) return;
|
||||
downloadWAV(currentBuffer, 'synth-sound.wav');
|
||||
}
|
||||
|
||||
function handleVolumeChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
volume = parseFloat(target.value);
|
||||
audioService.setVolume(volume);
|
||||
saveVolume(volume);
|
||||
}
|
||||
|
||||
function handleDurationChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
duration = parseFloat(target.value);
|
||||
saveDuration(duration);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<div class="top-bar">
|
||||
<div class="mode-buttons">
|
||||
{#each modes as mode}
|
||||
<button
|
||||
class:active={currentMode === mode}
|
||||
onclick={() => currentMode = mode}
|
||||
>
|
||||
{mode}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
<div class="controls-group">
|
||||
<div class="slider-control duration-slider">
|
||||
<label for="duration">Duration: {duration.toFixed(2)}s</label>
|
||||
<input
|
||||
id="duration"
|
||||
type="range"
|
||||
min="0.05"
|
||||
max="8"
|
||||
step="0.01"
|
||||
value={duration}
|
||||
oninput={handleDurationChange}
|
||||
/>
|
||||
</div>
|
||||
<div class="slider-control">
|
||||
<label for="volume">Volume</label>
|
||||
<input
|
||||
id="volume"
|
||||
type="range"
|
||||
min="0"
|
||||
max="1"
|
||||
step="0.01"
|
||||
value={volume}
|
||||
oninput={handleVolumeChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="main-area">
|
||||
<div class="waveform-container">
|
||||
<WaveformDisplay
|
||||
buffer={currentBuffer}
|
||||
color={waveformColor}
|
||||
playbackPosition={playbackPosition}
|
||||
onclick={replaySound}
|
||||
/>
|
||||
<div class="bottom-controls">
|
||||
<button onclick={generateRandom}>Random</button>
|
||||
<button onclick={mutate}>Mutate</button>
|
||||
<button onclick={download}>Download</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="vu-meter-container">
|
||||
<VUMeter
|
||||
buffer={currentBuffer}
|
||||
playbackPosition={playbackPosition}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.5rem;
|
||||
background-color: #1a1a1a;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.mode-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.mode-buttons button {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.mode-buttons button.active {
|
||||
opacity: 1;
|
||||
border-color: #646cff;
|
||||
}
|
||||
|
||||
.controls-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.slider-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.slider-control label {
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.slider-control input[type="range"] {
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.duration-slider input[type="range"] {
|
||||
width: 300px;
|
||||
}
|
||||
|
||||
.main-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
background-color: #0a0a0a;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.waveform-container {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.vu-meter-container {
|
||||
width: 5%;
|
||||
min-width: 40px;
|
||||
max-width: 80px;
|
||||
border-left: 1px solid #333;
|
||||
}
|
||||
|
||||
.bottom-controls {
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
input[type="range"] {
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
background: transparent;
|
||||
height: 20px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-track {
|
||||
background: #333;
|
||||
height: 4px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #000;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
margin-top: -6px;
|
||||
}
|
||||
|
||||
input[type="range"]::-webkit-slider-thumb:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-track {
|
||||
background: #333;
|
||||
height: 4px;
|
||||
border: 1px solid #444;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #000;
|
||||
border-radius: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input[type="range"]::-moz-range-thumb:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user