Ripper et encoder ses CD Audio

Table des matières

L’article arrive 20 ans après la bataille et vous avez déjà peut être tous jeté vos CD Audio et pris des abonnements Spotify ou Deezer. Cependant pour les autres qui ont encore des étagères de CD Audio qui prennent la poussière, il serait temps d’archiver proprement tout ce fonds sur serveur.

Sachez qu’écouter ses propres acquisitions, sans abonnement internet, sans abonnement à un service de musique, anonymement en mode hors ligne et en n’utilisant que des outils libres, c’est pas ringuard et ça peut être ça la vraie liberté :) En plus vous n’emcombrerez pas le réseau #écologie. Voici quelques lignes de commande utiles pour réaliser ce projet.

Le CD Audio

Le CD Audio suit la norme Red Book. Les données stockées contiennent :

  • 2 canaux (stéreo)
  • une fréquence d’échantillonnage (sampling rate) à 44100Hz
  • une quantification (quantization ou bit bepth) sur 16 bits LPCM, signé, format petit-boutiste

L’acquisition

On ne va pas “numériser” le CD Audio car il l’est déjà mais plutôt extraire bit à bit son contenu.

L’outil cdparanoia permet le rip (l’extraction) des données d’un CD Audio, sans les dénaturer. On aura une copie exacte à l’entête prêt, exportée au format .wav

Sous MacOS avec homebrew ou sous Debian/Ubuntu avec apt :

brew install cdparanoia
apt install cdparanoira

Mettre un CD dans son lecteur puis lancer la commande d’extraction (comptez 15min pour un CD de 60min)

cdparanoia -XB

Va créer dans le répertoire courant des fichiers track01.cdda.wav, track01.cdda.wav, etc …

L’encodage

On va utiliser la trousse à outil magique ffmpeg.

Sous MacOS avec homebrew ou sous Debian/Ubuntu avec apt :

brew install ffmpeg
apt install ffmpeg

Pour encoder en MP3 :

ffmpeg -i track01.cdda.wav -c:a libmp3lame -b:a 320k track01.mp3

Pour encoder en AAC :

ffmpeg -i track01.cdda.wav -c:a aac -b:a 160k track01.m4a

Pour encoder en FLAC (format sans perte) :

ffmpeg -i track01.cdda.wav -c:a flac track01.flac

Récupération des métadonnées

On voudrait récupérer des métadonnées (titre de la piste) pour nommer nos fichiers de façon plus lisible. Une grande base de données libre existe, elle s’appelle gnudb.

Chaque CD Audio est identifié de façon non officielle par une empreinte, calculée en fonction du nombre de pistes et de la durée de chacune d’elle (plus précisement du décalage à l’origine, en frames, du début de chaque piste). Cet identifiant est le DiscId. Un outil cd-discid permet de le calculer.

Sous MacOS avec homebrew ou sous Debian/Ubuntu avec apt :

brew install cd-discid
apt install cd-discid

Sur mon MacOS, mon lecteur de CD est identifié par /dev/rdisk3 (trouvable avec la commande diskutil list), j’invoque donc la commande cd-discid avec le chemin du lecteur :

$ ./cd-discid /dev/rdisk3
6a09de08 8 150 49017 76397 94357 112290 137397 154280 164597 2528

Notre disque a comme discid 6a09de08, il possède 8 pistes. La 1ère piste commence à la frame 150 (= 2 secondes de blanc initial), la 2nde piste à la frame 49017 et ainsi de suite.

On peut faire un appel http sur l’api de gnudb à l’url suivante https://gnudb.org/gnudb/6a09de08 :

# xmcd
# 
# Track frame offsets:
#     150
#     49017
#     76397
#     94357
#     112290
#     137397
#     154280
#     164597
# 
# Disc length: 2528
# 
# Revision: 11
# Processed by: cddbd v1.5.2PL0 Copyright (c) Steve Scherf et al.
# Submitted via: fre:ac v1.0.31
# 
DISCID=6a09de08
DTITLE=Jean- Michel Jarre / Chronologie
DYEAR=1993
DGENRE=Electronica
TTITLE0=Chronologie 1
TTITLE1=Chronologie 2
TTITLE2=Chronologie 3
TTITLE3=Chronologie 4
TTITLE4=Chronologie 5
TTITLE5=Chronologie 6
TTITLE6=Chronologie 7
TTITLE7=Chronologie 8
EXTD= YEAR: 1993
EXTT0=
EXTT1=
EXTT2=
EXTT3=
EXTT4=
EXTT5=
EXTT6=
EXTT7=
PLAYORDER=

C’est un résultat texte au format .ini facilement parsable.

Voilà, on a toutes les briques en place. On peut maintenant automatiser tout le processus en le scriptant.

Le script complet

Dépôt github

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

#######################
# ripenc.py
#######################
# - Rip d'un CD Audio
# - Récupération Titre/Artiste
# - Encodage AAC, MP3, FLAC
# - Génération de la playlist
# - Ménage
#######################
# Compatibilité :
# - MacOS
# - Linux
#######################

import subprocess # commandes externes
import urllib.request # requête http
import configparser # lecture .ini
import re # regexp
import os # renommage fichiers
import glob # recherche de fichiers

# Dépendances binaires externes devant se trouver dans le path
DEPENDENCIES = [
  'cdparanoia',
  'cd-discid',
  'ffmpeg'
]

# Ripper le CD Audio ?
CD_RIP = True

# Chemin du lecteur de CD (à adapter)
CD_PATH = "/dev/rdisk3"

# Récupérer les titres sur gnudb et renommer les fichiers extraits
# Le CD Audio doit toujours être dans le lecteur
GNUDB_REQUEST = True

# type d'encodage à effectuer : aac|flac|mp3|False
ENCODE_FORMAT = 'aac'

# effacer le .wav d'origine à la fin de l'encodage ?
CLEAN_WAV = True

# générer la liste de lecture
PLAYLIST_CREATE = True

# nom de la liste de lecture
PLAYLIST_NAME = "playlist.m3u"

def check_dependencies():
  print("- Vérification des dépendances")
  for cmd in DEPENDENCIES:
    subprocess.run(['command', '-v', cmd], check=True, universal_newlines=True)
  print("OK")

def cd_rip():
  print("- RIP du CD audio")
  subprocess.run(['cdparanoia', '-XB'], check=True, universal_newlines=True)

def get_discid():
  print("- Récupération du discid")
  cp = subprocess.run(['cd-discid', CD_PATH], check=True, universal_newlines=True, stdout=subprocess.PIPE)
  # ex: 6a09de08 8 150 49017 76397 94357 112290 137397 154280 164597 2528
  ret = cp.stdout.split()
  return ret[0]

def get_gnudb(discid):
  print('- Récupération des metadonnées sur gnudb.org')
  url = 'https://gnudb.org/gnudb/' + discid
  str = urllib.request.urlopen(url).read().decode("utf-8")
  cfg = configparser.ConfigParser()
  cfg.read_string('[root]\n' + str) # section root factice
  dict = {}
  for option in cfg.options('root'):
    dict[option] = cfg.get('root', option) # les options ont été lowercasées
  return dict

def rename_tracks(dict):
  print('- Renommage des fichiers')
  for key in dict:
    m = re.search('ttitle([0-9]{1,2})', key)
    if m:
      track_number = format(int(m.group(1)) + 1, '02d')
      orig_file = 'track' + track_number + '.cdda.wav'
      dest_file = track_number + ' - ' + dict[key] + '.wav'
      print('key: ' + key + ' value= ' + dict[key] + ' track_number = ', track_number)
      print('rename ' + orig_file + ' -> ' + dest_file)
      if os.path.isfile(orig_file):
        os.rename(orig_file, dest_file)
        print('OK')
      else:
        print('fichier ' + orig_file + ' non trouvé')

def encode_wavs():
  files = glob.glob('*.wav')
  files.sort() # glob ne fait aucun tri
  for file in files:
    print('- Traitement ' + file)

    if ENCODE_FORMAT == 'aac':
      encode_aac(file)
    elif ENCODE_FORMAT == 'flac':
      encode_flac(file)
    elif ENCODE_FORMAT == 'mp3':
      encode_mp3(file)

    if CLEAN_WAV:
      clean_wav(file)

def clean_wav(file):
  print("- Suppression " + file)
  os.remove(file)

def encode_aac(input):
  output = input.replace('.cdda.wav', '').replace('.wav', '') + '.m4a'
  print('- Encodage AAC : ' + input + ' -> ' + output)
  subprocess.run(['ffmpeg', '-y', '-hide_banner', '-i', input, '-c:a', 'aac', '-b:a', '160k', output], check=True, universal_newlines=True)
  if PLAYLIST_CREATE:
    playlist_append(output)

def encode_flac(input):
  output = input.replace('.cdda.wav', '').replace('.wav', '') + '.flac'
  print('- Encodage FLAC : ' + input + ' -> ' + output)
  subprocess.run(['ffmpeg', '-y', '-hide_banner', '-i', input, '-c:a', 'flac', output], check=True, universal_newlines=True)
  if PLAYLIST_CREATE:
    playlist_append(output)

def encode_mp3(input):
  output = input.replace('.cdda.wav', '').replace('.wav', '') + '.mp3'
  print('- Encodage MP3 : ' + input + ' -> ' + output)
  subprocess.run(['ffmpeg', '-y', '-hide_banner', '-i', input, '-c:a', 'libmp3lame', '-b:a', '320k', output], check=True, universal_newlines=True)
  if PLAYLIST_CREATE:
    playlist_append(output)

def playlist_append(file):
  print('- Ajout ' + file + ' à la liste de lecture')
  f = open(PLAYLIST_NAME, 'a')
  f.write(file + "\n")
  f.close()

if __name__ == '__main__':
  check_dependencies()
  if CD_RIP:
    cd_rip()
  if GNUDB_REQUEST:
    discid = get_discid()
    print('discid: ' + discid)

    info = get_gnudb(discid)
    if info:
      rename_tracks(info)

  encode_wavs()

Améliorations possibles

  • Tagger les fichiers en extrayant DTITLE, DYEAR, DGENRE (quel outil ?)
  • Récupérer la pochette (quel outil ? via quel site ?)

Ressources

comments powered by Disqus