diff options
-rw-r--r-- | .gitignore | 2 | ||||
-rw-r--r-- | Cargo.lock | 343 | ||||
-rw-r--r-- | Cargo.toml | 11 | ||||
-rw-r--r-- | README.md | 45 | ||||
-rw-r--r-- | rustfmt.toml | 1 | ||||
-rw-r--r-- | sawtooth.wav | bin | 0 -> 132344 bytes | |||
-rw-r--r-- | sine.wav | bin | 0 -> 132344 bytes | |||
-rw-r--r-- | src/README.md | 1 | ||||
-rw-r--r-- | src/main.rs | 103 | ||||
-rw-r--r-- | voice.wav | bin | 0 -> 112172 bytes |
10 files changed, 506 insertions, 0 deletions
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 Binary files differnew file mode 100644 index 0000000..9647d78 --- /dev/null +++ b/sawtooth.wav diff --git a/sine.wav b/sine.wav Binary files differnew file mode 100644 index 0000000..4b4b3c2 --- /dev/null +++ b/sine.wav 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<String>, + + /// 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<T: AudioSample>(data: &mut Vec<T>, 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 Binary files differnew file mode 100644 index 0000000..4967d54 --- /dev/null +++ b/voice.wav |