Ripper et encoder ses CD Audio

Sommaire

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 :

1brew install cdparanoia
2apt install cdparanoira

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

1cdparanoia -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 :

1brew install ffmpeg
2apt install ffmpeg

Pour encoder en MP3 :

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

Pour encoder en AAC :

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

Pour encoder en FLAC (format sans perte) :

1ffmpeg -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 :

1brew install cd-discid
2apt 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 :

1$ ./cd-discid /dev/rdisk3
26a09de08 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 :

 1# xmcd
 2# 
 3# Track frame offsets:
 4#     150
 5#     49017
 6#     76397
 7#     94357
 8#     112290
 9#     137397
10#     154280
11#     164597
12# 
13# Disc length: 2528
14# 
15# Revision: 11
16# Processed by: cddbd v1.5.2PL0 Copyright (c) Steve Scherf et al.
17# Submitted via: fre:ac v1.0.31
18# 
19DISCID=6a09de08
20DTITLE=Jean- Michel Jarre / Chronologie
21DYEAR=1993
22DGENRE=Electronica
23TTITLE0=Chronologie 1
24TTITLE1=Chronologie 2
25TTITLE2=Chronologie 3
26TTITLE3=Chronologie 4
27TTITLE4=Chronologie 5
28TTITLE5=Chronologie 6
29TTITLE6=Chronologie 7
30TTITLE7=Chronologie 8
31EXTD= YEAR: 1993
32EXTT0=
33EXTT1=
34EXTT2=
35EXTT3=
36EXTT4=
37EXTT5=
38EXTT6=
39EXTT7=
40PLAYORDER=

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

  1#!/usr/bin/env python3
  2# -*- coding: utf-8 -*-
  3
  4#######################
  5# ripenc.py
  6#######################
  7# - Rip d'un CD Audio
  8# - Récupération Titre/Artiste
  9# - Encodage AAC, MP3, FLAC
 10# - Génération de la playlist
 11# - Ménage
 12#######################
 13# Compatibilité :
 14# - MacOS
 15# - Linux
 16#######################
 17
 18import subprocess # commandes externes
 19import urllib.request # requête http
 20import configparser # lecture .ini
 21import re # regexp
 22import os # renommage fichiers
 23import glob # recherche de fichiers
 24
 25# Dépendances binaires externes devant se trouver dans le path
 26DEPENDENCIES = [
 27  'cdparanoia',
 28  'cd-discid',
 29  'ffmpeg'
 30]
 31
 32# Ripper le CD Audio ?
 33CD_RIP = True
 34
 35# Chemin du lecteur de CD (à adapter)
 36CD_PATH = "/dev/rdisk3"
 37
 38# Récupérer les titres sur gnudb et renommer les fichiers extraits
 39# Le CD Audio doit toujours être dans le lecteur
 40GNUDB_REQUEST = True
 41
 42# type d'encodage à effectuer : aac|flac|mp3|False
 43ENCODE_FORMAT = 'aac'
 44
 45# effacer le .wav d'origine à la fin de l'encodage ?
 46CLEAN_WAV = True
 47
 48# générer la liste de lecture
 49PLAYLIST_CREATE = True
 50
 51# nom de la liste de lecture
 52PLAYLIST_NAME = "playlist.m3u"
 53
 54def check_dependencies():
 55  print("- Vérification des dépendances")
 56  for cmd in DEPENDENCIES:
 57    subprocess.run(['command', '-v', cmd], check=True, universal_newlines=True)
 58  print("OK")
 59
 60def cd_rip():
 61  print("- RIP du CD audio")
 62  subprocess.run(['cdparanoia', '-XB'], check=True, universal_newlines=True)
 63
 64def get_discid():
 65  print("- Récupération du discid")
 66  cp = subprocess.run(['cd-discid', CD_PATH], check=True, universal_newlines=True, stdout=subprocess.PIPE)
 67  # ex: 6a09de08 8 150 49017 76397 94357 112290 137397 154280 164597 2528
 68  ret = cp.stdout.split()
 69  return ret[0]
 70
 71def get_gnudb(discid):
 72  print('- Récupération des metadonnées sur gnudb.org')
 73  url = 'https://gnudb.org/gnudb/' + discid
 74  str = urllib.request.urlopen(url).read().decode("utf-8")
 75  cfg = configparser.ConfigParser()
 76  cfg.read_string('[root]\n' + str) # section root factice
 77  dict = {}
 78  for option in cfg.options('root'):
 79    dict[option] = cfg.get('root', option) # les options ont été lowercasées
 80  return dict
 81
 82def rename_tracks(dict):
 83  print('- Renommage des fichiers')
 84  for key in dict:
 85    m = re.search('ttitle([0-9]{1,2})', key)
 86    if m:
 87      track_number = format(int(m.group(1)) + 1, '02d')
 88      orig_file = 'track' + track_number + '.cdda.wav'
 89      dest_file = track_number + ' - ' + dict[key] + '.wav'
 90      print('key: ' + key + ' value= ' + dict[key] + ' track_number = ', track_number)
 91      print('rename ' + orig_file + ' -> ' + dest_file)
 92      if os.path.isfile(orig_file):
 93        os.rename(orig_file, dest_file)
 94        print('OK')
 95      else:
 96        print('fichier ' + orig_file + ' non trouvé')
 97
 98def encode_wavs():
 99  files = glob.glob('*.wav')
100  files.sort() # glob ne fait aucun tri
101  for file in files:
102    print('- Traitement ' + file)
103
104    if ENCODE_FORMAT == 'aac':
105      encode_aac(file)
106    elif ENCODE_FORMAT == 'flac':
107      encode_flac(file)
108    elif ENCODE_FORMAT == 'mp3':
109      encode_mp3(file)
110
111    if CLEAN_WAV:
112      clean_wav(file)
113
114def clean_wav(file):
115  print("- Suppression " + file)
116  os.remove(file)
117
118def encode_aac(input):
119  output = input.replace('.cdda.wav', '').replace('.wav', '') + '.m4a'
120  print('- Encodage AAC : ' + input + ' -> ' + output)
121  subprocess.run(['ffmpeg', '-y', '-hide_banner', '-i', input, '-c:a', 'aac', '-b:a', '160k', output], check=True, universal_newlines=True)
122  if PLAYLIST_CREATE:
123    playlist_append(output)
124
125def encode_flac(input):
126  output = input.replace('.cdda.wav', '').replace('.wav', '') + '.flac'
127  print('- Encodage FLAC : ' + input + ' -> ' + output)
128  subprocess.run(['ffmpeg', '-y', '-hide_banner', '-i', input, '-c:a', 'flac', output], check=True, universal_newlines=True)
129  if PLAYLIST_CREATE:
130    playlist_append(output)
131
132def encode_mp3(input):
133  output = input.replace('.cdda.wav', '').replace('.wav', '') + '.mp3'
134  print('- Encodage MP3 : ' + input + ' -> ' + output)
135  subprocess.run(['ffmpeg', '-y', '-hide_banner', '-i', input, '-c:a', 'libmp3lame', '-b:a', '320k', output], check=True, universal_newlines=True)
136  if PLAYLIST_CREATE:
137    playlist_append(output)
138
139def playlist_append(file):
140  print('- Ajout ' + file + ' à la liste de lecture')
141  f = open(PLAYLIST_NAME, 'a')
142  f.write(file + "\n")
143  f.close()
144
145if __name__ == '__main__':
146  check_dependencies()
147  if CD_RIP:
148    cd_rip()
149  if GNUDB_REQUEST:
150    discid = get_discid()
151    print('discid: ' + discid)
152
153    info = get_gnudb(discid)
154    if info:
155      rename_tracks(info)
156
157  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