Music

Introduction

This page is where I note the commands I use to manipulate audio files (WAV files and Ogg files).

Ogg file decoding

Decode an Ogg file to a WAV file with a command like:

oggdec -o output.wav input.ogg

The oggdec command is provided by package vorbis-tools on a Debian GNU/Linux system).

WAV files duration

SoX can show (among other things) the duration in seconds of WAV files:

sox my_wav_file.wav -n stat 2>&1 |grep ^Length
soxi my_wav_file.wav|grep ^Duration

You can specify multiple files (or use wildcards) in the sox command to get the cumulative duration of the files:

sox *.wav -n stat 2>&1 |grep ^Length

The sox and soxi commands are provided by package sox on a Debian GNU/Linux system).

Burning WAV files to a CD-R

You can burn some WAV files to a blank CD with a command like:

cdrskin -v -eject -audio -pad *.wav

If you need to specify the device for the optical drive, use the -d switch:

cdrskin -d /dev/sr1 -v -eject -audio -pad *.wav

cdrskin should be able to list the optical drives devices with the --devices option:

cdrskin --devices

Alternatively, you can use wodim. It may work better than cdrskin on Debian Buster:

wodim -v dev=/dev/sr1 -sao -audio -pad *.wav

Building audio files from a video

I once wanted to make audio files from a nice TV show that I had recorded on the hard drive of my Freebox (V5 generation).

The TV show was recorded to a .ts file and could be downloaded to the computer from ftp://hd1.freebox.fr.

I created the audio files with a shell script. The script is mostly a loop over the lines of a here-document. There is one line in the here-document for each audio file that I wanted to create. Each line provides the start time of the audio file in the .ts file, the duration of the audio file and the title of the audio file.

The code is provided below. The here-document contains two lines:

00:16:44 00:03:43 The mission (Ennio Morricone)
00:33:20 00:03:19 Cinema Paradiso (Andrea & Ennio Morricone)

This will cause the creation of two WAV files in the directory denoted by script variable INTERMEDIATE_FILES_DIR:

  • 01_-_the_mission__ennio_morricone_.wav
  • 02_-_cinema_paradiso__andrea_and_ennio_morricone_.wav

You will also get two Ogg files in the directory denoted by script variable OUTPUT_DIR:

  • 01_-_the_mission__ennio_morricone_.ogg
  • 02_-_cinema_paradiso__andrea_and_ennio_morricone_.ogg

The WAV files are created from the .ts file using ffmpeg, provided by package ffmpeg on a Debian GNU/Linux system. I’m using the version from deb-multimedia, which is newer than the one in the Debian archive. If you are not sure which version you have installed, check with:

apt-cache policy ffmpeg

The output file names are built from the titles provided in the here-document, but with a few changes:

  1. Occurrences of ampersand are substituted with “and”.
  2. Accents, cedillas, tildes, etc… are removed using the unaccent command (provided by package unaccent on a Debian GNU/Linux system).
  3. Uppercase characters are converted to lowercase.
  4. Remaining characters that are not letters, digits or hyphen are substituted with underscores.

A triangular (i.e. linear) fade out is created at the end of each file using SoX.

The output Ogg files are encoded with oggenc. They contain some comments:

  • “tracknumber” (2-digits track number)
  • “title” (title as provided in the here-document)
  • “artist” (provided by script variable ARTIST)
  • “album” (provided by script variable ALBUM)

To list the comments of an Ogg file, use vorbiscomment -l:

vorbiscomment -l my_ogg_file.ogg

oggenc and vorbiscomment are provided by package vorbis-tools on a Debian GNU/Linux system.

And here is the code:

#!/bin/sh

SOURCE_DIR=directory/of/ts/file;                     # Adapt to your needs.
SOURCE_FILE=ts_file.ts;                              # Adapt to your needs.
SOURCE_PATH="$SOURCE_DIR/$SOURCE_FILE";

OUTPUT_DIR=output/directory;                         # Adapt to your needs.

INTERMEDIATE_FILES_DIR=intermediate/files/directory; # Adapt to your needs.

ARTIST="Artist name";                                # Adapt to your needs.
ALBUM="Album name";                                  # Adapt to your needs.

OGGEXT=.ogg;
WAVEXT=.wav;

TEMP_SUFF=_;

mkdir -p "$OUTPUT_DIR";
rm -rf "$OUTPUT_DIR"/*;

mkdir -p "$INTERMEDIATE_FILES_DIR";
rm "$INTERMEDIATE_FILES_DIR"/*;

K=0;

# Loop over the lines of the here-document below.
while IFS= read -r LINE; do
    START_TIME=$(echo "$LINE"|sed "s/ .*$//");
    LINE=$(echo "$LINE"|sed "s/^[^ ]\+ //");
    DURATION=$(echo "$LINE"|sed "s/ .*$//");
    LINE=$(echo "$LINE"|sed "s/^[^ ]\+ //");

    TITLE=$(echo "$LINE");

    # Substitute occurrences of ampersand with "and".
    OUTPUT_PATH=$(echo "$TITLE"|sed "s/&/and/");

    # Remove accents, cedillas, tildes, etc...
    OUTPUT_PATH=$(echo "$OUTPUT_PATH"|unaccent UTF-8);

    # Convert uppercase characters to lowercase.
    OUTPUT_PATH=$(echo "$OUTPUT_PATH"|tr '[:upper:]' '[:lower:]');

    # Substitute characters that are not lowercase letters, digits or hyphen
    # with underscores.
    OUTPUT_PATH=$(echo -n "$OUTPUT_PATH"|tr -c "a-z0-9-" [_*]);

    # Prepend track number (2 digits).
    K=$((K + 1));
    K_STR=$(printf "%02d" $K);
    OUTPUT_PATH="${K_STR}_-_$OUTPUT_PATH";

    WAV_PATH="$INTERMEDIATE_FILES_DIR/$OUTPUT_PATH$WAVEXT";
    OUTPUT_PATH="$OUTPUT_DIR/$OUTPUT_PATH$OGGEXT";

    echo "Creating $OUTPUT_PATH";

    # Create a WAV file. The -nostdin option is needed to prevent ffmpeg from
    # "eating" parts of the here-document.
    ffmpeg -nostdin -loglevel quiet -ss "$START_TIME" -t "$DURATION" \
        -i "$SOURCE_PATH" -ar 44100 "$WAV_PATH";

    # Fade out the WAV file.
    mv "$WAV_PATH" "$WAV_PATH$TEMP_SUFF";
    sox "$WAV_PATH$TEMP_SUFF" "$WAV_PATH" fade t 0 -0 7;
    rm "$WAV_PATH$TEMP_SUFF";

    # Encode to Ogg format.
    oggenc -a "$ARTIST" -t "$TITLE" -l "$ALBUM" -c "tracknumber=$K_STR" \
        -o "$OUTPUT_PATH" "$WAV_PATH";
done<<HEREDOC
00:16:44 00:03:43 The mission (Ennio Morricone)
00:33:20 00:03:19 Cinema Paradiso (Andrea & Ennio Morricone)
HEREDOC

Getting the track list of an audio CD

The cdown command (provided by package cdtool on a Debian GNU/Linux system) queries the CDDB database. Just insert the Audio CD in your optical drive and run:

cdown -H gnudb.gnudb.org -P 8880

If you need to specify the device for the optical drive, use the -d switch:

cdown -H gnudb.gnudb.org -P 8880 -d /dev/sr1

If you have issues with accented letters, try:

cdown -H gnudb.gnudb.org -P 8880|iconv -f latin1

Alternatively, you can search your audio CD directly on the freedb.org website.

Building audio (Ogg) files from an audio CD

When I want to rip an audio CD, I write a shell script, which has a lot of similarities with the script I use to build audio files from a video. The script is also mostly a loop over the lines of a here-document. There is one line in the here-document for each track of the audio CD. Each line provides the title of the track.

cdparanoia is used to extract the audio CD tracks and oggenc is used to encode to Ogg file.

You may need to use the -d switch off cdparanoia to specify the device of your optical drive.

If cdparanoia has difficulties extracting the audio tracks, try to add the -Z option. See the manual for more details (man cdparanoia).

And here is the code:

#!/bin/sh

OUTPUT_DIR=output/directory;                         # Adapt to your needs.
ARTIST="Artist name";                                # Adapt to your needs.
ALBUM="Album name";                                  # Adapt to your needs.

DEVICE=/dev/cdrom;                                   # Adapt to your needs.

OGGEXT=.ogg;

mkdir -p "$OUTPUT_DIR";

K=0;

# Loop over the lines of the here-document below.
while IFS= read -r LINE; do

    TITLE=$(echo "$LINE");

    # Substitute occurrences of ampersand with "and".
    OUTPUT_PATH=$(echo "$TITLE"|sed "s/&/and/");

    # Remove accents, cedillas, tildes, etc...
    OUTPUT_PATH=$(echo "$OUTPUT_PATH"|unaccent UTF-8);

    # Convert uppercase characters to lowercase.
    OUTPUT_PATH=$(echo "$OUTPUT_PATH"|tr '[:upper:]' '[:lower:]');

    # Substitute characters that are not lowercase letters, digits or hyphen
    # with underscores.
    OUTPUT_PATH=$(echo -n "$OUTPUT_PATH"|tr -c "a-z0-9-" [_*]);

    # Prepend track number (2 digits).
    K=$((K + 1));
    K_STR=$(printf "%02d" $K);
    OUTPUT_PATH="${K_STR}_-_$OUTPUT_PATH";

    OUTPUT_PATH="$OUTPUT_DIR/$OUTPUT_PATH$OGGEXT";

    echo "Creating $OUTPUT_PATH";

    cdparanoia -d "$DEVICE" $K -|oggenc -a "$ARTIST" -t "$TITLE" -l \
    "$ALBUM" -c "tracknumber=$K_STR" -o "$OUTPUT_PATH" -;

done<<HEREDOC
Title for track #1
Title for track #2
...
Title for last track
HEREDOC

Converting Ogg files to MP3

Converting an Ogg file to MP3 is pretty easy with ffmpeg:

ffmpeg -i my_audio_file.ogg my_audio_file.mp3

Metadata can be included in the MP3 output file. Example:

ffmpeg -i my_audio_file.ogg \
    -metadata artist="The artist" \
    -metadata album="The album" \
    -metadata title="The track title" \
    my_audio_file.mp3

I have a collection of Ogg files, and I need to convert them to MP3 and store the MP3 files on a USB stick to be able to listen to them in my car. The Ogg files are scattered in various directory trees on my hard drive. The directory trees organizations are such that an Ogg file is always in a directory with a name that denotes an album, which is in a directory with a name that denotes an artist.

I use a shell script like the following to populate a (FAT32 formatted) USB stick with the MP3 files. The tree organization on the USB stick is similar to the Ogg files trees organizations since an MP3 file is always in a directory with a name that denotes an album, which is in a directory with a name that denotes an artist. But all the directories denoting artists are directly in the USB stick root tree.

Here’s the code of the script:

#!/bin/sh

OGG_TREE_ROOT=path/to/root/of/ogg/trees;             # Adapt to your needs.
OUTPUT_DIR=path/to/usb/stick/root;                   # Adapt to your needs.
OGGEXT=.ogg;
MP3EXT=.mp3;

# -----------------------------------------------------------------------------

ogg2mp3() {

    local SOURCE_FILE="$1";
    local INTERMEDIATE_FILE="$2";
    local TARGET_FILE="$3";

    # vorbiscomment with -l switch lists the comments in the Ogg file (one line
    # per comment, key=value format). The output is piped to a group of
    # commands (delimited with curly braces). The whole group is executed in
    # one subshell. This makes it possible to initialize variables before the
    # while loop, assign the variables in the loop and use them after the loop.
    vorbiscomment -l "$SOURCE_FILE"|
    {
        local LINE=
        local ARTIST=
        local ALBUM=
        local TITLE=
        local VAL=
        while IFS= read -r LINE; do
            VAL=$(echo "$LINE"|sed "s/^[^=]\+=//");
            echo "$LINE"|grep -q ^artist= && ARTIST="$VAL";
            echo "$LINE"|grep -q ^album= && ALBUM="$VAL";
            echo "$LINE"|grep -q ^title= && TITLE="$VAL";
        done;

        # Convert Ogg source file to MP3 (INTERMEDIATE_FILE).
        ffmpeg -nostdin -nostats -loglevel quiet -i "$SOURCE_FILE" \
            -metadata artist="$ARTIST" \
            -metadata album="$ALBUM" \
            -metadata title="$TITLE" \
            "$INTERMEDIATE_FILE";
    }

    mkdir -p "${TARGET_FILE%/*}";
    mv "$INTERMEDIATE_FILE" "$TARGET_FILE";
}

# -----------------------------------------------------------------------------

process_file() {

    local SOURCE_FILE="$1";

    # Initialize TEMPO_TARGET_FILE with SOURCE_FILE, but with extension changed
    # to MP3 extension.
    local TEMPO_TARGET_FILE="${SOURCE_FILE%$OGGEXT}$MP3EXT";

    # Store a copy of TEMPO_TARGET_FILE initial value, will be useful later.
    local INTERMEDIATE_FILE="$TEMPO_TARGET_FILE";

    # Remove the beginning of the path of TEMPO_TARGET_FILE (keep only the
    # parent directory, the directory and the basename).
    while [ "${TEMPO_TARGET_FILE%/*/*/*}" != "$TEMPO_TARGET_FILE" ]; do
        TEMPO_TARGET_FILE="${TEMPO_TARGET_FILE#*/}";
    done;

    local TARGET_FILE="$OUTPUT_DIR/$TEMPO_TARGET_FILE";

    if [ -e "$TARGET_FILE" ]; then
        echo "Already exists in output directory: $TEMPO_TARGET_FILE";
    else
        echo "Creating $TEMPO_TARGET_FILE";
        ogg2mp3 "$SOURCE_FILE" "$INTERMEDIATE_FILE" "$TARGET_FILE";
    fi;
}

# -----------------------------------------------------------------------------

process_dir() {
    local DIR="$1";
    for OGGFILE in $(find "$DIR" -maxdepth 1 -type f -name "*$OGGEXT"|sort); do
        process_file "$OGGFILE";
    done;
}

# -----------------------------------------------------------------------------

for DIR in $(find "$OGG_TREE_ROOT" -type d); do
    process_dir "$DIR";
done;