diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..cf2c45d7 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -2,15 +2,32 @@ 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 { Dices as DiceIcon } 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 handleRandomize = () => { + if (window.Telegram?.WebApp?.HapticFeedback) { + window.Telegram.WebApp.HapticFeedback.impactOccurred('medium') + } + randomizeDrums() + + // Sync internal params after randomization + if (drumMachine) { + const state = useDrumStore.getState() + drumMachine.setSaturation(state.drive) + const drums: ('kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell')[] = ['kick', 'snare', 'hihat', 'hihatOpen', 'clap', 'cowbell'] + drums.forEach(d => { + drumMachine.setDrumParams(d, state[d].pitch, state[d].decay) + }) + } + } + const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => { setParams(drum, params) if (drumMachine) { @@ -38,6 +55,24 @@ export function DrumsView() {

Настройки

+ 14 and 13 (0-indexed) + const bit = ((lfsr >> 14) ^ (lfsr >> 13)) & 1; + lfsr = ((lfsr << 1) | bit) & 0x7FFF; + + // Convert bit to signal [-1, 1] + data[i] = (lfsr & 1) ? 1 : -1; + } + + return buffer; +} + +/** + * Generates an AudioBuffer containing white noise. + */ +export function generateWhiteNoise(context: Tone.BaseContext, duration: number = 2.0): AudioBuffer { + const sampleRate = context.sampleRate; + const bufferSize = Math.floor(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; +} + /** * Creates a soft-clipping saturation curve (hyperbolic tangent approximation). * Formula: (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x)) diff --git a/src/logic/drums/TR808Clap.ts b/src/logic/drums/TR808Clap.ts index 3334a6fe..5f1b8ede 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -1,18 +1,16 @@ import * as Tone from 'tone' -import { applyVariance } from '../DrumUtils' +import { 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) { const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + const randomStart = Math.random() * (this.noiseBuffer.duration * 0.5); 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 +37,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/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index 461f73fd..cfdac418 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -30,11 +30,18 @@ export class TR808Kick { osc.frequency.setValueAtTime(startFreq, time); osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05); - // VCA Amp Envelope: Instant attack, adjustable exponential decay + // VCA Amp Envelope: Instant attack, two-stage decay emulating diode damping + // Stage 1: Fast initial damping (diode effect) - 20ms to 50% + const dampingTime = 0.02; + const dampingLevel = Math.max(0.001, velocity * 0.5); masterGain.gain.setValueAtTime(velocity, time); - masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); + masterGain.gain.exponentialRampToValueAtTime(dampingLevel, time + dampingTime); - osc.start(time).stop(time + finalDecay); + // Stage 2: Natural exponential decay + const safeFinalDecay = Math.max(dampingTime + 0.01, finalDecay); + masterGain.gain.exponentialRampToValueAtTime(0.001, time + safeFinalDecay); + + osc.start(time).stop(time + safeFinalDecay); osc.onstop = () => { osc.dispose(); diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index 23f8f2ac..edfc2940 100644 --- a/src/logic/drums/TR808Snare.ts +++ b/src/logic/drums/TR808Snare.ts @@ -1,17 +1,11 @@ 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) { @@ -48,6 +42,8 @@ export class TR808Snare { // Snappy Layer const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + const randomStart = Math.random() * (this.noiseBuffer.duration * 0.5); + // High-pass filter (>1800Hz) to prevent phase trap with tonal body // Q = 0.707 (Butterworth) const noiseFilter = new Tone.Filter({ @@ -66,7 +62,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..6866de4f 100644 --- a/src/logic/drums/TR909Kick.ts +++ b/src/logic/drums/TR909Kick.ts @@ -6,8 +6,8 @@ export class TR909Kick { private bodyCurve: Float32Array; constructor(private destination: Tone.ToneAudioNode) { - // Soft Clipping curve from research - this.bodyCurve = makeDistortionCurve(10); + // Soft Clipping curve from research (intensity 30 for aggressive 909) + this.bodyCurve = makeDistortionCurve(30); const sampleRate = Tone.getContext().sampleRate; const bufferSize = sampleRate * 0.05; // 50ms click @@ -58,6 +58,8 @@ export class TR909Kick { // Click Layer (Noise) const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + const randomStart = Math.random() * (this.noiseBuffer.duration * 0.5); + const noiseFilter = new Tone.Filter(noiseFilterFreq, "highpass"); // HPF > 1kHz to avoid phase trap const noiseGain = new Tone.Gain(0); @@ -80,7 +82,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..641c4a81 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; @@ -9,14 +9,8 @@ export class TR909Snare { // 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; - } + // Use authentic LFSR noise for 909 snappy character + this.noiseBuffer = generateLFSRNoise(Tone.getContext()); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { @@ -65,6 +59,9 @@ export class TR909Snare { // Snappy Layer const noiseSrc = new Tone.BufferSource(this.noiseBuffer); + // Start from random offset to avoid "machine-gun" effect + const randomStart = Math.random() * (this.noiseBuffer.duration * 0.5); + const hpf = new Tone.Filter(noiseHPFFreq, "highpass"); // HPF to protect fundamental // LPF controlled by 'Tone' (pitch parameter here), range 4kHz to 8kHz (research: toneCutoff) const toneCutoff = 4000 + pitch * 4000; @@ -81,7 +78,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..c1e9c9b3 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -41,22 +41,37 @@ 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 }, + 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) => { + const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; + const randF = (min: number, max: number) => Math.random() * (max - min) + min; + + return { + kick: { ...state.kick, pulses: rand(2, 6), pitch: randF(0.3, 0.7), decay: randF(0.4, 0.8) }, + snare: { ...state.snare, pulses: rand(2, 8), rotate: rand(0, 15), pitch: randF(0.2, 0.8), decay: randF(0.3, 0.7) }, + hihat: { ...state.hihat, pulses: rand(8, 14), pitch: randF(0.4, 0.9) }, + hihatOpen: { ...state.hihatOpen, pulses: rand(2, 6), rotate: rand(0, 15), decay: randF(0.4, 0.7) }, + clap: { ...state.clap, pulses: rand(0, 4), rotate: rand(0, 15), probability: randF(0.5, 1.0) }, + cowbell: { ...state.cowbell, pulses: rand(0, 6), rotate: rand(0, 15), probability: randF(0.3, 0.9) }, + drive: rand(10, 40) + } + }) })) // Pad Store