CUE
这个格式对我而言一直是个很神秘的存在。在各种地方都能看见它的身影,直接打开也能看懂一些东西,但想要细说就做不到了。说到底,还是不知道它到底代表了什么,只能把它当播放列表来看。于是下定了决心要好好研究一下,于是就有了这篇文章。
ToC
研究目标
我们的研究目标是 libcue
[1]。根据仓库的说法,这是 cuetools
中 cue
相关部分的 fork
增强。不管怎么说,这是一个 CUE
解释器的完整实现。
这篇文章我们来研究其中的词法分析部分[2]。
开始
%{/* * Copyright (c) 2004, 2005, 2006, 2007, Svend Sorensen * Copyright (c) 2009, 2010 Jochen Keil * For license terms, see the file COPYING in this distribution. */
#include <stdlib.h>#include <string.h>
#include "cd.h"#include "cue_parser.h"
char yy_buffer[PARSER_BUFFER];
int yylex(void);%}
开头没什么太多特别的。根据 lex
的语法,由 %{
和 }%
包裹的部分都会被原样复制到输出中。
空白符与非空白符
ws [ \t\r]nonws [^ \t\r\n]
这里定义了空格、制表符和 \r
为空白符 ws
,而除空白符和 \n
之外的符号都为非空白符 nonws
。
词法分析器选项
%option yylineno%option noyywrap%option noinput%option nounput
接下来的四行规定了词法分析器的一些选项,具体如下:
yylineno
:使用全局变量yylineno
表示当前词法分析的行号noyywrap
:不定义yywrap
函数noinput
:不使用input
函数nounput
:不适用unput
函数
关于 noinput
和 nounput
的相关信息,可以参考 StackOverflow
的这篇文章[3]。
开始条件
%s NAME%x REM%x RPG%x SKIP
%s
和 %x
可以用于定义词法规则的开始条件。其中:
%s
定义包含性(inclusive
)的开始条件,匹配时包含无开始条件的规则。%x
定义排他性(exclusive
)的开始条件,匹配时不包含无开始条件的规则。
通过中括号 <nameA, nameB>
使用开始条件,逗号表示关系或。
开始条件 INITIAL
表示默认的开始条件,即无开始条件。
通过 BEGIN
行为(Action
)激活对应的开始条件,如:BEGIN(INITIAL)
。
更加详细的说明可以查阅文档[4]。
字符串
显式字符串
\'([^\']|\\\')*\' |\"([^\"]|\\\")*\" { yylval.sval = strncpy( yy_buffer, ++yytext, (yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng)); yylval.sval[(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng) - 2] = '\0'; BEGIN(INITIAL); return STRING;}
显式字符串表示的是由引号(单引号或双引号)包裹的字符串,通过转义字符 \
可以保留引号本身。
隐式字符串
<NAME>{nonws}+ { yylval.sval = strncpy( yy_buffer, yytext, (yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng)); yylval.sval[(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng)] = '\0'; BEGIN(INITIAL); return STRING;}
隐式字符串表示的是不由引号包裹的字符串。这样的字符串在开始条件 NAME
之后出现,由非空白字符(nonws
)组成。
典型的此类字符串是 REM GENRE Game
。在这个字符串中,REM
和 GENRE
是 Token
,而 Game
则是一个没有引号包裹的字符串。
字符串处理
字符串捕获后被复制到 yy_buffer
中,返回 Token
的类型为 STRING
。
字符串捕获完成后,开始条件被重设为 INITIAL
。
词法单元
依赖后续名称(1)
CATALOG { BEGIN(NAME); return CATALOG; }CDTEXTFILE { BEGIN(NAME); return CDTEXTFILE; }
FILE { BEGIN(NAME); return FFILE; }
上述的三个 Token
,包括 CATALOG
、CDTEXTFILE
和 FILE
都依赖后续字符串。
简单词法单元
BINARY { return BINARY; }MOTOROLA { return MOTOROLA; }AIFF { return AIFF; }WAVE { return WAVE; }MP3 { return MP3; }FLAC { return FLAC; }
TRACK { return TRACK; }AUDIO { yylval.ival = MODE_AUDIO; return AUDIO; }MODE1\/2048 { yylval.ival = MODE_MODE1; return MODE1_2048; }MODE1\/2352 { yylval.ival = MODE_MODE1_RAW; return MODE1_2352; }MODE2\/2336 { yylval.ival = MODE_MODE2; return MODE2_2336; }MODE2\/2048 { yylval.ival = MODE_MODE2_FORM1; return MODE2_2048; }MODE2\/2342 { yylval.ival = MODE_MODE2_FORM2; return MODE2_2342; }MODE2\/2332 { yylval.ival = MODE_MODE2_FORM_MIX; return MODE2_2332; }MODE2\/2352 { yylval.ival = MODE_MODE2_RAW; return MODE2_2352; }
FLAGS { return FLAGS; }PRE { yylval.ival = FLAG_PRE_EMPHASIS; return PRE; }DCP { yylval.ival = FLAG_COPY_PERMITTED; return DCP; }4CH { yylval.ival = FLAG_FOUR_CHANNEL; return FOUR_CH; }SCMS { yylval.ival = FLAG_SCMS; return SCMS; }
PREGAP { return PREGAP; }INDEX { return INDEX; }POSTGAP { return POSTGAP; }
上述的这些都是简单词法单元,只需要进行文本匹配即可。
依赖后续名称(2)
TITLE { BEGIN(NAME); yylval.ival = PTI_TITLE; return TITLE; }PERFORMER { BEGIN(NAME); yylval.ival = PTI_PERFORMER; return PERFORMER; }SONGWRITER { BEGIN(NAME); yylval.ival = PTI_SONGWRITER; return SONGWRITER; }COMPOSER { BEGIN(NAME); yylval.ival = PTI_COMPOSER; return COMPOSER; }ARRANGER { BEGIN(NAME); yylval.ival = PTI_ARRANGER; return ARRANGER; }MESSAGE { BEGIN(NAME); yylval.ival = PTI_MESSAGE; return MESSAGE; }DISC_ID { BEGIN(NAME); yylval.ival = PTI_DISC_ID; return DISC_ID; }GENRE { BEGIN(NAME); yylval.ival = PTI_GENRE; return GENRE; }TOC_INFO1 { BEGIN(NAME); yylval.ival = PTI_TOC_INFO1; return TOC_INFO1; }TOC_INFO2 { BEGIN(NAME); yylval.ival = PTI_TOC_INFO2; return TOC_INFO2; }UPC_EAN { BEGIN(NAME); yylval.ival = PTI_UPC_ISRC; return UPC_EAN; }ISRC/{ws}+\" { BEGIN(NAME); yylval.ival = PTI_UPC_ISRC; return ISRC; }SIZE_INFO { BEGIN(NAME); yylval.ival = PTI_SIZE_INFO; return SIZE_INFO; }
ISRC { BEGIN(NAME); return TRACK_ISRC; }
这些词法单元在依赖后续名称(NAME
)的基础上还调整了对应的 yylval
的值。
注释(REM
)
注释类型
<REM>DATE { BEGIN(NAME); yylval.ival = REM_DATE; return DATE; }<REM>GENRE { BEGIN(NAME); yylval.ival = PTI_GENRE; return XXX_GENRE; }<REM>REPLAYGAIN_ALBUM_GAIN { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_ALBUM_GAIN; return REPLAYGAIN_ALBUM_GAIN; }<REM>REPLAYGAIN_ALBUM_PEAK { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_ALBUM_PEAK; return REPLAYGAIN_ALBUM_PEAK; }<REM>REPLAYGAIN_TRACK_GAIN { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_TRACK_GAIN; return REPLAYGAIN_TRACK_GAIN; }<REM>REPLAYGAIN_TRACK_PEAK { BEGIN(RPG); yylval.ival = REM_REPLAYGAIN_TRACK_PEAK; return REPLAYGAIN_TRACK_PEAK; }
这里定义了 REM
的种类,包括 DATE
、GENRE
等六种类型。其中 REPLAYGAIN
的四种在解析后会进入 RPG
开始条件。
空白字符与错误容忍
<REM>{ws}+ { BEGIN(REM); }<REM>. { BEGIN(REM); }
REM
条件下跳过所有的空白字符,而对于没有被之前规则匹配到的任意字符(.
)也跳过处理。
结束
<REM>\n { BEGIN(INITIAL); }
当遇到换行符时,REM
结束。这也意味着 REM
注释只能在一行内完成。
回放增益(RPG
)
回放增益(英语:Replay Gain
)是一个于2001年7月12日被公开提出的标准,用于将像 MP3
或 Ogg Vorbis
等格式的数字化音频的可感知响度进行标准化(Normalize
)处理[5]。
回访增益处理
<RPG>{nonws}+ { yylval.sval = strncpy( yy_buffer, yytext, (yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng)); yylval.sval[(yyleng > sizeof(yy_buffer) ? sizeof(yy_buffer) : yyleng)] = '\0'; BEGIN(SKIP); return STRING;}
回访增益只取一个由非空字符构成的串。在获得这个字符串后,进入 SKIP
开始条件。
空字符处理
<RPG>{ws}+ { BEGIN(RPG); }
RPG
中忽视所有空字符。
SKIP
处理
<SKIP>.*\n { BEGIN(INITIAL); yylineno++; return '\n'; }
忽略所有 SKIP
条件下的字符,直到新行出现,进入 INITIAL
状态。
全局空字符处理
{ws}+ { /* ignore whitespace */ }
全局忽略空字符。
数字
[[:digit:]]+ { yylval.ival = atoi(yytext); return NUMBER; }
对数字进行解析。
冒号
: { return yytext[0]; }
不是很清楚为什么冒号需要单独处理,总之这里是捕获了冒号(
空行
^{ws}*\n { yylineno++; /* blank line */ }\n { yylineno++; return '\n'; }
定义只存在空白字符或不存在字符的行为空行,对空行只增加行号,不进行其他处理。
结束:全局错误
. { fprintf(stderr, "bad character '%c'\n", yytext[0]); }
对解析到这一步还没有被辨识的字符,报 bad character
错误。
结语
对于 CUE
格式,还有其他各种资料可考。在这篇文章写完之后,我在 GitHub
上找到了一份整理好的说明[6],可供参考。
这里遗漏了一些很关键的东西,比如词法单元的声明,这些我们留到下一篇介绍 cue_parser.y
的时候再谈也不迟(笑)
参考
- https://github.com/lipnitsk/libcue
- https://github.com/lipnitsk/libcue/blob/f6a11cbfd6029abb9cbc50264d2b747d3f9e427f/cue_scanner.l
- https://stackoverflow.com/questions/39075510/option-noinput-nounput-what-are-they-for
- http://dinosaur.compilertools.net/flex/flex_11.html
- https://zh.wikipedia.org/wiki/%E5%9B%9E%E6%94%BE%E5%A2%9E%E7%9B%8A
- https://github.com/libyal/libodraw/blob/main/documentation/CUE%20sheet%20format.asciidoc