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;