Last active
May 14, 2024 13:08
-
-
Save blakek/f3a55458af03ff7d5ee3424daaa06cfe to your computer and use it in GitHub Desktop.
Audio file tools
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -eo pipefail | |
directory="$1" | |
cd "$directory" | |
files=() | |
# Get `wav` files, ignoring `other.wav` | |
for file in *.wav; do | |
if [[ $file != "other.wav" ]]; then | |
files+=("$file") | |
fi | |
done | |
if [[ ${#files[@]} -eq 0 ]]; then | |
echo "No files found" | |
exit 1 | |
fi | |
# Create ffmpeg -i commands | |
commands=() | |
for file in "${files[@]}"; do | |
echo "Adding $file" | |
commands+=("-i $file") | |
done | |
# Layer the audio files | |
command="ffmpeg -y ${commands[*]} -filter_complex amix=inputs=${#files[@]}:duration=longest -ac 2 other.wav" | |
echo "Executing: $command" | |
# Execute the command | |
$command |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -eo pipefail | |
## | |
# Returns the smallest semitone distance between two notes. | |
## | |
version='0.2.0' | |
verbose='false' | |
# Formatting functions | |
bold() { | |
format_bold='\033[1m' | |
format_reset='\033[0m' | |
echo -e "${format_bold}$1${format_reset}" | |
} | |
dim() { | |
format_dim='\033[2m' | |
format_reset='\033[0m' | |
echo -e "${format_dim}$1${format_reset}" | |
} | |
debug() { | |
if [[ $verbose == "true" ]]; then | |
# shellcheck disable=SC2059 # We're purposely wrapping printf | |
dim "$(printf "$@")" | |
fi | |
} | |
error() { | |
format_red='\033[0;31m' | |
format_reset='\033[0m' | |
echo -e "${format_red}$1${format_reset}" >&2 | |
} | |
panic() { | |
error "$1" | |
exit 1 | |
} | |
show_usage() { | |
cat <<-EOF | |
$(bold note-distance) - Returns the smallest semitone distance between two notes | |
$(bold USAGE) | |
$ note-distance --help | |
$ note-distance --version | |
$ note-distance note1 note2 | |
For example, to find the distance between C and E: | |
$ note-distance C E # » 4 | |
or, to find the distance between Bb and G#: | |
$ note-distance Bb 'G#' # » -2 | |
It may be necessary to quote notes with a sharp symbol to prevent the shell from interpreting it as a comment. | |
$(bold OPTIONS) | |
-h, --help | |
Show this help message and exit | |
-v, --version | |
Show the version of this script and exit | |
EOF | |
} | |
abs() { | |
if (($1 < 0)); then | |
echo $((-$1)) | |
else | |
echo "$1" | |
fi | |
} | |
# Check if the note is valid (case insensitive base note followed by zero or | |
# more sharps or flats | |
is_valid_note() { | |
[[ $1 =~ ^[A-Ga-g][#b]*$ ]] | |
} | |
# Parse the note into an uppercase base note and any accidentals | |
parse_note() { | |
if ! is_valid_note "$1"; then | |
echo "" | |
return | |
fi | |
local base_note=${1:0:1} | |
local accidental=${1:1} | |
# Convert the base note to uppercase | |
base_note=${base_note^^} | |
printf '%s%s' "$base_note" "$accidental" | |
} | |
note_index() { | |
note_map=( | |
C 0 | |
D 2 | |
E 4 | |
F 5 | |
G 7 | |
A 9 | |
B 11 | |
) | |
# Get the base note from the argument | |
local base_note=${1:0:1} | |
# Get the index of the base note in the array | |
local base_note_index | |
for ((i = 0; i < ${#note_map[@]}; i += 2)); do | |
if [[ ${note_map[i]} == "$base_note" ]]; then | |
base_note_index=${note_map[i + 1]} | |
break | |
fi | |
done | |
# Get accidental(s) from the argument | |
local accidental=${1:1} | |
# Add 1 for each sharp and subtract 1 for each flat | |
for ((i = 0; i < ${#accidental}; i++)); do | |
if [[ ${accidental:i:1} == '#' ]]; then | |
((base_note_index++)) | |
elif [[ ${accidental:i:1} == 'b' ]]; then | |
((base_note_index--)) | |
fi | |
done | |
echo "$base_note_index" | |
} | |
note-distance() { | |
declare -a passed_notes | |
# Parse command line flags | |
while [[ $# -gt 0 ]]; do | |
case "$1" in | |
-h | --help) | |
show_usage | |
exit 0 | |
;; | |
-v | --version) | |
echo "$version" | |
exit 0 | |
;; | |
-V | --verbose) | |
verbose="true" | |
shift | |
;; | |
-*) | |
panic "Unknown option: $1" | |
;; | |
*) | |
parsed_note=$(parse_note "$1") | |
if [[ $parsed_note == "" ]]; then | |
panic "Invalid note: $1" | |
fi | |
if [[ ${#passed_notes[@]} -eq 2 ]]; then | |
panic "Too many notes" | |
fi | |
passed_notes+=( | |
"$(parse_note "$1")" | |
) | |
shift | |
;; | |
esac | |
done | |
local base_note=${passed_notes[0]?"Missing base note"} | |
local target_note=${passed_notes[1]?"Missing target note"} | |
debug 'Transposing %s to %s\n' "$base_note" "$target_note" | |
local base_note_index | |
local target_note_index | |
base_note_index=$(note_index "$base_note") | |
target_note_index=$(note_index "$target_note") | |
if [[ $verbose == "true" ]]; then | |
printf 'Base note index: %s\n' "$base_note_index" | |
printf 'Target note index: %s\n' "$target_note_index" | |
fi | |
# Calculate the distance between the two notes | |
local distance=$((target_note_index - base_note_index)) | |
local wrapped_distance=$((distance % 12)) | |
# Adjust wrapped distance to be within -6 to 5 | |
if ((wrapped_distance > 6)); then | |
wrapped_distance=$((wrapped_distance - 12)) | |
elif ((wrapped_distance <= -6)); then | |
wrapped_distance=$((wrapped_distance + 12)) | |
fi | |
# Choose the shorter distance based on absolute value, preserving direction | |
if ((${distance#-} < ${wrapped_distance#-})); then | |
echo "$distance" | |
else | |
echo "$wrapped_distance" | |
fi | |
} | |
note-distance "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -eo pipefail | |
## | |
# Runs the `spleeter` command in a Docker container. | |
## | |
cli_version='1.0.0' | |
image='deezer/spleeter:3.8' | |
function bold() { | |
format_bold='\033[1m' | |
format_reset='\033[0m' | |
echo -e "${format_bold}$1${format_reset}" | |
} | |
function error() { | |
format_red='\033[0;31m' | |
format_reset='\033[0m' | |
echo -e "${format_red}$1${format_reset}" >&2 | |
} | |
function panic() { | |
error "$1" | |
exit 1 | |
} | |
function show_usage() { | |
cat <<-EOF | |
$(bold spleeter) - A wrapper for deezer/spleeter using Docker | |
$(bold USAGE) | |
$ spleeter --help | |
$ spleeter --version | |
$ spleeter -o <output_dir> -m <model_name> <input_file>... | |
$(bold OPTIONS) | |
-h, --help | |
Show this help message and exit | |
-v, --version | |
Show the version of this script and exit | |
-o, --output | |
The directory to write the separated audio files into. | |
-m, --model | |
Which model to use for separation. Can be one of the following: | |
- 2stems | |
- 4stems | |
- 5stems (default) | |
- 2stems-16kHz | |
- 4stems-16kHz | |
- 5stems-16kHz | |
- 2stems-22kHz | |
- 4stems-22kHz | |
- 5stems-22kHz | |
- 2stems-44kHz | |
- 4stems-44kHz | |
- 5stems-44kHz | |
EOF | |
} | |
function spleeter() { | |
local input_files=() | |
local model='spleeter:5stems' | |
output_dir="${PWD}" | |
# Parse arguments | |
while [[ $# -gt 0 ]]; do | |
case "$1" in | |
-h | --help) # Help | |
show_usage | |
exit 0 | |
;; | |
-m) # Model | |
model="spleeter:$2" | |
shift 2 | |
;; | |
-o | --output) # Output directory | |
output_dir="$2" | |
shift 2 | |
;; | |
-v | --version) # Version | |
echo "CLI version: $cli_version" | |
exit 0 | |
;; | |
*) # Input files | |
input_files+=("$1") | |
shift | |
;; | |
esac | |
done | |
# Validate arguments | |
if [[ ${#input_files[@]} -eq 0 ]]; then | |
panic "No input files specified" | |
fi | |
# Ensure input files exist | |
for input_file in "${input_files[@]}"; do | |
if [[ ! -f $input_file ]]; then | |
panic "Input file does not exist: $input_file" | |
fi | |
done | |
# Ensure output directory exists | |
if [[ ! -d $output_dir ]]; then | |
mkdir -p "$output_dir" | |
fi | |
# Ensure model directory exists | |
# This keeps from needing to re-download the model every time | |
export MODEL_DIRECTORY="${HOME}/.spleeter/model" | |
if [[ ! -d $MODEL_DIRECTORY ]]; then | |
mkdir -p "$MODEL_DIRECTORY" | |
fi | |
# Copy all input files to a temporary directory so the Docker container | |
# can access them all at once | |
local tmp_dir | |
tmp_dir="$(mktemp -d)" | |
trap 'rm -rf $tmp_dir' EXIT | |
echo "Copying input files to temporary directory: $tmp_dir" | |
for input_file in "${input_files[@]}"; do | |
cp "$input_file" "$tmp_dir" | |
done | |
# Files on Docker side | |
local audio_in=() | |
for input_file in "${input_files[@]}"; do | |
audio_in+=("/files/$(basename "$input_file")") | |
done | |
docker run \ | |
--rm \ | |
-v "$tmp_dir:/files" \ | |
-v "$(readlink -f "$output_dir"):/output" \ | |
-v "$MODEL_DIRECTORY:/model" \ | |
-e MODEL_PATH=/model \ | |
"$image" \ | |
separate -p "$model" -o /output "${audio_in[@]}" | |
} | |
spleeter "$@" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env bash | |
set -eo pipefail | |
## | |
# Transpose a song from one key to another. | |
## | |
version='0.0.1' | |
# Formatting functions | |
bold() { | |
format_bold='\033[1m' | |
format_reset='\033[0m' | |
echo -e "${format_bold}$1${format_reset}" | |
} | |
error() { | |
format_red='\033[0;31m' | |
format_reset='\033[0m' | |
echo -e "${format_red}$1${format_reset}" >&2 | |
} | |
panic() { | |
error "$1" | |
exit 1 | |
} | |
# Floating point calculations | |
calculate() { | |
# `bc` cannot do fractional exponents but `awk` can | |
awk "BEGIN { print $1 }" | |
} | |
show_usage() { | |
cat <<-EOF | |
$(bold transpose) - Transpose a song from one key to another | |
$(bold USAGE) | |
$ transpose --help | |
$ transpose --version | |
$ transpose input_file output_file semitones | |
For example, to transpose a song from C to E: | |
$ transpose input_file output_file 4 | |
or, to transpose a song from E to C: | |
$ transpose input_file output_file -4 | |
$(bold OPTIONS) | |
-h, --help | |
Show this help message and exit | |
-v, --version | |
Show the version of this script and exit | |
EOF | |
} | |
transpose() { | |
# Check if usage/version was requested | |
if [[ $# -ne 3 ]]; then | |
show_usage | |
exit 0 | |
fi | |
for arg in "$@"; do | |
if [[ $arg == "--help" || $arg == "-h" ]]; then | |
show_usage | |
exit 0 | |
elif [[ $arg == "--version" || $arg == "-v" ]]; then | |
echo "$version" | |
exit 0 | |
fi | |
done | |
local input_file="$1" | |
local output_file="$2" | |
local semitones="$3" | |
# Validate arguments | |
if [[ ! -r $input_file ]]; then | |
panic "Could not read input file" | |
fi | |
if [[ $semitones -lt -12 || $semitones -gt 12 ]]; then | |
panic "Semitones must be between -12 and 12" | |
fi | |
# Find the difference between the start and end keys, picking the closest one | |
local pitch_difference | |
pitch_difference=$(calculate "2 ^ ($semitones / 12)") | |
# Show progress if pv is installed | |
if [[ $(hash pv 2>/dev/null) ]]; then | |
pv -i 0.15 "$input_file" | ffmpeg -i - -y -af "rubberband=pitch=$pitch_difference" -v quiet "$output_file" | |
else | |
ffmpeg -i "$input_file" -y -af "rubberband=pitch=$pitch_difference" -v quiet -stats "$output_file" | |
fi | |
} | |
transpose "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment