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
17 changes: 16 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, randomizeTechno } = 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 = () => {
randomizeTechno()
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,14 @@ export function DrumsView() {
onChange={handleDriveChange}
size={40}
/>
<button
className="icon-button"
onClick={handleRandomize}
title="Randomize Techno Patterns"
style={{ padding: '8px' }}
>
<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
12 changes: 12 additions & 0 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,18 @@ export class DrumMachine {
this.params[drum] = { pitch, decay }
}

/**
* Synchronizes all internal engine parameters with the application state.
* Called during initialization to ensure parity.
*/
syncInternalParams(kit: '808' | '909', drive: number, drumParams: Record<string, { pitch: number, decay: number }>) {
this.setKit(kit);
this.setSaturation(drive);
Object.entries(drumParams).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
Expand Down
26 changes: 26 additions & 0 deletions src/logic/DrumUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,29 @@ 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 and other vintage digital textures.
* Characteristic polynomial: x^15 + x^14 + 1
* @param context - The AudioContext to create the buffer for (typed as any for Tone compatibility)
* @param duration - Duration in seconds
*/
export function generateLFSRNoise(context: any, duration: number = 0.5): AudioBuffer {
const sampleRate = context.sampleRate;
const numSamples = Math.floor(sampleRate * duration);
const buffer = context.createBuffer(1, numSamples, sampleRate);
const output = buffer.getChannelData(0);

let reg = 0x7FFF; // Initial state (must be non-zero)

for (let i = 0; i < numSamples; i++) {
// Feedback bits: 15 and 14 (indices 14 and 13)
const bit = ((reg >> 14) ^ (reg >> 13)) & 1;
reg = ((reg << 1) | bit) & 0x7FFF;

// Map bit to -1.0 or 1.0
output[i] = (reg & 1) ? 1 : -1;
}
return buffer;
}
8 changes: 5 additions & 3 deletions src/logic/drums/TR808Clap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class TR808Clap {
}

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;
const noiseSrc = new Tone.BufferSource(this.noiseBuffer);
const bpfFreq = (1000 + pitch * 1000);
const bpf = new Tone.Filter(applyVariance(bpfFreq, 0.02), "bandpass");
Expand All @@ -25,18 +26,19 @@ export class TR808Clap {
const snapIntervalBase = 0.01;
const snapInterval = applyVariance(snapIntervalBase, 0.02);

const startVal = Math.max(0.001, velocity);
for (let i = 0; i < snapCount; i++) {
const snapTime = time + i * snapInterval;
gain.gain.setValueAtTime(velocity, snapTime);
gain.gain.exponentialRampToValueAtTime(velocity * 0.1, snapTime + snapInterval * 0.8);
gain.gain.setValueAtTime(startVal, snapTime);
gain.gain.exponentialRampToValueAtTime(Math.max(0.001, velocity * 0.1), snapTime + snapInterval * 0.8);
}

// Final decay
const finalDecayStart = time + snapCount * snapInterval;
const decayTimeBase = 0.1 + decay * 0.5;
const decayTime = applyVariance(decayTimeBase, 0.02);

gain.gain.setValueAtTime(velocity, finalDecayStart);
gain.gain.setValueAtTime(startVal, finalDecayStart);
gain.gain.exponentialRampToValueAtTime(0.001, finalDecayStart + decayTime);

noiseSrc.start(time).stop(finalDecayStart + decayTime);
Expand Down
21 changes: 13 additions & 8 deletions src/logic/drums/TR808Cowbell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import * as Tone from 'tone'
import { applyPitchDrift, applyVariance } from '../DrumUtils'

export class TR808Cowbell {
private activeGains: Set<Tone.Gain> = new Set();
private activeVoices: Set<{ oscillators: Tone.Oscillator[], gain: Tone.Gain }> = new Set();

constructor(private destination: Tone.ToneAudioNode) { }

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;

// Core: Dual square wave oscillators from Schmitt trigger matrix
// Standard frequencies: 540Hz and 800Hz
const pitchMultiplier = 0.5 + pitch; // Range approx 0.5x to 1.5x
Expand All @@ -30,10 +32,12 @@ export class TR808Cowbell {

const decayTime = applyVariance(0.1 + decay * 0.4, 0.02);

vca.gain.setValueAtTime(velocity, time);
const startVal = Math.max(0.001, velocity);
vca.gain.setValueAtTime(startVal, time);
vca.gain.exponentialRampToValueAtTime(0.001, time + decayTime);

this.activeGains.add(vca);
const voice = { oscillators: [osc1, osc2], gain: vca };
this.activeVoices.add(voice);

osc1.start(time).stop(time + decayTime);
osc2.start(time).stop(time + decayTime);
Expand All @@ -45,15 +49,16 @@ export class TR808Cowbell {
bpf.dispose();
hpf.dispose();
vca.dispose();
this.activeGains.delete(vca);
this.activeVoices.delete(voice);
};
}

stop(time: number) {
this.activeGains.forEach(vca => {
vca.gain.cancelScheduledValues(time);
vca.gain.exponentialRampToValueAtTime(0.001, time + 0.02);
this.activeVoices.forEach(voice => {
voice.gain.gain.cancelScheduledValues(time);
voice.gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02);
voice.oscillators.forEach(osc => osc.stop(time + 0.02));
});
this.activeGains.clear();
this.activeVoices.clear();
}
}
21 changes: 13 additions & 8 deletions src/logic/drums/TR808HiHat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { applyPitchDrift, applyVariance } from '../DrumUtils'

export class TR808HiHat {
private frequencies = [205.3, 304.4, 369.6, 522.7, 800, 540];
private activeGains: Set<Tone.Gain> = new Set();
private activeVoices: Set<{ oscillators: Tone.Oscillator[], gain: Tone.Gain }> = new Set();

constructor(private destination: Tone.ToneAudioNode) { }

trigger(time: number, isOpen: boolean, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;

// Create nodes
const mixGain = new Tone.Gain(0.15);
const bpf1 = new Tone.Filter(3440, "bandpass");
Expand Down Expand Up @@ -48,10 +50,12 @@ export class TR808HiHat {
const decayTime = applyVariance(decayBase, 0.02);

// VCA Envelope
envGain.gain.setValueAtTime(velocity, time);
const startVal = Math.max(0.001, velocity);
envGain.gain.setValueAtTime(startVal, time);
envGain.gain.exponentialRampToValueAtTime(0.001, time + decayTime);

this.activeGains.add(envGain);
const voice = { oscillators, gain: envGain };
this.activeVoices.add(voice);

// Scheduling
oscillators.forEach(osc => {
Expand All @@ -67,15 +71,16 @@ export class TR808HiHat {
bpf2.dispose();
envGain.dispose();
hpf.dispose();
this.activeGains.delete(envGain);
this.activeVoices.delete(voice);
};
}

stop(time: number) {
this.activeGains.forEach(gain => {
gain.gain.cancelScheduledValues(time);
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02);
this.activeVoices.forEach(voice => {
voice.gain.gain.cancelScheduledValues(time);
voice.gain.gain.exponentialRampToValueAtTime(0.001, time + 0.02);
voice.oscillators.forEach(osc => osc.stop(time + 0.02));
});
this.activeGains.clear();
this.activeVoices.clear();
}
}
5 changes: 4 additions & 1 deletion src/logic/drums/TR808Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ export class TR808Kick {
constructor(private destination: Tone.ToneAudioNode) { }

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;

// pitch: 0.5 -> 52.5Hz, maps to 45-60Hz range
const tune = 45 + pitch * 15;
// decay: 0.5 -> 1.7s, maps to 0.4-3.0s range
Expand All @@ -31,7 +33,8 @@ export class TR808Kick {
osc.frequency.exponentialRampToValueAtTime(endFreq, time + 0.05);

// VCA Amp Envelope: Instant attack, adjustable exponential decay
masterGain.gain.setValueAtTime(velocity, time);
const startVal = Math.max(0.001, velocity);
masterGain.gain.setValueAtTime(startVal, time);
masterGain.gain.exponentialRampToValueAtTime(0.001, time + finalDecay);

osc.start(time).stop(time + finalDecay);
Expand Down
7 changes: 4 additions & 3 deletions src/logic/drums/TR808Snare.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export class TR808Snare {
}

trigger(time: number, pitch: number, snappy: number, velocity: number = 0.8) {
if (velocity <= 0) return;
// pitch maps to tone balance here (balance between low and high modes)
const toneBalance = pitch;

Expand All @@ -40,10 +41,10 @@ export class TR808Snare {

// Independent envelopes for maximum authenticity as per research
// High mode decays faster (approx 75% of low mode decay)
gainLow.gain.setValueAtTime(velocity * (1 - toneBalance), time);
gainLow.gain.setValueAtTime(Math.max(0.001, velocity * (1 - toneBalance)), time);
gainLow.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay);

gainHigh.gain.setValueAtTime(velocity * toneBalance, time);
gainHigh.gain.setValueAtTime(Math.max(0.001, velocity * toneBalance), time);
gainHigh.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay * 0.75);

// Snappy Layer
Expand All @@ -61,7 +62,7 @@ export class TR808Snare {
noiseFilter.connect(snappyGain);
snappyGain.connect(this.destination);

snappyGain.gain.setValueAtTime(velocity * 0.8, time);
snappyGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.8), time);
snappyGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay);

oscLow.start(time).stop(time + vcaDecay);
Expand Down
9 changes: 6 additions & 3 deletions src/logic/drums/TR909Kick.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ export class TR909Kick {
}

trigger(time: number, pitch: number, decay: number, velocity: number = 0.8) {
if (velocity <= 0) return;

// 909 Kick base frequency is fixed around 50Hz
const tune = 50;
// The 'Pitch' parameter on 909 maps to the frequency sweep duration
Expand Down Expand Up @@ -53,7 +55,8 @@ export class TR909Kick {
bodyOsc.frequency.exponentialRampToValueAtTime(endFreq, time + sweepDuration);

// VCA Envelope
bodyGain.gain.setValueAtTime(velocity, time);
const startVal = Math.max(0.001, velocity);
bodyGain.gain.setValueAtTime(startVal, time);
bodyGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay);

// Click Layer (Noise)
Expand All @@ -67,7 +70,7 @@ export class TR909Kick {

// Ultra short envelope (10-20ms) for the click
const clickDecay = applyVariance(0.02, 0.02);
noiseGain.gain.setValueAtTime(velocity * 0.7, time);
noiseGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.7), time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + clickDecay);

// Rectangular Pulse Click: Short 5ms impulse for attack articulation
Expand All @@ -76,7 +79,7 @@ export class TR909Kick {
pulseOsc.connect(pulseGain);
pulseGain.connect(this.destination);

pulseGain.gain.setValueAtTime(velocity * 0.5, time);
pulseGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.5), time);
pulseGain.gain.exponentialRampToValueAtTime(0.001, time + 0.005);

bodyOsc.start(time).stop(time + vcaDecay);
Expand Down
17 changes: 5 additions & 12 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 @@ -8,18 +8,11 @@ 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) {
if (velocity <= 0) return;
// 909 Snare Body: 2 triangle oscillators fixed at ~160Hz and ~220Hz
const freq1 = 160;
const freq2 = 220;
Expand Down Expand Up @@ -60,7 +53,7 @@ export class TR909Snare {
osc2.frequency.setValueAtTime(startFreq2, time);
osc2.frequency.exponentialRampToValueAtTime(toneDrift2, time + sweepTime);

tonalGain.gain.setValueAtTime(velocity, time);
tonalGain.gain.setValueAtTime(Math.max(0.001, velocity), time);
tonalGain.gain.exponentialRampToValueAtTime(0.001, time + vcaDecay);

// Snappy Layer
Expand All @@ -76,7 +69,7 @@ export class TR909Snare {
lpf.connect(noiseGain);
noiseGain.connect(this.destination);

noiseGain.gain.setValueAtTime(velocity * 0.7, time);
noiseGain.gain.setValueAtTime(Math.max(0.001, velocity * 0.7), time);
noiseGain.gain.exponentialRampToValueAtTime(0.001, time + snappyDecay);

osc1.start(time).stop(time + vcaDecay);
Expand Down
12 changes: 12 additions & 0 deletions src/store/audioStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -63,6 +64,17 @@ export const useAudioStore = create<AudioState>((set, get) => ({
Tone.Transport.bpm.value = get().bpm
Tone.Transport.swing = get().swing

// Sync DrumMachine internal state with instrumentStore initial values
const ds = useDrumStore.getState()
drums.syncInternalParams(ds.kit, ds.drive, {
kick: { pitch: ds.kick.pitch, decay: ds.kick.decay },
snare: { pitch: ds.snare.pitch, decay: ds.snare.decay },
hihat: { pitch: ds.hihat.pitch, decay: ds.hihat.decay },
hihatOpen: { pitch: ds.hihatOpen.pitch, decay: ds.hihatOpen.decay },
clap: { pitch: ds.clap.pitch, decay: ds.clap.decay },
cowbell: { pitch: ds.cowbell.pitch, decay: ds.cowbell.decay }
})

set({
isInitialized: true,
bassSynth: bassSynth,
Expand Down
Loading