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
31 changes: 29 additions & 2 deletions src/components/DrumsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px' }}>
<TransportControls title="Драм-машина" />
Expand All @@ -38,6 +57,14 @@ export function DrumsView() {
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<h3 style={{ margin: 0 }}>Настройки</h3>
<div style={{ display: 'flex', gap: '12px', alignItems: 'center' }}>
<button
className="icon-button"
onClick={randomizeTechno}
title="Techno Randomize"
style={{ padding: '8px', opacity: 0.7 }}
>
<Dices size={20} />
</button>
<Knob
label="DRIVE"
value={drive}
Expand Down Expand Up @@ -105,9 +132,9 @@ export function DrumsView() {
/>
<Knob
label="Vol"
value={volumes[d.id === 'cowbell' ? 'cow' : d.id]}
value={volumes[d.id]}
min={0} max={1} step={0.01}
onChange={(v) => setVolume(d.id === 'cowbell' ? 'cow' : d.id, v)}
onChange={(v) => setVolume(d.id, v)}
size={40}
/>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/components/MixerView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ export function MixerView() {
/>
<Knob
label="Cow"
value={volumes.cow}
value={volumes.cowbell}
min={0} max={1} step={0.01}
onChange={(v) => setVolume('cow', v)}
onChange={(v) => setVolume('cowbell', v)}
size={48}
/>
<Knob
Expand Down
8 changes: 8 additions & 0 deletions src/logic/DrumMachine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,14 @@ export class DrumMachine {
this.params[drum] = { pitch, decay }
}

syncInternalParams(kit: '808' | '909', saturation: number, drumParams: Record<string, { pitch: number, decay: number }>) {
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
Expand Down
22 changes: 22 additions & 0 deletions src/logic/DrumUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
12 changes: 2 additions & 10 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,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) {
Expand Down
29 changes: 25 additions & 4 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 All @@ -22,15 +23,15 @@ export interface AudioState {
hihat: number,
hihatOpen: number,
clap: number,
cow: number,
cowbell: number,
pads: number
}
initialize: () => Promise<void>
togglePlay: () => void
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<AudioState>((set, get) => ({
Expand All @@ -43,7 +44,7 @@ export const useAudioStore = create<AudioState>((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
Expand All @@ -63,6 +64,26 @@ export const useAudioStore = create<AudioState>((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,
Expand Down Expand Up @@ -96,7 +117,7 @@ export const useAudioStore = create<AudioState>((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)
Expand Down
4 changes: 2 additions & 2 deletions src/store/instrumentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,11 @@ interface DrumState {

export const useDrumStore = create<DrumState>((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) => ({
Expand Down