diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..b250953d 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -30,6 +30,19 @@ export function DrumsView() { if (drumMachine) drumMachine.setSaturation(v) } + const randomizeTechno = () => { + if (window.Telegram?.WebApp?.HapticFeedback) { + window.Telegram.WebApp.HapticFeedback.impactOccurred('rigid') + } + + updateDrum('kick', { pulses: 4, rotate: 0, probability: 1.0 }) + updateDrum('snare', { pulses: 4, rotate: 4, probability: 1.0 }) + updateDrum('hihat', { pulses: 12, rotate: 0, probability: 1.0 }) + updateDrum('hihatOpen', { pulses: 4, rotate: 2, probability: 1.0 }) + updateDrum('clap', { pulses: 2, rotate: 4, probability: 1.0 }) + updateDrum('cowbell', { pulses: 3, rotate: 2, probability: 0.8 }) + } + return (
@@ -38,6 +51,17 @@ export function DrumsView() {

Настройки

+ setVolume(d.id === 'cowbell' ? 'cow' : d.id, v)} + onChange={(v) => setVolume(d.id, v)} size={40} />
@@ -122,7 +146,7 @@ export function DrumsView() { { name: 'HIHAT', data: hihat }, { name: 'OPEN', data: hihatOpen }, { name: 'CLAP', data: clap }, - { name: 'COW', data: useDrumStore((state) => state.cowbell) } + { name: 'COWBELL', data: useDrumStore((state) => state.cowbell) } ].map((d, idx) => { const pattern = rotateArray(bjorklund(d.data.steps, d.data.pulses), d.data.rotate) return ( diff --git a/src/components/MixerView.tsx b/src/components/MixerView.tsx index cadf2b00..8236d21c 100644 --- a/src/components/MixerView.tsx +++ b/src/components/MixerView.tsx @@ -58,10 +58,10 @@ export function MixerView() { size={48} /> setVolume('cow', v)} + onChange={(v) => setVolume('cowbell', v)} size={48} /> ) { + this.setKit(kit); + this.setSaturation(saturation); + Object.entries(params).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..e51cf6d9 100644 --- a/src/logic/DrumUtils.ts +++ b/src/logic/DrumUtils.ts @@ -20,6 +20,31 @@ export function makeDistortionCurve(amount: number = 20): Float32Array { return curve; } +/** + * Generates an AudioBuffer containing authentic 15-bit LFSR pseudo-random noise. + * Based on TR-909 digital noise specs. + */ +export function generateLFSRNoise(context: any, duration: number = 0.5): AudioBuffer { + const sampleRate = context.sampleRate; + const bufferSize = sampleRate * duration; + const buffer = context.createBuffer(1, bufferSize, sampleRate); + const data = buffer.getChannelData(0); + + // 15-bit LFSR: x^15 + x^14 + 1 + let state = 0x7FFF; // Non-zero initial state + + for (let i = 0; i < bufferSize; i++) { + // Simple LFSR step + const bit = ((state >> 0) ^ (state >> 1)) & 1; + state = (state >> 1) | (bit << 14); + + // Normalize to [-1, 1] + data[i] = (state / 0x7FFF) * 2 - 1; + } + + return buffer; +} + /** * Applies micro-randomization to a base frequency (Pitch Drift). * Typically +/- 1Hz as per research. diff --git a/src/logic/drums/TR808Kick.ts b/src/logic/drums/TR808Kick.ts index 461f73fd..5c7e7a38 100644 --- a/src/logic/drums/TR808Kick.ts +++ b/src/logic/drums/TR808Kick.ts @@ -22,19 +22,28 @@ 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 - const startFreq = tuneDrift * 2.5; - const endFreq = tuneDrift; + // Pitch Envelope: Two-stage frequency sweep for authentic 'tonk' + // 1. Rapid snap (5ms) from Tune*2.5 to Tune*1.25 + // 2. Slower sweep (45ms) down to the fundamental Tune + const freqSnap = tuneDrift * 2.5; + const freqMid = tuneDrift * 1.25; + const freqEnd = tuneDrift; + + osc.frequency.setValueAtTime(freqSnap, time); + osc.frequency.exponentialRampToValueAtTime(freqMid, time + 0.005); + osc.frequency.exponentialRampToValueAtTime(freqEnd, time + 0.05); + + // VCA Amp Envelope: Two-stage decay to emulate diode damping + // 1. Initial rapid damping (20ms) down to 50% velocity + // 2. Natural exponential decay for the remainder + const dampingTime = 0.02; + const safeFinalDecay = Math.max(dampingTime + 0.01, finalDecay); - osc.frequency.setValueAtTime(startFreq, time); - osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05); - - // VCA Amp Envelope: Instant attack, adjustable exponential decay masterGain.gain.setValueAtTime(velocity, time); - masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay); + masterGain.gain.exponentialRampToValueAtTime(velocity * 0.5, time + dampingTime); + masterGain.gain.exponentialRampToValueAtTime(0.001, time + safeFinalDecay); - osc.start(time).stop(time + finalDecay); + 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..48bf0146 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; - } + // Authentically generate 15-bit LFSR noise for 909 snappy + this.noiseBuffer = generateLFSRNoise(Tone.getContext(), 0.5); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { @@ -68,7 +62,11 @@ export class TR909Snare { 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; - const lpf = new Tone.Filter(applyVariance(toneCutoff, 0.02), "lowpass"); + const lpf = new Tone.Filter({ + frequency: applyVariance(toneCutoff, 0.02), + type: "lowpass", + rolloff: -12 + }); const noiseGain = new Tone.Gain(0); noiseSrc.connect(hpf); diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts index c90d17e8..3bcd4290 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,26 @@ export const useAudioStore = create((set, get) => ({ Tone.Transport.bpm.value = get().bpm Tone.Transport.swing = get().swing + // Synchronize initial state to the engine + 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 + }); + + // Set initial volumes + const vols = get().volumes; + drums.outputKick.gain.value = vols.kick; + drums.outputSnare.gain.value = vols.snare; + drums.outputHihat.gain.value = vols.hihat; + drums.outputOpenHat.gain.value = vols.hihatOpen; + drums.outputClap.gain.value = vols.clap; + drums.outputCowbell.gain.value = vols.cowbell; + set({ isInitialized: true, bassSynth: bassSynth, @@ -96,7 +117,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) => ({