Compare commits

...

10 commits

2 changed files with 192 additions and 128 deletions

View file

@ -1,41 +1,89 @@
# Get Tracks
`get-tracks.sh` allows you to easily extract music tracks from a file with multiple songs, like a CD-in-a-file.
# Required applications # Required applications
* ffmpeg `get-tracks.sh` is a simple shell script based around `ffmpeg`, using only POSIX tools.
* xxd (with '-r' and '-p' options)
* awk It was tested on both OpenBSD and Linux (Ubuntu, Alpine).
# Usage # Usage
Download a CD and the starting time of each track in a file, and you're good to go.
```Bash ```Bash
get-tracks.sh rip <audio-file> <time-file> get-tracks.sh <audio-file> <time-file>
# Example # Example
get-track.sh rip doom-eternal.opus doom-eternal.txt get-track.sh doom-eternal.opus doom-eternal.txt
``` ```
`audio-file` can be in any format understood by `ffmpeg`. *audio-file* can be in any format understood by `ffmpeg`.
The `time-file` must have this format: The *time-file* must have this format:
``` ```
0:00 First song 0:00 intro
2:20 Second song 2:20 First song
3:18 Awesome song 3:18 Awesome song
``` ```
# Environment variables # Environment variables
* **SIMULATION**: if non empty do not invoke ffmpeg The behavior of the script can be changed by several environment variables.
* **FORMAT** [mp3,ogg,opus,…]: for the song file format
* **NONUMBER**: if equals 1, do not write song number * **SIMULATION** [empty or not]\
* **SEPARATOR**: (default: ' - '), write song number, with this separator do not invoke ffmpeg
example with SEPARATOR='_': song names will be 01_song.opus 02_song.opus…
* **VERBOSITY** [0-3] (default: 1)\ * **FORMAT** [mp3,ogg,opus,…]\
verbosity 0: no output exept errors from ffmpeg\ see the ffmpeg documentation for the output formats available
verbosity 1: simple indications on the current track being extracted\ * **FFOPTS** [any ffmpeg options] *(default: '-c:a copy')*\
verbosity 2: print actual ffmpeg commands the script currently runs ffmpeg options, can be used to change audio quality\
can be **required** to change in case input and output file formats differ\
see the ffmpeg documentation for available parameters
* **NONUMBER** [empty or 1]\
do not write song numbers
* **SEPARATOR** [separator] *(default: ' - ')*\
separator between number and name\
example with SEPARATOR='_': 01_intro.opus 02_blah.opus…
* **HEADERS** [empty or 1]\
print environment parameters (verbosity, simulation, etc.)
* **VERBOSITY** [0-3] *(default: 1)*\
0: no output except errors from ffmpeg\
1: simple indications on the current track being extracted\
2: print actual ffmpeg commands the script currently runs
# Different input and output file formats
In case you want to change the file format, let's say from `flac` to `opus`, you need to override the default ffmpeg options provided by `get-tracks.sh`.
This is done through the `FFOPTS` environment variable, **which needs to NOT be empty** in order to replace the default `get-tracks.sh` behavior (which is `-c:a copy`).
By default, `ffmpeg` performs re-encoding by itself.
```Bash
FORMAT=opus FFOPTS=" " get-tracks.sh cd.flac cd.txt
```
### Warning: sometimes you don't even need to
You may encounter files in some format like `webm` and you want to convert the output files in `opus`.
But, inside the `webm` format, you **may** have `opus`-encoded audio.
In these cases, no re-encoding is necessary, and you can do something like:
```Bash
FORMAT=opus get-tracks.sh cd.webm cd.txt
```
You'll have a warning mentionning FFOPTS (based on different formats).
But the generated audio files won't have any quality loss.
This happens sometimes with the `youtube-dl` utility.
In case there are actual `ffmpeg` errors, and you don't have output audio files, then the contained audio hadn't the right format.
You'll have to re-encode.
# More # More
Run `get-track.sh` without arguments. You can get some help by running `get-track.sh` without arguments.

View file

@ -1,9 +1,9 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# From a single byte in hexadecimal per line # From a single byte in hexadecimal per line to lines ending with 0a
# to lines ending with 0a (hex for '\n'). # (hex for '\n'). Ex: 61 62 63 0a
regroup_lines(){ # Required to easily match (and remove) multi-byte characters.
awk ' regroup_lines() awk '
BEGIN { BEGIN {
line_start=1 line_start=1
} }
@ -26,39 +26,47 @@ regroup_lines(){
print line print line
} }
' '
}
# From to ' # From to '
simple_quote(){ simple_quote() sed "s/e2 80 99/27/g"
sed "s/e2 80 99/27/g"
}
remove_multibyte_characters(){ # From / to '-'
sed "s/e2 80 .. //g" replace_slashes() sed "s/2f/2d/g"
}
remove_backslashes() sed "s/5c//g"
remove_multibyte_characters() sed "s/e2 80 .. //g"
uppercase() tr "[a-z]" "[A-Z]"
# One column decimal to plain text.
from_dec() awk '{ printf ("%c", $1 + 0) }'
# Replace spaces by line returns, outputs a single column.
spaces_to_line_returns() tr " " "\n"
# Convert input into hexadecimal and a single byte per line. # Convert input into hexadecimal and a single byte per line.
to_hex_one_column(){ to_hex_one_column() { od -An -tx1 | awk '{for(i=1;i<=NF;i++){ print $i }}'; }
xxd -p -c 1
}
# Reverse hexadecimal to original value. # One column hexa to one column decimal.
from_hex(){ hex_to_dec() { { echo "obase=10;ibase=16;" ; cat ; } | bc ; }
xxd -p -r
}
# Remove non ascii characters, convert "" to "'". # Reverse hexadecimal (with space separators) to original value.
from_hex() { spaces_to_line_returns | uppercase | hex_to_dec | from_dec; }
# Remove non ascii, backslashes and invalid filename characters,
# convert "" to "'", "/" to " - ".
to_ascii(){ to_ascii(){
to_hex_one_column | # Convert input into hexadecimal and a single byte per line. to_hex_one_column | # Input to hexadecimal, 1-byte representation per line.
regroup_lines | # Required to easily match multi-byte characters. regroup_lines | # From 1-byte to x-byte lines with space separators.
simple_quote | # Convert "" to "'". simple_quote | # From "" to "'".
replace_slashes | # From / to '-'.
remove_multibyte_characters | # Remove non ascii values. remove_multibyte_characters | # Remove non ascii values.
from_hex # Convert back from hex. remove_backslashes | # Can mess with the script.
from_hex # Convert back from hex (x-byte per line, space separator).
} }
process_end_of_songs(){ comp_end_of_tracks() awk -v NONUMBER="$NONUMBER" -v SEPARATOR="$SEPARATOR" '
awk -v NONUMBER="$NONUMBER" -v SEPARATOR="$SEPARATOR" '
BEGIN { BEGIN {
OFS=" " OFS=" "
} }
@ -90,10 +98,8 @@ process_end_of_songs(){
print timestamp, "END_OF_FILE", title; print timestamp, "END_OF_FILE", title;
} }
' '
}
first_column_to_seconds(){ first_column_to_seconds() awk '
awk '
{ {
# from 10:30 to 630 # from 10:30 to 630
n = split ($1, arr, ":") n = split ($1, arr, ":")
@ -114,12 +120,9 @@ first_column_to_seconds(){
v = 0; v = 0;
} }
' '
}
# Get a more usable time representation for the beginning and the end of songs. # Get a more usable time representation for the beginning and the end of songs.
process_time_file(){ get_timestamps(){ to_ascii | first_column_to_seconds | comp_end_of_tracks; }
to_ascii | first_column_to_seconds | process_end_of_songs
}
run_ffmpeg(){ run_ffmpeg(){
file=$1 file=$1
@ -133,7 +136,7 @@ run_ffmpeg(){
if [ "$to" != "" ]; then if [ "$to" != "" ]; then
TO="-to $to" TO="-to $to"
fi fi
INPUT_FILE="-i $file" INPUT_FILE="$file"
OUTPUT_FILE="$final_title" OUTPUT_FILE="$final_title"
case "v$VERBOSITY" in case "v$VERBOSITY" in
@ -143,7 +146,7 @@ run_ffmpeg(){
echo "extracting '$final_title'" echo "extracting '$final_title'"
;; ;;
v2) v2)
echo "ffmpeg $LOG_LEVEL $FROM $TO $INPUT_FILE '$OUTPUT_FILE'" echo "ffmpeg $LOG_LEVEL $FROM $TO -i $INPUT_FILE $FFOPTS '$OUTPUT_FILE'"
;; ;;
*) *)
echo "verbosity is not set properly" >&2 echo "verbosity is not set properly" >&2
@ -152,15 +155,15 @@ run_ffmpeg(){
esac esac
if [ "$SIMULATION" = "" ]; then if [ "$SIMULATION" = "" ]; then
$(< /dev/null ffmpeg $LOG_LEVEL $FROM $TO $INPUT_FILE "$OUTPUT_FILE") ffmpeg $LOG_LEVEL $FROM $TO -i "$INPUT_FILE" $FFOPTS "$OUTPUT_FILE"
fi fi
} }
rip(){ extraction(){
audio_file="$1" audio_file="$1"
time_file="$2" time_file="$2"
process_time_file < "$time_file" | while read LINE; do get_timestamps < "$time_file" | while read LINE; do
track_start=$(echo $LINE | cut -d ' ' -f 1) track_start=$(echo $LINE | cut -d ' ' -f 1)
track_end=$(echo $LINE | cut -d ' ' -f 2) track_end=$(echo $LINE | cut -d ' ' -f 2)
track_title=$(echo $LINE | cut -d ' ' -f 3-) track_title=$(echo $LINE | cut -d ' ' -f 3-)
@ -169,107 +172,120 @@ rip(){
track_end="" track_end=""
fi fi
run_ffmpeg "${audio_file}" "${track_start}" "${track_end}" "${track_title}.${FORMAT}" # Input is /dev/null, otherwise subshells will take the output
# of "get_timestamps" as input.
# Be careful: "while read X" is a dangerous shell design.
< /dev/null run_ffmpeg "${audio_file}" \
"${track_start}" "${track_end}" \
"${track_title}.${FORMAT}"
done done
} }
usage(){ usage(){
echo "usage: $0 command" cat <<END
echo "command: show <song-list>" Get tracks:
echo "command: rip <single-file-playlist> <song-list>" usage: $0 <single-file-playlist> <song-list>
echo
echo "song-list line format example: 1:30 My second track of the playlist" Debug mode (displays starting and ending times for each song):
echo "show output format: start end title" usage: $0 <song-list>
echo
echo "envvar: SIMULATION, if non empty, do not invoke ffmpeg"
echo "envvar: NONUMBER, if equals 1, do not write song number" Format for <song-list>:
echo "envvar: FORMAT [mp3,ogg,opus,…], see the ffmpeg documentation" 0:00 First track
echo "envvar: SEPARATOR [separator] (default: ' - '), write song number, with this separator" 1:30 Second track
echo " example with SEPARATOR='_': song names will be 01_song.opus 02_song.opus…"
echo "envvar: VERBOSITY [0-3] (default: 1)" Environment variables:
echo " verbosity 0: no output exept errors from ffmpeg" - SIMULATION [empty or not] do not invoke ffmpeg
echo " verbosity 1: simple indications on the current track being extracted"
echo " verbosity 2: print actual ffmpeg commands the script currently runs" - FORMAT [mp3,ogg,opus,…] see ffmpeg documentation
- FFOPTS (default: '-c:a copy') see ffmpeg documentation
- NONUMBER [empty or 1] do not write song numbers
- SEPARATOR [separator] (default: ' - ')
separator between number and name
example with SEPARATOR='_': 01_intro.opus 02_blah.opus…
- HEADERS [empty or 1] print env params (verbosity, quality, etc.)
- VERBOSITY [0-3] (default: 1)
0: no output except errors from ffmpeg
1: simple indications on the current track being extracted
2: print actual ffmpeg commands the script currently runs
END
} }
if [ $# -lt 1 ]; then header(){
usage if [ "$HEADERS" = "1" ]; then
exit 0 echo $*
fi
}
warning(){
echo "WARNING: $*"
}
# Default output format is based on the extension of the input audio file.
if [ $# -eq 2 ]; then
DEFAULT_FORMAT="$(echo $1 | awk -F . '{print $NF}')"
else
header "no default FORMAT selected"
fi fi
command=$1
shift
if [ "$FORMAT" = "" ]; then if [ "$FORMAT" = "" ]; then
echo "default FORMAT: opus" FORMAT="$DEFAULT_FORMAT"
FORMAT="opus" header "default FORMAT: ${FORMAT}"
else else
echo "FORMAT: $FORMAT" header "FORMAT: $FORMAT"
fi
# For unexperienced users, print a warning when input and output formats differ.
# In case FFOPTS is set, encoding is expected to be handled, drop the warning.
# Example (remove the get-tracks.sh default behavior, perform re-encoding):
# FFOPTS=" "
if [ "$FFOPTS" = "" ] && [ "$FORMAT" != "$DEFAULT_FORMAT" ]; then
warning "input and output formats seem to differ"
warning "1. re-encoding may be required (through the FFOPTS envvar)"
warning "2. FFOPTS represents ffmpeg options, directly given to ffmpeg"
warning ' (default: "-c:a copy" = copy without re-encoding)'
warning ' You can put FFOPTS=" " if you want to perform re-encoding.'
fi fi
if [ "$VERBOSITY" = "" ]; then if [ "$VERBOSITY" = "" ]; then
echo "default VERBOSITY: 1" header "default VERBOSITY: 1"
VERBOSITY=1 VERBOSITY=1
else else
echo "VERBOSITY level: $VERBOSITY" header "VERBOSITY level: $VERBOSITY"
fi fi
if [ "$NONUMBER" = "" ]; then if [ "$NONUMBER" = "" ]; then
echo "default NONUMBER: disabled" header "default NONUMBER: disabled"
NONUMBER=0 NONUMBER=0
# Assume that there should be a separator. # Assume that there should be a separator.
if [ "$SEPARATOR" = "" ]; then if [ "$SEPARATOR" = "" ]; then
echo "default SEPARATOR: ' - '" header "default SEPARATOR: ' - '"
SEPARATOR=" - " SEPARATOR=" - "
else else
echo "SEPARATOR: '$SEPARATOR'" header "SEPARATOR: '$SEPARATOR'"
fi fi
else else
echo "NONUMBER: won't prefix tracks" header "NONUMBER: won't prefix tracks"
SEPARATOR="" SEPARATOR=""
fi fi
if [ "$FFOPTS" != "" ]; then
header "FFOPTS envvar is set: ${FFOPTS}."
else
FFOPTS="-c:a copy"
header "default FFOPTS: ${FFOPTS}"
fi
if [ "$SIMULATION" != "" ]; then if [ "$SIMULATION" != "" ]; then
echo "SIMULATION envvar is set: this is a simulation." header "SIMULATION envvar is set: this is a simulation."
fi fi
</dev/null xxd -p >/dev/null 2>/dev/null case $# in
if [ $? -ne 0 ]; then 0) usage; exit 0;;
echo "xxd: you don't have an xxd program with '-p' option." 1>&2 1) get_timestamps < "$1";;
exit 1 2) extraction "$1" "$2";;
fi *) usage 1>&2; exit 1;;
</dev/null xxd -r >/dev/null 2>/dev/null
if [ $? -ne 0 ]; then
echo "xxd: you don't have an xxd program with '-r' option." 1>&2
exit 1
fi
case "x-${command}" in
x-show)
# Takes the audio file in first parameter
if [ $# -ne 1 ]; then
echo "Usage: $0 show time-stamps-file" >&2
exit 1
fi
process_time_file < "$1"
;;
x-rip)
# Takes the audio file in first parameter
if [ $# -ne 2 ]; then
echo "Usage: $0 show music-file time-stamps-file" >&2
exit 1
fi
rip "$1" "$2"
;;
*)
usage 1>&2
exit 1
esac esac