Skip to content

Instantly share code, notes, and snippets.

@blakek
Last active May 14, 2024 13:08
Show Gist options
  • Save blakek/f3a55458af03ff7d5ee3424daaa06cfe to your computer and use it in GitHub Desktop.
Save blakek/f3a55458af03ff7d5ee3424daaa06cfe to your computer and use it in GitHub Desktop.
Audio file tools
#!/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
#!/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 "$@"
#!/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 "$@"
#!/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