tdpsola 0.1.0

An implementation of the TD-PSOLA algorithm (formants-preserving time stretching and pitch-shifting).
Documentation
// Copyright (C) 2020 Pieter Penninckx
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
extern crate anyhow;
use anyhow::{anyhow, Context, Result};
use std::env;
use std::fs::File;
use std::path::Path;
use tdpsola::{AlternatingHann, Speed, TdpsolaAnalysis, TdpsolaSynthesis};
use wav::BitDepth;

struct CliArguments {
    input_filename: String,
    output_filename: String,
    speed: f32,
    source_frequency: f32,
    target_frequency: f32,
}

const USAGE: &'static str = "Usage:
    tdpsola_demo INPUT_FILENAME OUTPUT_FILENAME SPEED SOURCE_FREQUENCY TARGET_FREQUENCY
    The output file will be overwritten without warning.
    Only mono .wav files are supported.
    The output will always be 16 bits, regardless of the input.";

impl CliArguments {
    fn parse() -> Result<Self> {
        let args: Vec<String> = env::args().collect();
        if args.len() < 4 {
            Err(anyhow!("Missing command line argument."))
        } else {
            let input_filename = args[1].clone();
            let output_filename = args[2].clone();
            let speed = args[3].parse()?;
            let source_frequency: f32 = args[4].parse()?;
            let target_frequency = args[5].parse()?;

            if source_frequency <= 0.0 {
                return Err(anyhow!("Source frequency must be positive."));
            }
            let minimum_source_frequency = 1e-6;
            if source_frequency.abs() < minimum_source_frequency {
                return Err(anyhow!(
                    "Source frequencies lower than {} Hz are not supported.",
                    minimum_source_frequency
                ));
            }
            if target_frequency <= 0.0 {
                return Err(anyhow!("Target frequency must be positive."));
            }
            let minimum_target_frequency = 1e-6;
            if target_frequency < minimum_target_frequency {
                return Err(anyhow!(
                    "Target frequencies lower than {} Hz are not supported.",
                    minimum_target_frequency
                ));
            }
            let minimum_speed = 1e-6;
            if speed < minimum_speed {
                return Err(anyhow!(
                    "Speeds slower than {} are not supported.",
                    minimum_speed
                ));
            }
            Ok(CliArguments {
                input_filename,
                output_filename,
                speed,
                source_frequency,
                target_frequency,
            })
        }
    }
}

fn extract_data(bit_depth: BitDepth) -> Vec<f32> {
    match bit_depth {
        BitDepth::Eight(v) => v.iter().map(|x| *x as f32).collect(),
        BitDepth::Sixteen(v) => v.iter().map(|x| *x as f32).collect(),
        BitDepth::TwentyFour(v) => v.iter().map(|x| *x as f32).collect(),
        BitDepth::Empty => Vec::new(),
    }
}

fn repackage_data(input: &[f32]) -> BitDepth {
    if input.is_empty() {
        return BitDepth::Empty;
    }
    BitDepth::Sixteen(input.iter().map(|x| (*x) as i16).collect())
}

fn main() -> Result<()> {
    let arguments = CliArguments::parse().context(USAGE)?;

    let mut input_file = File::open(Path::new(&arguments.input_filename)).context(format!(
        "Failed to open input file '{}'.",
        arguments.input_filename
    ))?;
    let (input_header, input_data) = wav::read(&mut input_file).context(format!(
        "Failed to read input file {}",
        arguments.input_filename
    ))?;
    if input_header.channel_count != 1 {
        return Err(anyhow!(format!(
            "Input file {} is not mono. Only mono files are supported.",
            arguments.input_filename
        )));
    }

    let mut out_file = File::create(Path::new(&arguments.output_filename)).context(format!(
        "Failed to open output file '{}'.",
        arguments.output_filename
    ))?;

    let sampling_frequency = input_header.sampling_rate;
    let source_wavelength = (sampling_frequency as f32) / arguments.source_frequency;
    let target_wavelength = (sampling_frequency as f32) / arguments.target_frequency;

    let input = extract_data(input_data);
    let mut alternating_hann = AlternatingHann::new(source_wavelength);
    let mut analysis = TdpsolaAnalysis::new(&alternating_hann);

    let padding_length = source_wavelength as usize + 1;
    for _ in 0..padding_length {
        analysis.push_sample(0.0, &mut alternating_hann);
    }

    println!("Analysing...");
    for sample in input.iter() {
        analysis.push_sample(*sample, &mut alternating_hann);
    }

    let speed = Speed::from_f32(arguments.speed);
    let mut synthesis = TdpsolaSynthesis::new(speed, target_wavelength);

    let mut output = Vec::new();
    println!("Synthesizing...");
    for output_sample in synthesis.iter(&analysis).skip(padding_length) {
        output.push(output_sample);
    }

    let output_data = repackage_data(&output);

    wav::write(input_header, output_data, &mut out_file).context(format!(
        "Unable to write to output file '{}'.",
        arguments.output_filename
    ))?;
    println!("Done.");

    Ok(())
}