使用 Gstreamer 和 Python 翻录 CD

在上一篇文章中,您学习了如何使用 MusicBrainz 服务通过简单的 Python 脚本为您的音频文件提供标签信息。 本文向您展示了如何编写一个多合一的解决方案,以将您的 CD 复制到您选择的格式的音乐库文件夹中。

不幸的是,这些权力使它不可能 Fedora 携带必要的位在官方回购中编码 MP3。 所以这部分留给读者作为练习。 但是,如果您使用诸如 Google Play 之类的云服务来托管您的音乐,则此脚本可以让您轻松上传音频文件。

该脚本会将您的 CD 录制为以下文件格式之一:

  • 未压缩的 WAV,您可以进一步编码或播放。
  • 压缩但无损的 FLAC。 无损文件保留原始音频的所有保真度。
  • 压缩的有损 Ogg Vorbis。 与 MP3 和 Apple 的 AAC 一样,Ogg Vorbis 使用特殊算法和心理声学特性来发出声音 close 到原始音频。 但是,Ogg Vorbis 通常会在相同文件大小下产生优于其他压缩格式的结果。 你可以 在此处阅读更多信息 如果你喜欢技术细节。

组件

脚本的第一个元素是 GStreamer 管道。 GStreamer 是一个功能齐全的多媒体框架,包含在 Fedora. 它来了 Workstation 中默认安装, 也。 GStreamer 被许多多媒体应用在幕后使用 Fedora. 它允许应用程序处理各种视频和音频文件。

该脚本的第二个主要组成部分是选择和使用多媒体标记库。 在这种情况下 诱变剂库 可以轻松标记多种多媒体文件。 本文中的脚本使用诱变剂标记 Ogg Vorbis 或 FLAC 文件。

最后,脚本使用 Python 的 argparse,标准库的一部分, 一些易于使用的选项和帮助文本。 argparse 库对于您希望用户提供参数的大多数 Python 脚本很有用。 本文不会详细介绍脚本的这一部分。

剧本

您可能还记得之前使用 MusicBrainz 获取标签信息的文章。 该脚本包含该代码,并进行了一些调整以使其与新功能更好地集成。 (如果您将这个脚本复制并粘贴到您最喜欢的编辑器中,您可能会发现它更容易阅读。)

#!/usr/bin/python3

import os, sys
import subprocess
from argparse import ArgumentParser
import libdiscid
import musicbrainzngs as mb
import requests
import json
from getpass import getpass
      
parser = ArgumentParser()
parser.add_argument('-f', '--flac', action='store_true', dest="flac",
                    default=False, help='Rip to FLAC format')
parser.add_argument('-w', '--wav', action='store_true', dest="wav",
                    default=False, help='Rip to WAV format')
parser.add_argument('-o', '--ogg', action='store_true', dest="ogg",
                    default=False, help='Rip to Ogg Vorbis format')
options = parser.parse_args()

# Set up output varieties
if options.wav + options.ogg + options.flac > 1:
    raise parser.error("Only one of -f, -o, -w please")
if options.wav:
    fmt="wav"
    encoding = 'wavenc'
elif options.flac:
    fmt="flac"
    encoding = 'flacenc'
    from mutagen.flac import FLAC as audiofile
elif options.ogg:
    fmt="oga"
    quality = 'quality=0.3'
    encoding = 'vorbisenc {} ! oggmux'.format(quality)
    from mutagen.oggvorbis import OggVorbis as audiofile

# Get MusicBrainz info
this_disc = libdiscid.read(libdiscid.default_device())
mb.set_useragent(app='get-contents', version='0.1')
mb.auth(u=input('Musicbrainz username: '), p=getpass())

release = mb.get_releases_by_discid(this_disc.id, includes=['artists',
                                                            'recordings'])
if release.get('disc'):
    this_release=release['disc']['release-list'][0]

    album = this_release['title']
    artist = this_release['artist-credit'][0]['artist']['name']
    year = this_release['date'].split('-')[0]

    for medium in this_release['medium-list']:
        for disc in medium['disc-list']:
            if disc['id'] == this_disc.id:
                tracks = medium['track-list']
                break

    # We assume here the disc was found. If you see this:
    #   NameError: name 'tracks' is not defined
    # ...then the CD doesn't appear in MusicBrainz and can't be
    # tagged.  Use your MusicBrainz account to create a release for
    # the CD and then try again.
            
    # Get cover art to cover.jpg
    if this_release['cover-art-archive']['artwork'] == 'true':
        url="https://coverartarchive.org/release/" + this_release['id']
        art = json.loads(requests.get(url, allow_redirects=True).content)
        for image in art['images']:
            if image['front'] == True:
                cover = requests.get(image['image'], allow_redirects=True)
                fname="{0} - {1}.jpg".format(artist, album)
                print('Saved cover art as {}'.format(fname))
                f = open(fname, 'wb')
                f.write(cover.content)
                f.close()
                break

for trackn in range(len(tracks)):
    track = tracks[trackn]['recording']['title']

    # Output file name based on MusicBrainz values
    outfname="{:02} - {}.{}".format(trackn+1, track, fmt).replace('/', '-')

    print('Ripping track {}...'.format(outfname))
    cmd = 'gst-launch-1.0 cdiocddasrc track={} ! '.format(trackn+1) + 
            'audioconvert ! {} ! '.format(encoding) + 
            'filesink location="{}"'.format(outfname)
    msgs = subprocess.getoutput(cmd)

    if not options.wav:
        audio = audiofile(outfname)
        print('Tagging track {}...'.format(outfname))
        audio['TITLE'] = track
        audio['TRACKNUMBER'] = str(trackn+1)
        audio['ARTIST'] = artist
        audio['ALBUM'] = album
        audio['DATE'] = year
        audio.save()

确定输出格式

这部分脚本让用户决定如何格式化输出文件:

parser = ArgumentParser()
parser.add_argument('-f', '--flac', action='store_true', dest="flac",
                    default=False, help='Rip to FLAC format')
parser.add_argument('-w', '--wav', action='store_true', dest="wav",
                    default=False, help='Rip to WAV format')
parser.add_argument('-o', '--ogg', action='store_true', dest="ogg",
                    default=False, help='Rip to Ogg Vorbis format')
options = parser.parse_args()

# Set up output varieties
if options.wav + options.ogg + options.flac > 1:
    raise parser.error("Only one of -f, -o, -w please")
if options.wav:
    fmt="wav"
    encoding = 'wavenc'
elif options.flac:
    fmt="flac"
    encoding = 'flacenc'
    from mutagen.flac import FLAC as audiofile
elif options.ogg:
    fmt="oga"
    quality = 'quality=0.3'
    encoding = 'vorbisenc {} ! oggmux'.format(quality)
    from mutagen.oggvorbis import OggVorbis as audiofile

从 argparse 库构建的解析器为您提供了一个内置的 –help 函数:

$ ipod-cd --help
usage: ipod-cd [-h] [-b BITRATE] [-w] [-o]

optional arguments:
  -h, --help            show this help message and exit
  -b BITRATE, --bitrate BITRATE
                        Set a target bitrate
  -w, --wav             Rip to WAV format
  -o, --ogg             Rip to Ogg Vorbis format

该脚本允许用户在命令行上使用 -f、-w 或 -o 来选择格式。 由于这些存储为 True(Python 布尔值),它们也可以被视为整数值 1。如果选择了多个,解析器会生成错误。

否则,脚本会设置适当的编码字符串,以便稍后在脚本中与 GStreamer 一起使用。 请注意,Ogg Vorbis 选择还包括质量设置,然后将其包含在编码中。 想尝试一下简单的改变吗​​? 尝试制作解析器参数和其他格式代码,以便用户可以选择 -0.1 和 1.0 之间的质量值。

另请注意,对于允许标记(WAV 不允许)的每种文件格式,脚本会导入不同的标记类。 这样,脚本可以在脚本后面有更简单、更少混淆的标记代码。 在这个脚本中,Ogg Vorbis 和 FLAC 都使用了 mutagen 库中的类。

获取 CD 信息

脚本的下一部分尝试为光盘加载 MusicBrainz 信息。 您会发现使用此脚本翻录的音频文件的数据未包含在此处的 Python 代码中。 这是因为 GStreamer 还能够在母带制作和制造过程中检测包含在某些光盘上的 CD 文本。 但是,这些数据通常都是大写的(例如“TRACK TITLE”)。 MusicBrainz info 与现代应用程序和其他平台更兼容。

有关此部分的更多信息,请参阅该杂志上的上一篇文章。 这里出现了一些微不足道的更改,以使脚本作为单个进程更好地工作。

需要注意的一项是此警告:

# We assume here the disc was found. If you see this:
# NameError: name 'tracks' is not defined
# ...then the CD doesn't appear in MusicBrainz and can't be
# tagged. Use your MusicBrainz account to create a release for
# the CD and then try again.

所示脚本不包含处理未找到 CD 信息的情况的方法。 这是故意的。 如果发生这种情况,请花点时间通过以下方式帮助社区 在 MusicBrainz 上输入 CD 信息,使用您的登录帐户。

翻录和标记轨道

脚本的下一部分实际上完成了这项工作。 这是一个简单的循环,遍历通过 MusicBrainz 找到的曲目列表。

首先,脚本根据用户选择的格式设置单个轨道的输出文件名:

for trackn in range(len(tracks)):
    track = tracks[trackn]['recording']['title']

    # Output file name based on MusicBrainz values
    outfname="{:02} - {}.{}".format(trackn+1, track, fmt)

然后,该脚本调用 CLI GStreamer 实用程序来执行翻录和编码过程。 该过程将每个 CD 曲目转换为当前目录中的音频文件:

    print('Ripping track {}...'.format(outfname))
    cmd = 'gst-launch-1.0 cdiocddasrc track={} ! '.format(trackn+1) + 
            'audioconvert ! {} ! '.format(encoding) + 
            'filesink location="{}"'.format(outfname)
    msgs = subprocess.getoutput(cmd)

完整的 GStreamer 管道在命令行中如下所示:

gst-launch-1.0 cdiocddasrc track=1 ! audioconvert ! vorbisenc quality=0.3 ! oggmux ! filesink location="01 - Track Name.oga"

GStreamer 具有 Python 库,可让您以有趣的方式直接使用框架,而无需使用子进程。 为了使本文不那么复杂,脚本从 Python 调用命令行实用程序来完成多媒体工作。

最后,如果输出文件不是 WAV 文件,则脚本会标记输出文件。 Ogg Vorbis 和 FLAC 在它们的诱变剂类中都使用了类似的方法。 这意味着这段代码可以保持非常简单:

    if not options.wav:
        audio = audiofile(outfname)
        print('Tagging track {}...'.format(outfname))
        audio['TITLE'] = track
        audio['TRACKNUMBER'] = str(trackn+1)
        audio['ARTIST'] = artist
        audio['ALBUM'] = album
        audio['DATE'] = year
        audio.save()

如果您决定为另一种文件格式编写代码,则需要提前导入正确的类,然后正确执行标记。 您不必使用诱变剂类。 例如,您可以选择使用 eyed3 来标记 MP3 文件。 在这种情况下,标记代码可能如下所示:

...
# In the parser handling for MP3 format
from eyed3 import load as audiofile
...
# In the handling for MP3 tags
audio.tag.version = (2, 3, 0)
audio.tag.artist = artist
audio.tag.title = track
audio.tag.album = album
audio.tag.track_num = (trackn+1, len(tracks))
audio.tag.save()

(请注意,编码功能由您提供。)

运行脚本

这是一个 example 脚本的输出:

$ ipod-cd -o
Ripping track 01 - Shout, Pt. 1.oga...
Tagging track 01 - Shout, Pt. 1.oga...
Ripping track 02 - Stars of New York.oga...
Tagging track 02 - Stars of New York.oga...
Ripping track 03 - Breezy.oga...
Tagging track 03 - Breezy.oga...
Ripping track 04 - Aeroplane.oga...
Tagging track 04 - Aeroplane.oga...
Ripping track 05 - Minor Is the Lonely Key.oga...
Tagging track 05 - Minor Is the Lonely Key.oga...
Ripping track 06 - You Can Come Round If You Want To.oga...
Tagging track 06 - You Can Come Round If You Want To.oga...
Ripping track 07 - I'm Gonna Haunt This Place.oga...
Tagging track 07 - I'm Gonna Haunt This Place.oga...
Ripping track 08 - Crash That Piano.oga...
Tagging track 08 - Crash That Piano.oga...
Ripping track 09 - Save Yourself.oga...
Tagging track 09 - Save Yourself.oga...
Ripping track 10 - Get on Home.oga...
Tagging track 10 - Get on Home.oga...

享受将您的旧 CD 刻录成易于携带的音频文件!