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