diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..683b7425 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -30,13 +30,40 @@ export function DrumsView() { if (drumMachine) drumMachine.setSaturation(v) } + const randomizeTechno = () => { + // Haptic feedback via Telegram WebApp API if available + // @ts-ignore + if (window.Telegram?.WebApp?.HapticFeedback) { + // @ts-ignore + window.Telegram.WebApp.HapticFeedback.impactOccurred('medium') + } + + // Standard techno patterns from research/memory + updateDrum('kick', { pulses: 4, rotate: 0, steps: 16, probability: 1.0 }) + updateDrum('snare', { pulses: 4, rotate: 4, steps: 16, probability: 1.0 }) + updateDrum('hihat', { pulses: 12, rotate: 0, steps: 16, probability: 1.0 }) + updateDrum('hihatOpen', { pulses: 4, rotate: 2, steps: 16, probability: 1.0 }) + updateDrum('clap', { pulses: 2, rotate: 4, steps: 16, probability: 1.0 }) + updateDrum('cowbell', { pulses: 3, rotate: 2, steps: 16, probability: 0.8 }) + } + return (
-

Настройки

+
+

Настройки

+ +
setVolume(d.id === 'cowbell' ? 'cow' : d.id, v)} + onChange={(v) => setVolume(d.id, v)} size={40} />
diff --git a/src/components/MixerView.tsx b/src/components/MixerView.tsx index cadf2b00..4f9723ff 100644 --- a/src/components/MixerView.tsx +++ b/src/components/MixerView.tsx @@ -59,9 +59,9 @@ export function MixerView() { /> setVolume('cow', v)} + onChange={(v) => setVolume('cowbell', v)} size={48} /> , + volumes: Record + ) { + this.setKit(kit) + this.setSaturation(drive) + + for (const [drum, p] of Object.entries(drumParams)) { + this.setDrumParams(drum, p.pitch, p.decay) + } + + if (volumes.kick !== undefined) this.outputKick.gain.value = volumes.kick + if (volumes.snare !== undefined) this.outputSnare.gain.value = volumes.snare + if (volumes.hihat !== undefined) this.outputHihat.gain.value = volumes.hihat + if (volumes.hihatOpen !== undefined) this.outputOpenHat.gain.value = volumes.hihatOpen + if (volumes.clap !== undefined) this.outputClap.gain.value = volumes.clap + if (volumes.cowbell !== undefined) this.outputCowbell.gain.value = volumes.cowbell + } + 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..8dac6292 100644 --- a/src/logic/DrumUtils.ts +++ b/src/logic/DrumUtils.ts @@ -40,3 +40,26 @@ 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 authentic 15-bit LFSR (Linear Feedback Shift Register) noise buffer. + * Used in TR-909 snare to provide characteristic "digital" texture. + * @param audioCtx - AudioContext to create the buffer + * @param duration - Duration in seconds + */ +export function generateLFSRNoise(audioCtx: BaseAudioContext, duration: number): AudioBuffer { + const bufferSize = Math.floor(audioCtx.sampleRate * duration); + const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate); + const data = buffer.getChannelData(0); + + let lfsr = 0x7FFF; // 15-bit seed + + for (let i = 0; i < bufferSize; i++) { + // Feedback for 15-bit LFSR (taps at 15 and 14) + const bit = ((lfsr >> 0) ^ (lfsr >> 1)) & 1; + lfsr = (lfsr >> 1) | (bit << 14); + data[i] = bit * 2 - 1; + } + + return buffer; +} diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index 461f73fd..97e1c23c 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: Diode damping emulation + // Initial 20ms rapid decay to 50% volume for the punch + const punchDecay = 0.02; masterGain.gain.setValueAtTime(velocity, time); - masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); + masterGain.gain.exponentialRampToValueAtTime(velocity * 0.5, time + punchDecay); - osc.start(time).stop(time + finalDecay); + // Final exponential decay to zero + // Ensure finalDecay is always longer than punchDecay + const safeFinalDecay = Math.max(punchDecay + 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/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..8e5a2128 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 Snare Snappy component + this.noiseBuffer = generateLFSRNoise(Tone.getContext().rawContext, 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..7e00ec11 100644 --- a/src/store/audioStore.ts +++ b/src/store/audioStore.ts @@ -3,6 +3,7 @@ import * as Tone from 'tone' import { AcidSynth } from '../logic/AcidSynth' import { DrumMachine } from '../logic/DrumMachine' import { PadSynth } from '../logic/PadSynth' +import { useDrumStore } from './instrumentStore' export interface AudioState { isInitialized: boolean @@ -22,7 +23,7 @@ export interface AudioState { hihat: number, hihatOpen: number, clap: number, - cow: number, + cowbell: number, pads: number } initialize: () => Promise @@ -30,7 +31,7 @@ export interface AudioState { setBpm: (bpm: number) => void setSwing: (swing: number) => void setCurrentStep: (step: number) => void - setVolume: (channel: 'bass' | 'lead' | 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cow' | 'pads', value: number) => void + setVolume: (channel: 'bass' | 'lead' | 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell' | 'pads', value: number) => void } export const useAudioStore = create((set, get) => ({ @@ -43,7 +44,7 @@ export const useAudioStore = create((set, get) => ({ leadSynth: null, drumMachine: null, padSynth: null, - volumes: { bass: 0.8, lead: 0.8, kick: 0.8, snare: 0.8, hihat: 0.8, hihatOpen: 0.8, clap: 0.8, cow: 0.8, pads: 0.5 }, + volumes: { bass: 0.8, lead: 0.8, kick: 0.8, snare: 0.8, hihat: 0.8, hihatOpen: 0.8, clap: 0.8, cowbell: 0.8, pads: 0.5 }, initialize: async () => { if (get().isInitialized) return @@ -63,6 +64,22 @@ export const useAudioStore = create((set, get) => ({ Tone.Transport.bpm.value = get().bpm Tone.Transport.swing = get().swing + // Synchronize initial state from stores to internal engine params + const drumStore = useDrumStore.getState() + drums.syncInternalParams( + drumStore.kit, + drumStore.drive, + { + kick: drumStore.kick, + snare: drumStore.snare, + hihat: drumStore.hihat, + hihatOpen: drumStore.hihatOpen, + clap: drumStore.clap, + cowbell: drumStore.cowbell + }, + get().volumes + ) + set({ isInitialized: true, bassSynth: bassSynth, @@ -96,7 +113,7 @@ export const useAudioStore = create((set, get) => ({ if (channel === 'hihat') drumMachine.outputHihat.gain.value = value if (channel === 'hihatOpen') drumMachine.outputOpenHat.gain.value = value if (channel === 'clap') drumMachine.outputClap.gain.value = value - if (channel === 'cow') drumMachine.outputCowbell.gain.value = value + if (channel === 'cowbell') drumMachine.outputCowbell.gain.value = value } if (channel === 'pads' && padSynth) padSynth.synth.volume.value = Tone.gainToDb(value) diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 9e96a9b2..a99cd244 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -45,11 +45,11 @@ interface DrumState { 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) => ({