summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore2
-rw-r--r--Cargo.lock343
-rw-r--r--Cargo.toml11
-rw-r--r--README.md45
-rw-r--r--rustfmt.toml1
-rw-r--r--sawtooth.wavbin0 -> 132344 bytes
-rw-r--r--sine.wavbin0 -> 132344 bytes
-rw-r--r--src/README.md1
-rw-r--r--src/main.rs103
-rw-r--r--voice.wavbin0 -> 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
new file mode 100644
index 0000000..9647d78
--- /dev/null
+++ b/sawtooth.wav
Binary files differ
diff --git a/sine.wav b/sine.wav
new file mode 100644
index 0000000..4b4b3c2
--- /dev/null
+++ b/sine.wav
Binary files 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<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
new file mode 100644
index 0000000..4967d54
--- /dev/null
+++ b/voice.wav
Binary files differ