/** * Track utility functions */ import type { Track, TrackColor } from '@/types/track'; import { DEFAULT_TRACK_HEIGHT, TRACK_COLORS } from '@/types/track'; import { createEffectChain } from '@/lib/audio/effects/chain'; import { createAutomationLane } from '@/lib/audio/automation-utils'; /** * Generate a unique track ID */ export function generateTrackId(): string { return `track-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * Create a new empty track */ export function createTrack(name?: string, color?: TrackColor, height?: number): Track { const colors: TrackColor[] = ['blue', 'green', 'purple', 'orange', 'pink', 'indigo', 'yellow', 'red']; const randomColor = colors[Math.floor(Math.random() * colors.length)]; // Ensure name is always a string, handle cases where event objects might be passed const trackName = typeof name === 'string' && name.trim() ? name.trim() : 'New Track'; const trackId = generateTrackId(); return { id: trackId, name: trackName, color: TRACK_COLORS[color || randomColor], height: height ?? DEFAULT_TRACK_HEIGHT, audioBuffer: null, volume: 0.8, pan: 0, mute: false, solo: false, recordEnabled: false, effectChain: createEffectChain(`${trackName} Effects`), automation: { lanes: [ createAutomationLane(trackId, 'volume', 'Volume', { min: 0, max: 1, unit: 'dB', }), createAutomationLane(trackId, 'pan', 'Pan', { min: -1, max: 1, formatter: (value: number) => { if (value === 0) return 'C'; if (value < 0) return `${Math.abs(value * 100).toFixed(0)}L`; return `${(value * 100).toFixed(0)}R`; }, }), ], showAutomation: false, }, collapsed: false, selected: false, showEffects: false, selection: null, }; } /** * Create a track from an audio buffer */ export function createTrackFromBuffer( buffer: AudioBuffer, name?: string, color?: TrackColor, height?: number ): Track { // Ensure name is a string before passing to createTrack const trackName = typeof name === 'string' && name.trim() ? name.trim() : undefined; const track = createTrack(trackName, color, height); track.audioBuffer = buffer; return track; } /** * Mix multiple tracks into a single stereo buffer */ export function mixTracks( tracks: Track[], sampleRate: number, duration: number ): AudioBuffer { // Determine the length needed const length = Math.ceil(duration * sampleRate); // Create output buffer (stereo) const offlineContext = new OfflineAudioContext(2, length, sampleRate); const outputBuffer = offlineContext.createBuffer(2, length, sampleRate); const leftChannel = outputBuffer.getChannelData(0); const rightChannel = outputBuffer.getChannelData(1); // Check if any tracks are soloed const soloedTracks = tracks.filter((t) => t.solo && !t.mute); const audibleTracks = soloedTracks.length > 0 ? soloedTracks : tracks.filter((t) => !t.mute); // Mix each audible track for (const track of audibleTracks) { if (!track.audioBuffer) continue; const buffer = track.audioBuffer; const trackLength = Math.min(buffer.length, length); // Get source channels (handle mono/stereo) const sourceLeft = buffer.getChannelData(0); const sourceRight = buffer.numberOfChannels > 1 ? buffer.getChannelData(1) : sourceLeft; // Calculate pan gains (constant power panning) const panAngle = (track.pan * Math.PI) / 4; // -π/4 to π/4 const leftGain = Math.cos(panAngle + Math.PI / 4) * track.volume; const rightGain = Math.sin(panAngle + Math.PI / 4) * track.volume; // Mix into output buffer for (let i = 0; i < trackLength; i++) { leftChannel[i] += sourceLeft[i] * leftGain; rightChannel[i] += sourceRight[i] * rightGain; } } return outputBuffer; } /** * Get the maximum duration across all tracks */ export function getMaxTrackDuration(tracks: Track[]): number { let maxDuration = 0; for (const track of tracks) { if (track.audioBuffer) { const duration = track.audioBuffer.duration; if (duration > maxDuration) { maxDuration = duration; } } } return maxDuration; } /** * Calculate track mix gain (considering solo/mute) */ export function getTrackGain(track: Track, allTracks: Track[]): number { // If track is muted, gain is 0 if (track.mute) return 0; // Check if any tracks are soloed const hasSoloedTracks = allTracks.some((t) => t.solo); // If there are soloed tracks and this track is not soloed, gain is 0 if (hasSoloedTracks && !track.solo) return 0; // Otherwise, return the track's volume return track.volume; }