feat: added metronome feature
This commit is contained in:
@@ -10,6 +10,7 @@ import type { InstrumentType } from '../track/KGMidiTrack';
|
||||
import type { KGAudioRegion } from '../region/KGAudioRegion';
|
||||
import { KGCore } from '../KGCore';
|
||||
import { ConfigManager } from '../config/ConfigManager';
|
||||
import { KGMetronome } from './KGMetronome';
|
||||
|
||||
/**
|
||||
* KGAudioInterface - Audio engine interface for the DAW
|
||||
@@ -45,6 +46,10 @@ export class KGAudioInterface {
|
||||
// Master volume control
|
||||
private masterGain: Tone.Gain | null = null;
|
||||
|
||||
// Metronome
|
||||
private metronome: KGMetronome = new KGMetronome();
|
||||
private isMetronomeEnabled = false;
|
||||
|
||||
// Audio capture for screen sharing
|
||||
private captureDestination: MediaStreamAudioDestinationNode | null = null;
|
||||
private captureStream: MediaStream | null = null;
|
||||
@@ -88,6 +93,11 @@ export class KGAudioInterface {
|
||||
Tone.Transport.bpm.value = TIME_CONSTANTS.DEFAULT_BPM; // Default BPM
|
||||
Tone.Transport.timeSignature = [TIME_CONSTANTS.DEFAULT_TIME_SIGNATURE.numerator, TIME_CONSTANTS.DEFAULT_TIME_SIGNATURE.denominator]; // Default time signature
|
||||
|
||||
// Initialize metronome sampler in background (non-blocking)
|
||||
this.metronome.initialize(this.masterGain!).catch(err => {
|
||||
console.error('Failed to initialize metronome:', err);
|
||||
});
|
||||
|
||||
// Check config and setup audio capture if enabled
|
||||
const enableCapture = configManager.get('audio.enable_audio_capture_for_screen_sharing') as boolean;
|
||||
|
||||
@@ -144,6 +154,9 @@ export class KGAudioInterface {
|
||||
});
|
||||
this.trackAudioPlayerBuses.clear();
|
||||
|
||||
// Dispose metronome
|
||||
this.metronome.dispose();
|
||||
|
||||
// Dispose master gain
|
||||
if (this.masterGain) {
|
||||
this.masterGain.dispose();
|
||||
@@ -429,7 +442,12 @@ export class KGAudioInterface {
|
||||
|
||||
// Set transport position (convert beats to Tone.js format)
|
||||
this.setTransportPosition(startPosition);
|
||||
|
||||
|
||||
// Start metronome if enabled
|
||||
if (this.isMetronomeEnabled) {
|
||||
this.metronome.start(startPosition, timeSignature.numerator, playbackDelay);
|
||||
}
|
||||
|
||||
// Schedule all MIDI events
|
||||
project.getTracks().forEach(track => {
|
||||
const trackId = track.getId().toString();
|
||||
@@ -645,7 +663,8 @@ export class KGAudioInterface {
|
||||
public stopPlayback(): void {
|
||||
try {
|
||||
Tone.Transport.stop();
|
||||
|
||||
this.metronome.stop();
|
||||
|
||||
// Release all currently playing notes
|
||||
this.trackAudioBuses.forEach(audioBus => {
|
||||
audioBus.releaseAll();
|
||||
@@ -791,6 +810,23 @@ export class KGAudioInterface {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== METRONOME =====
|
||||
|
||||
public setMetronomeEnabled(enabled: boolean): void {
|
||||
this.isMetronomeEnabled = enabled;
|
||||
}
|
||||
|
||||
/** Start the metronome mid-playback without restarting the transport. */
|
||||
public startMetronomeDuringPlayback(currentPositionBeats: number, beatsPerBar: number): void {
|
||||
const playbackDelay = (ConfigManager.instance().get('audio.playback_delay') as number) ?? 0.2;
|
||||
this.metronome.start(currentPositionBeats, beatsPerBar, playbackDelay);
|
||||
}
|
||||
|
||||
/** Stop the metronome mid-playback without stopping the transport. */
|
||||
public stopMetronomeDuringPlayback(): void {
|
||||
this.metronome.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set transport BPM
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import * as Tone from 'tone';
|
||||
import { KGToneSamplerFactory } from './KGToneSamplerFactory';
|
||||
|
||||
/**
|
||||
* KGMetronome - click track synchronized with Tone.Transport
|
||||
* Uses Tone.Loop so it automatically respects loop points and play position.
|
||||
*/
|
||||
export class KGMetronome {
|
||||
private loop: Tone.Loop | null = null;
|
||||
private sampler: Tone.Sampler | null = null;
|
||||
|
||||
/**
|
||||
* Load the woodblock sampler. Called once from KGAudioInterface.initialize() —
|
||||
* runs in background (caller should not await).
|
||||
*/
|
||||
async initialize(masterOutput: Tone.ToneAudioNode): Promise<void> {
|
||||
try {
|
||||
this.sampler = await KGToneSamplerFactory.instance().createSampler('woodblock');
|
||||
this.sampler.connect(masterOutput);
|
||||
console.log('KGMetronome: woodblock sampler loaded');
|
||||
} catch (error) {
|
||||
console.error('KGMetronome: failed to load woodblock sampler', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the metronome click loop.
|
||||
* @param startPositionBeats - Transport start position in beats (unused beyond logging)
|
||||
* @param beatsPerBar - numerator of the time signature
|
||||
* @param playbackDelay - seconds to offset audio trigger, matching MIDI note scheduling delay
|
||||
*/
|
||||
start(startPositionBeats: number, beatsPerBar: number, playbackDelay = 0): void {
|
||||
this.stop();
|
||||
|
||||
const ppq = Tone.Transport.PPQ;
|
||||
|
||||
this.loop = new Tone.Loop((time) => {
|
||||
if (this.sampler?.loaded) {
|
||||
// Derive bar position from the exact Transport tick count at the
|
||||
// scheduled audio time — no beatCount tracking needed, which avoids
|
||||
// all phase initialisation errors when playing from mid-bar.
|
||||
const ticks = Tone.Transport.getTicksAtTime(time);
|
||||
const beatNumber = Math.round(ticks / ppq);
|
||||
const note = beatNumber % beatsPerBar === 0 ? 'C5' : 'C4';
|
||||
this.sampler.triggerAttackRelease(note, '16n', time + playbackDelay);
|
||||
}
|
||||
}, '4n');
|
||||
|
||||
this.loop.start(0);
|
||||
console.log(`KGMetronome: started at beat ${startPositionBeats} (${beatsPerBar} beats/bar), delay ${playbackDelay}s`);
|
||||
}
|
||||
|
||||
/** Stop and dispose the loop only — sampler is kept alive for reuse. */
|
||||
stop(): void {
|
||||
if (this.loop) {
|
||||
this.loop.dispose();
|
||||
this.loop = null;
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
this.stop();
|
||||
if (this.sampler) {
|
||||
this.sampler.dispose();
|
||||
this.sampler = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user