From aa7737e17a979fe00b1a1569365abb9763fd6e56 Mon Sep 17 00:00:00 2001 From: pommicket Date: Fri, 28 Apr 2023 09:30:04 -0400 Subject: initial (& perhaps final) commit --- .gitignore | 2 + Cargo.lock | 343 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Cargo.toml | 11 ++ README.md | 45 ++++++++ rustfmt.toml | 1 + sawtooth.wav | Bin 0 -> 132344 bytes sine.wav | Bin 0 -> 132344 bytes src/README.md | 1 + src/main.rs | 103 ++++++++++++++++++ voice.wav | Bin 0 -> 112172 bytes 10 files changed, 506 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 rustfmt.toml create mode 100644 sawtooth.wav create mode 100644 sine.wav create mode 100644 src/README.md create mode 100644 src/main.rs create mode 100644 voice.wav diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6cd4e9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +*-seamless.wav diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..a627073 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,343 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anstream" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6342bd4f5a1205d7f41e94a41a901f5647c938cdfa96036338e8533c9d6c2450" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is-terminal", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d" + +[[package]] +name = "anstyle-parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "clap" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a1f23fa97e1d1641371b51f35535cb26959b8e27ab50d167a8b996b5bada819" +dependencies = [ + "clap_builder", + "clap_derive", + "once_cell", +] + +[[package]] +name = "clap_builder" +version = "4.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdc5d93c358224b4d6867ef1356d740de2303e9892edc06c5340daeccd96bab" +dependencies = [ + "anstream", + "anstyle", + "bitflags", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9644cd56d6b87dbe899ef8b053e331c0637664e9e21a33dfcdc36093f5c5c4" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" + +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + +[[package]] +name = "errno" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys", +] + +[[package]] +name = "is-terminal" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f" +dependencies = [ + "hermit-abi", + "io-lifetimes", + "rustix", + "windows-sys", +] + +[[package]] +name = "libc" +version = "0.2.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" + +[[package]] +name = "linux-raw-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e8776872cdc2f073ccaab02e336fa321328c1e02646ebcb9d2108d0baab480d" + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "riff" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b1a3d5f46d53f4a3478e2be4a5a5ce5108ea58b100dcd139830eae7f79a3a1" + +[[package]] +name = "rustix" +version = "0.37.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0661814f891c57c930a610266415528da53c4933e6dea5fb350cbfe048a9ece" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "seamless-loop" +version = "0.1.0" +dependencies = [ + "anyhow", + "clap", + "wav", +] + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "2.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a34fcf3e8b60f57e6a14301a2e916d323af98b0ea63c599441eec8558660c822" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "wav" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a65e199c799848b4f997072aa4d673c034f80f40191f97fe2f0a23f410be1609" +dependencies = [ + "riff", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..741e92a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "seamless-loop" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.70" +clap = { version = "4.2.5", features = ["derive"] } +wav = "1.0.0" diff --git a/README.md b/README.md new file mode 100644 index 0000000..913ce62 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# seamless-loop + +Make a WAV file seamlessly loopable. + +Normally when you just loop an audio file, even if the end +sounds almost identical to the beginning, you'll get a click when the file loops. +This program fades a bit of the end of the file into the beginning +so that there's no click. + +For Windows and Linux executables, see the [releases page](https://github.com/pommicket/seamless-loop/releases). + + +## Q and A + +- How do I run this? + +You can run it from the command line, +or on Windows you can drag and drop the WAV file onto the executable. + +- What types of audio does this work with? + +The end of the audio file needs to be very similar to the beginning +for the loop to sound "nice". This program will work +very well with sine waves, white noise, and most synth-generated sounds. +It will probably not work so well with human voice, +due to its natural variation over time. + +- Why are only WAV files supported? + +It might reduce quality to re-encode lossy files... +and it's easier to test just one format. + +If you need to deal with a different format, +you can use ffmpeg or Audacity to convert the file to wav and back. + +- Why not make an Audacity plug-in? + +It would be convenient, but LADSPA and LV2 are really not designed for nonlocal effects like this. + +Perhaps Nyquist can do it, but I'd rather shoot myself in the arm than use Lisp. + +- Why does this program load the whole file into memory when it doesn't need to? + +I didn't want to write my own WAV parser... And anyways WAV files can only be up to 4GB and most people +have more memory than that nowadays. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..218e203 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +hard_tabs = true diff --git a/sawtooth.wav b/sawtooth.wav new file mode 100644 index 0000000..9647d78 Binary files /dev/null and b/sawtooth.wav differ diff --git a/sine.wav b/sine.wav new file mode 100644 index 0000000..4b4b3c2 Binary files /dev/null and b/sine.wav differ diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..ef294b7 --- /dev/null +++ b/src/README.md @@ -0,0 +1 @@ +# seamless-loop diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..dbdcbaf --- /dev/null +++ b/src/main.rs @@ -0,0 +1,103 @@ +/* +SEAMLESS LOOP +For more information see README.md. +--- +License: +do what the fuck you want to +*/ + +use clap::Parser; +use std::fs::File; +use std::fmt::{Debug, Display}; +use anyhow::{Result, Context}; + +/// Turn a .wav file into a seamless loop. +#[derive(Parser, Debug)] +struct Args { + /// Input file + file: String, + + /// Output file. Defaults to "x-seamless.wav" for input file "x.wav". + #[arg(short)] + output: Option, + + /// Duration in seconds of the fade. + #[arg(short, default_value_t = 0.03)] + duration: f32, +} + +trait AudioSample: Copy + Debug + Display { + fn interpolate(self, other: Self, t: f32) -> Self; +} + +macro_rules! impl_audio_sample { + ($type:ty, $min:expr, $max:expr) => { + impl AudioSample for $type { + fn interpolate(self, other: Self, t: f32) -> Self { + let a = self as f32; + let b = other as f32; + (a * (1.0 - t) + b * t).clamp($min, $max) as Self + } + } + } +} + +impl_audio_sample!(u8, 0.0, 255.0); +impl_audio_sample!(i16, -32767.0, 32767.0); +// NOTE: twenty-two bit samples are shifted left by 8 by the wav crate, so this is correct for them +impl_audio_sample!(i32, -i32::MAX as f32, i32::MAX as f32); +impl_audio_sample!(f32, -1.0, 1.0); + +fn make_seamless(data: &mut Vec, channels: u16, fade_samples: usize) -> Result<()> { + let channels: usize = channels.into(); + let audio_samples = data.len(); + if fade_samples * 2 >= audio_samples { + return Err(anyhow::anyhow!("Fade duration is too long (must be less than half of audio file's duration).")); + } + if audio_samples % channels != 0 { + return Err(anyhow::anyhow!("Sample count is not multiple of channel count (this shouldn't happen).")); + } + let fade_frames = fade_samples / channels; + let audio_frames = audio_samples / channels; + for i in 0..fade_frames { + let t = i as f32 / (fade_frames as f32); + let j = audio_frames - fade_frames + i; + for c in 0..channels { + data[channels * i + c] = data[channels * j + c].interpolate(data[channels * i + c], t); + } + } + data.truncate((audio_frames - fade_frames) * channels); + Ok(()) +} + +fn main() -> Result<()> { + let args = Args::parse(); + let input = &args.file; + let output = args.output.unwrap_or_else(|| { + let name = input.strip_suffix(".wav").unwrap_or(input); + name.to_string() + "-seamless.wav" + }); + let mut input_file = File::open(input).with_context(|| format!("Couldn't open input file {input}"))?; + let (header, mut data) = wav::read(&mut input_file)?; + drop(input_file); + + let samples = header.sampling_rate as f32 * args.duration; + if !samples.is_finite() || samples < 0.0 || samples > usize::MAX as f32 { + return Err(anyhow::anyhow!("Bad duration")); + } + let samples = samples as usize; + let channels = header.channel_count; + use wav::BitDepth; + match &mut data { + BitDepth::Eight(data) => make_seamless(data, channels, samples)?, + BitDepth::Sixteen(data) => make_seamless(data, channels, samples)?, + BitDepth::TwentyFour(data) => make_seamless(data, channels, samples)?, + BitDepth::ThirtyTwoFloat(data) => make_seamless(data, channels, samples)?, + BitDepth::Empty => return Err(anyhow::anyhow!("No audio data")), + } + + let mut output_file = File::create(&output).with_context(|| format!("Couldn't open output file {output}"))?; + wav::write(header, &data, &mut output_file)?; + + Ok(()) +} diff --git a/voice.wav b/voice.wav new file mode 100644 index 0000000..4967d54 Binary files /dev/null and b/voice.wav differ -- cgit v1.2.3