// Copyright 2026 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
fn gain_duration_to_nanos(duration : @moon_cpal.Duration) -> Double {
Double::from_int(duration.secs.to_int()) * 1_000_000_000.0 +
Double::from_int(duration.nanos)
}
///|
pub fn[S : Source] channel_volume(
source : S,
channel_factors : Array[Sample],
) -> DynSource {
guard !channel_factors.is_empty() else { panic() }
let src = to_dyn(source)
let from_channels = src.channels()
let out_channels = channel_factors.length()
let current_channel = @ref.new(out_channels)
let current_sample = @ref.new(None)
DynSource::new_dynamic(
fn() {
if current_channel.val >= out_channels {
current_channel.val = 0
current_sample.val = None
for _ in 0.. ()
Some(s) =>
current_sample.val = Some(current_sample.val.unwrap_or(0.0) + s)
}
}
current_sample.val = current_sample.val.map(fn(v) {
v / Double::from_int(from_channels)
})
}
let result = current_sample.val.map(fn(v) {
v * channel_factors[current_channel.val]
})
current_channel.val += 1
result
},
fn() { out_channels },
fn() { src.sample_rate() },
)
}
///|
pub fn[S : Source] linear_gain_ramp(
source : S,
start_gain : Sample,
end_gain : Sample,
duration : @moon_cpal.Duration,
) -> DynSource {
linear_gain_ramp_with_clamp(source, start_gain, end_gain, duration, true)
}
///|
pub fn[S : Source] linear_gain_ramp_with_clamp(
source : S,
start_gain : Sample,
end_gain : Sample,
duration : @moon_cpal.Duration,
clamp_end : Bool,
) -> DynSource {
guard duration.secs > (0 : UInt64) || duration.nanos > 0 else { panic() }
let src = to_dyn(source)
let elapsed_ns = @ref.new(0.0)
let total_ns = gain_duration_to_nanos(duration)
let sample_idx = @ref.new(0)
DynSource::new_dynamic(
fn() {
match src.next() {
None => None
Some(v) => {
let remaining_ns = total_ns - elapsed_ns.val
let factor = if remaining_ns < 0.0 && clamp_end {
end_gain
} else if remaining_ns < 0.0 {
1.0
} else {
let p = elapsed_ns.val / total_ns
start_gain * (1.0 - p) + end_gain * p
}
sample_idx.val += 1
let channels = src.channels()
if channels > 0 && sample_idx.val % channels == 0 {
elapsed_ns.val += 1_000_000_000.0 /
Double::from_int(src.sample_rate())
}
Some(v * factor)
}
}
},
fn() { src.channels() },
fn() { src.sample_rate() },
current_span_len=fn() { src.current_span_len() },
total_duration=fn() { src.total_duration() },
try_seek=fn(pos : @moon_cpal.Duration) {
elapsed_ns.val = gain_duration_to_nanos(pos)
try {
src.try_seek(pos)
Ok(())
} catch {
err => Err(err)
}
},
)
}
///|
pub fn[S : Source] fade_in(
source : S,
duration : @moon_cpal.Duration,
) -> DynSource {
linear_gain_ramp(source, 0.0, 1.0, duration)
}
///|
pub fn[S : Source] fadein(
source : S,
duration : @moon_cpal.Duration,
) -> DynSource {
fade_in(source, duration)
}
///|
pub fn[S : Source] fade_out(
source : S,
duration : @moon_cpal.Duration,
) -> DynSource {
// Match rodio fadeout semantics: apply a forward linear ramp 1.0 -> 0.0,
// then keep output clamped at 0.0 after the ramp duration.
linear_gain_ramp(source, 1.0, 0.0, duration)
}
///|
pub fn[S : Source] fadeout(
source : S,
duration : @moon_cpal.Duration,
) -> DynSource {
fade_out(source, duration)
}