找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 780|回复: 0

【Python】解析cue文件并切割媒体文件

[复制链接]
发表于 2023-4-2 10:06:59 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×

前言

很久以前搞到一些无损音频资源。然而并没有切割,只不过给了个多合一的wav文件和一个cue文件。
如果用播放器打开cue可以实现切割效果,但不代表所有播放器都支持这种操作。
本文的实现基于解析cue文件并切割。

文件格式

cue文件本质上是文本,以双空格为缩进,格式可以大致理解为:

元数据类型 元数据
元数据类型 元数据
...
FILE 文件名
  TRACK 编号 类型
    元数据类型 元数据
    元数据类型 元数据
    ...
  TRACK 编号 类型
    元数据类型 元数据
    元数据类型 元数据
    ...
  ...
FILE 文件名
  ...

详情可以看维基百科的介绍

程序设计

基于此,我设计了几个类:cue_list, media_file, track, media_time
此外,由于cue里字段和数据是用空格分隔的,因此可以用split,但要注意引号的情况。这里建议用shlex库来省事。
另外,我觉得不论是if-elif-else语句还是3.9之后有的match-case语句,都太繁琐了。我选择用字典+方法来解决问题。
还有就是Python 3.9开始可以指定类型了,虽然在执行上没啥用,但是在vscode里就方便Pylance进行提示了。

cue_list

这个类用于描述整个cue文件。
我这里解析了REM,也就是注释。虽然原则上不必解析,但我手头的音乐资源把一些元数据写进了注释里。
此外,我用列表砍头的方式进行解析,再由方法返回字段所占的行数。如此一来便可以顺序解析多行的数据。

class cue_list:
    def __init__(self,file_lines:list[str]):
        # Initialization
        i=0
        work_dict:dict={"REM":self._rem,"CATALOG":self._catalog,"PERFORMER":self._performer,"TITLE":self._title,"FILE":self._file}
        self.genre="unknown"
        self.date="unknown"
        self.disc_id="unknown"
        self.comment="unknown"
        self.catalog="unknown"
        self.performer="unknown"
        self.media_files:list[media_file]=[]
        # Parse
        while i<len(file_lines):
            s=shlex.split(file_lines[i])
            if s[0] in work_dict:
                i+=work_dict[s[0]](file_lines[i:])
            else:
                print("[CUE] Unknown directive: {}!".format(s[0]))
                break

    def _file(self,rest_of_content:list[str])->int:
        # Search until indentation is over
        i=1
        while i<len(rest_of_content):
            if not rest_of_content[i].startswith(' '*2):
                break
            i+=1
        self.media_files.append(media_file(rest_of_content[:i]))
        return i

    def _rem(self,rest_of_content:list[str])->int:
        cur=shlex.split(rest_of_content[0])
        match cur[1]:
            case "GENRE":
                self.genre=cur[2]
            case "DATE":
                self.date=cur[2]
            case "DISCID":
                self.disc_id=cur[2]
            case "COMMENT":
                self.comment=cur[2]
            case _:
                print("[CUE-REM] Unknown directive: {}!".format(cur[1]))
        return 1

    def _catalog(self,rest_of_content:list[str])->int:
        cur=shlex.split(rest_of_content[0])
        self.catalog=cur[1]
        return 1

    def _performer(self,rest_of_content:list[str])->int:
        cur=shlex.split(rest_of_content[0])
        self.performer=cur[1]
        return 1

    def _title(self,rest_of_content:list[str])->int:
        cur=shlex.split(rest_of_content[0])
        self.title=cur[1]
        return 1

media_file

这个类用于描述FILE字段展开的数据。
以我手中的音乐资源来看,FILE字段下只有TRACK字段。但为了扩展性,我仍然使用了字典+方法。

class media_file:
    def __init__(self,file_lines:list[str]):
        # Initialization
        first_directive=shlex.split(file_lines[0])
        self.file_name=first_directive[1]
        self.file_type=first_directive[2]
        self.tracks:list[track]=[]
        i=1
        work_dict:dict={"TRACK":self._track}
        # Parse
        while i<len(file_lines):
            s=shlex.split(file_lines[i])
            if s[0] in work_dict:
                i+=work_dict[s[0]](file_lines[i:])
            else:
                print("[Media] Unknown directive: {}!".format(s[0]))
                break
        i=0
        while i<len(self.tracks)-1:
            self.tracks[i].end_time=self.tracks[i+1].start_time
            i+=1

    def _track(self,rest_of_content:list[str])->int:
        # Search until indentation is over
        i=1
        while i<len(rest_of_content):
            if not rest_of_content[i].startswith(' '*4):
                break
            i+=1
        self.tracks.append(track(rest_of_content[:i]))
        return i

track

这个类用于描述FILE字段中,TRACK字段展开的数据。

class track:
    def __init__(self,file_lines:list[str]):
        # Initialization
        work_dict:dict={"TITLE":self._title,"PERFORMER":self._performer,"ISRC":self._isrc,"INDEX":self._index}
        first_directive=shlex.split(file_lines[0])
        self.track_id=first_directive[1]
        self.track_type=first_directive[2]
        self.title="unknown"
        self.performer="unknown"
        self.isrc="unknown"
        self.index=""
        self.start_time:media_time=None
        self.end_time:media_time=None
        # Parse
        for statement in file_lines[1:]:
            s=shlex.split(statement)
            if s[0] in work_dict:
                work_dict[s[0]](s)
            else:
                print("[Track] Unknown directive: {}!".format(s[0]))
                break

    def _title(self,directives:list[str])->None:
        self.title=directives[1]

    def _performer(self,directives:list[str])->None:
        self.performer=directives[1]

    def _isrc(self,directives:list[str])->None:
        self.isrc=directives[1]

    def _index(self,directives:list[str])->None:
        self.index=directives[1]
        self.start_time=media_time(directives[2])

media_file

这个类用于描述TRACK字段中,INDEX字段所描述的起始时间。

class media_time:
    def __init__(self,s:str=None,seconds=None):
        if not s is None:
            _s=s.split(':')
            self.hour=int(_s[0])//60
            self.minute=int(_s[0])%60
            self.second=int(_s[1])
            self.frame=int(_s[2])
        elif not seconds is None:
            self.hour=seconds//3600
            self.minute=(seconds%3600)//60
            self.second=seconds%60
            self.frame=0

    def __str__(self):
        return "{:02d}:{:02d}:{:02d}.{:03d}".format(self.hour,self.minute,self.second,self.frame)

构造FFmpeg命令行

Python的subprocess可以让我们直接喂一个list进去来创建进程,这样一来就不用摆平空格和引号造成的困扰了。

def make_ffmpeg_cmdline(dir_root:str,cue:cue_list,media:media_file,trk:track,prefix:str,suffix:str,out_dir:str)->str:
    cmd_lines=["ffmpeg","-i",os.path.join(dir_root,media.file_name),"-hide_banner","-ss"]
    cmd_lines.append(str(trk.start_time))
    if not trk.end_time is None:
        cmd_lines.append("-to")
        cmd_lines.append(str(trk.end_time))
    cmd_lines+=["-metadata","artist={}".format(trk.performer)]
    cmd_lines+=["-metadata","title={}".format(trk.title)]
    cmd_lines+=["-metadata","year={}".format(cue.date)]
    cmd_lines+=["-metadata","track={}".format(trk.track_id)]
    cmd_lines+=["-y"]
    cmd_lines.append(os.path.join(out_dir,"{}{}. {} - {}.{}".format(prefix,trk.track_id,trk.performer,trk.title,suffix)))
    return cmd_lines

主函数

由于我手头的cue文件,它™竟然是UTF8-BOM编码的!所以程序的参数还得稍做打磨一下来支持各种字符串编码。顺便支持一下输出目录,以及后缀名啥的

def main()->None:
    if len(sys.argv)<2:
        print("Error: missing input file!")
        return
    else:
        files:list[str]=[]
        prefix=""
        suffix="flac"
        out_dir=None
        text_codec=None
        i=1
        # Parse Arguments...
        while i<len(sys.argv):
            if sys.argv[i].startswith("--"):
                if sys.argv[i]=="--prefix":
                    i+=1
                    prefix=sys.argv[i]
                elif sys.argv[i]=="--output":
                    i+=1
                    out_dir=sys.argv[i]
                elif sys.argv[i]=="--suffix":
                    i+=1
                    suffix=sys.argv[i]
                elif sys.argv[i]=="--text-codec":
                    i+=1
                    text_codec=sys.argv[i]
                else:
                    print("Unknown argument: {}!".format(sys.argv[i]),file=sys.stderr)
            else:
                files.append(sys.argv[i])
            i+=1
        # Process the cue files...
        for fn in files:
            dir_root=os.path.dirname(fn)
            if out_dir is None:
                out_dir=dir_root
            fs=open(fn,'r',encoding=text_codec)
            lines=fs.readlines()
            fs.close()
            # Analyze the cue file.
            playlist=cue_list(lines)
            for f in playlist.media_files:
                for t in f.tracks:
                    # Construct a cmdline.
                    cmd_line=make_ffmpeg_cmdline(dir_root,playlist,f,t,prefix,suffix,out_dir)
                    print(cmd_line)
                    ffmpeg_ret=subprocess.call(cmd_line)
                    if ffmpeg_ret:
                        print("FFmpeg failed! Return value: ",ffmpeg_ret)
                        return

if __name__=="__main__":
    t1=time.time()
    main()
    t2=time.time()
    print("Total processing time: {} seconds...".format(t2-t1))

总结

好像也没啥好总结的。

调用方式:

python split_by_cue.py <cue文件> --text-codec <编码> --output <输出目录> --suffix <后缀名>

由于我的cue是™的UTF8-BOM,所以编码这里填utf-8-sig

split_by_cue.py

6.1 KB, 下载次数: 2

下载源码不回帖是一种很欠扁的行为

回复

使用道具 举报

本版积分规则

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-11-21 20:46 , Processed in 0.037289 second(s), 28 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表