From 917636807b40d2ed36774431aec6155104c632f2 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Mon, 15 Jun 2026 02:41:28 +0000
Subject: [PATCH] Implement drum synthesis enhancements based on DSP research
- Added 15-bit LFSR noise generation for authentic TR-909 textures.
- Refined TR-909 Snare to use LFSR noise.
- Standardized drum naming (cow -> cowbell) across components and stores.
- Implemented syncInternalParams in DrumMachine for engine-UI parity.
- Added 'Techno Randomize' feature to DrumsView with haptic feedback.
- Set optimized techno default Euclidean patterns.
Co-authored-by: Pitrat-wav <255843145+Pitrat-wav@users.noreply.github.com>
---
src/components/DrumsView.tsx | 31 +++++++++++++++++++++++++++++--
src/components/MixerView.tsx | 4 ++--
src/logic/DrumMachine.ts | 8 ++++++++
src/logic/DrumUtils.ts | 22 ++++++++++++++++++++++
src/logic/drums/TR909Snare.ts | 12 ++----------
src/store/audioStore.ts | 29 +++++++++++++++++++++++++----
src/store/instrumentStore.ts | 4 ++--
7 files changed, 90 insertions(+), 20 deletions(-)
diff --git a/src/components/DrumsView.tsx b/src/components/DrumsView.tsx
index 7d191e90..feff125a 100644
--- a/src/components/DrumsView.tsx
+++ b/src/components/DrumsView.tsx
@@ -30,6 +30,25 @@ export function DrumsView() {
if (drumMachine) drumMachine.setSaturation(v)
}
+ const randomizeTechno = () => {
+ const technoPatterns = {
+ kick: { steps: 16, pulses: 4, rotate: 0, probability: 1.0 },
+ snare: { steps: 16, pulses: 4, rotate: 4, probability: 1.0 },
+ hihat: { steps: 16, pulses: 12, rotate: 0, probability: 1.0 },
+ hihatOpen: { steps: 16, pulses: 4, rotate: 2, probability: 1.0 },
+ clap: { steps: 16, pulses: 2, rotate: 4, probability: 1.0 },
+ cowbell: { steps: 16, pulses: 3, rotate: 2, probability: 0.8 }
+ }
+
+ Object.entries(technoPatterns).forEach(([drum, params]) => {
+ updateDrum(drum as any, params)
+ })
+
+ if (window.Telegram?.WebApp?.HapticFeedback) {
+ window.Telegram.WebApp.HapticFeedback.impactOccurred('light')
+ }
+ }
+
return (
@@ -38,6 +57,14 @@ export function DrumsView() {
Настройки
+
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}
/>
) {
+ this.setKit(kit);
+ this.setSaturation(saturation);
+ for (const [drum, p] of Object.entries(drumParams)) {
+ 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..73d0cae0 100644
--- a/src/logic/DrumUtils.ts
+++ b/src/logic/DrumUtils.ts
@@ -40,3 +40,25 @@ 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 an authentic 15-bit LFSR (Linear Feedback Shift Register) noise buffer.
+ * Used for TR-909 Snappy/Noise emulation.
+ * @param context - AudioContext or Tone.BaseContext
+ * @param duration - Duration in seconds
+ */
+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 output = buffer.getChannelData(0);
+
+ let lfsr = 0x7FFF; // 15-bit seed
+ for (let i = 0; i < bufferSize; i++) {
+ // Galois LFSR 15-bit: x^15 + x^14 + 1
+ const bit = ((lfsr >> 0) ^ (lfsr >> 1)) & 1;
+ lfsr = (lfsr >> 1) | (bit << 14);
+ output[i] = (lfsr & 1) ? 1 : -1;
+ }
+ return buffer;
+}
diff --git a/src/logic/drums/TR909Snare.ts b/src/logic/drums/TR909Snare.ts
index 4841f055..f2ca3d37 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) {
diff --git a/src/store/audioStore.ts b/src/store/audioStore.ts
index c90d17e8..738a907a 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
+ // Sync initial drum parameters
+ const drumStore = useDrumStore.getState()
+ drums.syncInternalParams(drumStore.kit, drumStore.drive, {
+ kick: { pitch: drumStore.kick.pitch, decay: drumStore.kick.decay },
+ snare: { pitch: drumStore.snare.pitch, decay: drumStore.snare.decay },
+ hihat: { pitch: drumStore.hihat.pitch, decay: drumStore.hihat.decay },
+ hihatOpen: { pitch: drumStore.hihatOpen.pitch, decay: drumStore.hihatOpen.decay },
+ clap: { pitch: drumStore.clap.pitch, decay: drumStore.clap.decay },
+ cowbell: { pitch: drumStore.cowbell.pitch, decay: drumStore.cowbell.decay }
+ })
+
+ // Sync 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) => ({