feat: added playback delay configuration
This commit is contained in:
+2
-1
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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: ''
|
||||||
|
|||||||
Reference in New Issue
Block a user