Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion src/components/DrumsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ 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 updateDrum = (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial<DrumParams>) => {
Expand All @@ -30,6 +30,13 @@ export function DrumsView() {
if (drumMachine) drumMachine.setSaturation(v)
}

const handleRandomize = () => {
randomizeDrums()
if (window.Telegram?.WebApp?.HapticFeedback) {
window.Telegram.WebApp.HapticFeedback.impactOccurred('medium')
}
}

return (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<TransportControls title="Драм-машина" />
Expand All @@ -45,6 +52,23 @@ export function DrumsView() {
onChange={handleDriveChange}
size={40}
/>
<button
onClick={handleRandomize}
style={{
background: 'rgba(0,0,0,0.05)',
border: 'none',
borderRadius: '8px',
width: '40px',
height: '40px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: 'var(--tg-theme-text-color)',
cursor: 'pointer'
}}
>
<Dices size={20} />
</button>
<div style={{ display: 'flex', gap: '4px', background: 'rgba(0,0,0,0.05)', padding: '4px', borderRadius: '8px' }}>
{(['808', '909'] as const).map(k => (
<button
Expand Down
30 changes: 30 additions & 0 deletions src/logic/DrumUtils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,37 @@
import * as Tone from 'tone'

/**
* Shared DSP utilities for drum synthesis based on research specs.
*/

/**
* Generates an authentic 15-bit LFSR (Linear Feedback Shift Register) noise buffer.
* Based on the characteristic polynomial x^15 + x^14 + 1.
* This provides the "digital crunch" characteristic of the TR-909.
* @param context - Tone.BaseContext
* @param duration - Duration in seconds
*/
export function generateLFSRNoise(context: Tone.BaseContext, duration: number = 0.5): AudioBuffer {
const sampleRate = context.sampleRate;
const bufferSize = Math.floor(sampleRate * duration);
const buffer = context.createBuffer(1, bufferSize, sampleRate);
const data = buffer.getChannelData(0);

let reg = 0x7FFF; // 15-bit register, all 1s initially

for (let i = 0; i < bufferSize; i++) {
// x^15 + x^14 + 1
// bit 14 and bit 13 (0-indexed) are the taps for a 15-bit LFSR
let bit = ((reg >> 14) ^ (reg >> 13)) & 1;
reg = ((reg << 1) | bit) & 0x7FFF;

// Output bit 0 as noise
data[i] = (reg & 1) ? 1 : -1;
}

return buffer;
}

/**
* Creates a soft-clipping saturation curve (hyperbolic tangent approximation).
* Formula: (3 + k) * x * 20 * deg / (Math.PI + k * Math.abs(x))
Expand Down
9 changes: 7 additions & 2 deletions src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,15 @@ export class TR808Kick {
osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05);

// VCA Amp Envelope: Instant attack, adjustable exponential decay
// Research: Damping effect (diodes) causes faster initial decay (20ms)
masterGain.gain.setValueAtTime(velocity, time);
masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay);
const dampingTime = 0.02;
const safeFinalDecay = Math.max(dampingTime + 0.01, finalDecay);

osc.start(time).stop(time + finalDecay);
masterGain.gain.exponentialRampToValueAtTime(velocity * 0.5, time + dampingTime);
masterGain.gain.exponentialRampToValueAtTime(0.001, time + safeFinalDecay);

osc.start(time).stop(time + safeFinalDecay);

osc.onstop = () => {
osc.dispose();
Expand Down
12 changes: 3 additions & 9 deletions src/logic/drums/TR909Snare.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 LFSR noise for 909 Snappy
this.noiseBuffer = generateLFSRNoise(Tone.getContext(), 0.5);
}

trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) {
Expand Down
16 changes: 15 additions & 1 deletion src/store/instrumentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ interface DrumState {
setParams: (drum: 'kick' | 'snare' | 'hihat' | 'hihatOpen' | 'clap' | 'cowbell', params: Partial<DrumParams>) => void
setKit: (kit: '808' | '909') => void
setDrive: (drive: number) => void
randomizeDrums: () => void
}

export const useDrumStore = create<DrumState>((set) => ({
Expand All @@ -56,7 +57,20 @@ export const useDrumStore = create<DrumState>((set) => ({
[drum]: { ...state[drum], ...params }
})),
setKit: (kit) => set({ kit }),
setDrive: (drive) => set({ drive })
setDrive: (drive) => set({ drive }),
randomizeDrums: () => set((state) => {
// Musical randomization logic
const rand = (min: number, max: number) => Math.floor(Math.random() * (max - min + 1)) + min;

return {
kick: { ...state.kick, pulses: [4, 4, 8, 4, 6].sort(() => Math.random() - 0.5)[0], rotate: 0 },
snare: { ...state.snare, pulses: [2, 4, 4, 0, 2].sort(() => Math.random() - 0.5)[0], rotate: 4 },
hihat: { ...state.hihat, pulses: [8, 12, 16, 10, 14].sort(() => Math.random() - 0.5)[0], rotate: rand(0, 4) },
hihatOpen: { ...state.hihatOpen, pulses: [2, 4, 4, 2, 0].sort(() => Math.random() - 0.5)[0], rotate: rand(0, 8) },
Comment on lines +68 to +69

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve randomized rotation in exported drums

When the dice picks a non-zero rotate here, playback and the visualizer apply it via rotateArray(...) in SequencerLoop/DrumsView, but handleExport in src/App.tsx still exports plain bjorklund(...) patterns without rotation. After randomizing hats/open hats/cowbell, the MIDI export can therefore differ from the groove the user just heard; either avoid randomizing rotate or apply the same rotation when preparing export patterns.

Useful? React with 👍 / 👎.

clap: { ...state.clap, pulses: [0, 2, 4, 2, 0].sort(() => Math.random() - 0.5)[0], rotate: 4 },
cowbell: { ...state.cowbell, pulses: rand(0, 5), rotate: rand(0, 15), probability: 0.5 + Math.random() * 0.4 }
};
})
}))

// Pad Store
Expand Down