Skip to content

Instantly share code, notes, and snippets.

@crischutu07
Last active February 10, 2026 01:09
Show Gist options
  • Select an option

  • Save crischutu07/9b43a24a84d667d68d74f1280fec666d to your computer and use it in GitHub Desktop.

Select an option

Save crischutu07/9b43a24a84d667d68d74f1280fec666d to your computer and use it in GitHub Desktop.
flac to opus audio conversion with fancy stuff (just why?)
#!/usr/bin/env bash
# flac2opus.sh
#
# a conversion script that i use for doing cross directories audio conversion (from FLAC to opus container/codec or file format)
#
# Author: Nguyen Hong Son (crischutu07) <cris@crischutu07.is-a.dev>
#
# enable this => /**/*
shopt -s globstar
# exit if a command fails
set -e
set -o pipefail
DEPS=(ffmpeg opusenc)
function debug() {
if [ "$DEBUG" -eq 1 ]; then
echo "[debug] $@"
fi
}
function log() {
echo "$@"
}
function warn() {
echo "[warn] $@"
}
function fatal() {
echo "[fatal] $@"
[ -z "$2" ] && exit 1 || exit $2
}
# force overwrite data when encoding
# also this is easier
OVERWRITE=1
DEBUG=1
OUTPUT_FOLDER="out"
COVERJPG=0
INSTALLED=()
debug "List dependencies: ${DEPS[@]}"
for i in "${DEPS[@]}"; do
if ! command -v "$i" >/dev/null 2>&1; then
case "$i" in
opusenc)
warn "You don't have opusenc installed!"
warn "FFmpeg doesn't support embedding album cover for opus file except opusenc. We will create a cover.jpg as a replacement."
warn "If you don't want to create cover.jpg, set COVERJPG=0"
OPUSENC=0
;;
*)
debug "Can't find $i, exitting.."
fatal "This script requires $i"
;;
esac
else
INSTALLED+="$i"
debug "Installed tool: $i"
fi
done
unset DEPS INSTALLED
AUDIO_FILES=()
OPUSENC=1
function scanfile() {
for i in "$1"/**/*; do
if [[ "${i##*.}" == "flac" ]]; then
debug "scanfile: $i"
AUDIO_FILES+=("$i")
fi
done
debug "scanfile: scanned ${#AUDIO_FILES[@]} file(s) inside AUDIO_FILES"
}
# TODO: scan folder that may or may not contains a flac file, otherwise do the code below:
if [ -z "$1" ]; then
AUDIO_FOLDER="$(pwd)"
else
AUDIO_FOLDER="${1}"
fi
debug "AUDIO_FOLDER: $AUDIO_FOLDER"
scanfile "$AUDIO_FOLDER"
if [ -z "$AUDIO_FILES" ]; then
log "$0: cannot find any file inside: ${AUDIO_FOLDER}"
exit 1
fi
if [ -z "${OVERWRITE}" ]; then
OVERWRITE="-n"
else
OVERWRITE="-y"
fi
for i in "${AUDIO_FILES[@]}"; do
coverfile=$(mktemp --suffix=".jpg")
# remove relative path
file="${i/"$AUDIO_FOLDER"}"
debug "1st parse: $file"
# remove "/" suffix
file="${file#/}"
debug "2nd parse: $file"
# remove extension
file="${file%%.*}"
debug "3rd parse: $file"
# relative path without filename for create folder
filedir="${OUTPUT_FOLDER}/${file%/*}"
debug "creating filedir: ${filedir}"
mkdir -p "${filedir}"
filepath="${OUTPUT_FOLDER}/${file}.opus"
debug "input: $i"
debug "filepath: ${filepath}"
if [ "$OPUSENC" -eq 0 ]; then
debug "opusenc is disabled, fallback to ffmpeg"
ffmpeg -i "$i" \
-hide_banner \
-loglevel warning \
-stats \
-c:a libopus \
-b:a 512k \
-vbr on \
-ar 48000 \
-map_metadata 0 \
"${filepath}" \
"$OVERWRITE"
else
debug "opusenc is enabled, attempting to use it"
opusenc \
--bitrate 512k \
--comp 10 \
--vbr \
--framesize 20 \
"$i" \
"${filepath}"
fi
# create front cover art.
# compress level 10 is lossless
# ^ i know, it's the default value but i'm too lazy to read ffmpeg docs.
if [ "$COVERJPG" -eq 1 ]; then
COVERPATH="${filedir}/cover.jpg"
debug "creating $COVERPATH"
ffmpeg -i "$i" \
-loglevel warning \
-an \
-frames:v 1 \
-c:v copy \
"$coverfile" -y
cp "$coverfile" "$COVERPATH"
else
# get only the directory path
dir="$(basename "$i")"
if [ -f "$dir/cover.jpg" ]; then
# copy directory's cover.jpg to temp coverfile
# if it's available.
cp -v "$dir/cover.jpg" "$coverfile"
fi
fi
rm "$coverfile"
done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment