diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..2cbd7df7 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -8,7 +8,7 @@ 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, cowbell, kit, drive, setParams, setKit, setDrive, randomizeTechno } = useDrumStore() const { drumMachine, volumes, setVolume } = useAudioStore() const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => { @@ -30,13 +30,51 @@ export function DrumsView() { if (drumMachine) drumMachine.setSaturation(v) } + const handleRandomize = () => { + if (window.Telegram?.WebApp?.HapticFeedback) { + window.Telegram.WebApp.HapticFeedback.impactOccurred('medium') + } + randomizeTechno() + + if (drumMachine) { + const s = useDrumStore.getState() + drumMachine.syncInternalParams(s.kit, s.drive, { + kick: s.kick, + snare: s.snare, + hihat: s.hihat, + hihatOpen: s.hihatOpen, + clap: s.clap, + cowbell: s.cowbell + }) + } + } + return (
-

Настройки

+
+

Настройки

+ +
) { + this.setKit(kit) + this.setSaturation(drive) + Object.entries(drumParams).forEach(([drum, p]) => { + this.setDrumParams(drum, p.pitch, p.decay) + }) + } + triggerDrum(drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', time: number, velocity: number = 0.8) { const p = this.params[drum] const kit808 = this.kit808 diff --git a/src/logic/DrumUtils.ts b/src/logic/DrumUtils.ts index b9917c79..b6e3e44d 100644 --- a/src/logic/DrumUtils.ts +++ b/src/logic/DrumUtils.ts @@ -40,3 +40,28 @@ 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 pseudo-random digital noise buffer using LFSR. + * Used for TR-909 snappy and digital textures. + * Polynomial: x^15 + x^14 + 1 + */ +export function generateLFSRNoise(context: any, duration: number): AudioBuffer { + const sampleRate = context.sampleRate; + const bufferSize = sampleRate * duration; + const buffer = context.createBuffer(1, bufferSize, sampleRate); + const data = buffer.getChannelData(0); + + let lfsr = 0x7FFF; // 15-bit state, initialized to non-zero + + for (let i = 0; i < bufferSize; i++) { + // Tap at bits 15 and 14 (1-indexed) -> bits 14 and 13 (0-indexed) + const bit = ((lfsr >> 14) ^ (lfsr >> 13)) & 1; + lfsr = ((lfsr << 1) | bit) & 0x7FFF; + + // Convert 15-bit state to range [-1, 1] + data[i] = (lfsr / 16384.0) - 1.0; + } + + return buffer; +} diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index 461f73fd..fa61be3f 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -22,19 +22,24 @@ export class TR808Kick { const tuneDrift = applyPitchDrift(tune, 1.0); 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 + // Two-stage Pitch Envelope: 5ms click + 45ms sweep for authentic 'tonk' const startFreq = tuneDrift * 2.5; + const midFreq = tuneDrift * 1.5; const endFreq = tuneDrift; osc.frequency.setValueAtTime(startFreq, time); + osc.frequency.exponentialRampToValueAtTime(midFreq, time + 0.005); osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05); - // VCA Amp Envelope: Instant attack, adjustable exponential decay + // Two-stage Amp Envelope: 20ms diode damping (decay to 50%) followed by long final decay masterGain.gain.setValueAtTime(velocity, time); - masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); + masterGain.gain.exponentialRampToValueAtTime(velocity * 0.5, time + 0.02); - osc.start(time).stop(time + finalDecay); + // Ensure final decay is always longer than the damping stage + const safeFinalDecay = Math.max(0.021, finalDecay); + masterGain.gain.exponentialRampToValueAtTime(0.001, time + safeFinalDecay); + + osc.start(time).stop(time + safeFinalDecay); osc.onstop = () => { osc.dispose(); diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index dd2582ed..594b063b 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 (amount 30 for authentic 909 drive) + this.bodyCurve = makeDistortionCurve(30); const sampleRate = Tone.getContext().sampleRate; const bufferSize = sampleRate * 0.05; // 50ms click diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..e4230cf1 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; - } + // Authentic 15-bit LFSR noise for 909 digital texture + this.noiseBuffer = generateLFSRNoise(Tone.getContext(), 0.5); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts index c90d17e8..1e6c4e3e 100644 --- a/src/store/audioStore.ts +++ b/src/store/audioStore.ts @@ -63,6 +63,12 @@ export const useAudioStore = create((set, get) => ({ Tone.Transport.bpm.value = get().bpm Tone.Transport.swing = get().swing + // Sync initial state from instrument store to drum engine + const { kit, drive, kick, snare, hihat, hihatOpen, clap, cowbell } = (await import('./instrumentStore')).useDrumStore.getState() + drums.syncInternalParams(kit, drive, { + kick, snare, hihat, hihatOpen, clap, cowbell + }) + set({ isInitialized: true, bassSynth: bassSynth, diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 9e96a9b2..61b9ccbb 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -41,6 +41,7 @@ interface DrumState { setParams: (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => void setKit: (kit: '808' | '909') => void setDrive: (drive: number) => void + randomizeTechno: () => void } export const useDrumStore = create((set) => ({ @@ -56,7 +57,16 @@ export const useDrumStore = create((set) => ({ [drum]: { ...state[drum], ...params } })), setKit: (kit) => set({ kit }), - setDrive: (drive) => set({ drive }) + setDrive: (drive) => set({ drive }), + randomizeTechno: () => set((state) => ({ + kick: { ...state.kick, pulses: 4, rotate: 0, probability: 1.0 }, + snare: { ...state.snare, pulses: 4, rotate: 4, probability: 1.0 }, + hihat: { ...state.hihat, pulses: 12, rotate: 0, probability: 1.0 }, + hihatOpen: { ...state.hihatOpen, pulses: 4, rotate: 2, probability: 1.0 }, + cowbell: { ...state.cowbell, pulses: 3, rotate: 2, probability: 0.8 }, + kit: '909', + drive: 30 + })) })) // Pad Store