clean interface
This commit is contained in:
343
src/App.svelte
343
src/App.svelte
@ -5,17 +5,18 @@
|
||||
import WelcomeModal from "./lib/components/WelcomeModal.svelte";
|
||||
import ProcessorPopup from "./lib/components/ProcessorPopup.svelte";
|
||||
import { engines } from "./lib/audio/engines/registry";
|
||||
import type { SynthEngine } from "./lib/audio/engines/SynthEngine";
|
||||
import type { SynthEngine, PitchLock } from "./lib/audio/engines/SynthEngine";
|
||||
import type { EngineType } 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 { loadVolume, saveVolume, loadDuration, saveDuration, loadPitchLockEnabled, savePitchLockEnabled, loadPitchLockFrequency, savePitchLockFrequency } from "./lib/utils/settings";
|
||||
import { generateRandomColor } from "./lib/utils/colors";
|
||||
import { getRandomProcessor } from "./lib/audio/processors/registry";
|
||||
import type { AudioProcessor } from "./lib/audio/processors/AudioProcessor";
|
||||
import { Sample } from "./lib/audio/engines/Sample";
|
||||
import { Input } from "./lib/audio/engines/Input";
|
||||
import { createKeyboardHandler } from "./lib/utils/keyboard";
|
||||
import { parseFrequencyInput, formatFrequency } from "./lib/utils/pitch";
|
||||
|
||||
let currentEngineIndex = $state(0);
|
||||
const engine = $derived(engines[currentEngineIndex]);
|
||||
@ -34,12 +35,18 @@
|
||||
let popupTimeout = $state<ReturnType<typeof setTimeout> | null>(null);
|
||||
let isRecording = $state(false);
|
||||
let isDragOver = $state(false);
|
||||
let pitchLockEnabled = $state(loadPitchLockEnabled());
|
||||
let pitchLockFrequency = $state(loadPitchLockFrequency());
|
||||
let pitchLockInput = $state(formatFrequency(loadPitchLockFrequency()));
|
||||
let pitchLockInputValid = $state(true);
|
||||
|
||||
const showDuration = $derived(engineType !== 'sample');
|
||||
const showRandomButton = $derived(engineType === 'generative');
|
||||
const showRecordButton = $derived(engineType === 'input');
|
||||
const showFileDropZone = $derived(engineType === 'sample' && !currentBuffer);
|
||||
const showMutateButton = $derived(engineType === 'generative' && !isProcessed && currentBuffer);
|
||||
const showPitchLock = $derived(engineType === 'generative');
|
||||
const pitchLock = $derived<PitchLock>({ enabled: pitchLockEnabled, frequency: pitchLockFrequency });
|
||||
|
||||
$effect(() => {
|
||||
audioService.setVolume(volume);
|
||||
@ -50,6 +57,14 @@
|
||||
saveDuration(duration);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
savePitchLockEnabled(pitchLockEnabled);
|
||||
});
|
||||
|
||||
$effect(() => {
|
||||
savePitchLockFrequency(pitchLockFrequency);
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
audioService.setPlaybackUpdateCallback((position) => {
|
||||
playbackPosition = position;
|
||||
@ -78,7 +93,7 @@
|
||||
});
|
||||
|
||||
function generateRandom() {
|
||||
currentParams = engine.randomParams();
|
||||
currentParams = engine.randomParams(pitchLock);
|
||||
waveformColor = generateRandomColor();
|
||||
isProcessed = false;
|
||||
regenerateBuffer();
|
||||
@ -89,11 +104,28 @@
|
||||
generateRandom();
|
||||
return;
|
||||
}
|
||||
currentParams = engine.mutateParams(currentParams);
|
||||
currentParams = engine.mutateParams(currentParams, 0.15, pitchLock);
|
||||
waveformColor = generateRandomColor();
|
||||
regenerateBuffer();
|
||||
}
|
||||
|
||||
function handlePitchLockInput(event: Event) {
|
||||
const input = event.target as HTMLInputElement;
|
||||
pitchLockInput = input.value;
|
||||
|
||||
const parsed = parseFrequencyInput(pitchLockInput);
|
||||
if (parsed !== null) {
|
||||
pitchLockFrequency = parsed;
|
||||
pitchLockInputValid = true;
|
||||
} else {
|
||||
pitchLockInputValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
function togglePitchLock() {
|
||||
pitchLockEnabled = !pitchLockEnabled;
|
||||
}
|
||||
|
||||
function regenerateBuffer() {
|
||||
if (!currentParams) return;
|
||||
|
||||
@ -154,7 +186,7 @@
|
||||
|
||||
try {
|
||||
await engine.loadFile(file);
|
||||
currentParams = engine.randomParams();
|
||||
currentParams = engine.randomParams(pitchLock);
|
||||
waveformColor = generateRandomColor();
|
||||
isProcessed = false;
|
||||
regenerateBuffer();
|
||||
@ -170,7 +202,7 @@
|
||||
try {
|
||||
isRecording = true;
|
||||
await engine.record(duration);
|
||||
currentParams = engine.randomParams();
|
||||
currentParams = engine.randomParams(pitchLock);
|
||||
waveformColor = generateRandomColor();
|
||||
isProcessed = false;
|
||||
regenerateBuffer();
|
||||
@ -247,9 +279,44 @@
|
||||
{/each}
|
||||
</div>
|
||||
<div class="controls-group">
|
||||
{#if showPitchLock}
|
||||
<div class="control-item pitch-lock-control">
|
||||
<div class="control-header">
|
||||
<label for="pitch">Pitch</label>
|
||||
<label class="custom-checkbox" title="Lock pitch across random/mutate">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={pitchLockEnabled}
|
||||
/>
|
||||
<span class="checkbox-box" class:checked={pitchLockEnabled}>
|
||||
{#if pitchLockEnabled}
|
||||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none">
|
||||
<path d="M1 5L4 8L9 2" stroke="currentColor" stroke-width="2" stroke-linecap="square"/>
|
||||
</svg>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="checkbox-text">Lock</span>
|
||||
</label>
|
||||
</div>
|
||||
<input
|
||||
id="pitch"
|
||||
type="text"
|
||||
class="pitch-input"
|
||||
class:locked={pitchLockEnabled}
|
||||
class:invalid={!pitchLockInputValid}
|
||||
value={pitchLockInput}
|
||||
oninput={handlePitchLockInput}
|
||||
placeholder="440 or A4"
|
||||
title="Enter frequency (Hz) or note (e.g., A4, C#3)"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
{#if showDuration}
|
||||
<div class="slider-control duration-slider">
|
||||
<label for="duration">Duration: {duration.toFixed(2)}s</label>
|
||||
<div class="control-item">
|
||||
<div class="control-header">
|
||||
<label for="duration">Duration</label>
|
||||
<span class="control-value-display">{duration.toFixed(2)}s</span>
|
||||
</div>
|
||||
<input
|
||||
id="duration"
|
||||
type="range"
|
||||
@ -260,8 +327,11 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="slider-control">
|
||||
<label for="volume">Volume</label>
|
||||
<div class="control-item">
|
||||
<div class="control-header">
|
||||
<label for="volume">Volume</label>
|
||||
<span class="control-value-display">{Math.round(volume * 100)}%</span>
|
||||
</div>
|
||||
<input
|
||||
id="volume"
|
||||
type="range"
|
||||
@ -431,26 +501,166 @@
|
||||
.controls-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-control {
|
||||
.control-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
background-color: #0f0f0f;
|
||||
padding: 0.5rem 0.65rem;
|
||||
border: 1px solid #2a2a2a;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.control-item:hover {
|
||||
border-color: #3a3a3a;
|
||||
}
|
||||
|
||||
.control-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.slider-control label {
|
||||
font-size: 0.85rem;
|
||||
.control-header label {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
.slider-control input[type="range"] {
|
||||
flex: 1;
|
||||
.control-value-display {
|
||||
font-size: 0.8rem;
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 3.5rem;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.control-item input[type="range"] {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.custom-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.custom-checkbox input[type="checkbox"] {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.checkbox-box {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border: 1px solid #3a3a3a;
|
||||
background-color: #1a1a1a;
|
||||
transition: all 0.2s;
|
||||
color: #0a0a0a;
|
||||
}
|
||||
|
||||
.checkbox-box.checked {
|
||||
background-color: #646cff;
|
||||
border-color: #646cff;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.custom-checkbox:hover .checkbox-box {
|
||||
border-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.custom-checkbox:hover .checkbox-box.checked {
|
||||
background-color: #535bf2;
|
||||
border-color: #535bf2;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #999;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.custom-checkbox:hover .checkbox-text {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.pitch-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 0.35rem 0.5rem;
|
||||
background-color: #1a1a1a;
|
||||
border: 1px solid #3a3a3a;
|
||||
color: #fff;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 600;
|
||||
transition: border-color 0.2s, background-color 0.2s, box-shadow 0.2s;
|
||||
font-variant-numeric: tabular-nums;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pitch-input:focus {
|
||||
outline: none;
|
||||
border-color: #646cff;
|
||||
background-color: #0a0a0a;
|
||||
}
|
||||
|
||||
.pitch-input.locked {
|
||||
border-color: #646cff;
|
||||
background-color: #0a0a0a;
|
||||
box-shadow: 0 0 0 1px rgba(100, 108, 255, 0.2);
|
||||
}
|
||||
|
||||
.pitch-input.locked:focus {
|
||||
box-shadow: 0 0 0 1px rgba(100, 108, 255, 0.4);
|
||||
}
|
||||
|
||||
.pitch-input.invalid {
|
||||
border-color: #ff4444;
|
||||
box-shadow: 0 0 0 1px rgba(255, 68, 68, 0.2);
|
||||
}
|
||||
|
||||
.pitch-input::placeholder {
|
||||
color: #555;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@media (min-width: 640px) {
|
||||
.controls-group {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.control-item {
|
||||
min-width: 140px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.pitch-lock-control {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.control-item input[type="range"] {
|
||||
min-width: 80px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
@ -484,17 +694,51 @@
|
||||
}
|
||||
|
||||
.controls-group {
|
||||
flex-direction: row;
|
||||
width: auto;
|
||||
flex-shrink: 0;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.slider-control input[type="range"] {
|
||||
width: 80px;
|
||||
.control-item {
|
||||
min-width: 120px;
|
||||
padding: 0.45rem 0.6rem;
|
||||
}
|
||||
|
||||
.duration-slider input[type="range"] {
|
||||
width: 150px;
|
||||
.pitch-lock-control {
|
||||
min-width: 140px;
|
||||
}
|
||||
|
||||
.control-header label {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.control-value-display {
|
||||
font-size: 0.75rem;
|
||||
min-width: 3rem;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 0.65rem;
|
||||
}
|
||||
|
||||
.checkbox-box {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.checkbox-box svg {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.pitch-input {
|
||||
font-size: 0.8rem;
|
||||
padding: 0.3rem 0.45rem;
|
||||
}
|
||||
|
||||
.control-item input[type="range"] {
|
||||
min-width: 70px;
|
||||
}
|
||||
}
|
||||
|
||||
@ -503,18 +747,59 @@
|
||||
max-width: 65%;
|
||||
}
|
||||
|
||||
.slider-control input[type="range"] {
|
||||
width: 100px;
|
||||
.control-item {
|
||||
min-width: 140px;
|
||||
padding: 0.5rem 0.65rem;
|
||||
}
|
||||
|
||||
.duration-slider input[type="range"] {
|
||||
width: 200px;
|
||||
.pitch-lock-control {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.control-header label {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.control-value-display {
|
||||
font-size: 0.8rem;
|
||||
min-width: 3.5rem;
|
||||
}
|
||||
|
||||
.checkbox-text {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.checkbox-box {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.checkbox-box svg {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.pitch-input {
|
||||
font-size: 0.85rem;
|
||||
padding: 0.35rem 0.5rem;
|
||||
}
|
||||
|
||||
.control-item input[type="range"] {
|
||||
min-width: 90px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1280px) {
|
||||
.duration-slider input[type="range"] {
|
||||
width: 300px;
|
||||
.control-item {
|
||||
min-width: 160px;
|
||||
}
|
||||
|
||||
.pitch-lock-control {
|
||||
min-width: 180px;
|
||||
}
|
||||
|
||||
.control-item input[type="range"] {
|
||||
min-width: 110px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
enum EnvCurve {
|
||||
Linear,
|
||||
@ -380,9 +380,11 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
||||
}
|
||||
}
|
||||
|
||||
randomParams(): AdditiveParams {
|
||||
randomParams(pitchLock?: PitchLock): AdditiveParams {
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
const baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
|
||||
const harmonicSeriesType = this.randomInt(0, 3) as HarmonicSeriesType;
|
||||
const distributionStrategy = this.randomInt(0, 9) as DistributionStrategy;
|
||||
@ -514,7 +516,7 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: AdditiveParams, mutationAmount: number = 0.15): AdditiveParams {
|
||||
mutateParams(params: AdditiveParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): AdditiveParams {
|
||||
const newHarmonicSeriesType = Math.random() < 0.08 ? this.randomInt(0, 3) as HarmonicSeriesType : params.harmonicSeriesType;
|
||||
const newDistributionStrategy = Math.random() < 0.08 ? this.randomInt(0, 9) as DistributionStrategy : params.distributionStrategy;
|
||||
const ratios = this.generateRatiosForStrategy(newDistributionStrategy);
|
||||
@ -530,8 +532,10 @@ export class AdditiveEngine implements SynthEngine<AdditiveParams> {
|
||||
}
|
||||
}
|
||||
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq: params.baseFreq,
|
||||
baseFreq,
|
||||
partials: newPartials,
|
||||
stereoWidth: this.mutateValue(params.stereoWidth, mutationAmount, 0, 1),
|
||||
brightness: this.mutateValue(params.brightness, mutationAmount, 0, 1),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
interface BenjolinParams {
|
||||
// Core oscillators
|
||||
@ -808,7 +808,7 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
|
||||
}
|
||||
}
|
||||
|
||||
randomParams(): BenjolinParams {
|
||||
randomParams(pitchLock?: PitchLock): BenjolinParams {
|
||||
// Choose a random preset configuration
|
||||
const preset = Math.floor(Math.random() * PRESET_COUNT);
|
||||
|
||||
@ -818,7 +818,9 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
|
||||
// Generate full parameter set with preset biases
|
||||
const params: BenjolinParams = {
|
||||
// Core oscillators
|
||||
osc1Freq: presetParams.osc1Freq ?? (20 + Math.random() * 800),
|
||||
osc1Freq: pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: presetParams.osc1Freq ?? (20 + Math.random() * 800),
|
||||
osc2Freq: presetParams.osc2Freq ?? (30 + Math.random() * 1200),
|
||||
osc1Wave: presetParams.osc1Wave ?? Math.random(),
|
||||
osc2Wave: presetParams.osc2Wave ?? Math.random(),
|
||||
@ -876,17 +878,17 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
|
||||
return params;
|
||||
}
|
||||
|
||||
mutateParams(params: BenjolinParams): BenjolinParams {
|
||||
mutateParams(params: BenjolinParams, mutationAmount?: number, pitchLock?: PitchLock): BenjolinParams {
|
||||
const mutated = { ...params };
|
||||
|
||||
// Determine mutation strength based on current "stability"
|
||||
const stability = (params.crossMod1to2 + params.crossMod2to1) / 2 +
|
||||
params.runglerChaos + params.evolutionDepth;
|
||||
const mutationAmount = stability > 1.5 ? 0.05 : stability < 0.5 ? 0.2 : 0.1;
|
||||
const mutAmount = mutationAmount ?? (stability > 1.5 ? 0.05 : stability < 0.5 ? 0.2 : 0.1);
|
||||
|
||||
// Helper for correlated mutations
|
||||
const mutateValue = (value: number, min: number, max: number, correlation = 1): number => {
|
||||
const delta = (Math.random() - 0.5) * mutationAmount * (max - min) * correlation;
|
||||
const delta = (Math.random() - 0.5) * mutAmount * (max - min) * correlation;
|
||||
return Math.max(min, Math.min(max, value + delta));
|
||||
};
|
||||
|
||||
@ -895,9 +897,15 @@ export class Benjolin implements SynthEngine<BenjolinParams> {
|
||||
|
||||
if (strategy < 0.3) {
|
||||
// Mutate frequency relationships
|
||||
const freqRatio = mutated.osc2Freq / mutated.osc1Freq;
|
||||
mutated.osc1Freq = mutateValue(mutated.osc1Freq, 20, 2000);
|
||||
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
|
||||
if (pitchLock?.enabled) {
|
||||
mutated.osc1Freq = pitchLock.frequency;
|
||||
const freqRatio = mutated.osc2Freq / params.osc1Freq;
|
||||
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
|
||||
} else {
|
||||
const freqRatio = mutated.osc2Freq / mutated.osc1Freq;
|
||||
mutated.osc1Freq = mutateValue(mutated.osc1Freq, 20, 2000);
|
||||
mutated.osc2Freq = mutated.osc1Freq * mutateValue(freqRatio, 0.5, 4);
|
||||
}
|
||||
|
||||
// Correlate cross-mod amounts
|
||||
const crossModDelta = (Math.random() - 0.5) * mutationAmount;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
enum OscillatorWaveform {
|
||||
Sine,
|
||||
@ -427,22 +427,33 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
|
||||
return [output, v1Next, v2Next];
|
||||
}
|
||||
|
||||
randomParams(): DubSirenParams {
|
||||
const freqPairs = [
|
||||
[100, 1200],
|
||||
[200, 800],
|
||||
[300, 2000],
|
||||
[50, 400],
|
||||
[500, 3000],
|
||||
[150, 600],
|
||||
];
|
||||
randomParams(pitchLock?: PitchLock): DubSirenParams {
|
||||
let startFreq: number;
|
||||
let endFreq: number;
|
||||
|
||||
const [startFreq, endFreq] = this.randomChoice(freqPairs);
|
||||
const shouldReverse = Math.random() < 0.3;
|
||||
if (pitchLock?.enabled) {
|
||||
// When pitch locked, sweep around the locked frequency
|
||||
startFreq = pitchLock.frequency;
|
||||
endFreq = pitchLock.frequency * (Math.random() < 0.5 ? 0.5 : 2);
|
||||
} else {
|
||||
const freqPairs = [
|
||||
[100, 1200],
|
||||
[200, 800],
|
||||
[300, 2000],
|
||||
[50, 400],
|
||||
[500, 3000],
|
||||
[150, 600],
|
||||
];
|
||||
|
||||
const [freq1, freq2] = this.randomChoice(freqPairs);
|
||||
const shouldReverse = Math.random() < 0.3;
|
||||
startFreq = shouldReverse ? freq2 : freq1;
|
||||
endFreq = shouldReverse ? freq1 : freq2;
|
||||
}
|
||||
|
||||
return {
|
||||
startFreq: shouldReverse ? endFreq : startFreq,
|
||||
endFreq: shouldReverse ? startFreq : endFreq,
|
||||
startFreq,
|
||||
endFreq,
|
||||
sweepCurve: this.randomInt(0, 4) as SweepCurve,
|
||||
waveform: this.randomInt(0, 4) as OscillatorWaveform,
|
||||
pulseWidth: this.randomRange(0.1, 0.9),
|
||||
@ -464,10 +475,27 @@ export class DubSiren implements SynthEngine<DubSirenParams> {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: DubSirenParams, mutationAmount: number = 0.15): DubSirenParams {
|
||||
mutateParams(params: DubSirenParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): DubSirenParams {
|
||||
let startFreq: number;
|
||||
let endFreq: number;
|
||||
|
||||
if (pitchLock?.enabled) {
|
||||
// When pitch locked, keep one frequency at the locked value
|
||||
if (Math.random() < 0.5) {
|
||||
startFreq = pitchLock.frequency;
|
||||
endFreq = this.mutateValue(params.endFreq, mutationAmount, 20, 5000);
|
||||
} else {
|
||||
startFreq = this.mutateValue(params.startFreq, mutationAmount, 20, 5000);
|
||||
endFreq = pitchLock.frequency;
|
||||
}
|
||||
} else {
|
||||
startFreq = this.mutateValue(params.startFreq, mutationAmount, 20, 5000);
|
||||
endFreq = this.mutateValue(params.endFreq, mutationAmount, 20, 5000);
|
||||
}
|
||||
|
||||
return {
|
||||
startFreq: this.mutateValue(params.startFreq, mutationAmount, 20, 5000),
|
||||
endFreq: this.mutateValue(params.endFreq, mutationAmount, 20, 5000),
|
||||
startFreq,
|
||||
endFreq,
|
||||
sweepCurve: Math.random() < 0.1 ? this.randomInt(0, 4) as SweepCurve : params.sweepCurve,
|
||||
waveform: Math.random() < 0.1 ? this.randomInt(0, 4) as OscillatorWaveform : params.waveform,
|
||||
pulseWidth: this.mutateValue(params.pulseWidth, mutationAmount, 0.05, 0.95),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
enum EnvCurve {
|
||||
Linear,
|
||||
@ -420,12 +420,14 @@ export class FourOpFM implements SynthEngine<FourOpFMParams> {
|
||||
}
|
||||
}
|
||||
|
||||
randomParams(): FourOpFMParams {
|
||||
randomParams(pitchLock?: PitchLock): FourOpFMParams {
|
||||
const algorithm = this.randomInt(0, 5) as Algorithm;
|
||||
|
||||
// More musical frequency ratios including inharmonic ones
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
const baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
|
||||
const baseFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.9, 1.1);
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
@ -481,9 +483,11 @@ export class FourOpFM implements SynthEngine<FourOpFMParams> {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: FourOpFMParams, mutationAmount: number = 0.15): FourOpFMParams {
|
||||
mutateParams(params: FourOpFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): FourOpFMParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq: params.baseFreq,
|
||||
baseFreq,
|
||||
algorithm: Math.random() < 0.08 ? this.randomInt(0, 5) as Algorithm : params.algorithm,
|
||||
operators: params.operators.map((op, i) =>
|
||||
this.mutateOperator(op, mutationAmount, i === 3, params.algorithm)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
type HarmonicMode =
|
||||
| 'single' // Just fundamental
|
||||
@ -323,7 +323,7 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
|
||||
}
|
||||
}
|
||||
|
||||
randomParams(): KarplusStrongParams {
|
||||
randomParams(pitchLock?: PitchLock): KarplusStrongParams {
|
||||
// Musical frequencies (notes from E2 to E5)
|
||||
const frequencies = [
|
||||
82.41, 87.31, 92.50, 98.00, 103.83, 110.00, 116.54, 123.47, 130.81, 138.59,
|
||||
@ -333,6 +333,10 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
|
||||
830.61, 880.00, 932.33, 987.77, 1046.50, 1108.73, 1174.66, 1244.51, 1318.51
|
||||
];
|
||||
|
||||
const frequency = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: frequencies[Math.floor(Math.random() * frequencies.length)];
|
||||
|
||||
// Randomly choose harmonic mode
|
||||
// Weighted selection: favor more consonant intervals
|
||||
const modes: HarmonicMode[] = [
|
||||
@ -362,7 +366,7 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
|
||||
];
|
||||
|
||||
return {
|
||||
frequency: frequencies[Math.floor(Math.random() * frequencies.length)],
|
||||
frequency,
|
||||
damping: 0.7 + Math.random() * 0.29, // 0.7 to 0.99
|
||||
brightness: Math.random(), // 0 to 1
|
||||
decayCharacter: (Math.random() * 2 - 1) * 0.8, // -0.8 to 0.8 (mostly natural darkening)
|
||||
@ -378,15 +382,17 @@ export class KarplusStrong implements SynthEngine<KarplusStrongParams> {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: KarplusStrongParams, mutationAmount: number = 0.15): KarplusStrongParams {
|
||||
mutateParams(params: KarplusStrongParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): KarplusStrongParams {
|
||||
const mutate = (value: number, range: number, min: number, max: number) => {
|
||||
const delta = (Math.random() * 2 - 1) * range * mutationAmount;
|
||||
return Math.max(min, Math.min(max, value + delta));
|
||||
};
|
||||
|
||||
// Occasionally jump to harmonic/subharmonic
|
||||
let newFreq = params.frequency;
|
||||
if (Math.random() < 0.15) {
|
||||
// Occasionally jump to harmonic/subharmonic (unless pitch locked)
|
||||
let newFreq: number;
|
||||
if (pitchLock?.enabled) {
|
||||
newFreq = pitchLock.frequency;
|
||||
} else if (Math.random() < 0.15) {
|
||||
const multipliers = [0.5, 2, 1.5, 3];
|
||||
newFreq = params.frequency * multipliers[Math.floor(Math.random() * multipliers.length)];
|
||||
newFreq = Math.max(50, Math.min(2000, newFreq));
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
enum LFOWaveform {
|
||||
Sine,
|
||||
@ -386,9 +386,11 @@ export class Ring implements SynthEngine<RingParams> {
|
||||
}
|
||||
}
|
||||
|
||||
randomParams(): RingParams {
|
||||
randomParams(pitchLock?: PitchLock): RingParams {
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
const carrierFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
const carrierFreq = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
|
||||
const ratioChoices = [0.5, 0.707, 1, 1.414, 1.732, 2, 2.236, 3, 3.732, 5, 7, 11, 13, 17];
|
||||
const modulatorRatio = this.randomChoice(ratioChoices);
|
||||
@ -434,27 +436,29 @@ export class Ring implements SynthEngine<RingParams> {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: RingParams, mutationAmount: number = 0.15): RingParams {
|
||||
mutateParams(params: RingParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): RingParams {
|
||||
const ratioChoices = [0.5, 0.707, 1, 1.414, 1.732, 2, 2.236, 3, 3.732, 5, 7, 11, 13, 17];
|
||||
let modulatorFreq = params.modulatorFreq;
|
||||
let secondModulatorFreq = params.secondModulatorFreq;
|
||||
|
||||
const carrierFreq = pitchLock?.enabled ? pitchLock.frequency : params.carrierFreq;
|
||||
|
||||
if (Math.random() < 0.1) {
|
||||
const newRatio = this.randomChoice(ratioChoices);
|
||||
modulatorFreq = params.carrierFreq * newRatio * this.randomRange(0.98, 1.02);
|
||||
modulatorFreq = carrierFreq * newRatio * this.randomRange(0.98, 1.02);
|
||||
} else {
|
||||
modulatorFreq = this.mutateValue(params.modulatorFreq, mutationAmount, 20, 2000);
|
||||
}
|
||||
|
||||
if (Math.random() < 0.1) {
|
||||
const newRatio = this.randomChoice(ratioChoices);
|
||||
secondModulatorFreq = params.carrierFreq * newRatio * this.randomRange(0.97, 1.03);
|
||||
secondModulatorFreq = carrierFreq * newRatio * this.randomRange(0.97, 1.03);
|
||||
} else {
|
||||
secondModulatorFreq = this.mutateValue(params.secondModulatorFreq, mutationAmount, 20, 2000);
|
||||
}
|
||||
|
||||
return {
|
||||
carrierFreq: params.carrierFreq,
|
||||
carrierFreq,
|
||||
modulatorFreq,
|
||||
secondModulatorFreq,
|
||||
carrierLevel: this.mutateValue(params.carrierLevel, mutationAmount, 0.3, 1.0),
|
||||
|
||||
@ -2,14 +2,20 @@
|
||||
// The duration parameter should be used to scale time-based parameters (envelopes, LFOs, etc.)
|
||||
// Time-based parameters should be stored as ratios (0-1) and scaled by duration during generation
|
||||
// Engines must generate stereo output: [leftChannel, rightChannel]
|
||||
// When pitch lock is provided, engines must use the locked frequency and preserve it across randomization/mutation
|
||||
|
||||
export type EngineType = 'generative' | 'sample' | 'input';
|
||||
|
||||
export interface PitchLock {
|
||||
enabled: boolean;
|
||||
frequency: number; // Frequency in Hz
|
||||
}
|
||||
|
||||
export interface SynthEngine<T = any> {
|
||||
getName(): string;
|
||||
getDescription(): string;
|
||||
getType(): EngineType;
|
||||
generate(params: T, sampleRate: number, duration: number): [Float32Array, Float32Array];
|
||||
randomParams(): T;
|
||||
mutateParams(params: T, mutationAmount?: number): T;
|
||||
generate(params: T, sampleRate: number, duration: number, pitchLock?: PitchLock): [Float32Array, Float32Array];
|
||||
randomParams(pitchLock?: PitchLock): T;
|
||||
mutateParams(params: T, mutationAmount?: number, pitchLock?: PitchLock): T;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
enum EnvCurve {
|
||||
Linear,
|
||||
@ -356,11 +356,16 @@ export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
|
||||
}
|
||||
}
|
||||
|
||||
randomParams(): TwoOpFMParams {
|
||||
randomParams(pitchLock?: PitchLock): TwoOpFMParams {
|
||||
const algorithm = this.randomInt(0, 2) as Algorithm;
|
||||
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
const baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
let baseFreq: number;
|
||||
if (pitchLock?.enabled) {
|
||||
baseFreq = pitchLock.frequency;
|
||||
} else {
|
||||
const baseFreqChoices = [55, 82.4, 110, 146.8, 220, 293.7, 440, 587.3, 880];
|
||||
baseFreq = this.randomChoice(baseFreqChoices) * this.randomRange(0.95, 1.05);
|
||||
}
|
||||
|
||||
return {
|
||||
baseFreq,
|
||||
@ -433,9 +438,11 @@ export class TwoOpFM implements SynthEngine<TwoOpFMParams> {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15): TwoOpFMParams {
|
||||
mutateParams(params: TwoOpFMParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): TwoOpFMParams {
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
baseFreq: params.baseFreq,
|
||||
baseFreq,
|
||||
algorithm: Math.random() < 0.1 ? this.randomInt(0, 2) as Algorithm : params.algorithm,
|
||||
operators: params.operators.map((op, i) =>
|
||||
this.mutateOperator(op, mutationAmount, i === 1, params.algorithm)
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
|
||||
interface WavetableParams {
|
||||
bankIndex: number;
|
||||
@ -211,12 +211,14 @@ export class WavetableEngine implements SynthEngine<WavetableParams> {
|
||||
return [left, right];
|
||||
}
|
||||
|
||||
randomParams(): WavetableParams {
|
||||
randomParams(pitchLock?: PitchLock): WavetableParams {
|
||||
const freqs = [110, 146.8, 220, 293.7, 440];
|
||||
return {
|
||||
bankIndex: Math.random(),
|
||||
position: 0.2 + Math.random() * 0.6,
|
||||
baseFreq: freqs[Math.floor(Math.random() * freqs.length)],
|
||||
baseFreq: pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: freqs[Math.floor(Math.random() * freqs.length)],
|
||||
filterCutoff: 0.5 + Math.random() * 0.4,
|
||||
filterResonance: Math.random() * 0.5,
|
||||
attack: 0.001 + Math.random() * 0.05,
|
||||
@ -226,15 +228,17 @@ export class WavetableEngine implements SynthEngine<WavetableParams> {
|
||||
};
|
||||
}
|
||||
|
||||
mutateParams(params: WavetableParams): WavetableParams {
|
||||
mutateParams(params: WavetableParams, mutationAmount?: number, pitchLock?: PitchLock): WavetableParams {
|
||||
const mutate = (v: number, amount: number = 0.1) => {
|
||||
return Math.max(0, Math.min(1, v + (Math.random() - 0.5) * amount));
|
||||
};
|
||||
|
||||
const baseFreq = pitchLock?.enabled ? pitchLock.frequency : params.baseFreq;
|
||||
|
||||
return {
|
||||
bankIndex: Math.random() < 0.2 ? Math.random() : params.bankIndex,
|
||||
position: mutate(params.position, 0.2),
|
||||
baseFreq: params.baseFreq,
|
||||
baseFreq,
|
||||
filterCutoff: mutate(params.filterCutoff, 0.2),
|
||||
filterResonance: mutate(params.filterResonance, 0.15),
|
||||
attack: mutate(params.attack, 0.1),
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { SynthEngine } from './SynthEngine';
|
||||
import type { SynthEngine, PitchLock } from './SynthEngine';
|
||||
// @ts-ignore
|
||||
import { ZZFX } from 'zzfx';
|
||||
|
||||
@ -125,15 +125,18 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return [leftBuffer, rightBuffer];
|
||||
}
|
||||
|
||||
randomParams(): ZzfxParams {
|
||||
randomParams(pitchLock?: PitchLock): ZzfxParams {
|
||||
const preset = Math.floor(Math.random() * 20);
|
||||
|
||||
const getFrequency = (min: number, max: number) =>
|
||||
pitchLock?.enabled ? pitchLock.frequency : this.randomRange(min, max);
|
||||
|
||||
switch (preset) {
|
||||
case 0: // Clean tones
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.randomRange(0, 0.05),
|
||||
frequency: this.randomRange(200, 800),
|
||||
frequency: getFrequency(200, 800),
|
||||
attack: this.randomRange(0, 0.02),
|
||||
sustain: this.randomRange(0.2, 0.5),
|
||||
release: this.randomRange(0.1, 0.3),
|
||||
@ -157,7 +160,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(400, 1200),
|
||||
frequency: getFrequency(400, 1200),
|
||||
attack: this.randomRange(0, 0.01),
|
||||
sustain: this.randomRange(0.15, 0.35),
|
||||
release: this.randomRange(0.05, 0.2),
|
||||
@ -181,7 +184,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(300, 1000),
|
||||
frequency: getFrequency(300, 1000),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.2, 0.4),
|
||||
release: this.randomRange(0.05, 0.15),
|
||||
@ -205,7 +208,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.randomRange(0, 0.1),
|
||||
frequency: this.randomRange(150, 600),
|
||||
frequency: getFrequency(150, 600),
|
||||
attack: this.randomRange(0, 0.03),
|
||||
sustain: this.randomRange(0.15, 0.35),
|
||||
release: this.randomRange(0.05, 0.2),
|
||||
@ -229,7 +232,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(200, 1500),
|
||||
frequency: getFrequency(200, 1500),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.1, 0.25),
|
||||
release: this.randomRange(0.02, 0.1),
|
||||
@ -253,7 +256,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(40, 200),
|
||||
frequency: getFrequency(40, 200),
|
||||
attack: this.randomRange(0, 0.02),
|
||||
sustain: this.randomRange(0.25, 0.5),
|
||||
release: this.randomRange(0.1, 0.25),
|
||||
@ -277,7 +280,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(300, 1200),
|
||||
frequency: getFrequency(300, 1200),
|
||||
attack: this.randomRange(0, 0.03),
|
||||
sustain: this.randomRange(0.2, 0.4),
|
||||
release: this.randomRange(0.08, 0.2),
|
||||
@ -301,7 +304,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(800, 2000),
|
||||
frequency: getFrequency(800, 2000),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.05, 0.15),
|
||||
release: this.randomRange(0.02, 0.08),
|
||||
@ -325,7 +328,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.randomRange(0.1, 0.15),
|
||||
frequency: this.randomRange(50, 150),
|
||||
frequency: getFrequency(50, 150),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.15, 0.3),
|
||||
release: this.randomRange(0.15, 0.35),
|
||||
@ -349,7 +352,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(500, 1000),
|
||||
frequency: getFrequency(500, 1000),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.05, 0.1),
|
||||
release: this.randomRange(0.1, 0.2),
|
||||
@ -373,7 +376,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(600, 1400),
|
||||
frequency: getFrequency(600, 1400),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.03, 0.08),
|
||||
release: this.randomRange(0.02, 0.05),
|
||||
@ -397,7 +400,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(200, 500),
|
||||
frequency: getFrequency(200, 500),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.2, 0.35),
|
||||
release: this.randomRange(0.1, 0.2),
|
||||
@ -421,7 +424,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(300, 600),
|
||||
frequency: getFrequency(300, 600),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.08, 0.15),
|
||||
release: this.randomRange(0.05, 0.12),
|
||||
@ -445,7 +448,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(400, 800),
|
||||
frequency: getFrequency(400, 800),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.3, 0.5),
|
||||
release: this.randomRange(0.05, 0.1),
|
||||
@ -469,7 +472,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(60, 150),
|
||||
frequency: getFrequency(60, 150),
|
||||
attack: this.randomRange(0, 0.02),
|
||||
sustain: this.randomRange(0.3, 0.5),
|
||||
release: this.randomRange(0.1, 0.2),
|
||||
@ -493,7 +496,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(400, 800),
|
||||
frequency: getFrequency(400, 800),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.25, 0.4),
|
||||
release: this.randomRange(0.15, 0.3),
|
||||
@ -517,7 +520,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.randomRange(0.05, 0.1),
|
||||
frequency: this.randomRange(200, 400),
|
||||
frequency: getFrequency(200, 400),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.05, 0.12),
|
||||
release: this.randomRange(0.05, 0.15),
|
||||
@ -541,7 +544,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: 0,
|
||||
frequency: this.randomRange(400, 1000),
|
||||
frequency: getFrequency(400, 1000),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.2, 0.35),
|
||||
release: this.randomRange(0.05, 0.12),
|
||||
@ -565,7 +568,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.randomRange(0.08, 0.15),
|
||||
frequency: this.randomRange(80, 250),
|
||||
frequency: getFrequency(80, 250),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.03, 0.08),
|
||||
release: this.randomRange(0.05, 0.15),
|
||||
@ -589,7 +592,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.randomRange(0, 0.05),
|
||||
frequency: this.randomRange(1000, 2000),
|
||||
frequency: getFrequency(1000, 2000),
|
||||
attack: 0,
|
||||
sustain: this.randomRange(0.15, 0.3),
|
||||
release: this.randomRange(0.15, 0.3),
|
||||
@ -613,7 +616,7 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.randomRange(0, 0.05),
|
||||
frequency: this.randomRange(200, 1200),
|
||||
frequency: getFrequency(200, 1200),
|
||||
attack: this.randomRange(0, 0.03),
|
||||
sustain: this.randomRange(0.15, 0.4),
|
||||
release: this.randomRange(0.05, 0.2),
|
||||
@ -635,11 +638,15 @@ export class ZzfxEngine implements SynthEngine<ZzfxParams> {
|
||||
}
|
||||
}
|
||||
|
||||
mutateParams(params: ZzfxParams, mutationAmount: number = 0.15): ZzfxParams {
|
||||
mutateParams(params: ZzfxParams, mutationAmount: number = 0.15, pitchLock?: PitchLock): ZzfxParams {
|
||||
const frequency = pitchLock?.enabled
|
||||
? pitchLock.frequency
|
||||
: this.mutateValue(params.frequency, mutationAmount * 2, 40, 1500);
|
||||
|
||||
return {
|
||||
volume: 1,
|
||||
randomness: this.mutateValue(params.randomness, mutationAmount, 0, 0.1),
|
||||
frequency: this.mutateValue(params.frequency, mutationAmount * 2, 40, 1500),
|
||||
frequency,
|
||||
attack: this.mutateValue(params.attack, mutationAmount, 0, 0.03),
|
||||
sustain: this.mutateValue(params.sustain, mutationAmount, 0.1, 0.5),
|
||||
release: this.mutateValue(params.release, mutationAmount, 0.02, 0.3),
|
||||
|
||||
33
src/lib/utils/pitch.ts
Normal file
33
src/lib/utils/pitch.ts
Normal file
@ -0,0 +1,33 @@
|
||||
const NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'];
|
||||
const A4_FREQUENCY = 440;
|
||||
const A4_MIDI_NOTE = 69;
|
||||
|
||||
export function noteToFrequency(noteName: string): number | null {
|
||||
const match = noteName.match(/^([A-G]#?)(-?\d+)$/i);
|
||||
if (!match) return null;
|
||||
|
||||
const [, note, octave] = match;
|
||||
const noteIndex = NOTE_NAMES.indexOf(note.toUpperCase());
|
||||
if (noteIndex === -1) return null;
|
||||
|
||||
const octaveNum = parseInt(octave, 10);
|
||||
const midiNote = (octaveNum + 1) * 12 + noteIndex;
|
||||
const semitonesDiff = midiNote - A4_MIDI_NOTE;
|
||||
|
||||
return A4_FREQUENCY * Math.pow(2, semitonesDiff / 12);
|
||||
}
|
||||
|
||||
export function parseFrequencyInput(input: string): number | null {
|
||||
const trimmed = input.trim();
|
||||
|
||||
const asNumber = parseFloat(trimmed);
|
||||
if (!isNaN(asNumber) && asNumber > 0 && asNumber < 20000) {
|
||||
return asNumber;
|
||||
}
|
||||
|
||||
return noteToFrequency(trimmed);
|
||||
}
|
||||
|
||||
export function formatFrequency(frequency: number): string {
|
||||
return frequency.toFixed(2);
|
||||
}
|
||||
@ -1,9 +1,13 @@
|
||||
const DEFAULT_VOLUME = 0.7;
|
||||
const DEFAULT_DURATION = 1.0;
|
||||
const DEFAULT_PITCH_LOCK_ENABLED = false;
|
||||
const DEFAULT_PITCH_LOCK_FREQUENCY = 440;
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
VOLUME: 'volume',
|
||||
DURATION: 'duration',
|
||||
PITCH_LOCK_ENABLED: 'pitchLockEnabled',
|
||||
PITCH_LOCK_FREQUENCY: 'pitchLockFrequency',
|
||||
} as const;
|
||||
|
||||
export function loadVolume(): number {
|
||||
@ -23,3 +27,21 @@ export function loadDuration(): number {
|
||||
export function saveDuration(duration: number): void {
|
||||
localStorage.setItem(STORAGE_KEYS.DURATION, duration.toString());
|
||||
}
|
||||
|
||||
export function loadPitchLockEnabled(): boolean {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.PITCH_LOCK_ENABLED);
|
||||
return stored ? stored === 'true' : DEFAULT_PITCH_LOCK_ENABLED;
|
||||
}
|
||||
|
||||
export function savePitchLockEnabled(enabled: boolean): void {
|
||||
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_ENABLED, enabled.toString());
|
||||
}
|
||||
|
||||
export function loadPitchLockFrequency(): number {
|
||||
const stored = localStorage.getItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY);
|
||||
return stored ? parseFloat(stored) : DEFAULT_PITCH_LOCK_FREQUENCY;
|
||||
}
|
||||
|
||||
export function savePitchLockFrequency(frequency: number): void {
|
||||
localStorage.setItem(STORAGE_KEYS.PITCH_LOCK_FREQUENCY, frequency.toString());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user