I’ve write a python script to order audacity cut .wav(many tracks) to separately file by .cue file as once, only need set the file path of .wav, it will choose the .cue file in the same folder with same name, if don’t find the .cue, it will pop a dialog to choose the .cue file by yourself.
Note:
- Need a modified easygui from tyysoft/easygui: easygui for Python
- Or you can download the exe file from https://files.cnblogs.com/files/blogs/831999/Audacity-Cue拆分音频v1.1.zip that I’ve build by nuitka.
Features:
- cut .wav file(or others) by .cue file
- set tags for the output file, e.g. title, artist, album…
- set cover picture for .mp3, .flac
Screenshots:
Usage:
If you don’t know anything about chinese, it’s also easy to use, just remember
- the first line is fullpath of the source file to cut to pieces
- the second line is the fullpath of cover picture to set to the out file(.wav not support)
- the third line is the output format of the file, if not choosed then follow the source file
the whole code the as follows(some was created by doubao AI)
# encoding: utf-8
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 eyed3
import psutil
import logging
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)
logging.info(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', 'cp936']
for encoding in encodings:
try:
with open(cue_file_path, 'r', encoding=encoding) as file:
print(f"尝试使用 {encoding} 编码打开文件")
logging.info(f"尝试使用 {encoding} 编码打开文件")
for line in file:
line = line.strip()
if line.startswith('PERFORMER'):
performer = line.split('"')[1]
print(f"找到表演者: {performer}")
logging.info(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}")
logging.info(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}")
logging.info(f"找到轨道时间和标题: {time_in_seconds}, {track_title}")
elif line.startswith('REM DATE'):
year = line.split('REM DATE')[1].strip()
print(f"找到年代: {year}")
logging.info(f"找到年代: {year}")
elif line.startswith('REM GENRE'):
genre = line.split('REM GENRE')[1].strip().strip('"')
print(f"找到流派: {genre}")
logging.info(f"找到流派: {genre}")
break
except UnicodeDecodeError:
print(f"使用 {encoding} 编码打开文件时出现解码错误,尝试下一个编码")
logging.error(f"使用 {encoding} 编码打开文件时出现解码错误,尝试下一个编码")
except Exception as e:
print(f"解析 CUE 文件 {cue_file_path} 时出错: {e}")
logging.error(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: 封面图片的路径
"""
max_retries = 5
retries = 0
while retries < max_retries:
try:
# 尝试以写入模式打开文件,检查文件是否被占用
with open(file_path, 'a'):
pass
break
except Exception:
print(f"文件 {file_path} 被占用,等待 1 秒后重试(第 {retries + 1} 次)")
logging.error(f"文件 {file_path} 被占用,等待 1 秒后重试(第 {retries + 1} 次)")
time.sleep(0.5)
retries += 1
if retries == max_retries:
print(f"无法访问文件 {file_path},已达到最大重试次数。")
logging.error(f"无法访问文件 {file_path},已达到最大重试次数。")
return
try:
file_ext = os.path.splitext(file_path)[1].lower()
if file_ext == '.mp3':
song = eyed3.load(file_path)
if not song.tag:
song.initTag()
if artist:
song.tag.artist = artist
if album:
song.tag.album = album
if genre:
song.tag.genre = genre
if year:
song.tag.recording_date = year
if title:
song.tag.title = title
with open(cover_path, "rb") as cover_art:
song.tag.images.set(3, cover_art.read(), "image/jpeg")
song.tag.save()
# audio = ID3(file_path, v2_version=4)
# 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}")
logging.error(f"不支持的文件格式: {file_ext}")
except Exception as e:
print(f"处理文件 {file_path} 时出错: {e}")
logging.error(f"处理文件 {file_path} 时出错: {e}")
def remove_illegal_characters(filename):
illegal_characters = ('\\', '/', ':', '*', '?', '"', '<', '>', '|')
for char in illegal_characters:
filename = filename.replace(char, '')
return filename
def cue_callback(eb):
wav_file = eb.values[0]
pic_file = eb.values[1]
out_format = eb.values[2]
log_file = get_file_directory_os(wav_file) + '\\log.txt'
# 设置日志输出到指定文件,日志级别为DEBUG,指定日志格式
logging.basicConfig(
filename=log_file, # 日志文件名
level=logging.ERROR, # 日志级别,DEBUG表示记录所有级别日志
format='%(asctime)s - %(levelname)s - %(message)s' # 日志格式,包含时间、级别、消息
)
# 检查文件是否存在
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}-{remove_illegal_characters(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}-{remove_illegal_characters(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='Audacity-Cue拆分器 v1.2 Powered by TYYSOFT!',
fields=['音频文件路径', '封面图片', {"输出格式": ["", ".mp3", ".flac", ".wav"]}],
values=['', ''], callback=cue_callback)
main()