CubieBoard中文论坛

 找回密码
 立即注册
搜索
热搜: unable
查看: 15025|回复: 9

MIDI实时合成器+混音折腾全记录

[复制链接]
发表于 2015-4-19 19:46:27 | 显示全部楼层 |阅读模式
sec-1:前奏

#略长,属于吐槽系,可自行取消下一行的注释
#goto sec-2

继上次用mpd的fluidsynth插件播放MIDI音乐,这次有个更高的要求:连接MIDI键盘弹奏。
这不是第一次尝试了,几年前想玩玩电钢嫌贵买了个键盘,之后水平一直也没多大进步。
懒人总结到,是弹琴准备过程太烦人了——开电脑,连键盘,开音源。
实在不喜欢开电脑,虽然现在已经换用了SSD,开机时间短了一半,但整个过程还是有一种心理负担:我TM不弹一小时都对不起我开这次机!{:soso_e130:}

然后软件,TruePiano或Pianissimo都是要钱的,前者快比我键盘贵了。
而免费的soundfont钢琴音色库有些还真不错,比如splendid那个,虽然延音有点问题,但和表现力比起来算小毛病了,能忍。
顺便吐槽一下我机器上的x-fi声卡,本来和soundfont是天作之合,可是创新都有十多年不好好写声卡驱动了,现在MIDI系统居然成了半残,丢音爆音什么情况都有。最不能忍的是每个采样第一次播放的时候都是杂音,第二次才正常,然而这个splendid钢琴音色每个音符有4层采样,意味着我每次要先遛一遍88X4个音,才能保证后面弹奏不出状况。分特~
fluidsynth成了救星,但windows下表现不佳。于是准备过程变成了——开电脑,切换到linux系统(win8引导,你懂的),连接键盘,启动Qsynth,加载音色。
我TM不弹俩小时都对不起我开这次机!{:soso_e134:}

想让键盘看起来更像一个琴而不是电脑周边,还是得有个专门的合成器才行,然而适合它身价的却没有。
有追求的穷人,那就是一个尴尬的存在。{:soso_e141:}

理想的情况就是有一ARM设备,能跑得起fluidsynth,还得保证基本的实时性的,想多了?
当时还没有CB,但手上有个MK808,试着玩了一下picuntu,各种问题,fluidsynth倒是能运行起来,延迟基本能忍,但midi驱动模块没有,连不上键盘;它没有模拟声音输出,必须外接USB声卡或者HDMI解码器。总之,当时没有折腾它的功力,放弃。

而随后入手了CB,折腾的第一件事也是fluidsynth。结果是连接设备没问题,但音频延迟却调不下来。
还是那句话,功力不够,我只是默默地对比了下俩设备的CPU:瑞芯rk3066 vs 全志A10,然后又放弃了。你说你为啥就不玩玩A9或者A15非用个老旧的A8还敢自称A10?

情况到了CB2上,依然没有任何改观。我以为要再等一个世代,直到树莓派2入手。

但我最终折腾的不是2pi,因为它的毛病一开始我就已经无法忍受:底噪太大,也没有数字输出,除非我继续买hifiberry那个怪兽。
我发现两者在相近CPU(A7)和频率上,相同的系统下(archlinux arm),2pi的延迟就能成功调低,fluidsynth设置c和z到4*256就可以接受(此时延迟25ms左右),再低就破音,但它也不拒绝;反而CB2只允许低到4*1024,更低无效。

我左看右看上看下看,没发现2pi的soc音频方案哪强于CB2,只能是一个原因:CB2驱动编译参数太保守!


回复

使用道具 举报

 楼主| 发表于 2015-4-19 19:58:48 | 显示全部楼层
本帖最后由 kappa8086 于 2015-4-19 21:44 编辑

sec-2: 解决alsa延迟

所有的折腾,都是为了降低延迟,如果低到20ms以下,作为业余音乐设备,就算及格,5ms以下,可以和专业的玩一玩了。

说起来一直在ALSA上折腾也算一个误区,我以为其他方案都是在ALSA体系之上的,只可能让延迟增加,当然实际情况不完全是这样,pulseaudio和jack都有自己管理缓冲区的办法。不过后来的折腾证明还就是ALSA靠谱。

先说下折腾的环境:Cubieboard A20,archlinux,使用linux-sun7i内核;
连接:USB 连接 MIDI键盘;GPIO的spdif端口连接光纤头输出到音响。
用户配置:mpd/fluidsynth等程序运行于audioman用户下,该用户加到audio组中。

当用fluidsynth的c(buff count)和z(buff size)指定缓冲区分别到2,512时,fluidsynth会回报c实际值是4,z实际值是1024,只能高,不能低,此时延迟高达100多ms,没法玩。
这个最低值是在哪定的呢,之前还搜索了fluidsynth代码,当然只要试过在树莓派上使用相同软件源没有这个限制,就知道它不在那里,而在内核或alsa驱动模块上。

当我终于鼓气勇气开始对内核代码下手,居然很顺利找到,sound部分,soc那几个sunxi音频模块的初始化部分,periods_min的值,改了就好。

重新编译内核是个可怕的过程,要么去电脑上准备交叉编译链,要么在CB2上准备一天的时间。

archlinux sun7i的内核要从这里得到PKGBUILD和相关patch:

如果在CB上做,要放在硬盘处理,编译过程中的体积你懂的。
应用补丁:
  1. --- /sound/soc/sunxi/spdif/sunxi_spdma.c        2015-04-13 20:36:33.198665029 +0800
  2. +++ /sound/soc/sunxi/spdif/sunxi_spdma.c        2015-04-13 20:37:39.602619023 +0800
  3. @@ -48,9 +48,9 @@
  4.         .channels_min           = 1,
  5.         .channels_max           = 2,
  6.         .buffer_bytes_max       = 128*1024,  //1024*1024  /* value must be (2^n)Kbyte size */
  7. -       .period_bytes_min       = 1024*4,//1024*4,
  8. +       .period_bytes_min       = 256*2,//1024*4,
  9.         .period_bytes_max       = 1024*32,//1024*128,
  10. -       .periods_min            = 4,//8,
  11. +       .periods_min            = 2,//8,
  12.         .periods_max            = 8,//8,
  13.         .fifo_size              = 32,//32,
  14. };

  15. --- /sound/soc/sunxi/sunxi-codec.c      2015-04-15 13:57:55.860161879 +0800
  16. +++ /sound/soc/sunxi/sunxi-codec.c      2015-04-15 13:57:24.880159778 +0800
  17. @@ -110,9 +110,9 @@
  18.         .channels_min           = 1,
  19.         .channels_max           = 2,
  20.         .buffer_bytes_max       = 128*1024,//最大的缓冲区大小
  21. -       .period_bytes_min       = 1024*4,//最小周期大小
  22. +       .period_bytes_min       = 256*2,//最小周期大小
  23.         .period_bytes_max       = 1024*32,//最大周期大小
  24. -       .periods_min            = 4,//最小周期数
  25. +       .periods_min            = 2,//最小周期数
  26.         .periods_max            = 8,//最大周期数
  27.         .fifo_size              = 32,//fifo字节数
  28. };

  29. --- /sound/soc/sunxi/hdmiaudio/sunxi-hdmipcm.c  2015-04-15 13:59:46.240170026 +0800
  30. +++ /sound/soc/sunxi/hdmiaudio/sunxi-hdmipcm.c  2015-04-15 14:00:18.980172622 +0800
  31. @@ -48,9 +48,9 @@
  32.         .channels_min           = 1,
  33.         .channels_max           = 2,
  34.         .buffer_bytes_max       = 128*1024, /* value must be (2^n)Kbyte size */
  35. -       .period_bytes_min       = 1024*4,
  36. +       .period_bytes_min       = 256*2,
  37.         .period_bytes_max       = 1024*32,
  38. -       .periods_min            = 4,
  39. +       .periods_min            = 2,
  40.         .periods_max            = 8,
  41.         .fifo_size              = 128,
  42. };
复制代码
把补丁放在PKGBUILD同级目录里,并修改PKGBUILD,在prepare阶段增加
  1. git apply ../../sunxi_periods.patch
复制代码
开始 makepkg,缺啥补啥,然后睡觉,第二天验收结果,替换内核。

后来发现,只有spdif的相关模块是独立的,而我刚好用自制光纤输出,也就是只编译替换sound/soc/sunxi/spdif/sunxi_spdma 这一个模块,就够了。T_T

单独编译sunxi-spdif模块,在可以make的目录下:
make SUBDIRS=./sound/soc/sunxi/spdif modules

然后fluidsynth测试一下:
  1. fluidsynth -aalsa -c2 -z256 -g1 -o synth.cpu-cores=2 -o synth.polyphony=32 -o audio.alsa.device=hw:1,0 "%sf2路径%"
复制代码
同样的参数,2pi已经破音了,而CB2还很正常,看来我改的参数还是涉嫌保守。不过这样已经让延迟低至12ms以内了,如果还能安全的更低一倍,这玩艺就可以笑傲江湖了。值得入手未来的CB5试试?


回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-19 20:20:26 | 显示全部楼层
本帖最后由 kappa8086 于 2015-4-19 21:22 编辑

sec-3: Udev自动连接

折腾的哲学是为了不折腾。为了能让CB2看起来更像一个合成器,不能每次手动启动 fluidsynth 再 aconnect 连接键盘。话说这本来是艺术家的事,还敢再Geek一点吗?-_-
目标是,让折腾的结果更产品化,随插随用。{:soso_e185:}

开始思路很简单,
一,启动fluidsynth,加 & 后台化(否则卡住脚本),
二,等待一定时间让fluidsynth完成初始化(加载sf2),
三,aconnect找出对应的MIDI端口并连接之。
退出比较简单,只需要killall fluidsynth即可。
写个脚本让udev启用fluidsynth,开始觉得很简单的一件事,却足足折腾了我两个晚上。

先遇到的问题是,add规则能顺利执行,remove却不执行,或很久才执行,后来知道是被udev强制干掉了,因为fluidsynth的启动脚本卡住了udev系统。
于是深入研究了下,udev是不能用于启用daemon类程序的,udev执行它会一直等待进程树退出,哪怕用了后台化手段,在shell下没问题,但udev下就会变成僵尸进程;最后被杀掉时也是一个不留。

按照网上的建议用systemd的服务单元来处理,用udev规则里用systemctl启动它,这样进程会挂到systemd上去,不会卡住udev了。然而同样的脚本当service启动的结果和之前相反,几乎立刻就退出了,fluidsynth一样被杀死。
究其原因,systemd监控的脚本也不能daemon化,意味着fluidsynth不允许被& 后台化处理。

那么脚本的后台部分就没法在同一脚本里执行了,必须要拆出来另找出路。好在,service文件有适用的句法,用ExecStartPost。

两个脚本,一用来运行fluidsynth,一用来连接设备。

[~/bin/fluid-start.sh]
  1. #!/bin/bash

  2. sf_path="${HOME}/midi/synthesizer.sf2"
  3. init_path="${HOME}/midi/synthinit.cmd"
  4. midi_device=$1
  5. alsa_device="hw:1,0"

  6. fluidsynth -si -aalsa -c2 -z256 -g1 -p"fluid${midi_device}" -o synth.cpu-cores=2 -o synth.polyphony=32 -o audio.alsa.device=${alsa_device} -f"${init_path}" "${sf_path}"

复制代码

[~/bin/midi-connect.sh]
  1. #!/bin/bash

  2. midi_device=$1
  3. inport=`aconnect -i | sed -n "s/^client \([0-9]\+\)\: .*/\1/p" | tail -1`  #最后一个MIDI输入
  4. outport=""
  5. timeout=10      #等待fluidsynth加载完成
  6. while [ "$outport" == "" -a $timeout -gt 0 ]; do
  7.   outport=`aconnect -o | sed -n "s/^client \([0-9]\+\)\: 'fluid${midi_device}'.*/\1/p"`
  8.   sleep 1
  9.   let timeout--
  10. done
  11. if [  "$inport" != "" -a "$inport" != "0" -a "$outport" != "" ]; then
  12.   aconnect $inport $outport
  13. fi
复制代码

然后写一个service文件
[/etc/systemd/system/handle_midi_service@.service]
  1. [Unit]
  2. Description=Fluidsynth daemon to handling midi keyboard playing

  3. [Service]
  4. User=audioman
  5. LimitRTPRIO=infinity
  6. LimitMEMLOCK=infinity
  7. Type=simple
  8. ExecStart=/home/audioman/bin/fluid-start.sh %i
  9. ExecStartPost=/home/audioman/bin/midi-connect.sh %i
复制代码

最后是udev规则
[/etc/udev/rules.d/90-usbmidi-kb.rules]
  1. ACTION=="add", SUBSYSTEM=="sound", ENV{DEVNAME}=="/dev/snd/midi*", RUN+="/usr/bin/systemctl --no-block start handle_midi_device@$env{DEVNAME}"
  2. ACTION=="remove", SUBSYSTEM=="sound", ENV{DEVNAME}=="/dev/snd/midi*", RUN+="/usr/bin/systemctl --no-block stop handle_midi_device@$env{DEVNAME}"
复制代码

为了支持多个usb midi设备同时使用不冲突,service的一个 @ 充分表达了本LZ先天下之忧而蛋疼的情怀-OvO-。
OK,现在已经可以做到每天只需要打开琴的开关就能快乐的弹了,LZ先去弹10分钟先{:soso_e129:}




改进的余地还有很多,比如怎么选择ALSA PCM设备。如果有同时播放声音的需要,需要用到混音端口,代价肯定是提高延迟,所以需要条件判断以作决策。

这一点已经超过了折腾的初衷,但因为实际用下来确实有需要,因此,又有了更蛋疼的sec-4。


回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-19 20:46:30 | 显示全部楼层
sec-4: 边听边弹

这里要返回来赞下莓2,他的声音硬件不怎么样,soc却支持硬件8通道混音,而CB2不行。
否则,就不用这篇了。

混音不是为了让CB2既当合成器,又兼职混音器,是不得以而为之。所以除了明确要求,平常都是绕过的,需要能够外围控制是否使用。

fluidsynth推荐的服务是jackd,而且还是做为依赖强制安装的,它当然适用于这种情况,当midi键盘连接时会启动jack,只要用mpd的客户端选择jack输出就好了。同时,它标榜低延迟,是不是能做到(最好是在不改内核的情况下),我还没能证明。
因为,fluidsynth连上它根本就不出声!
  arch下这个版本的jackd(0.124.1,或fluidsynth的jack插件)绝对有问题,不只是CB2,连电脑上的也一样,不出声罪一,频繁崩溃罪二,无论如何都抱怨realtime权限问题不给-r不工作罪三。
  整个一BUG合集。
  为折腾jack,所有音频后台,以及mpd都迁移了用户,结果白忙。

pulseaudio则表现是极不稳定,fluidsynth在上面需要热身很长时间声音才正常,但还不能离开太久,因为不知道为什么无声休眠这种多余的事,在spdif输出上好像禁止不了;加上各种给桌面用户设计的参数和session环境依赖,不太适合这种headless场合。

最后,反倒最稳定的方案是之前避之不及的ALSA dmix。

给fluidsynth服务运行的用户的.asoundrc加上dmix配置。
我的整个alsa配置是这样的:
  1. pcm.spdif_out {
  2.     type hw
  3.     card sunxisndspdif
  4. }

  5. ctl.spdif_out {
  6.     type hw
  7.     card sunxisndspdif
  8. }

  9. pcm.analog_out {
  10.     type hw
  11.     card sunxicodec
  12. }

  13. ctl.analog_out {
  14.     type hw
  15.     card sunxicodec
  16. }

  17. pcm.analog_in=analog_out

  18. pcm.mix44100l {
  19.     type dmix
  20.     ipc_key 1025
  21.     ipc_key_add_uid false
  22.     ipc_perm 0666
  23.     slave {
  24.         pcm spdif_out
  25.         period_size 256
  26.         buffer_size 512
  27.         periods 2
  28.         rate 44100
  29.     }
  30.     bindings {
  31.         0 0
  32.         1 1
  33.     }
  34. }

  35. pcm.mix_out = mix44100l
复制代码
给mpd也配置上这个输出,必要时选择,最好在audio_output列表中保持固定位置。
  1. audio_output {
  2.           type            "alsa"
  3.           name            "Mix"
  4.           device          "mix_out"
  5.           mixer_type      "software"
  6. }
复制代码
计划是这样,在单独听音乐或弹琴时,都倾向于独占设备,而一旦mpd客户端选择了dmix的输出端口,fluid启动脚本也指定使用它。
判断mpd的选择很容易,用mpc outputs的结果处理即可,只是先要约定使用第几个output(所以要求固定位置)。

增加设备选择脚本 select_alsa_device.sh
  1. #!/bin/bash

  2. mpd_mixout_num=2
  3. alsa_devices=("spdif_out" "mix_out")

  4. mpd_mixout=`mpc outputs | grep "Output ${mpd_mixout_num} (.*) is enabled"`

  5. if [ "$mpd_mixout" != "" ]; then
  6.   alsa_device="${alsa_devices[1]}"
  7. else
  8.   alsa_device="${alsa_devices[0]}"
  9. fi

  10. echo $alsa_device
复制代码
然后把上面脚本中的asla_device变量换为脚本输出结果即可。


回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-19 21:07:43 | 显示全部楼层
本帖最后由 kappa8086 于 2015-4-19 21:12 编辑

sec-4.1: 边看边弹

有道是蛋一疼就根本停不下来,边听边弹做到了,想边看边弹呢?说到底,盒子一打,计算设备一堆,听音设备只有一个,光切换就停不下来了。
在不增加任何附属设备的情况下,CB还有个LineIn可以动动脑筋。

我这用来看的设备是个小米盒子,把它调成模拟输出,再插一个2.5转3.5音频线就OK了。
启动loopback很简单。
  1. arecord -r44100 -c2 -D analog_in -f cd | aplay -f cd -D mix_out
复制代码
如果无声的话,开alsamixer把那个奇怪的ADC Input Mux拉到0,另外Linein Pre-AMP也最好拉到0,否则也会有受不了的底噪。

真正蛋疼的问题是,谁来启动它。很显然不能让这个管道一直启用着。
曾考虑过做个GPIO开关,不过这主意既不好折腾也不够fashion。最好是写个web控制端,考虑到执行身份的问题,用python+Flask单独实现一个服务器,并包装为systemd service unit,自动启动。

Flask这东东很不错,几十行之间就能实现一个web功能和服务器,还包括html代码。
  1. #!/bin/python
  2. # 混音WEB控制台脚本,需要 Flask
  3. # python-pip => pip install Flask

  4. from flask import Flask,redirect
  5. import os,subprocess

  6. app = Flask(__name__)

  7. pagetpl = """<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
  8. <html>
  9. <head>
  10. <meta name="viewport" content="width=256; initial-scale=1.5; target-densitydpi=device_dpi" />
  11. <title>Mix control</title><style>.on{border-style:inset;}button{height:50px;width:200px;}</style></head>
  12. <body>
  13. <form action="toggle_av" method="post"><button class="%s">Connect AV input</button></form>
  14. </body>
  15. </html>
  16. """

  17. avcproc=None

  18. @app.route('/')
  19. def index():
  20.     global avcproc,pagetpl
  21.     avbtn_class = "on" if avcproc != None else "off"
  22.     result = pagetpl % (avbtn_class,)
  23.     return result

  24. @app.route('/toggle_av', methods=['GET', 'POST'])
  25. def toggle_av():
  26.     global avcproc
  27.     if avcproc != None:
  28.         os.system('pkill -P'+str(avcproc.pid))
  29.         os.system('kill '+str(avcproc.pid))
  30.         avcproc = None
  31.     else:
  32.         avcproc = subprocess.Popen(['~/bin/avconnect.sh'], shell=True)
  33.     return redirect('/')

  34. if __name__ == '__main__':
  35.     app.run(host= '0.0.0.0')
复制代码
运行之,手机浏览打开 http://IP of CB2:5000/

这个页面过于简单,只有一个按钮,用来开关Linein输入监听。
如果必要的话,可以把混音控制也做进来,代替mpc的输出作为条件;fluidsynth的音库切换则是另一个值得控制的目标。

评分

参与人数 1威望 +15 金钱 +15 贡献 +15 收起 理由
sunbeyond + 15 + 15 + 15

查看全部评分

回复 支持 反对

使用道具 举报

发表于 2015-4-20 10:15:52 | 显示全部楼层
叼炸天了。  楼主有没去了解商业用的。 有多大差距。{:soso_e100:}
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-20 11:51:01 | 显示全部楼层
sunbeyond 发表于 2015-4-20 10:15
叼炸天了。  楼主有没去了解商业用的。 有多大差距。

商用的MIDI合成器么?其实了解不多,只知道淘宝上就能找到两档,三四百档(好像就一个MidiPlus那两款),和三四千档。后者不说了。。。买不动嘛。前者音色什么的肯定不用多指望,按一般经验,它能有64MB GM音色我就跪拜了,还不能换的;复音数64硬件复音,刚好是在CB2+fluidsynth能力之内;就延迟不好说,0延迟只是个广告词不是参数,而我的手速还体会不出20ms和10ms之间的差别
优点是作为一个消费级产品,切换音色的操作反馈更直观一点,同时带电池,适合配耳机用。
回复 支持 反对

使用道具 举报

 楼主| 发表于 2015-4-21 12:50:48 | 显示全部楼层
似乎放错版了,DIY作品展示区更好一点
回复 支持 反对

使用道具 举报

发表于 2015-4-22 09:59:52 | 显示全部楼层
kappa8086 发表于 2015-4-21 12:50
似乎放错版了,DIY作品展示区更好一点

再来几张图就更好了。   要不贴个视频
回复 支持 反对

使用道具 举报

发表于 2016-1-21 15:21:11 | 显示全部楼层
提示: 作者被禁止或删除 内容自动屏蔽
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|粤ICP备13051116号|cubie.cc---深刻的嵌入式技术讨论社区

GMT+8, 2024-3-28 20:55 , Processed in 0.030290 second(s), 16 queries .

Powered by Discuz! X3.4

© 2001-2012 Comsenz Inc. | Style by Coxxs

返回顶部