(o・∇・o)
(o・∇・o)
从 cue_scanner.l 看 CUE Sheet 的词法单元

CUE 这个格式对我而言一直是个很神秘的存在。在各种地方都能看见它的身影,直接打开也能看懂一些东西,但想要细说就做不到了。说到底,还是不知道它到底代表了什么,只能把它当播放列表来看。于是下定了决心要好好研究一下,于是就有了这篇文章。

研究目标

我们的研究目标是 libcue[1]。根据仓库的说法,这是 cuetoolscue 相关部分的 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 函数

关于 noinputnounput 的相关信息,可以参考 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。在这个字符串中,REMGENREToken,而 Game 则是一个没有引号包裹的字符串。

字符串处理

字符串捕获后被复制到 yy_buffer 中,返回 Token 的类型为 STRING

字符串捕获完成后,开始条件被重设为 INITIAL

词法单元

依赖后续名称(1)

CATALOG		{ BEGIN(NAME); return CATALOG; }
CDTEXTFILE	{ BEGIN(NAME); return CDTEXTFILE; }

FILE		{ BEGIN(NAME); return FFILE; }

上述的三个 Token,包括 CATALOGCDTEXTFILEFILE 都依赖后续字符串。

简单词法单元

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 的种类,包括 DATEGENRE 等六种类型。其中 REPLAYGAIN 的四种在解析后会进入 RPG 开始条件。

空白字符与错误容忍

<REM>{ws}+	{ BEGIN(REM); }
<REM>.		{ BEGIN(REM); }

REM 条件下跳过所有的空白字符,而对于没有被之前规则匹配到的任意字符(.)也跳过处理。

结束

<REM>\n		{ BEGIN(INITIAL); }

当遇到换行符时,REM 结束。这也意味着 REM 注释只能在一行内完成。

回放增益(RPG

回放增益(英语:Replay Gain)是一个于2001年7月12日被公开提出的标准,用于将像 MP3Ogg 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 的时候再谈也不迟(笑)

参考

  1. https://github.com/lipnitsk/libcue
  2. https://github.com/lipnitsk/libcue/blob/f6a11cbfd6029abb9cbc50264d2b747d3f9e427f/cue_scanner.l
  3. https://stackoverflow.com/questions/39075510/option-noinput-nounput-what-are-they-for
  4. http://dinosaur.compilertools.net/flex/flex_11.html
  5. https://zh.wikipedia.org/wiki/%E5%9B%9E%E6%94%BE%E5%A2%9E%E7%9B%8A
  6. https://github.com/libyal/libodraw/blob/main/documentation/CUE%20sheet%20format.asciidoc

发表评论

textsms
account_circle
email

(o・∇・o)

从 cue_scanner.l 看 CUE Sheet 的词法单元
CUE 这个格式对我而言一直是个很神秘的存在。在各种地方都能看见它的身影,直接打开也能看懂一些东西,但想要细说就做不到了。说到底,还是不知道它到底代表了什么,只能把它当播放列表来…
扫描二维码继续阅读
2020-12-20