madplay 源码工作原理分析: 本人因项目工作需要,要在基于6410的开发板上移植madplay的代码,其中要修改相关代码以适应 项目需求,于是有了对madplay源码的分析,整理后发出以方便后来人移植改写madplay用
1. 目标:弄清madplay的主要流程机理,结合工作目标重点摸清有关Play,Pause,Resume,Next mp3,Prev mp3,Stop, shuffle, loop,Vol adjustment 的工作机理,为改写代码作准备
2.总体机理: 版本:madplay-0.15.2b,libmad-0.15.1b,libid3tag-0.15.1b 入口点:madplay.c: main() 下面主要集中在所关心的骨架流程方面,各个小细节除非与工作目标相关,否则一律略去
******************************************** main() 流程分析 ******************************************** ---------------------------- player{}用于全局记录有关变量
---------------------------- step 1: main():player_init(&player)初始化player{}参数
---------------------------- step 2: main():get_options(argc, argv, &player)对argv[]各类参数进行解析,记录在player->options,或player->output或 其它相关变量中: 看几个与工作目标有关的: 1)输出声音通道及Mono还是Stereo选择: -1,-2,-m,-S:用于设置player->output.select为:PLAYER_CHANNEL_LEFT or PLAYER_CHANNEL_RIGHT or PLAYER_CHANNEL_MONO or PLAYER_CHANNEL_STEREO
2)loop -r:player->repeat保存重复播放的次数,如无重复播放,则设为-1
3)volume方面 player->output.attamp_db 减弱的分贝 player->output.voladj_db 设定的分贝
---------------------------- step 3: main():player_run()执行播放,这里实现对mp3(单个或多个)进行播放,是主要函数
---------------------------- step 4: main():player_finish()
小结: madplay()主体流程简单: player_init()+get_options()==>player_run()==>player_finish() 也即初始化全局结构,及cmd命令选项解析==>mp3播放==>结束扫尾工作
******************************************** main() 分析Over ********************************************
******************************************** player_run() 流程分析 -- 核心 ******************************************** 1.player_run()入口参数分析: player_run(&player, argc - optind, (char const **) &argv[optind]) argc:为多少首播放歌,argv[]为mp3路径 见下面的调试记录
------------------------------- ~/test$ ls madplay shell test2.mp3 test3.wav test5.mp3 test7.mp3 test9.mp3 madplay_test test1.mp3 test3.mp3 test4.mp3 test6.mp3 test8.mp3 ~/test$ gdb --args ./madplay test1.mp3 test2.mp3 test3.mp3 GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later < This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu"... (gdb) b player_run Breakpoint 1 at 0x804f845: file player.c, line 2700. (gdb) r Starting program: /home/yuxu/test/madplay test1.mp3 test2.mp3 test3.mp3 [Thread debugging using libthread_db enabled] MPEG Audio Decoder 0.15.2 (beta) - Copyright (C) 2000-2004 Robert Leslie et al. [New Thread 0xb7c446b0 (LWP 6724)] [Switching to Thread 0xb7c446b0 (LWP 6724)]
Breakpoint 1, player_run (player=0xbf95631c, argc=3, argv=0xbf9565d8) at player.c:2700 warning: Source file is more recent than executable. 2700 { (gdb) p argv[0] $1 = 0xbf95760e "test1.mp3" (gdb) p argv[1] $2 = 0xbf957618 "test2.mp3" (gdb) p argv[2] $3 = 0xbf957622 "test3.mp3" (gdb) -------------------------------
2.player_run():player->playlist.entries,player->playlist.length的意义: 有了上面的分析,这两个->playlist.entries,->playlist.length就清楚了,不多说
player->playlist.entries = argv; player->playlist.length = argc;
3.player_run()==>setup_tty()==>tty_fd = open(TTY_DEVICE, O_RDONLY); TTY_DEVICE:/dev/tty 这里打开终端,用于后面在播放过程中,对键盘进行读取工作,以判定用户发出什么指令如:Stop,Pause,Resume,Vol Adjustment ,Next Mp3,Prev Mp3 再就是一些其它操作:如设置相应的TYTY属性及信号处理函数,略去不表
------------------------------- 具体调试分析见下: ~/test$ gdb --args ./madplay test1.mp3 test2.mp3 test3.mp3 GNU gdb 6.8-debian Copyright (C) 2008 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later < This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "i486-linux-gnu"... (gdb) b player_run Breakpoint 1 at 0x804f845: file player.c, line 2700. (gdb) r Starting program: /home/yuxu/test/madplay test1.mp3 test2.mp3 test3.mp3 [Thread debugging using libthread_db enabled] MPEG Audio Decoder 0.15.2 (beta) - Copyright (C) 2000-2004 Robert Leslie et al. [New Thread 0xb7bf66b0 (LWP 6832)] [Switching to Thread 0xb7bf66b0 (LWP 6832)]
Breakpoint 1, player_run (player=0xbfe087dc, argc=3, argv=0xbfe08a98) at player.c:2700 warning: Source file is more recent than executable. 2700 { (gdb) s 2704 player->playlist.entries = argv; (gdb) 2705 player->playlist.length = argc; (gdb) s 2710 if ((player->options & PLAYER_OPTION_TTYCONTROL) && (gdb) p /x player->options $1 = 0x40 (gdb) p /x PLAYER_OPTION_TTYCONTROL $2 = 0x40 (gdb) s 54 return __open_alias (__path, __oflag, __va_arg_pack ()); (gdb) 2538 tty_fd = open(TTY_DEVICE, O_RDONLY); (gdb) bt #0 player_run (player=0xbfe087dc, argc=3, argv=0xbfe08a98) at player.c:2538 #1 0x0804b9fd in main (argc=3, argv=0xbfe08a94) at madplay.c:816 (gdb) s 2539 if (tty_fd == -1) { (gdb) bt #0 player_run (player=0xbfe087dc, argc=3, argv=0xbfe08a98) at player.c:2539 #1 0x0804b9fd in main (argc=3, argv=0xbfe08a94) at madplay.c:816 (gdb) s 2546 if (tcgetattr(tty_fd, &save_tty) == -1) { (gdb) ------------------------------- 强调一点:这里主要意义就在于后面的播放过程中,要读取用户的键盘输入,就是用read(tty_fd, &key, 1)来处理的
4.player_run():setup_filters()==>addfilter(player, tty_filter, player)
将会执行代码段: # if defined(USE_TTY) if ((player->options & PLAYER_OPTION_TTYCONTROL) && addfilter(player, tty_filter, player) == -1) return -1; # endif
而在addfilter()中==>filter_new()中malloc()一个新filter==>filter_init()初始化:filter->func = tty_filter() player->output.filters将指向这个新分配的filter,而且这个新分配的filter->func为tty_filter():
特别说明: tty_filter()==>readkey()==>swtich...case处理各类键盘输入的命令:
void filter_init(struct filter *filter,filter_func_t *func, void *data, struct filter *chain) { filter->func = func; ... }
int addfilter(struct player *player, filter_func_t *func, void *data) { struct filter *filter;
filter = filter_new(func, data, player->output.filters); ...
player->output.filters = filter; return 0; }
========================================== 有关tty_filter函数调用链的说明: 1):流程: tty_filter()==>command=readkey(blocking:阻塞否)==>switch(command)...case处理各种命令:如:KEY_STOP,KEY_PAUSE KEY_FORWARD,KEY_BACK,KEY_QUIT,KEY_GAINDECR,KEY_GAININCR,KEY_GAINZERO,
如下为清理过的流程示意图: 提醒一点:KEY_PAUSE:readkey(1)即,当Pause时,读键操作是阻塞的,即进入readkey()执行以下代码: do count = read(tty_fd, &key, 1); while (count == -1 && errno == EINTR); 当无键按下时,read()阻塞睡眠,用top查看果然没有占用cpu了 而总的流程是这样的: 先tty_filter()查看有无key按下,此时是readkey(0)不阻塞形式,无键按下即返回之,有键按下则执行命令,见下面的 代码框架,再去每次取40, 000Bmp3数据进行mp3解码,然后送往alsa驱动去pcm播放,再又回到tty_filter查看有无键按下 就是这样的一个播放循环,后面还会讲到
2)代码框架: enum mad_flow tty_filter(void *data, struct mad_frame *frame) { command = readkey(0); if (command == -1) return MAD_FLOW_BREAK;
again: switch (command) { case KEY_STOP: ...
case KEY_PAUSE: stop_audio(player, stopped); command = readkey(1); ...
break;
case KEY_FORWARD: case KEY_CTRL('n'): case '>': player->control = PLAYER_CONTROL_NEXT; goto stop;
case KEY_QUIT: case KEY_CTRL('c'): case 'Q': player->control = PLAYER_CONTROL_STOP; goto stop;
case KEY_GAINDECR: case KEY_GAININCR: case KEY_GAINZERO: case KEY_GAININFO: { switch (command) { case KEY_GAINDECR: db = set_gain(player, GAIN_ATTAMP | GAIN_RELATIVE, -0.5); break;
case KEY_GAININCR: db = set_gain(player, GAIN_ATTAMP | GAIN_RELATIVE, +0.5); break;
case KEY_GAINZERO: db = set_gain(player, GAIN_ATTAMP, 0); break;
default: db = set_gain(player, 0, 0); break; } ........... stop: stop_audio(player, 1); }
============================================ 5.音量Gain设定: set_gain(player, 0, 0);==> db = player->output.voladj_db + player->output.attamp_db; player->output.gain = db ? mad_f_tofixed(pow(10, db / 20)) : MAD_F_ONE; player->output.gain保存了最终的音量db设定值
那么音量Gain是如何执行的呢? 1.gain_filter的初始化设定: player_run()==>setup_filters()==> addfilter(player, gain_filter, &player->output.gain); ==>player->output.filters指向的filter list中挂上了gain_filter,同时,还有前面所说的tty_filter gain_filter用于调节音量,而tty_filter用于读取键盘输入确定用户的命令输入
补充参数初始值来源: main()==>get_options()==>依据选项"-a 或 -A"对参数进行player->output.attamp_db,player->output.voladj_db设定初 始值 case 'a': player->output.attamp_db = get_decibels(optarg); preamp = 1; break;
case 'A': player->output.voladj_db = get_decibels(optarg);
2.音量调节的方法: play_all()==>play_one()==>decode()==>mad_decoder_run()==>run_sync()==>decoder->filter_func()==> decoder->filter_func(decoder->cb_data, stream, frame)==>gain_filter() ==> # define mad_f_mul(x, y) ((x) * (y)) frame->sbsample[ch][s][sb] = mad_f_mul(frame->sbsample[ch][s][sb], gain); ==> mad_synth_frame()==>synth_full()==>dct32()
也就是:调用gain_filter进行对mp3声音原始数据进行gain处理,也就是直接将gain融入mp3数据中,然后再离散余弦变换 解码mp3文件的编码,这样声音音量级别就提高了,也就是音量调节是纯软件的,直接对数据进行的处理变换
============================================ 6.播放mp3的工作机理: 1)主架构流程: player_run()==>setup_tty()==>open TTY_DEVICE ==>setup_filters()==>gain_filter,tty_filter加入filter list ==>audio_control_init(&control, AUDIO_COMMAND_INIT)+player->output.command(&control)==>audio_alsa()=> init()==>snd_pcm_open() ==>play_all() === 核心函数 ==>audio_control_init(&control, AUDIO_COMMAND_FINISH)+player->output.command(&control)==>stop()==> snd_pcm_drop(),snd_pcm_prepare()
play_all()核心函数: ==>处理随机播放:见下代码,不多说,easy if (player->options & PLAYER_OPTION_SHUFFLE) { srand(time(0)); for (i = 0; i < count; ++i) { j = rand() % count;
tmp = playlist->entries[i]; playlist->entries[i] = playlist->entries[j]; playlist->entries[j] = tmp; } }
==>重要参数说明: count = playlist->length;播放mp3文件数量 playlist->current:当前所播放的mp3的位置 playlist->entries[playlist->current]:当前播放的mp3文件
==>开始播放一首mp3:若播放失败,则该项清零,记数变量-- if (play_one(player) == -1) { playlist->entries[i] = 0; --count; ... }
==>若该首歌播放成功,则依据player->control进行如下处理: 这里主要关注取下一首歌,上一首歌及重复随机播放及Stop 具体细节见下面的代码框架,不难,不详说 while (playlist->current < playlist->length) { ... switch (player->control) { case PLAYER_CONTROL_DEFAULT: if ((player->options & PLAYER_OPTION_SHUFFLE) && player->repeat && ++i < playlist->length) { 处理随机重复播放的情况 }
case PLAYER_CONTROL_NEXT: ++playlist->current; break;
case PLAYER_CONTROL_PREVIOUS: do { if (playlist->current-- == 0) playlist->current = playlist->length; } while (playlist->current < playlist->length && playlist->entries[playlist->current] == 0); break;
case PLAYER_CONTROL_STOP: playlist->current = playlist->length; count = 0; break; } }
可见,对取上一首歌的处理流程总结如下: 1.主播放循环过程中不断的tty_filter()+gain_filter, mp3解码, alsa PCM放声 2.tty_filter()==》readkey()==>switch(command){case KEY_FORWARD: ...}==>player->control = PLAYER_CONTROL_NEXT ==>goto stop;==>stop_audio()+return MAD_FLOW_STOP;==>
run_sync()播放mp3主循环检测到tty_filter()返回值为MAD_FLOW_STOP,立即终止mp3播放主循环 if (decoder->filter_func) { switch (decoder->filter_func(decoder->cb_data, stream, frame)) { case MAD_FLOW_STOP: goto done; .... } }
===>mad_decoder_run()结束返回至decode() ==>decode()结束返回至play_one() ==>play_one()结束,返回至play_all()的播放循环中即上面的代码框架所说的对switch (player->control)的处理
3.小结:对取上一首歌,下一首歌的处理,均由tty_filter()检测到后,设定player->control为相应的值,返回MAD_FLOW_STOP 提前结束play_one()单首歌的播放过程,返回至play_all()经switch.. case..处理,对playlist->current进行相应处理后 取上|下一首歌继续play_one
4.对stop命令的处理,类取上一首,下一首歌的处理完全一样,自己看了
5.对Pause命令的处理,上面在tty_filter中已经说明了是阻塞式read()一直等待有键按下才继续前进,而且在阻塞过程中 没有占用cpu,top可查看之,比mplayer,每20ms查一次键盘输入要好
|