@@ -9,7 +9,7 @@ use symphonia::core::probe::Hint;
99
1010use crate :: schema:: AudioTrack ;
1111
12- const TARGET_SAMPLE_RATE : u32 = 44100 ;
12+ const TARGET_SAMPLE_RATE : u32 = 48000 ;
1313const TARGET_CHANNELS : u32 = 2 ;
1414
1515/// Decode an audio file into PCM i16 samples (stereo, 44100Hz, interleaved)
@@ -122,10 +122,16 @@ pub fn mix_audio_tracks(tracks: &[AudioTrack], total_duration: f64) -> Result<Op
122122 break ;
123123 }
124124
125- let mut sample = resampled[ i] * track. volume ;
125+ let frame = i / TARGET_CHANNELS as usize ;
126+ let current_time = track. start + ( frame as f64 / TARGET_SAMPLE_RATE as f64 ) ;
127+ let vol = if !track. volume_keyframes . is_empty ( ) {
128+ interpolate_volume_keyframes ( & track. volume_keyframes , current_time)
129+ } else {
130+ track. volume
131+ } ;
132+ let mut sample = resampled[ i] * vol;
126133
127134 // Apply fade in
128- let frame = i / TARGET_CHANNELS as usize ;
129135 if fade_in_samples > 0.0 && ( frame as f64 ) < fade_in_samples {
130136 sample *= frame as f32 / fade_in_samples as f32 ;
131137 }
@@ -152,6 +158,33 @@ pub fn mix_audio_tracks(tracks: &[AudioTrack], total_duration: f64) -> Result<Op
152158 Ok ( Some ( pcm_bytes) )
153159}
154160
161+ /// Interpolate volume at a given time using volume keyframes with easing
162+ fn interpolate_volume_keyframes ( keyframes : & [ crate :: schema:: VolumeKeyframe ] , time : f64 ) -> f32 {
163+ if keyframes. is_empty ( ) {
164+ return 1.0 ;
165+ }
166+ if time <= keyframes[ 0 ] . time {
167+ return keyframes[ 0 ] . volume ;
168+ }
169+ if time >= keyframes. last ( ) . unwrap ( ) . time {
170+ return keyframes. last ( ) . unwrap ( ) . volume ;
171+ }
172+ for i in 0 ..keyframes. len ( ) - 1 {
173+ let kf0 = & keyframes[ i] ;
174+ let kf1 = & keyframes[ i + 1 ] ;
175+ if time >= kf0. time && time <= kf1. time {
176+ let duration = kf1. time - kf0. time ;
177+ if duration < 1e-9 {
178+ return kf1. volume ;
179+ }
180+ let t = ( time - kf0. time ) / duration;
181+ let progress = crate :: engine:: animator:: ease ( t, & kf0. easing ) ;
182+ return kf0. volume + ( kf1. volume - kf0. volume ) * progress as f32 ;
183+ }
184+ }
185+ keyframes. last ( ) . unwrap ( ) . volume
186+ }
187+
155188fn to_stereo ( samples : & [ f32 ] , channels : u32 ) -> Vec < f32 > {
156189 match channels {
157190 1 => {
0 commit comments