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) => ({