开发实践:使用Python操作Audacity拆分整张CD为多个单独音乐文件

1 开发实践:使用Python操作Audacity拆分整张CD为多个单独音乐文件

1.1 任务背景

  作者找了一些音乐,但CD导出的文件10首歌是一个WAV文件,需要进行拆分,音频处理软件里面,自由免费的当然首选Audacity,有些WAV文件没有附带Cue文件,就只能自己硬拆了,而部分音频文件是带cue文件的,里面有音轨的时间信息,有两种方式通过Audacity拆分音频文件

  1. 使用脚本将cue文件转成标签文件,Audacity导入音频文件和标签文件,再按标签导出
  2. 由Python脚本分析cue文件的音轨时间信息,再调用Audacity的脚本接口,将音轨分段导出成指定文件。

第1种方式手工处理的部分比较多,中间还要生成标签文件,不是太方便。
第2种方式是本文要介绍的Python脚本方式,由Python通过Audacity脚本接口发送命令,指挥Audacity逐段导出文件。

1.2 完整代码

  下面的代码是SplitWavByCue.py的完整代码。
::: alert-danger

  • easygui需要使用tyysoft修改版:easygui,该版本支持了下拉列表框,用于选择导出音频文件格式。
    安装方法:下载到本地后,在文件所在目录执行如下命令
    pip install easygui-0.98.6-py2.py3-none-any.whl
  • pipeclient.py文件下载到本地,下载地址:pipeclient.py
  • 使用pip install mutagen安装mutagen
  • 使用pip install psutil安装psutil
    :::

import sys
import os
import time
import easygui as eg
import pipeclient as pc
from mutagen.id3 import ID3, TIT2, TALB, TPE1, TDRC, TCON, APIC
from mutagen.flac import FLAC, Picture
from mutagen.apev2 import APEv2
from mutagen.aac import AAC
from mutagen.aiff import AIFF
from mutagen.dsf import DSF
from mutagen.mp4 import MP4
from mutagen.oggvorbis import OggVorbis
from mutagen.wavpack import WavPack
import psutil


timeout = 60.0
def get_reply (prnt=False):
    start = time.time()
    info = ""
    while info == "" and time.time() - start < timeout:
        info = client.read()
        time.sleep(0.05)
    if info == "":
        sys.exit(f'Timeout after {timeout} seconds')
    if info[-26:-1] != 'BatchCommand finished: OK':
        sys.exit(f'Command failed')
    if prnt:
        print(info)

def send_blocking_command (cmd):
    client.write(cmd)
    get_reply(1)
    client.write(f'Message:Text="{cmd.split(":")[0]} completed"')
    get_reply(1)

client = None

def replace_extension(filename, new_extension):
    """
    此函数用于将文件名的扩展名替换为指定的扩展名
    :param filename: 原始文件名
    :param new_extension: 新的扩展名,格式如 ".txt"
    :return: 替换扩展名后的文件名
    """
    # 分离文件名和扩展名
    name, _ = os.path.splitext(filename)
    # 组合新的文件名和扩展名
    return name + new_extension

def parse_cue_file(cue_file_path):
    """
    解析 CUE 文件,提取标签信息
    :param cue_file_path: CUE 文件的路径
    :return: 包含标签信息的列表,每个元素是一个字典,包含时间、歌曲名、艺术家、专辑名、年代、流派、轨道编号
    """
    labels = []
    performer = ""
    album_title = ""
    current_track = 0
    first_title_found = False
    year = ""
    genre = ""
    encodings = ['utf-8', 'gbk']
    for encoding in encodings:
        try:
            with open(cue_file_path, 'r', encoding=encoding) as file:
                print(f"尝试使用 {encoding} 编码打开文件")
                for line in file:
                    line = line.strip()
                    if line.startswith('PERFORMER'):
                        performer = line.split('"')[1]
                        print(f"找到表演者: {performer}")
                    elif line.startswith('TITLE'):
                        if not first_title_found:
                            album_title = line.split('"')[1]
                            first_title_found = True
                        else:
                            track_title = line.split('"')[1]
                    elif line.startswith('TRACK'):
                        current_track = int(line.split()[1])
                        print(f"找到轨道编号: {current_track}")
                    elif line.startswith('INDEX 01'):
                        time_str = line.split()[2]
                        minutes, seconds, frames = map(int, time_str.split(':'))
                        time_in_seconds = minutes * 60 + seconds + frames / 75
                        label = {
                            "time": time_in_seconds,
                            "title": track_title,
                            "artist": performer,
                            "album": album_title,
                            "year": year,
                            "genre": genre,
                            "track": current_track
                        }
                        labels.append(label)
                        print(f"找到轨道时间和标题: {time_in_seconds}, {track_title}")
                    elif line.startswith('REM DATE'):
                        year = line.split('REM DATE')[1].strip()
                        print(f"找到年代: {year}")
                    elif line.startswith('REM GENRE'):
                        genre = line.split('REM GENRE')[1].strip().strip('"')
                        print(f"找到流派: {genre}")
            break
        except UnicodeDecodeError:
            print(f"使用 {encoding} 编码打开文件时出现解码错误,尝试下一个编码")
        except Exception as e:
            print(f"解析 CUE 文件 {cue_file_path} 时出错: {e}")
    return labels
def get_extension_os(file_path):
    """
    使用 os.path 模块获取文件扩展名
    :param file_path: 文件路径
    :return: 文件扩展名(包含 .),若没有扩展名则返回空字符串
    """
    _, extension = os.path.splitext(file_path)
    return extension
def get_file_directory_os(full_path):
    """
    使用 os.path 模块根据全路径文件名获取文件路径
    :param full_path: 全路径文件名
    :return: 文件路径
    """
    return os.path.dirname(full_path)    

def set_music_metadata(file_path, title=None, album=None, artist=None, year=None, genre=None, cover_path=None):
    """
    为音乐文件设置元数据标签。

    :param file_path: 音乐文件的路径
    :param title: 音乐的标题
    :param album: 音乐所属的专辑
    :param artist: 音乐的艺术家
    :param year: 音乐发行的年代
    :param genre: 音乐的流派
    :param cover_path: 封面图片的路径
    """
    try:
        file_ext = os.path.splitext(file_path)[1].lower()
        if file_ext == '.mp3':
            audio = ID3(file_path)
            if title:
                audio['TIT2'] = TIT2(encoding=3, text=title)
            if album:
                audio['TALB'] = TALB(encoding=3, text=album)
            if artist:
                audio['TPE1'] = TPE1(encoding=3, text=artist)
            if year:
                audio['TDRC'] = TDRC(encoding=3, text=year)
            if genre:
                audio['TCON'] = TCON(encoding=3, text=genre)
            if cover_path:
                with open(cover_path, 'rb') as cover_file:
                    cover_data = cover_file.read()
                audio.add(
                    APIC(
                        encoding=3,
                        mime='image/jpeg',
                        type=3,
                        desc='Cover',
                        data=cover_data
                    )
                )
            audio.save()
        elif file_ext == '.flac':
            audio = FLAC(file_path)
            if title:
                audio['title'] = title
            if album:
                audio['album'] = album
            if artist:
                audio['artist'] = artist
            if year:
                audio['date'] = year
            if genre:
                audio['genre'] = genre
            if cover_path:
                with open(cover_path, 'rb') as cover_file:
                    cover_data = cover_file.read()
                picture = Picture()
                picture.type = 3
                picture.desc = 'Cover'
                picture.mime = 'image/jpeg'
                picture.data = cover_data
                audio.add_picture(picture)
            audio.save()
        elif file_ext == '.ape':
            audio = APEv2(file_path)
            if title:
                audio['Title'] = title
            if album:
                audio['Album'] = album
            if artist:
                audio['Artist'] = artist
            if year:
                audio['Year'] = year
            if genre:
                audio['Genre'] = genre
            if cover_path:
                with open(cover_path, 'rb') as cover_file:
                    cover_data = cover_file.read()
                audio['Cover Art (Front)'] = cover_data
            audio.save()
        elif file_ext == '.aac':
            audio = AAC(file_path)
            tags = audio.tags
            if not tags:
                from mutagen.id3 import ID3
                tags = ID3()
            if title:
                tags['TIT2'] = TIT2(encoding=3, text=title)
            if album:
                tags['TALB'] = TALB(encoding=3, text=album)
            if artist:
                tags['TPE1'] = TPE1(encoding=3, text=artist)
            if year:
                tags['TDRC'] = TDRC(encoding=3, text=year)
            if genre:
                tags['TCON'] = TCON(encoding=3, text=genre)
            audio.tags = tags
            audio.save()
        elif file_ext == '.aiff':
            audio = AIFF(file_path)
            if title:
                audio['TIT2'] = TIT2(encoding=3, text=title)
            if album:
                audio['TALB'] = TALB(encoding=3, text=album)
            if artist:
                audio['TPE1'] = TPE1(encoding=3, text=artist)
            if year:
                audio['TDRC'] = TDRC(encoding=3, text=year)
            if genre:
                audio['TCON'] = TCON(encoding=3, text=genre)
            audio.save()
        elif file_ext == '.dsf':
            audio = DSF(file_path)
            if title:
                audio['TIT2'] = TIT2(encoding=3, text=title)
            if album:
                audio['TALB'] = TALB(encoding=3, text=album)
            if artist:
                audio['TPE1'] = TPE1(encoding=3, text=artist)
            if year:
                audio['TDRC'] = TDRC(encoding=3, text=year)
            if genre:
                audio['TCON'] = TCON(encoding=3, text=genre)
            audio.save()
        elif file_ext == '.m4a':
            audio = MP4(file_path)
            if title:
                audio['\xa9nam'] = [title]
            if album:
                audio['\xa9alb'] = [album]
            if artist:
                audio['\xa9ART'] = [artist]
            if year:
                audio['\xa9day'] = [year]
            if genre:
                audio['\xa9gen'] = [genre]
            if cover_path:
                with open(cover_path, 'rb') as cover_file:
                    cover_data = cover_file.read()
                audio['covr'] = [cover_data]
            audio.save()
        elif file_ext == '.ogg':
            audio = OggVorbis(file_path)
            if title:
                audio['title'] = title
            if album:
                audio['album'] = album
            if artist:
                audio['artist'] = artist
            if year:
                audio['date'] = year
            if genre:
                audio['genre'] = genre
            audio.save()
        elif file_ext == '.wav':
            # WAV 文件没有标准的元数据支持,这里不做封面添加
            from mutagen.wave import WAVE
            audio = WAVE(file_path)
            if title:
                audio['TIT2'] = TIT2(encoding=3, text=title)
            if album:
                audio['TALB'] = TALB(encoding=3, text=album)
            if artist:
                audio['TPE1'] = TPE1(encoding=3, text=artist)
            if year:
                audio['TDRC'] = TDRC(encoding=3, text=year)
            if genre:
                audio['TCON'] = TCON(encoding=3, text=genre)
            audio.save()
        elif file_ext == '.wv':
            audio = WavPack(file_path)
            if title:
                audio['Title'] = title
            if album:
                audio['Album'] = album
            if artist:
                audio['Artist'] = artist
            if year:
                audio['Year'] = year
            if genre:
                audio['Genre'] = genre
            audio.save()
        else:
            print(f"不支持的文件格式: {file_ext}")
    except Exception as e:
        print(f"处理文件 {file_path} 时出错: {e}")

def cue_callback(eb):
    wav_file = eb.values[0]
    pic_file = eb.values[1]
    out_format = eb.values[2]
    # 检查文件是否存在
    if not os.access(wav_file, os.R_OK):
        eg.msgbox('音频文件不存在,处理结束!', '错误')
        return
    cue_file = replace_extension(wav_file, '.cue')
    # 同名替换成cue文件,检查cue文件是否存在,如果不存在则弹出文件对话框,让用户输入cue文件名
    if not os.access(cue_file, os.R_OK):
        cue_file = eg.fileopenbox("请选择1个Cue文件", "打开", "*.cue", filetypes=["*.cue"], multiple=False)
        if not os.access(cue_file, os.R_OK):
            eg.msgbox('Cue文件不存在, 处理结束!', '错误')
            return
    labels = parse_cue_file(cue_file)
    # 提前清理
    send_blocking_command(f'SelAllTracks:')
    send_blocking_command(f'RemoveTracks:')
    
    song_path = get_file_directory_os(wav_file)
    if out_format == "":
        song_ext = get_extension_os(wav_file)
    else:
        song_ext = out_format

    # 打开音频文件,并逐段选中后导出到与源文件同目录
    send_blocking_command(f'Import2:Filename="{wav_file}"')

    # 循环导出除最后1个音乐文件,循环结束后导出最后一个音乐文件
    for i in range(len(labels) - 1):
        time_start = labels[i]['time']
        title = labels[i]['title']
        artist = labels[i]['artist']
        album = labels[i]['album']
        year = labels[i]['year']
        genre = labels[i]['genre']
        track = labels[i]['track']
        
        time_end = labels[i + 1]['time']
        send_blocking_command(f'SelectTime:Start={time_start} End={time_end}')
        # new_wav_file = get_file_directory_os(wav_file) + "\\" + song_title + get_extension_os(wav_file)
        new_wav_file = f"{song_path}\\{artist}-{title}{song_ext}"
        send_blocking_command(f'Export2: Filename="{new_wav_file}" NumChannels=2')
        set_music_metadata(file_path = new_wav_file, 
                           title = title, 
                           album = album, 
                           artist = artist, 
                           year = year,
                           genre = genre,
                           cover_path=pic_file)
    # 导出最后1个文件
    time_last = labels[-1]['time']
    title = labels[-1]['title']
    artist = labels[-1]['artist']
    album = labels[-1]['album']
    year = labels[-1]['year']
    genre = labels[-1]['genre']
    track = labels[-1]['track']
    send_blocking_command(f'SelectTime:Start={time_last} End={time_last}')
    send_blocking_command(f'SelEnd:')
    new_wav_file = f"{song_path}\\{artist}-{title}{song_ext}"
    send_blocking_command(f'Export2: Filename="{new_wav_file}" NumChannels=2')
    set_music_metadata(file_path = new_wav_file, 
                           title = title, 
                           album = album, 
                           artist = artist, 
                           year = year,
                           genre = genre,
                           cover_path=pic_file)

    # 清理当前工程
    send_blocking_command(f'SelAllTracks:')
    send_blocking_command(f'RemoveTracks:')
    eg.msgbox(f'文件分割处理完成!\n音乐文件:{wav_file}\n共分割成{len(labels)}个文件!', '提示')

def is_process_running(process_name):
    """
    检查指定名称的进程是否正在运行,忽略大小写
    :param process_name: 要检查的进程名称,例如 "Audacity.exe"
    :return: 如果进程正在运行返回 True,否则返回 False
    """
    process_name = process_name.lower()
    for proc in psutil.process_iter(['name']):
        try:
            if proc.info['name'].lower() == process_name:
                return True
        except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
            pass
    return False
    
def main():
    
    if not is_process_running("Audacity.exe"):
        eg.msgbox('请先运行Audacity!', '错误')
        return
    
    global client
    client = pc.PipeClient(enc='utf-8')
    
    ret = eg.multenterbox(msg='请输入音频文件路径', 
                          title='用Cue拆分音频', 
                          fields=['音频文件路径', '封面图片', {"输出格式": [".mp3", ".flac", ".wav", ".ape"]}], 
                          values=['', ''], callback=cue_callback)

main()

1.3 代码说明

1.3.1 Audacity脚本接口函数封装

  send_blocking_command函数是封装的Audacity的脚本接口,该函数调用官方pipeclient库接口向audacity发送消息,由于Audacity执行命令需要时间,不建议读者直接使用pipeclient库。

1.3.2 解析cue文件函数parse_cue_file

  函数parse_cue_file读取.cue文件,并分析文件内容,提取每个音乐的开始时间,并将“时间”、“标题”、“艺术家”、“专辑”、“年代”、“流派”、“轨道”等信息存放在字典元素中,最后返回一个字典的列表labels供后续代码使用。

1.3.3 set_music_metadata函数给拆分的音乐文件设置Tags

  一般音乐文件都有元数据,这个数据可以嵌入音乐文件,用于存放“标题”、“艺术家”、“专辑”等标签,甚至有部分格式还支持嵌入图片封面(比如:FLAC文件)。

1.3.4 cue_callback函数是主要处理函数

  其主要功能如下:

  1. 判断界面输入的文件是否存在,如果存在,检查关联的同名.cue文件是否存在,不存在则打开文件对话框让用户重新选择。
  2. 调用parse_cue_file函数获取音乐标签、
  3. 清理Audacity当前工程中的残留音轨
send_blocking_command(f'SelAllTracks:')
send_blocking_command(f'RemoveTracks:')   

先需要选中所有音轨,再删除

  1. 导入待处理的音频文件
send_blocking_command(f'Import2:Filename="{wav_file}"')
  1. 循环遍历labels,读取每段音乐的开始时间,在Audacity上选中这段音乐,并导出
send_blocking_command(f'SelectTime:Start={time_start} End={time_end}')
send_blocking_command(f'Export2: Filename="{new_wav_file}" NumChannels=2')
  1. 给导出的文件添加元数据甚至封面图片
set_music_metadata(file_path = new_wav_file, 
                           title = title, 
                           album = album, 
                           artist = artist, 
                           year = year,
                           genre = genre,
                           cover_path=pic_file)
  1. 处理结束后再清理音轨
send_blocking_command(f'SelAllTracks:')
send_blocking_command(f'RemoveTracks:')

::: alert-danger

  • 脚本执行前,一定要确保Audacity已经运行!
  • 如果有多个Audacity在运行,则脚本会操作最后1个Audacity!
    :::

作者声明:本文用于记录和分享作者的学习心得,可能有部分文字或示例来自AI平台,如:豆包、DeepSeek(硅基流动)(注册链接)等,由于本人水平有限,难免存在表达错误,欢迎留言交流和指教!
Copyright © 2022~2025 All rights reserved.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值