feat: added playback delay configuration

This commit is contained in:
Xiaohan-Tian
2025-12-21 20:11:27 -08:00
parent aadf9e06df
commit eaee036fc2
5 changed files with 91 additions and 16 deletions
+2 -1
View File
@@ -63,7 +63,8 @@
}, },
"audio": { "audio": {
"enable_audio_capture_for_screen_sharing": false, "enable_audio_capture_for_screen_sharing": false,
"lookahead_time": 0.05 "lookahead_time": 0.05,
"playback_delay": 0.2
}, },
"templates": { "templates": {
"custom_instructions": "" "custom_instructions": ""
@@ -5,8 +5,10 @@ import { KGAudioInterface } from '../../../core/audio-interface/KGAudioInterface
const BehaviorSettings: React.FC = () => { const BehaviorSettings: React.FC = () => {
const [chatboxDefaultOpen, setChatboxDefaultOpen] = useState<boolean>(true); const [chatboxDefaultOpen, setChatboxDefaultOpen] = useState<boolean>(true);
const [audioLookaheadTime, setAudioLookaheadTime] = useState<string>('50'); const [audioLookaheadTime, setAudioLookaheadTime] = useState<string>('50');
const [playbackDelay, setPlaybackDelay] = useState<string>('200');
const [enableAudioCapture, setEnableAudioCapture] = useState<boolean>(false); const [enableAudioCapture, setEnableAudioCapture] = useState<boolean>(false);
const [validationErrors, setValidationErrors] = useState<string[]>([]); const [lookaheadValidationErrors, setLookaheadValidationErrors] = useState<string[]>([]);
const [playbackDelayValidationErrors, setPlaybackDelayValidationErrors] = useState<string[]>([]);
const configManager = ConfigManager.instance(); const configManager = ConfigManager.instance();
@@ -20,6 +22,8 @@ const BehaviorSettings: React.FC = () => {
setChatboxDefaultOpen((configManager.get('chatbox.default_open') as boolean) ?? true); setChatboxDefaultOpen((configManager.get('chatbox.default_open') as boolean) ?? true);
const lookaheadTimeSeconds = (configManager.get('audio.lookahead_time') as number) ?? 0.05; const lookaheadTimeSeconds = (configManager.get('audio.lookahead_time') as number) ?? 0.05;
setAudioLookaheadTime(((lookaheadTimeSeconds * 1000).toFixed(0))); setAudioLookaheadTime(((lookaheadTimeSeconds * 1000).toFixed(0)));
const playbackDelaySeconds = (configManager.get('audio.playback_delay') as number) ?? 0.2;
setPlaybackDelay(((playbackDelaySeconds * 1000).toFixed(0)));
setEnableAudioCapture((configManager.get('audio.enable_audio_capture_for_screen_sharing') as boolean) ?? false); setEnableAudioCapture((configManager.get('audio.enable_audio_capture_for_screen_sharing') as boolean) ?? false);
}; };
@@ -48,7 +52,7 @@ const BehaviorSettings: React.FC = () => {
errors.push('Lookahead time must be between 0 and 0.5 seconds (0-500ms)'); errors.push('Lookahead time must be between 0 and 0.5 seconds (0-500ms)');
} }
setValidationErrors(errors); setLookaheadValidationErrors(errors);
// Only apply if valid // Only apply if valid
if (errors.length === 0) { if (errors.length === 0) {
@@ -63,6 +67,32 @@ const BehaviorSettings: React.FC = () => {
} }
}; };
const handlePlaybackDelayChange = async (value: string) => {
// Allow empty string, treat as 0 ms
const numValueMs = value === '' ? 0 : parseFloat(value);
const numValueSeconds = numValueMs / 1000;
const errors: string[] = [];
// Validate the input
if (isNaN(numValueMs)) {
errors.push('Playback delay must be a valid number');
} else if (numValueSeconds < 0) {
errors.push('Playback delay must be between 0 and 0.5 seconds (0-500ms)');
} else if (numValueSeconds > 0.5) {
errors.push('Playback delay must be between 0 and 0.5 seconds (0-500ms)');
}
setPlaybackDelayValidationErrors(errors);
// Only apply if valid
if (errors.length === 0) {
setPlaybackDelay(value);
await configManager.set('audio.playback_delay', numValueSeconds);
console.log(`Playback delay changed to: ${numValueSeconds}s (${numValueMs}ms)`);
}
};
const handleEnableAudioCaptureChange = async (value: string) => { const handleEnableAudioCaptureChange = async (value: string) => {
const boolValue = value === 'yes'; const boolValue = value === 'yes';
setEnableAudioCapture(boolValue); setEnableAudioCapture(boolValue);
@@ -110,9 +140,9 @@ const BehaviorSettings: React.FC = () => {
max="500" max="500"
step="1" step="1"
/> />
{validationErrors.length > 0 && ( {lookaheadValidationErrors.length > 0 && (
<div className="settings-validation-errors"> <div className="settings-validation-errors">
{validationErrors.map((error, index) => ( {lookaheadValidationErrors.map((error, index) => (
<div key={index} className="settings-validation-error"> <div key={index} className="settings-validation-error">
{error} {error}
</div> </div>
@@ -124,6 +154,33 @@ const BehaviorSettings: React.FC = () => {
</div> </div>
</div> </div>
<div className="settings-item">
<label className="settings-label">
Playback Delay (ms)
</label>
<input
type="number"
className="settings-select"
value={playbackDelay}
onChange={(e) => handlePlaybackDelayChange(e.target.value)}
min="0"
max="500"
step="1"
/>
{playbackDelayValidationErrors.length > 0 && (
<div className="settings-validation-errors">
{playbackDelayValidationErrors.map((error, index) => (
<div key={index} className="settings-validation-error">
{error}
</div>
))}
</div>
)}
<div className="settings-help" style={{ fontSize: '12px', color: '#888', marginTop: '4px' }}>
Playback will start with a short delay after pressing the start button (0-500ms). Increasing this value might help stabilize playback, especially for the first few ticks if the lookahead value is too low. Changes apply immediately without restart.
</div>
</div>
<div className="settings-item"> <div className="settings-item">
<label className="settings-label"> <label className="settings-label">
Capture Audio for Screen Sharing Capture Audio for Screen Sharing
+11 -1
View File
@@ -320,9 +320,19 @@ export class KGCore {
// Calculate current playhead position based on elapsed time // Calculate current playhead position based on elapsed time
const elapsedMs = performance.now() - this.playbackStartTime; const elapsedMs = performance.now() - this.playbackStartTime;
// Get playback delay from config
const configManager = ConfigManager.instance();
const playbackDelaySeconds = (configManager.get('audio.playback_delay') as number) ?? 0.2;
const playbackDelayMs = playbackDelaySeconds * 1000;
// Subtract the delay from elapsed time for visual sync
// During the initial delay period, playhead stays at start position
const adjustedElapsedMs = Math.max(0, elapsedMs - playbackDelayMs);
const bpm = this.currentProject.getBpm(); const bpm = this.currentProject.getBpm();
const beatsPerMs = bpm / (60 * 1000); const beatsPerMs = bpm / (60 * 1000);
const newPosition = this.playbackStartPosition + (elapsedMs * beatsPerMs); const newPosition = this.playbackStartPosition + (adjustedElapsedMs * beatsPerMs);
// Stop playback at the end of project (maxBars) // Stop playback at the end of project (maxBars)
const maxBars = this.currentProject.getMaxBars(); const maxBars = this.currentProject.getMaxBars();
+10 -5
View File
@@ -64,8 +64,10 @@ export class KGAudioInterface {
} }
try { try {
const configManager = ConfigManager.instance();
// Reduce lookahead time to 0.05 seconds to improve MIDI input responsiveness // Reduce lookahead time to 0.05 seconds to improve MIDI input responsiveness
Tone.getContext().lookAhead = 0.05; Tone.getContext().lookAhead = configManager.get('audio.lookahead_time') as number;
// Set up master gain for volume control // Set up master gain for volume control
this.masterGain = new Tone.Gain(this.masterVolume).toDestination(); this.masterGain = new Tone.Gain(this.masterVolume).toDestination();
@@ -75,7 +77,6 @@ export class KGAudioInterface {
Tone.Transport.timeSignature = [TIME_CONSTANTS.DEFAULT_TIME_SIGNATURE.numerator, TIME_CONSTANTS.DEFAULT_TIME_SIGNATURE.denominator]; // Default time signature Tone.Transport.timeSignature = [TIME_CONSTANTS.DEFAULT_TIME_SIGNATURE.numerator, TIME_CONSTANTS.DEFAULT_TIME_SIGNATURE.denominator]; // Default time signature
// Check config and setup audio capture if enabled // Check config and setup audio capture if enabled
const configManager = ConfigManager.instance();
const enableCapture = configManager.get('audio.enable_audio_capture_for_screen_sharing') as boolean; const enableCapture = configManager.get('audio.enable_audio_capture_for_screen_sharing') as boolean;
if (enableCapture) { if (enableCapture) {
@@ -254,6 +255,10 @@ export class KGAudioInterface {
console.log("Preparing playback"); console.log("Preparing playback");
// Get playback delay from ConfigManager
const configManager = ConfigManager.instance();
const playbackDelay = (configManager.get('audio.playback_delay') as number) ?? 0.2;
try { try {
// Set project BPM and time signature FIRST (this affects timing calculations) // Set project BPM and time signature FIRST (this affects timing calculations)
Tone.Transport.bpm.value = project.getBpm(); Tone.Transport.bpm.value = project.getBpm();
@@ -301,15 +306,15 @@ export class KGAudioInterface {
const velocity = note.getVelocity() / 127; // Normalize to 0-1 const velocity = note.getVelocity() / 127; // Normalize to 0-1
console.log( console.log(
`Scheduling note ${noteName} at beat ${Number(noteStartBeat.toFixed ? noteStartBeat.toFixed(3) : noteStartBeat.toLocaleString(undefined, {maximumFractionDigits: 3}))}, Tone time: ${Number(Number(noteStartTime).toFixed(3))}, duration: ${Number(Number(noteDuration).toFixed(3))}` `Scheduling note ${noteName} at beat ${Number(noteStartBeat.toFixed ? noteStartBeat.toFixed(3) : noteStartBeat.toLocaleString(undefined, {maximumFractionDigits: 3}))}, Tone time: ${Number(Number(noteStartTime).toFixed(3))}, duration: ${Number(Number(noteDuration).toFixed(3))}, delay: ${playbackDelay}s`
); );
// Schedule the note // Schedule the note with delay offset
const eventId = Tone.Transport.schedule((time) => { const eventId = Tone.Transport.schedule((time) => {
// Check if track should play considering solo logic // Check if track should play considering solo logic
const hasSoloedTracks = this.hasSoloedTracks(); const hasSoloedTracks = this.hasSoloedTracks();
if (audioBus.shouldPlayWithSolo(hasSoloedTracks)) { if (audioBus.shouldPlayWithSolo(hasSoloedTracks)) {
audioBus.triggerAttackRelease(noteName, noteDuration, time, velocity); audioBus.triggerAttackRelease(noteName, noteDuration, time + playbackDelay, velocity);
} }
}, noteStartTime); }, noteStartTime);
+3 -1
View File
@@ -70,6 +70,7 @@ interface AppConfig {
audio: { audio: {
enable_audio_capture_for_screen_sharing: boolean; enable_audio_capture_for_screen_sharing: boolean;
lookahead_time: number; lookahead_time: number;
playback_delay: number;
}; };
templates: { templates: {
custom_instructions: string; custom_instructions: string;
@@ -229,7 +230,8 @@ export class ConfigManager {
}, },
audio: { audio: {
enable_audio_capture_for_screen_sharing: false, enable_audio_capture_for_screen_sharing: false,
lookahead_time: 0.05 lookahead_time: 0.05,
playback_delay: 0.2
}, },
templates: { templates: {
custom_instructions: '' custom_instructions: ''