kappa8086 发表于 2015-4-19 19:46:27

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

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驱动编译参数太保守!


kappa8086 发表于 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:https://github.com/archlinuxarm/PKGBUILDs/tree/master/core/linux-sun7i
如果在CB上做,要放在硬盘处理,编译过程中的体积你懂的。 应用补丁:--- /sound/soc/sunxi/spdif/sunxi_spdma.c      2015-04-13 20:36:33.198665029 +0800
+++ /sound/soc/sunxi/spdif/sunxi_spdma.c      2015-04-13 20:37:39.602619023 +0800
@@ -48,9 +48,9 @@
      .channels_min         = 1,
      .channels_max         = 2,
      .buffer_bytes_max       = 128*1024,//1024*1024/* value must be (2^n)Kbyte size */
-       .period_bytes_min       = 1024*4,//1024*4,
+       .period_bytes_min       = 256*2,//1024*4,
      .period_bytes_max       = 1024*32,//1024*128,
-       .periods_min            = 4,//8,
+       .periods_min            = 2,//8,
      .periods_max            = 8,//8,
      .fifo_size            = 32,//32,
};

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

--- /sound/soc/sunxi/hdmiaudio/sunxi-hdmipcm.c2015-04-15 13:59:46.240170026 +0800
+++ /sound/soc/sunxi/hdmiaudio/sunxi-hdmipcm.c2015-04-15 14:00:18.980172622 +0800
@@ -48,9 +48,9 @@
      .channels_min         = 1,
      .channels_max         = 2,
      .buffer_bytes_max       = 128*1024, /* value must be (2^n)Kbyte size */
-       .period_bytes_min       = 1024*4,
+       .period_bytes_min       = 256*2,
      .period_bytes_max       = 1024*32,
-       .periods_min            = 4,
+       .periods_min            = 2,
      .periods_max            = 8,
      .fifo_size            = 128,
};
把补丁放在PKGBUILD同级目录里,并修改PKGBUILD,在prepare阶段增加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测试一下: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试试?


kappa8086 发表于 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]#!/bin/bash

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

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]#!/bin/bash

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

然后写一个service文件

Description=Fluidsynth daemon to handling midi keyboard playing


User=audioman
LimitRTPRIO=infinity
LimitMEMLOCK=infinity
Type=simple
ExecStart=/home/audioman/bin/fluid-start.sh %i
ExecStartPost=/home/audioman/bin/midi-connect.sh %i
最后是udev规则
ACTION=="add", SUBSYSTEM=="sound", ENV{DEVNAME}=="/dev/snd/midi*", RUN+="/usr/bin/systemctl --no-block start handle_midi_device@$env{DEVNAME}"
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。


kappa8086 发表于 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配置是这样的:
pcm.spdif_out {
    type hw
    card sunxisndspdif
}

ctl.spdif_out {
    type hw
    card sunxisndspdif
}

pcm.analog_out {
    type hw
    card sunxicodec
}

ctl.analog_out {
    type hw
    card sunxicodec
}

pcm.analog_in=analog_out

pcm.mix44100l {
    type dmix
    ipc_key 1025
    ipc_key_add_uid false
    ipc_perm 0666
    slave {
      pcm spdif_out
      period_size 256
      buffer_size 512
      periods 2
      rate 44100
    }
    bindings {
      0 0
      1 1
    }
}

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

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

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

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

if [ "$mpd_mixout" != "" ]; then
alsa_device="${alsa_devices}"
else
alsa_device="${alsa_devices}"
fi

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


kappa8086 发表于 2015-4-19 21:07:43

本帖最后由 kappa8086 于 2015-4-19 21:12 编辑

sec-4.1: 边看边弹

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

我这用来看的设备是个小米盒子,把它调成模拟输出,再插一个2.5转3.5音频线就OK了。
启动loopback很简单。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代码。#!/bin/python
# 混音WEB控制台脚本,需要 Flask
# python-pip => pip install Flask

from flask import Flask,redirect
import os,subprocess

app = Flask(__name__)

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

avcproc=None

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

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

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

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

sunbeyond 发表于 2015-4-20 10:15:52

叼炸天了。楼主有没去了解商业用的。 有多大差距。{:soso_e100:}

kappa8086 发表于 2015-4-20 11:51:01

sunbeyond 发表于 2015-4-20 10:15 static/image/common/back.gif
叼炸天了。楼主有没去了解商业用的。 有多大差距。

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

kappa8086 发表于 2015-4-21 12:50:48

似乎放错版了,DIY作品展示区更好一点

sunbeyond 发表于 2015-4-22 09:59:52

kappa8086 发表于 2015-4-21 12:50 static/image/common/back.gif
似乎放错版了,DIY作品展示区更好一点

再来几张图就更好了。   要不贴个视频:lol

逗比男神i 发表于 2016-1-21 15:21:11

页: [1]
查看完整版本: MIDI实时合成器+混音折腾全记录