diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..578b41dc 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -1,14 +1,12 @@ import { useDrumStore, DrumParams } from '../store/instrumentStore' import { Knob } from './Knob' -import { useBassStore, useHarmonyStore } from '../store/instrumentStore' -import { generateBassPattern } from '../logic/StingGenerator' import { Dices } from 'lucide-react' import { useAudioStore, AudioState } from '../store/audioStore' import { bjorklund, rotateArray } from '../logic/bjorklund' import { TransportControls } from './TransportControls' export function DrumsView() { - const { kick, snare, hihat, hihatOpen, clap, kit, drive, setParams, setKit, setDrive } = useDrumStore() + const { kick, snare, hihat, hihatOpen, clap, kit, drive, setParams, setKit, setDrive, randomizeDrums } = useDrumStore() const { drumMachine, volumes, setVolume } = useAudioStore() const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => { @@ -30,6 +28,22 @@ export function DrumsView() { if (drumMachine) drumMachine.setSaturation(v) } + const handleRandomize = () => { + if (window.Telegram?.WebApp?.HapticFeedback) { + window.Telegram.WebApp.HapticFeedback.impactOccurred('medium') + } + randomizeDrums(); + + // Ensure engine is synced immediately + if (drumMachine) { + const drums: ('kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell')[] = ['kick', 'snare', 'hihat', 'hihatOpen', 'clap', 'cowbell']; + drums.forEach(id => { + const d = useDrumStore.getState()[id]; + drumMachine.setDrumParams(id, d.pitch, d.decay); + }); + } + } + return (
@@ -38,6 +52,24 @@ export function DrumsView() {

Настройки

+ ( -
-
{d.label}
- state[d.id].pulses)} - min={0} max={16} step={1} - onChange={(v) => updateDrum(d.id, { pulses: v })} - size={40} - /> - state[d.id].pitch)} - min={0} max={1} step={0.01} - onChange={(v) => updateDrum(d.id, { pitch: v })} - size={40} - /> - state[d.id].decay)} - min={0} max={1} step={0.01} - onChange={(v) => updateDrum(d.id, { decay: v })} - size={40} - /> - state[d.id].probability)} - min={0} max={1} step={0.01} - onChange={(v) => updateDrum(d.id, { probability: v })} - size={40} - /> - setVolume(d.id === 'cowbell' ? 'cow' : d.id, v)} - size={40} - /> -
- ))} + ].map(d => { + const currentParams = useDrumStore((state) => state[d.id]); + return ( +
+
{d.label}
+ updateDrum(d.id, { pulses: v })} + size={40} + /> + updateDrum(d.id, { pitch: v })} + size={40} + /> + updateDrum(d.id, { decay: v })} + size={40} + /> + updateDrum(d.id, { probability: v })} + size={40} + /> + setVolume(d.id === 'cowbell' ? 'cow' : d.id, v)} + size={40} + /> +
+ ); + })}
{/* Pattern Visualizer */} diff --git a/src/logic/DrumUtils.ts b/src/logic/DrumUtils.ts index b9917c79..d92bd6d7 100644 --- a/src/logic/DrumUtils.ts +++ b/src/logic/DrumUtils.ts @@ -1,3 +1,5 @@ +import * as Tone from 'tone' + /** * Shared DSP utilities for drum synthesis based on research specs. */ @@ -21,13 +23,15 @@ export function makeDistortionCurve(amount: number = 20): Float32Array { } /** - * Applies micro-randomization to a base frequency (Pitch Drift). - * Typically +/- 1Hz as per research. + * Applies micro-randomization to a base frequency (Pitch Drift) using cents. + * Typically +/- 1-2 cents as per research. + * Formula: f_new = f_base * Math.pow(2, cents / 1200) * @param base - Base frequency in Hz - * @param range - Drift range in Hz (default 1.0) + * @param centsRange - Drift range in cents (default 1.5) */ -export function applyPitchDrift(base: number, range: number = 1.0): number { - return base + (Math.random() * 2 - 1) * range; +export function applyPitchDrift(base: number, centsRange: number = 1.5): number { + const cents = (Math.random() * 2 - 1) * centsRange; + return base * Math.pow(2, cents / 1200); } /** @@ -40,3 +44,43 @@ export function applyVariance(base: number, variance: number = 0.02): number { // Math.random() * 0.04 - 0.02 gives range [-0.02, 0.02] return base * (1 + (Math.random() * (variance * 2) - variance)); } + +/** + * Generates an authentic 15-bit LFSR pseudo-random noise buffer. + * emulates the TR-909 digital noise generator. + * Polynomial: x^15 + x^14 + 1 + */ +export function generateLFSRNoise(context: Tone.BaseContext, duration: number = 2.0): AudioBuffer { + const sampleRate = context.sampleRate; + const bufferSize = sampleRate * duration; + const buffer = context.createBuffer(1, bufferSize, sampleRate); + const data = buffer.getChannelData(0); + + let reg = 0x7FFF; // 15-bit register, all ones initial state + + for (let i = 0; i < bufferSize; i++) { + // 909 LFSR typically runs much faster than sample rate, + // but here we generate per sample for the buffer. + const bit = ((reg >> 0) ^ (reg >> 1)) & 1; + reg = (reg >> 1) | (bit << 14); + data[i] = (bit * 2 - 1) * 0.5; // Scale to [-0.5, 0.5] + } + + return buffer; +} + +/** + * Generates a standard white noise buffer. + */ +export function generateWhiteNoise(context: Tone.BaseContext, duration: number = 2.0): AudioBuffer { + const sampleRate = context.sampleRate; + const bufferSize = sampleRate * duration; + const buffer = context.createBuffer(1, bufferSize, sampleRate); + const data = buffer.getChannelData(0); + + for (let i = 0; i < bufferSize; i++) { + data[i] = Math.random() * 2 - 1; + } + + return buffer; +} diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts index 3334a6fe..9af33371 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -1,18 +1,19 @@ import * as Tone from 'tone' -import { applyVariance } from '../DrumUtils' +import { applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils' export class TR808Clap { private noiseBuffer: AudioBuffer; constructor(private destination: Tone.ToneAudioNode) { - const sampleRate = Tone.getContext().sampleRate; - this.noiseBuffer = Tone.getContext().createBuffer(1, sampleRate * 0.5, sampleRate); - const data = this.noiseBuffer.getChannelData(0); - for (let i = 0; i < data.length; i++) data[i] = Math.random() * 2 - 1; + this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 2.0); } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + const randomStart = Math.random() * (this.noiseBuffer.duration - 1.0); + const bpfFreq = (1000 + pitch * 1000); const bpf = new Tone.Filter(applyVariance(bpfFreq, 0.02), "bandpass"); const gain = new Tone.Gain(0).connect(this.destination); @@ -39,7 +40,7 @@ export class TR808Clap { gain.gain.setValueAtTime(velocity, finalDecayStart); gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime); - noiseSrc.start(time).stop(finalDecayStart + decayTime); + noiseSrc.start(time, randomStart).stop(finalDecayStart + decayTime); noiseSrc.onended = () => { noiseSrc.dispose(); diff --git a/src/logic/drums/TR808Cowbell.ts b/src/logic/drums/TR808Cowbell.ts index 3dd1d4fd..cbbb67f4 100644 --- a/src/logic/drums/TR808Cowbell.ts +++ b/src/logic/drums/TR808Cowbell.ts @@ -2,11 +2,13 @@ import * as Tone from 'tone' import { applyPitchDrift, applyVariance } from '../DrumUtils' export class TR808Cowbell { - private activeGains: Set = new Set(); + private activeVoices: Set<{ oscillators: Tone.Oscillator[], gain: Tone.Gain }> = new Set(); constructor(private destination: Tone.ToneAudioNode) { } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // Core: Dual square wave oscillators from Schmitt trigger matrix // Standard frequencies: 540Hz and 800Hz const pitchMultiplier = 0.5 + pitch; // Range approx 0.5x to 1.5x @@ -15,6 +17,8 @@ export class TR808Cowbell { const osc1 = new Tone.Oscillator(freq1, "square"); const osc2 = new Tone.Oscillator(freq2, "square"); + osc1.phase = Math.random() * 360; + osc2.phase = Math.random() * 360; const mixGain = new Tone.Gain(0.5); const bpf = new Tone.Filter(applyVariance(800, 0.02), "bandpass"); @@ -33,7 +37,8 @@ export class TR808Cowbell { vca.gain.setValueAtTime(velocity, time); vca.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(vca); + const voice = { oscillators: [osc1, osc2], gain: vca }; + this.activeVoices.add(voice); osc1.start(time).stop(time + decayTime); osc2.start(time).stop(time + decayTime); @@ -45,15 +50,16 @@ export class TR808Cowbell { bpf.dispose(); hpf.dispose(); vca.dispose(); - this.activeGains.delete(vca); + this.activeVoices.delete(voice); }; } stop(time: number) { - this.activeGains.forEach(vca => { - vca.gain.cancelScheduledValues(time); - vca.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + this.activeVoices.forEach(voice => { + voice.gain.gain.cancelScheduledValues(time); + voice.gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + voice.oscillators.forEach(osc => osc.stop(time + 0.02)); }); - this.activeGains.clear(); + this.activeVoices.clear(); } } diff --git a/src/logic/drums/TR808HiHat.ts b/src/logic/drums/TR808HiHat.ts index acd1216d..d61560e8 100644 --- a/src/logic/drums/TR808HiHat.ts +++ b/src/logic/drums/TR808HiHat.ts @@ -3,11 +3,13 @@ import { applyPitchDrift, applyVariance } from '../DrumUtils' export class TR808HiHat { private frequencies = [205.3, 304.4, 369.6, 522.7, 800, 540]; - private activeGains: Set = new Set(); + private activeVoices: Set<{ oscillators: Tone.Oscillator[], gain: Tone.Gain }> = new Set(); constructor(private destination: Tone.ToneAudioNode) { } trigger(time: number, isOpen: boolean, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // Create nodes const mixGain = new Tone.Gain(0.15); const bpf1 = new Tone.Filter(3440, "bandpass"); @@ -20,7 +22,7 @@ export class TR808HiHat { // Create 6 Square Wave Oscillators (Schmitt Trigger Matrix) const oscillators = this.frequencies.map(freq => { - const driftedFreq = applyPitchDrift(freq * pitchMultiplier, 2.0); // +/- 2Hz drift for hats + const driftedFreq = applyPitchDrift(freq * pitchMultiplier, 2.0); const osc = new Tone.Oscillator(driftedFreq, "square"); osc.phase = Math.random() * 360; osc.connect(mixGain); @@ -28,7 +30,6 @@ export class TR808HiHat { }); // Routing Graph - // Oscillators -> MixGain -> [BPF1, BPF2] (Parallel) -> EnvGain -> HPF -> Destination mixGain.connect(bpf1); mixGain.connect(bpf2); bpf1.connect(envGain); @@ -51,15 +52,15 @@ export class TR808HiHat { envGain.gain.setValueAtTime(velocity, time); envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime); - this.activeGains.add(envGain); + const voice = { oscillators, gain: envGain }; + this.activeVoices.add(voice); // Scheduling oscillators.forEach(osc => { osc.start(time).stop(time + decayTime); }); - // Disposal - Explicitly clean up all 11-12 nodes to prevent memory leaks - // We use the first oscillator's onstop event to trigger the cleanup + // Disposal oscillators[0].onstop = () => { oscillators.forEach(o => o.dispose()); mixGain.dispose(); @@ -67,15 +68,21 @@ export class TR808HiHat { bpf2.dispose(); envGain.dispose(); hpf.dispose(); - this.activeGains.delete(envGain); + this.activeVoices.delete(voice); }; } stop(time: number) { - this.activeGains.forEach(gain => { - gain.gain.cancelScheduledValues(time); - gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + this.activeVoices.forEach(voice => { + voice.gain.gain.cancelScheduledValues(time); + voice.gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02); + // Oscillators will stop naturally because their stop(time) was already scheduled, + // but we ensure the gain cuts out. + // In a more robust system, we'd also call osc.stop(time + 0.02) here. + voice.oscillators.forEach(osc => { + osc.stop(time + 0.02); + }); }); - this.activeGains.clear(); + this.activeVoices.clear(); } } diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index 461f73fd..c329709f 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -5,6 +5,8 @@ export class TR808Kick { constructor(private destination: Tone.ToneAudioNode) { } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // pitch: 0.5 -> 52.5Hz, maps to 45-60Hz range const tune = 45 + pitch * 15; // decay: 0.5 -> 1.7s, maps to 0.4-3.0s range @@ -18,12 +20,11 @@ export class TR808Kick { osc.connect(masterGain); masterGain.connect(this.destination); - // Micro-randomization using shared utilities - const tuneDrift = applyPitchDrift(tune, 1.0); + // Micro-randomization using shared utilities (cents-based) + const tuneDrift = applyPitchDrift(tune, 1.5); const finalDecay = applyVariance(decayTime, 0.02); // Pitch Envelope: Start high (Tune * 2.5) and drop quickly (50ms) to simulate the membrane hit ('tonk') - // This rapid sweep generates the punch without needing a separate click oscillator const startFreq = tuneDrift * 2.5; const endFreq = tuneDrift; diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index 23f8f2ac..54fbab30 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -1,20 +1,16 @@ import * as Tone from 'tone' -import { applyPitchDrift, applyVariance } from '../DrumUtils' +import { applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils' export class TR808Snare { private noiseBuffer: AudioBuffer; constructor(private destination: Tone.ToneAudioNode) { - const sampleRate = Tone.getContext().sampleRate; - const bufferSize = sampleRate * 0.5; // 500ms - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); - const data = this.noiseBuffer.getChannelData(0); - for (let i = 0; i < data.length; i++) { - data[i] = Math.random() * 2 - 1; - } + this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 2.0); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // pitch maps to tone balance here (balance between low and high modes) const toneBalance = pitch; @@ -24,9 +20,9 @@ export class TR808Snare { const snappyDecay = applyVariance(snappyDecayBase, 0.02); const noiseFilterFreq = applyVariance(1800, 0.02); - // 808 Membrane modes: fixed at ~238Hz and ~476Hz according to research - const oscLow = new Tone.Oscillator(applyPitchDrift(238, 1.0), "sine"); - const oscHigh = new Tone.Oscillator(applyPitchDrift(476, 1.0), "sine"); + // 808 Membrane modes: fixed at ~238Hz and ~476Hz + const oscLow = new Tone.Oscillator(applyPitchDrift(238, 1.5), "sine"); + const oscHigh = new Tone.Oscillator(applyPitchDrift(476, 1.5), "sine"); oscLow.phase = Math.random() * 360; oscHigh.phase = Math.random() * 360; @@ -39,7 +35,6 @@ export class TR808Snare { gainHigh.connect(this.destination); // Independent envelopes for maximum authenticity as per research - // High mode decays faster (approx 75% of low mode decay) gainLow.gain.setValueAtTime(velocity * (1 - toneBalance), time); gainLow.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); @@ -48,8 +43,8 @@ export class TR808Snare { // Snappy Layer const noiseSrc = new Tone.BufferSource(this.noiseBuffer); - // High-pass filter (>1800Hz) to prevent phase trap with tonal body - // Q = 0.707 (Butterworth) + const randomStart = Math.random() * (this.noiseBuffer.duration - snappyDecay - 0.2); + const noiseFilter = new Tone.Filter({ frequency: noiseFilterFreq, type: "highpass", @@ -66,7 +61,7 @@ export class TR808Snare { oscLow.start(time).stop(time + vcaDecay); oscHigh.start(time).stop(time + vcaDecay); - noiseSrc.start(time).stop(time + snappyDecay + 0.1); + noiseSrc.start(time, randomStart).stop(time + snappyDecay + 0.1); // Cleanup oscLow.onstop = () => { diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index dd2582ed..fe1e40f7 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -1,5 +1,5 @@ import * as Tone from 'tone' -import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils' +import { makeDistortionCurve, applyPitchDrift, applyVariance, generateWhiteNoise } from '../DrumUtils' export class TR909Kick { private noiseBuffer: AudioBuffer; @@ -8,17 +8,12 @@ export class TR909Kick { constructor(private destination: Tone.ToneAudioNode) { // Soft Clipping curve from research this.bodyCurve = makeDistortionCurve(10); - - const sampleRate = Tone.getContext().sampleRate; - const bufferSize = sampleRate * 0.05; // 50ms click - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); - const data = this.noiseBuffer.getChannelData(0); - for (let i = 0; i < bufferSize; i++) { - data[i] = Math.random() * 2 - 1; - } + this.noiseBuffer = generateWhiteNoise(Tone.getContext(), 0.1); // Short buffer for click } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // 909 Kick base frequency is fixed around 50Hz const tune = 50; // The 'Pitch' parameter on 909 maps to the frequency sweep duration @@ -27,8 +22,8 @@ export class TR909Kick { // decay: 0.5 -> 0.45s, maps to 0.3-0.6s const decayTime = 0.3 + decay * 0.3; - // Micro-randomization using shared utilities - const tuneDrift = applyPitchDrift(tune, 0.5); + // Micro-randomization using shared utilities (cents-based) + const tuneDrift = applyPitchDrift(tune, 1.0); const vcaDecay = applyVariance(decayTime, 0.02); const noiseFilterFreq = applyVariance(1000, 0.02); @@ -58,6 +53,8 @@ export class TR909Kick { // Click Layer (Noise) const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + const randomStart = Math.random() * (this.noiseBuffer.duration - 0.03); + const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); // HPF > 1kHz to avoid phase trap const noiseGain = new Tone.Gain(0); @@ -71,7 +68,8 @@ export class TR909Kick { noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay); // Rectangular Pulse Click: Short 5ms impulse for attack articulation - const pulseOsc = new Tone.Oscillator(100, "square"); // Research: 100Hz square wave pulse + const pulseOsc = new Tone.Oscillator(100, "square"); + pulseOsc.phase = Math.random() * 360; const pulseGain = new Tone.Gain(0); pulseOsc.connect(pulseGain); pulseGain.connect(this.destination); @@ -80,7 +78,7 @@ export class TR909Kick { pulseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.005); bodyOsc.start(time).stop(time + vcaDecay); - noiseSrc.start(time).stop(time + clickDecay); + noiseSrc.start(time, randomStart).stop(time + clickDecay); pulseOsc.start(time).stop(time + 0.005); bodyOsc.onstop = () => { diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..cb90a4ba 100644 --- a/src/logic/drums/TR909Snare.ts +++ b/src/logic/drums/TR909Snare.ts @@ -1,5 +1,5 @@ import * as Tone from 'tone' -import { makeDistortionCurve, applyPitchDrift, applyVariance } from '../DrumUtils' +import { makeDistortionCurve, applyPitchDrift, applyVariance, generateLFSRNoise } from '../DrumUtils' export class TR909Snare { private noiseBuffer: AudioBuffer; @@ -8,18 +8,12 @@ export class TR909Snare { constructor(private destination: Tone.ToneAudioNode) { // Soft Clipping curve from research this.bodyCurve = makeDistortionCurve(15); - - const sampleRate = Tone.getContext().sampleRate; - const bufferSize = sampleRate * 0.5; - this.noiseBuffer = Tone.getContext().createBuffer(1, bufferSize, sampleRate); - const data = this.noiseBuffer.getChannelData(0); - // While original used LFSR, research says Math.random() is sufficient for Web Audio API context - for (let i = 0; i < data.length; i++) { - data[i] = Math.random() * 2 - 1; - } + this.noiseBuffer = generateLFSRNoise(Tone.getContext(), 2.0); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { + if (velocity <= 0) return; + // 909 Snare Body: 2 triangle oscillators fixed at ~160Hz and ~220Hz const freq1 = 160; const freq2 = 220; @@ -29,13 +23,16 @@ export class TR909Snare { const snappyDecayBase = 0.1 + snappy * 0.4; const snappyDecay = applyVariance(snappyDecayBase, 0.02); const noiseHPFFreq = applyVariance(1000, 0.02); - const toneDrift1 = applyPitchDrift(freq1, 1.0); - const toneDrift2 = applyPitchDrift(freq2, 1.0); + const toneDrift1 = applyPitchDrift(freq1, 1.5); + const toneDrift2 = applyPitchDrift(freq2, 1.5); const osc1 = new Tone.Oscillator(toneDrift1 * 2, "triangle"); const osc2 = new Tone.Oscillator(toneDrift2 * 2, "triangle"); + + // Analog free-running phase randomization osc1.phase = Math.random() * 360; osc2.phase = Math.random() * 360; + // Routing with gain compensation to prevent clipping before the shaper const preShaperGain = new Tone.Gain(0.5); const bodyShaper = new Tone.WaveShaper(this.bodyCurve); @@ -63,10 +60,13 @@ export class TR909Snare { tonalGain.gain.setValueAtTime(velocity, time); tonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay); - // Snappy Layer + // Snappy Layer (LFSR Noise) const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + // Randomize start offset within noise buffer to ensure variety + const randomStart = Math.random() * (this.noiseBuffer.duration - snappyDecay - 0.2); + const hpf = new Tone.Filter(noiseHPFFreq, "highpass"); // HPF to protect fundamental - // LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz (research: toneCutoff) + // LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz const toneCutoff = 4000 + pitch * 4000; const lpf = new Tone.Filter(applyVariance(toneCutoff, 0.02), "lowpass"); const noiseGain = new Tone.Gain(0); @@ -81,7 +81,7 @@ export class TR909Snare { osc1.start(time).stop(time + vcaDecay); osc2.start(time).stop(time + vcaDecay); - noiseSrc.start(time).stop(time + snappyDecay + 0.1); + noiseSrc.start(time, randomStart).stop(time + snappyDecay + 0.1); osc1.onstop = () => { osc1.dispose(); diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 9e96a9b2..a0771ed9 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -41,22 +41,31 @@ interface DrumState { setParams: (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => void setKit: (kit: '808' | '909') => void setDrive: (drive: number) => void + randomizeDrums: () => void } export const useDrumStore = create((set) => ({ kick: { steps: 16, pulses: 4, rotate: 0, decay: 0.5, pitch: 0.5, probability: 1.0 }, - snare: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, + snare: { steps: 16, pulses: 4, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, hihat: { steps: 16, pulses: 12, rotate: 0, decay: 0.5, pitch: 0.5, probability: 1.0 }, hihatOpen: { steps: 16, pulses: 4, rotate: 2, decay: 0.5, pitch: 0.5, probability: 1.0 }, - clap: { steps: 16, pulses: 2, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, - cowbell: { steps: 16, pulses: 2, rotate: 2, decay: 0.5, pitch: 0.5, probability: 1.0 }, + clap: { steps: 16, pulses: 4, rotate: 4, decay: 0.5, pitch: 0.5, probability: 1.0 }, + cowbell: { steps: 16, pulses: 3, rotate: 2, decay: 0.5, pitch: 0.5, probability: 0.8 }, kit: '909', drive: 20, setParams: (drum, params) => set((state) => ({ [drum]: { ...state[drum], ...params } })), setKit: (kit) => set({ kit }), - setDrive: (drive) => set({ drive }) + setDrive: (drive) => set({ drive }), + randomizeDrums: () => set((state) => ({ + kick: { ...state.kick, pulses: Math.floor(Math.random() * 4) + 1, rotate: Math.floor(Math.random() * 16) }, + snare: { ...state.snare, pulses: Math.floor(Math.random() * 4) + 1, rotate: Math.floor(Math.random() * 16) }, + hihat: { ...state.hihat, pulses: Math.floor(Math.random() * 8) + 8, rotate: Math.floor(Math.random() * 16) }, + hihatOpen: { ...state.hihatOpen, pulses: Math.floor(Math.random() * 4) + 2, rotate: Math.floor(Math.random() * 16) }, + clap: { ...state.clap, pulses: Math.floor(Math.random() * 3) + 1, rotate: Math.floor(Math.random() * 16) }, + cowbell: { ...state.cowbell, pulses: Math.floor(Math.random() * 4) + 1, rotate: Math.floor(Math.random() * 16) } + })) })) // Pad Store