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