diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx index 7d191e90..bb5112d2 100644 --- a/src/components/DrumsView.tsx +++ b/src/components/DrumsView.tsx @@ -8,9 +8,22 @@ 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 = () => { + randomizeDrums() + const state = useDrumStore.getState() + const drumList: ('kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell')[] = ['kick', 'snare', 'hihat', 'hihatOpen', 'clap', 'cowbell'] + drumList.forEach(id => { + if (drumMachine) drumMachine.setDrumParams(id, state[id].pitch, state[id].decay) + }) + + if (window.Telegram?.WebApp?.HapticFeedback) { + window.Telegram.WebApp.HapticFeedback.impactOccurred('medium') + } + } + const updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial) => { setParams(drum, params) if (drumMachine) { @@ -38,6 +51,25 @@ export function DrumsView() {

Настройки

+ ) { + this.setKit(kit) + this.setSaturation(saturation) + Object.entries(drumParams).forEach(([drum, p]) => { + this.setDrumParams(drum as any, 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..e9a3562d 100644 --- a/src/logic/DrumUtils.ts +++ b/src/logic/DrumUtils.ts @@ -1,7 +1,47 @@ +import * as Tone from 'tone' + /** * Shared DSP utilities for drum synthesis based on research specs. */ +/** + * Generates a White Noise AudioBuffer. + * @param context - Audio context + * @param duration - Duration in seconds + */ +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; +} + +/** + * Generates an LFSR (Linear Feedback Shift Register) Noise AudioBuffer. + * Implements a 15-bit LFSR with polynomial x^15 + x^14 + 1 for authentic TR-909 textures. + * @param context - Audio context + * @param duration - Duration in seconds + */ +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 state = 0x7FFF; + for (let i = 0; i < bufferSize; i++) { + // Taps at bit 14 and 13 (0-indexed) for x^15 + x^14 + 1 + const bit = ((state >> 14) ^ (state >> 13)) & 1; + state = ((state << 1) | bit) & 0x7FFF; + data[i] = (state & 1) ? 0.5 : -0.5; // Normalized amplitude + } + 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..119c3bdb 100644 --- a/src/logic/drums/TR808Clap.ts +++ b/src/logic/drums/TR808Clap.ts @@ -1,14 +1,11 @@ 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(), 0.5); } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { diff --git a/src/logic/drums/TR808Snare.ts b/src/logic/drums/TR808Snare.ts index 23f8f2ac..ace0e02b 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(), 0.5); // 500ms } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { diff --git a/src/logic/drums/TR909Kick.ts b/src/logic/drums/TR909Kick.ts index dd2582ed..9ed5c22c 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,14 +8,7 @@ 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.05); // 50ms click } trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) { diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts index 4841f055..27ace019 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,15 +8,7 @@ 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(), 0.5); } trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) { @@ -50,10 +42,10 @@ export class TR909Snare { postShaperGain.connect(tonalGain); tonalGain.connect(this.destination); - // Pitch Sweep: ~320Hz to ~160Hz over 30ms (as per research spec) + // Pitch Sweep: ~320Hz to ~160Hz over 20-30ms (as per research spec) const sweepTime = 0.03; - const startFreq1 = toneDrift1 * 2; - const startFreq2 = toneDrift2 * 2; + const startFreq1 = toneDrift1 * 2; // ~320Hz + const startFreq2 = toneDrift2 * 2; // ~440Hz (maintaining dissonance) osc1.frequency.setValueAtTime(startFreq1, time); osc1.frequency.exponentialRampToValueAtTime(toneDrift1, time + sweepTime); diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts index c90d17e8..db227a9b 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 @@ -63,6 +64,17 @@ export const useAudioStore = create((set, get) => ({ Tone.Transport.bpm.value = get().bpm Tone.Transport.swing = get().swing + // Sync initial drum parameters + const drumState = useDrumStore.getState() + drums.syncInternalParams(drumState.kit, drumState.drive, { + kick: drumState.kick, + snare: drumState.snare, + hihat: drumState.hihat, + hihatOpen: drumState.hihatOpen, + clap: drumState.clap, + cowbell: drumState.cowbell + }) + set({ isInitialized: true, bassSynth: bassSynth, diff --git a/src/store/instrumentStore.ts b/src/store/instrumentStore.ts index 9e96a9b2..d19c3e65 100644 --- a/src/store/instrumentStore.ts +++ b/src/store/instrumentStore.ts @@ -41,22 +41,34 @@ 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 randRange = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min; + return { + kick: { ...state.kick, pulses: randRange(3, 5), rotate: randRange(0, 3) }, + snare: { ...state.snare, pulses: randRange(2, 6), rotate: randRange(2, 6) }, + hihat: { ...state.hihat, pulses: randRange(8, 14), rotate: randRange(0, 4) }, + hihatOpen: { ...state.hihatOpen, pulses: randRange(2, 6), rotate: randRange(0, 8) }, + clap: { ...state.clap, pulses: randRange(1, 4), rotate: randRange(4, 12) }, + cowbell: { ...state.cowbell, pulses: randRange(2, 8), rotate: randRange(0, 15) } + } + }) })) // Pad Store