Spring naar hoofdtekst

Audio-cd met cd-text via commandline

Geplaatst op door .
Laatste aanpassing op .

Inleiding

Vroeger was k3b mijn favoriete programma om cd's en dvd's te branden. Het is flexibel en biedt (nog steeds) heel veel mogelijkheden. Het grootste nadeel vind ik het hele KDE-framework waar het gebruik van maakt. In mijn GNOME-desktop zou ik al die 'ballast' meeïnstalleren voor één enkel programma. Dat vond ik niet de bedoeling en dus ging ik op zoek naar een alternatief.

Al snel vond ik Brasero, een onderdeel van het GNOME-project. Dit programma maakt deel uit van de desktop omgeving en is heel eenvoudig te bedienen. Met de mogelijkheden en flexibiliteit van k3b in mijn achterhoofd, voelde dit echter als een stap terug - niet vooruit of een andere kant op.

Commandline

Vanwege mijn slechtziendheid werk ik graag zonder grafische vensters en een muiscursor die je overal zoekt, behalve waar hij is. Het toetsenbord heeft mijn voorkeur voor invoer, de commandoprompt oftewel commandline interface (CLI) voor uitvoer. Er bleek minimaal één programma geschikt om cd's te branden via de terminal: cdrdao.

Deze applicatie schrijft, op basis van een simpel tekstbestand (Table Of Contents, TOC), de gegevens of audio naar een lege cd-r. Ze biedt ondersteuning voor simulatie van het branden, instellen van brandsnelheid, keuze van het te gebruiken stuurprogramma en nog veel meer. Hiermee ging het branden zeker lukken!

CD-TEXT

Met name cd-spelers in autoradio's geven vaak de titel en artiest van afgespeelde muziek op hun display weer. Deze gegevens worden, samen met de muziek, op de cd-r opgeslagen. Dit heet CD-TEXT. Met dank aan een leerzame blogpost vond ik de mogelijkheid om mijn zelfgebrande audio-cd's zoals "Achter de naas aa" van deze extra informatie te voorzien.

Het artikeltje beschrijft het formaat van bovengenoemd TOC-bestand, met de velden voor metadata zoals artiest en titel. De auteur schrijft dat het een nogal spraakzaam, uitgeschreven syntax is, maar ook:

... a few seconds in any scripting language would be enough to create a quick template.

Dynamisch TOC-bestand

Ik wilde graag een reeks FLAC-bestanden min of meer direct op cd kunnen branden, met overname van de metadata in de vorm van CD-TEXT. Het was dus zaak om als eerste die gegevens uit te lezen; ik koos voor metaflac. Hieronder een voorbeeld van wat zo'n aanroep uitgeeft:

$ metaflac --list --block-type=VORBIS_COMMENT 01.flac
METADATA block #1
  type: 4 (VORBIS_COMMENT)
  is last: false
  length: 253
  vendor string: Lavf56.40.101
  comments: 10
    comment[0]: ALBUM=Achter de naas aa
    comment[1]: ALBUMARTIST=Frans-Willem Post
    comment[2]: ARTIST=Frans-Willem Post
    comment[3]: CONTACT=https://www.fwiep.nl/
    comment[4]: DATE=2016
    comment[5]: DISCNUMBER=1
    comment[6]: ENCODED-BY=FWieP
    comment[7]: TITLE=Recursive Love Affair
    comment[8]: TRACKNUMBER=01
    comment[9]: TRACKTOTAL=10

MetaFLAC

Omdat ik het beste thuis ben in de PHP-scripttaal, besloot ik dit stuk code in een shell-script te stoppen, dat ik op de commandoprompt kan aanroepen. De belangrijkste functie in dat script is deze:

function getInfoFromFlac($flacFile)
{
    $cmd = 'metaflac --list --block-type=VORBIS_COMMENT '
        .escapeshellarg($flacFile);
    $flacInfo = array();
    $dummy = exec($cmd, $flacInfo);
    $out = array(
        'CD_TITLE' => null,
        'CD_ARTIST' => null,
        'TRACK_TITLE' => null,
        'TRACK_ARTIST' => null
    );
    foreach ($flacInfo as $fi) {
        $m = array();

        if (preg_match('/(?<=ALBUM=).*$/', $fi, $m)) {
            $out['CD_TITLE'] = array_shift($m);
            continue;
        }
        if (preg_match('/(?<=ALBUMARTIST=).*$/', $fi, $m)) {
            $out['CD_ARTIST'] = array_shift($m);
            continue;
        }
        if (preg_match('/(?<=TITLE=).*$/', $fi, $m)) {
            $out['TRACK_TITLE'] = array_shift($m);
            continue;
        }
        if (preg_match('/(?<=ARTIST=).*$/', $fi, $m)) {
            $out['TRACK_ARTIST'] = array_shift($m);
            continue;
        }
    }
    return $out;
}

Op basis van een bestandsnaam ($flacFile) wordt metaflac aangeroepen en diens uitvoer gefilterd met preg_match(). Uiteindelijk geeft deze functie een array met vier sleutels: CD_TITLE, CD_ARTIST, TRACK_TITLE en TRACK_ARTIST.

PHP-script

$arguments = $argv;

$TEMPLATE_MAIN = <<<SHBSHDGGDSGDGYDGSGDGYSDGSD
CD_DA

CD_TEXT {
  LANGUAGE_MAP {
    0 : 29  // Dutch
  }

  LANGUAGE 0 {
    TITLE "%1\$s"
    PERFORMER "%2\$s"
  }
}


SHBSHDGGDSGDGYDGSGDGYSDGSD;

$TEMPLATE_TRACK = <<<SHBSHDGGDSGDGYDGSGDGYSDGSD
TRACK AUDIO
NO COPY
NO PRE_EMPHASIS
TWO_CHANNEL_AUDIO
CD_TEXT {
  LANGUAGE 0 {
    TITLE "%1\$s"
    PERFORMER "%2\$s"
  }
}
FILE "%3\$s" 0


SHBSHDGGDSGDGYDGSGDGYSDGSD;

$scriptName = array_shift($arguments);
$USAGE = sprintf(
    'Usage: %1$s file-1.flac [file-2.flac [file-X...]]',
    basename($scriptName)
);

if (count($arguments) == 0) {
    print $USAGE.PHP_EOL;
    exit(1);
}

$cdTitle = 'Onbekende CD';
$cdArtist = 'Onbekende Artiest';
$flacInfo = array();
$flacFile = array_shift($arguments);

if (!is_null($flacFile)
    && file_exists($flacFile)
    && in_array(mime_content_type($flacFile), array('audio/flac', 'audio/x-flac'))
) {
    $flacInfo = getInfoFromFlac($flacFile);
    $cdTitle = $flacInfo['CD_TITLE'];
    $cdArtist = $flacInfo['CD_ARTIST'];
}

// Output
printf(
    $TEMPLATE_MAIN,
    prepareStringForCdText($cdTitle),
    prepareStringForCdText($cdArtist)
);

while (
    !is_null($flacFile)
    && file_exists($flacFile)
    && in_array(mime_content_type($flacFile), array('audio/flac', 'audio/x-flac'))
) {
    $flacInfo = getInfoFromFlac($flacFile);
    // Output
    printf(
        $TEMPLATE_TRACK,
        prepareStringForCdText($flacInfo['TRACK_TITLE']),
        prepareStringForCdText($flacInfo['TRACK_ARTIST']),
        str_replace('.flac', '.wav', $flacFile)
    );
    $flacFile = array_shift($arguments);
}

exit(0);

De PHP-variabele $argv bevat altijd de argumenten van het aangeroepen script. In dit geval dus de bestandsnamen van de FLAC-bestanden die ik wil uitlezen. Met behulp van array_shift() wordt er stuk voor stuk doorheen gelopen, waarbij de eerste tevens wordt gebruikt om de CD-gegevens uit te lezen. Let op de controle op MIME-type. Met andere bestanden kan metaflac niets beginnen.

De uitvoer van het script bestaat uit twee delen: de kop die slechts één keer wordt uitgegeven ($TEMPLATE_MAIN) en het terugkerende deel per audiobestand ($TEMPLATE_TRACK). In eerstgenoemde worden de cd-artiest en -titel ingevuld, in laatstgenoemde de artiest en titel van de betreffende track, alsook de bestandsnaam die cdrdao moet gebruiken om vanuit te branden.

Een oplettende lezer zal opmerken, dat hier met .wav-bestanden wordt gewerkt. Zie daartoe de beschrijving van het volledige brand-script, hieronder.

To ASCII or not to ASCII?

Tijdens het testen van dit script stelde ik vast, dat CD-TEXT geen ondersteuning biedt voor volledige UTF-8. Tekens zoals é, ë, ü of Ø werden totaal verbouwd, in elk geval waren ze niet leesbaar op de genoemde autoradio… Wat blijkt? CD-TEXT komt uit een tijd waarin UTF-8 nog niet eens bestond; officieel is de te gebruiken codering niet vastgelegd. Het veiligste wat je kan doen, is alle tekst in simpele ASCII-codering opslaan. Aldus geschiedde, met de hulp van iconv(). "André Rieu" wordt "Andre Rieu", "Bløf" wordt "Blof".

function prepareStringForCdText($s)
{
    $s = is_string($s) ? $s : (string)$s;
    $s = trim($s);
    $s = @iconv('UTF-8', 'ASCII//TRANSLIT', $s);
    $s = str_replace('"', '', $s);
    return $s;
}

Alternatief: ISO-8859-1

Ik wist dat ik ooit, tijdens het luisteren van een audio-cd, letters met accenten had gezien op het display van de voornoemde autoradio. Aldus zocht ik zo'n cd en las het TOC-bestand daarvan in:

TITLE "Dreamer"
PERFORMER "Ren\351 Dehue & Frans-Willem Post"

De e-acute wordt blijkbaar omgezet naar een bepaalde code, die de cd-speler daarna vertaalt naar een specifiek niet-ASCII teken. Het blijkt het nummer van het é-karakter binnen de ISO-8859-1 codering te zijn, maar dan in octale notatie. Uiteindelijk heb ik de hulpfunctie als volgt aangepast:

function prepareStringForCdText($s)
{
    $o = '';
    $s = is_string($s) ? $s : (string)$s;
    $s = trim($s);
    $chars = preg_split('//u', $s, -1, PREG_SPLIT_NO_EMPTY);

    foreach ($chars as $c) {
        $ord = ord($c);
        if ($ord > 127 || $ord < 20) {
            $o .= sprintf(
                '\\%03o',
                ord(iconv('utf-8', 'ISO-8859-1//TRANSLIT', $c))
            );
        } else {
            $o .= $c;
        }
    }
    $o = str_replace('"', '\\034', $o);
    return $o;
}

Update

Het moest er gewoon van komen: de afgelopen week (voorjaar 2020) wilde ik opnieuw een audio-cd branden met cd-text. Ik voerde mijn scripts uit en zie daar:

Writing to media...
ERROR: toc.txt:20: Illegal token: \47
ERROR: toc.txt:20: syntax error at "EOF" missing EndString 
ERROR: toc.txt:20: syntax error at "EOF" missing \}

Ik zocht en vond de boosdoener in het titelveld van één van de FLAC-bestanden. Daar stond "A swingin‘ safari". Mijn script vertaalde het typografische enkele aanhalingsteken naar een apostrophe, maar dan in octale notatie: "A swingin\47 safari". Blijkbaar kan cdrdao daar niet goed mee overweg. Of toch wel?

Ik probeerde of ik met een octale notatie van drie cijfers (met voorloopnul) wél het gewenste teken kon invoeren - eureka! In het script hierboven is deze fout verholpen met '\\%03o' als formaat voor sprintf().

Brand-script

Het shell-script dat het volledige brandproces automatiseert, maakt gebruik van avconv (het vroegere ffmpeg), cdrdao en het hierboven beschreven metaflac-script. De eerste paar blokken code zijn controles of de betreffende programma's voorhanden zijn. Daarna volgt een loop door alle opgegeven FLAC-bestanden. Waar nodig worden ze geconverteerd naar WAVE-bestand. Daarna wordt het TOC-bestand aangemaakt en het brandproces gestart.

#!/bin/bash
# Process a series of flac-audio files into an audio-cd
# using avconv, metaflac and cdrdao

FLACS=("${@}");
USAGE="Usage: $( basename ${0} ) file1.flac [file2.flac [fileX.flac]]";

if [ ${#} = 0 ]; then
    echo "${USAGE}";
    exit 1;
fi;

if ! hash "avconv" 2>/dev/null; then
    echo "avconv utility not found in PATH. Exiting.";
    exit 2;
fi;

if ! hash "metaflac" 2>/dev/null; then
    echo "metaflac utility not found in PATH. Exiting.";
    exit 3;
fi;

if ! hash "cdtext.php" 2>/dev/null; then
    echo "cdtext.php script not found in PATH. Exiting.";
    exit 4;
fi;

if ! hash "cdrdao" 2>/dev/null; then
    echo "cdrdao utility not found in PATH. Exiting.";
    exit 5;
fi;

for i in "${!FLACS[@]}"; do
    F="${FLACS[${i}]}";
    MIME="$( file --brief --mime-type "${F}")";
    if [ "${MIME}" = "audio/x-flac" -o "${MIME}" = "audio/flac" ]; then
        if [ -f "${F%%.flac}.wav" ]; then
            echo "WAV-file exists, skipping conversion...";
            continue;
        fi         
        echo "Converting "${F}"...";
        avconv -v -8 -i "${F}" -ac 2 -ar 44100 "${F%%.flac}.wav";
    fi;
done;

echo "Extracting metadata, writing TOC-file...";
cdtext.php "${@}" > toc.txt;

echo "Writing to media...";
# NOTE:
# the ":0x10" suffix to the driver option is mandatory for FWiePs
# HL-DT-ST DVDRAM GH24NSB0 (LN00) dvd-burner, when writing CD-TEXT
cdrdao write --speed 8 --device /dev/sr0 --driver generic-mmc:0x10 -v 1 -n --eject toc.txt

echo "All done! :-)";
exit 0;
Terug naar boven

Inhoudsopgave

Delen

Met de deel-knop van uw browser, of met onderstaande koppelingen deelt u deze pagina via sociale media of e-mail.

Atom-feed van FWiePs weblog

Artikelen


Categorieën

Doorzoek de onderstaande categorieën om de lijst met artikelen te filteren.


Terug naar boven