|
use clap::Parser; |
|
use hound; |
|
use num_complex::Complex; |
|
use realfft::RealFftPlanner; |
|
use std::f32::consts::PI; |
|
|
|
#[derive(Parser, Debug)] |
|
#[command(author, version, about = "频谱线性缩放工具")] |
|
struct Args { |
|
input: String, |
|
|
|
output: String, |
|
|
|
/// 缩放倍率 (例如 2.0 将频谱压缩一半) |
|
#[arg(short, long, default_value_t = 2.0)] |
|
scale: f32, |
|
|
|
/// 处理块的大小 |
|
#[arg(short, long, default_value_t = 4096)] |
|
chunk_size: usize, |
|
|
|
/// 跳跃步长比例 (0.1 ~ 1.0, 越小越平滑) |
|
#[arg(long, default_value_t = 0.25)] |
|
hop_rate: f32, |
|
|
|
/// 是否禁用窗函数 (禁用后会产生大量 Clicking) |
|
#[arg(long, default_value_t = false)] |
|
no_window: bool, |
|
} |
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> { |
|
let args = Args::parse(); |
|
|
|
let mut reader = hound::WavReader::open(&args.input)?; |
|
let spec = reader.spec(); |
|
let channels = spec.channels as usize; |
|
|
|
let full_samples: Vec<f32> = reader |
|
.samples::<i16>() |
|
.map(|s| s.unwrap() as f32 / i16::MAX as f32) |
|
.collect(); |
|
|
|
let mut processed_channels = Vec::new(); |
|
for c in 0..channels { |
|
let channel_data: Vec<f32> = full_samples.iter().enumerate() |
|
.filter(|&(i, _)| i % channels == c) |
|
.map(|(_, &s)| s) |
|
.collect(); |
|
|
|
processed_channels.push(process_channel(&channel_data, &args)); |
|
} |
|
|
|
// 采样率随倍率缩放以保持时长 |
|
let mut out_spec = spec; |
|
out_spec.sample_rate = (spec.sample_rate as f32 / args.scale) as u32; |
|
let mut writer = hound::WavWriter::create(&args.output, out_spec)?; |
|
|
|
let output_len = processed_channels[0].len(); |
|
for i in 0..output_len { |
|
for c in 0..channels { |
|
let s = processed_channels[c][i]; |
|
writer.write_sample((s.clamp(-1.0, 1.0) * i16::MAX as f32) as i16)?; |
|
} |
|
} |
|
|
|
println!("处理完成: {} -> {}", args.input, args.output); |
|
Ok(()) |
|
} |
|
|
|
fn process_channel(input: &[f32], args: &Args) -> Vec<f32> { |
|
let n = args.chunk_size; |
|
let out_n = (n as f32 / args.scale) as usize; |
|
let hop_in = (n as f32 * args.hop_rate) as usize; |
|
let hop_out = (out_n as f32 * args.hop_rate) as usize; |
|
|
|
let mut planner = RealFftPlanner::<f32>::new(); |
|
let fft = planner.plan_fft_forward(n); |
|
let ifft = planner.plan_fft_inverse(out_n); |
|
|
|
// 准备窗函数 (Hanning) |
|
let window: Vec<f32> = if args.no_window { |
|
vec![1.0; n] |
|
} else { |
|
(0..n).map(|i| 0.5 * (1.0 - (2.0 * PI * i as f32 / (n - 1) as f32).cos())).collect() |
|
}; |
|
|
|
// 预估输出长度 |
|
let expected_len = (input.len() as f32 / args.scale) as usize + out_n; |
|
let mut output = vec![0.0; expected_len]; |
|
let mut weight_sum = vec![0.0; expected_len]; |
|
|
|
let mut pos_in = 0; |
|
let mut pos_out = 0; |
|
|
|
while pos_in + n <= input.len() { |
|
// 1. 加窗切片 |
|
let mut indata = fft.make_input_vec(); |
|
for j in 0..n { |
|
indata[j] = input[pos_in + j] * window[j]; |
|
} |
|
|
|
// 2. FFT |
|
let mut spectrum = fft.make_output_vec(); |
|
fft.process(&mut indata, &mut spectrum).unwrap(); |
|
|
|
// 3. 频谱重采样 (线性搬移) |
|
let new_spec_len = out_n / 2 + 1; |
|
let mut new_spectrum = vec![Complex::new(0.0, 0.0); new_spec_len]; |
|
for j in 0..new_spec_len { |
|
let src_idx = (j as f32 * args.scale).round() as usize; |
|
if src_idx < spectrum.len() { |
|
new_spectrum[j] = spectrum[src_idx]; |
|
} |
|
} |
|
new_spectrum[0].im = 0.0; |
|
if let Some(last) = new_spectrum.last_mut() { last.im = 0.0; } |
|
|
|
// 4. IFFT |
|
let mut outdata = ifft.make_output_vec(); |
|
ifft.process(&mut new_spectrum, &mut outdata).unwrap(); |
|
|
|
// 5. 重叠相加 (OLA) |
|
let norm = 1.0 / out_n as f32; |
|
for j in 0..out_n { |
|
if pos_out + j < output.len() { |
|
// 如果加了窗,输出时也要再乘一遍窗以保证平滑过渡 |
|
let w = if args.no_window { 1.0 } else { |
|
// 对应的输出端窗函数索引 |
|
let out_w_idx = (j as f32 * args.scale).round() as usize; |
|
if out_w_idx < n { window[out_w_idx] } else { 0.0 } |
|
}; |
|
output[pos_out + j] += outdata[j] * norm * w; |
|
weight_sum[pos_out + j] += w * w; |
|
} |
|
} |
|
|
|
pos_in += hop_in; |
|
pos_out += hop_out; |
|
} |
|
|
|
// 6. 归一化能量 (防止重叠部分声音变大) |
|
for i in 0..output.len() { |
|
if weight_sum[i] > 1e-6 { |
|
output[i] /= weight_sum[i]; |
|
} |
|
} |
|
|
|
output |
|
} |