Linux 声音编程教程(五)
协议:CC BY-NC-SA 4.0
十四、LADSPA
Linux Audio Plug-Ins (LADSPA)是一组插件,应用可以使用它们来添加延迟和滤镜等效果。它的设计考虑到了简单性,所以只能产生有限的效果。然而,这些可以是相当广泛的,并足以用于各种各样的应用。
资源
以下是一些资源:
- “Linux 音频插件:LADSPA 研究”(
www.linuxdevcenter.com/pub/a/linux/2001/02/02/ladspa.html) - Linux 音频开发者的简单插件 API (
www.ladspa.org/)
用户级工具
LADSPA 插件位于默认为/usr/lib/ladspa的目录中。这可以通过环境变量LADSPA_PATH来控制。这个目录将包含一组作为 LADSPA 插件的.so文件。
每个插件都包含关于自身的信息,您可以通过运行命令行工具listplugins来检查插件集。通过只安装 LADPSA,缺省插件如下:
/usr/lib/ladspa/amp.so:
Mono Amplifier (1048/amp_mono)
Stereo Amplifier (1049/amp_stereo)
/usr/lib/ladspa/delay.so:
Simple Delay Line (1043/delay_5s)
/usr/lib/ladspa/filter.so:
Simple Low Pass Filter (1041/lpf)
Simple High Pass Filter (1042/hpf)
/usr/lib/ladspa/sine.so:
Sine Oscillator (Freq:audio, Amp:audio) (1044/sine_faaa)
Sine Oscillator (Freq:audio, Amp:control) (1045/sine_faac)
Sine Oscillator (Freq:control, Amp:audio) (1046/sine_fcaa)
Sine Oscillator (Freq:control, Amp:control) (1047/sine_fcac)
/usr/lib/ladspa/noise.so:
White Noise Source (1050/noise_white)
您可以从工具analyseplugin中找到关于每个插件的更多详细信息。例如,下面是amp插件的信息:
$analyseplugin amp
Plugin Name: "Mono Amplifier"
Plugin Label: "amp_mono"
Plugin Unique ID: 1048
Maker: "Richard Furse (LADSPA example plugins)"
Copyright: "None"
Must Run Real-Time: No
Has activate() Function: No
Has deactivate() Function: No
Has run_adding() Function: No
Environment: Normal or Hard Real-Time
Ports: "Gain" input, control, 0 to ..., default 1, logarithmic
"Input" input, audio
"Output" output, audio
Plugin Name: "Stereo Amplifier"
Plugin Label: "amp_stereo"
Plugin Unique ID: 1049
Maker: "Richard Furse (LADSPA example plugins)"
Copyright: "None"
Must Run Real-Time: No
Has activate() Function: No
Has deactivate() Function: No
Has run_adding() Function: No
Environment: Normal or Hard Real-Time
Ports: "Gain" input, control, 0 to ..., default 1, logarithmic
"Input (Left)" input, audio
"Output (Left)" output, audio
"Input (Right)" input, audio
"Output (Right)" output, audio
可以使用applyplugin对每个插件进行简单的测试。当不带参数运行时,它会给出一条用法消息。
$applyplugin
Usage: applyplugin [flags] <input Wave file> <output Wave file>
<LADSPA plugin file name> <plugin label> <Control1> <Control2>...
[<LADSPA plugin file name> <plugin label> <Control1> <Control2>...]...
Flags: -s<seconds> Add seconds of silence after end of input file.
这将输入和输出 WAV 文件作为第一个和第二个参数。接下来是.so文件的名称和选择的插件标签。接下来是控件的值。对于amp插件,文件名为amp.so,立体声插件为amp_stereo,增益只有一个控制,取值范围为 0-1。要将包含立体声 WAV 数据的文件的音量减半,请使用:
applyplugin 54154.wav tmp.wav amp.so amp_stereo 0.5
LADSPA_Descriptor 类型
应用和 LADSPA 插件之间的通信通过类型为LADSPA_Descriptor的数据结构进行。它的字段包含了由listplugins和analyseplugins显示的所有信息。此外,它还包含控制内存布局、是否支持硬实时等等的字段。
unsigned long UniqueID
- 每个插件在 LADSPA 系统中必须有一个唯一的 ID。
const char * Label
- 这是用于指代 LADSPA 系统内插件的标签。
const char * Name
- 这是插件的“用户友好”名称。例如,
amp文件(稍后显示)包含两个插件。单声道放大器的 ID 为 1048,标签为amp_mono,命名为单声道放大器,而立体声放大器的 ID 为 1049,标签为amp_stereo,命名为立体声放大器。
const char * Maker, * Copyright
- 这应该很明显。
unsigned long PortCount
- 这表示插件上存在的端口(输入和输出)的数量。
const``LADSPA_PortDescriptor
- 这个成员表示一个端口描述符数组。有效索引从
0到PortCount-1不等。
const char * const * PortNames
- 此成员指示描述端口的空终止字符串数组。例如,单声道放大器有两个输入端口和一个输出端口,分别标记为增益、输入和输出。输入端口有端口描述符
(LADSPA_PORT_INPUT | LADSPA_PORT_AUDIO),而输出端口有端口描述符(LADSPA_PORT_OUTPUT | LADSPA_PORT_AUDIO)
LADSPA_PortRangeHint * PortRangeHints
- 这是一个类型为
LADSPA_PortRangeHint的数组,每个端口一个元素。这允许插件传递信息,比如它是否有一个上界或下界的值,如果有,这个上界是什么,它是否应该被当作一个布尔值,等等。比方说,GUI 可以使用这些提示为插件提供可视化的控制显示。
此外,它还包含作为函数指针的字段,LADSPA 运行时调用这些字段来初始化插件、处理数据和清理。这些字段如下:
instantiate
- 这将采样速率作为一个参数。它负责插件的一般实例化、设置本地参数、分配内存等等。它返回一个指向特定于插件的数据结构的指针,该数据结构包含与该插件相关的所有信息。这个指针将作为第一个参数传递给其他函数,以便它们可以检索这个插件的信息。
connect_port
- 这需要三个参数,第二个和第三个分别是端口号和数据可读/可写的地址。对于每个端口,插件只能使用该地址从 LADSPA 运行时读取/写入数据。它将在
run或run_adding之前被调用。
activate/deactivate
- 可以调用这些函数来重新初始化插件状态。他们可能是
NULL。
run
- 这个函数是插件完成所有实际工作的地方。它的第二个参数是准备好读/写的样本数。
cleanup
- 这是显而易见的。
其他功能字段通常设置为NULL。
加载插件
应用可以通过用一个参数调用loadLADSPAPluginLibrary来加载插件,这个参数是插件文件的名称。请注意,没有 LADSPA 库。LADPSA 提供了一个名为ladspa.h的头文件,发行版可能包含一个文件load.c,它实现了loadLADSPAPluginLibrary(它搜索LADSPA_PATH中的目录)。
当插件被dlopen加载时,函数_init被无参数调用。这可能用于设置插件和构建,例如,LADSPA_Descriptor。
DLL 必须有一个可以挂接的入口点。对于 LADSPA,每个插件必须定义一个函数LADSPA_Descriptor * ladspa_descriptor(unsigned long Index)。索引 0、1…的值是文件中包含的每个插件的LADSPA_Descriptor值。
单声道放大器客户端
analyseplugin amp命令显示amp插件包含两个插件模块:一个单声道和一个立体声插件。单声道插件有一个插件标签amp_mono,对应一个LADSPA_Descriptor的Label字段。
使用这个插件意味着您必须加载插件文件,获得一个ladspa_descriptor结构的句柄,然后浏览描述符,检查标签,直到找到amp_mono插件。
加载插件文件是通过 LADSPA 包中的load.c程序中的函数来完成的。相关代码如下:
char *pcPluginFilename = "amp.so";
void *pvPluginHandle = loadLADSPAPluginLibrary(pcPluginFilename);
dlerror();
pfDescriptorFunction
= (LADSPA_Descriptor_Function)dlsym(pvPluginHandle, "ladspa_descriptor");
if (!pfDescriptorFunction) {
const char * pcError = dlerror();
if (pcError)
fprintf(stderr,
"Unable to find ladspa_descriptor() function in plugin file "
"\"%s\": %s.\n"
"Are you sure this is a LADSPA plugin file?\n",
pcPluginFilename,
pcError);
return 1;
}
加载后,搜索amp_mono插件:
char *pcPluginLabel = "amp_mono";
for (lPluginIndex = 0;; lPluginIndex++) {
psDescriptor = pfDescriptorFunction(lPluginIndex);
if (!psDescriptor)
break;
if (pcPluginLabel != NULL) {
if (strcmp(pcPluginLabel, psDescriptor->Label) != 0)
continue;
}
// got mono_amp
您知道有三个端口——控制、输入和输出——所以您查看端口列表来分配索引并将相关数组连接到插件描述符。
隐藏在这里的是一个关键部分:您不仅要设置插件的输入和输出,还要设置控制机制。analyseplugin报告显示有一个带控制的增益端口。这需要输入。控制端口只需要一个浮点值的地址,这是将要发生的放大量。这是通过以下代码完成的:
handle = psDescriptor->instantiate(psDescriptor, SAMPLE_RATE);
if (handle == NULL) {
fprintf(stderr, "Can't instantiate plugin %s\n", pcPluginLabel);
exit(1);
}
// get ports
int lPortIndex;
printf("Num ports %lu\n", psDescriptor->PortCount);
for (lPortIndex = 0;
lPortIndex < psDescriptor->PortCount;
lPortIndex++) {
if (LADSPA_IS_PORT_INPUT
(psDescriptor->PortDescriptors[lPortIndex])
&& LADSPA_IS_PORT_AUDIO
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("input %d\n", lPortIndex);
lInputPortIndex = lPortIndex;
psDescriptor->connect_port(handle,
lInputPortIndex, pInBuffer);
} else if (LADSPA_IS_PORT_OUTPUT
(psDescriptor->PortDescriptors[lPortIndex])
&& LADSPA_IS_PORT_AUDIO
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("output %d\n", lPortIndex);
lOutputPortIndex = lPortIndex;
psDescriptor->connect_port(handle,
lOutputPortIndex, pOutBuffer);
}
if (LADSPA_IS_PORT_CONTROL
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("control %d\n", lPortIndex);
LADSPA_Data control = 0.5f; // here is where we say to halve the volume
psDescriptor->connect_port(handle,
lPortIndex, &control);
}
}
// we've got what we wanted
然后,run_plugin函数开始循环,从输入文件中读取样本,应用插件的run函数,并写入输出文件。
void run_plugin() {
sf_count_t numread;
open_files();
// it's NULL for the amp plugin
if (psDescriptor->activate != NULL)
psDescriptor->activate(handle);
while ((numread = fill_input_buffer()) > 0) {
printf("Num read %d\n", numread);
psDescriptor->run(handle, numread);
empty_output_buffer(numread);
}
}
我已经使用了 libsndfile 库,通过使用fill_input_buffer和empty_output_buffer来简化任何格式的文件的读写。
完整的程序称为mono_amp.c,如下所示:
#include <stdlib.h>
#include <stdio.h>
#include <ladspa.h>
#include <dlfcn.h>
#include <sndfile.h>
#include "utils.h"
const LADSPA_Descriptor * psDescriptor;
LADSPA_Descriptor_Function pfDescriptorFunction;
LADSPA_Handle handle;
// choose the mono plugin from the amp file
char *pcPluginFilename = "amp.so";
char *pcPluginLabel = "amp_mono";
long lInputPortIndex = -1;
long lOutputPortIndex = -1;
SNDFILE* pInFile;
SNDFILE* pOutFile;
// for the amplifier, the sample rate doesn't really matter
#define SAMPLE_RATE 44100
// the buffer size isn't really important either
#define BUF_SIZE 2048
LADSPA_Data pInBuffer[BUF_SIZE];
LADSPA_Data pOutBuffer[BUF_SIZE];
// How much we are amplifying the sound by
LADSPA_Data control = 0.5f;
char *pInFilePath = "/home/local/antialize-wkhtmltopdf-7cb5810/scripts/static-build/linux-local/qts/demos/mobile/quickhit/plugins/LevelTemplate/sound/enableship.wav";
char *pOutFilePath = "tmp.wav";
void open_files() {
// using libsndfile functions for easy read/write
SF_INFO sfinfo;
sfinfo.format = 0;
pInFile = sf_open(pInFilePath, SFM_READ, &sfinfo);
if (pInFile == NULL) {
perror("can't open input file");
exit(1);
}
pOutFile = sf_open(pOutFilePath, SFM_WRITE, &sfinfo);
if (pOutFile == NULL) {
perror("can't open output file");
exit(1);
}
}
sf_count_t fill_input_buffer() {
return sf_read_float(pInFile, pInBuffer, BUF_SIZE);
}
void empty_output_buffer(sf_count_t numread) {
sf_write_float(pOutFile, pOutBuff
er, numread);
}
void run_plugin() {
sf_count_t numread;
open_files();
// it's NULL for the amp plugin
if (psDescriptor->activate != NULL)
psDescriptor->activate(handle);
while ((numread = fill_input_buffer()) > 0) {
printf("Num read %d\n", numread);
psDescriptor->run(handle, numread);
empty_output_buffer(numread);
}
}
int main(int argc, char *argv[]) {
int lPluginIndex;
void *pvPluginHandle = loadLADSPAPluginLibrary(pcPluginFilename);
dlerror();
pfDescriptorFunction
= (LADSPA_Descriptor_Function)dlsym(pvPluginHandle, "ladspa_descriptor");
if (!pfDescriptorFunction) {
const char * pcError = dlerror();
if (pcError)
fprintf(stderr,
"Unable to find ladspa_descriptor() function in plugin file "
"\"%s\": %s.\n"
"Are you sure this is a LADSPA plugin file?\n",
pcPluginFilename,
pcError);
return 1;
}
for (lPluginIndex = 0;; lPluginIndex++) {
psDescriptor = pfDescriptorFunction(lPluginIndex);
if (!psDescriptor)
break;
if (pcPluginLabel != NULL) {
if (strcmp(pcPluginLabel, psDescriptor->Label) != 0)
continue;
}
// got mono_amp
handle = psDescriptor->instantiate(psDescriptor, SAMPLE_RATE);
if (handle == NULL) {
fprintf(stderr, "Can't instantiate plugin %s\n", pcPluginLabel);
exit(1);
}
// get ports
int lPortIndex;
printf("Num ports %lu\n", psDescriptor->PortCount);
for (lPortIndex = 0;
lPortIndex < psDescriptor->PortCount;
lPortIndex++) {
if (LADSPA_IS_PORT_INPUT
(psDescriptor->PortDescriptors[lPortIndex])
&& LADSPA_IS_PORT_AUDIO
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("input %d\n", lPortIndex);
lInputPortIndex = lPortIndex;
psDescriptor->connect_port(handle,
lInputPortIndex, pInBuffer);
} else if (LADSPA_IS_PORT_OUTPUT
(psDescriptor->PortDescriptors[lPortIndex])
&& LADSPA_IS_PORT_AUDIO
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("output %d\n", lPortIndex);
lOutputPortIndex = lPortIndex;
psDescriptor->connect_port(handle,
lOutputPortIndex, pOutBuffer);
}
if (LADSPA_IS_PORT_CONTROL
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("control %d\n", lPortIndex);
psDescriptor->connect_port(handle,
lPortIndex, &control);
}
}
// we've got what we wanted, get out of this loop
break;
}
if ((psDescriptor == NULL) ||
(lInputPortIndex == -1) ||
(lOutputPortIndex == -1)) {
fprintf(stderr, "Can't find plugin information\n");
exit(1);
}
run_plugin();
exit(0);
}
它只是通过调用mono_amp来运行,没有参数,因为输入和输出文件是硬编码到程序中的。
具有图形用户界面的立体声放大器
amp文件包含一个立体声放大器和一个单声道放大器。这导致了管理插件的几个差异。现在有两个输入端口和两个输出端口,但是仍然只有一个用于放大系数的控制端口。您需要一组输入端口和一组输出端口。这只是增加了一点复杂性。
主要区别在于处理流:libsndfile返回声音帧,立体声信号的两个声道交错。对于每个输入端口,这些必须被分离到单独的通道中,然后两个输出端口必须交错在一起。
添加像 GTK 这样的 GUI 相当简单。下面的代码只是显示了一个滑块来控制音量。GUI 代码和 LADSPA 代码显然必须在不同的(POSIX)线程中运行。实际上只有一个棘手的问题:在执行run函数的过程中,控制值不应该改变。这可以用锁来保护,但是在这种情况下,这太重了:只需保存一份被滑块修改过的控件的副本,并在每次调用run之前把它带过来。
代码是使用 GTK v3 编写的,如下所示:
#include <gtk/gtk.h>
#include <stdlib.h>
#include <stdio.h>
#include <ladspa.h>
#include <dlfcn.h>
#include <sndfile.h>
#include "utils.h"
gint count = 0;
char buf[5];
pthread_t ladspa_thread;
const LADSPA_Descriptor * psDescriptor;
LADSPA_Descriptor_Function pfDescriptorFunction;
LADSPA_Handle handle;
// choose the mono plugin from the amp file
char *pcPluginFilename = "amp.so";
char *pcPluginLabel = "amp_stereo";
long lInputPortIndex = -1;
long lOutputPortIndex = -1;
int inBufferIndex = 0;
int outBufferIndex = 0;
SNDFILE* pInFile;
SNDFILE* pOutFile;
// for the amplifier, the sample rate doesn't really matter
#define SAMPLE_RATE 44100
// the buffer size isn't really important either
#define BUF_SIZE 2048
LADSPA_Data pInStereoBuffer[2*BUF_SIZE];
LADSPA_Data pOutStereoBuffer[2*BUF_SIZE];
LADSPA_Data pInBuffer[2][BUF_SIZE];
LADSPA_Data pOutBuffer[2][BUF_SIZE];
// How much we are amplifying the sound by
// We aren't allowed to change the control values
// during execution of run(). We could put a lock
// around run() or simpler, change the value of
// control only outside of run()
LADSPA_Data control;
LADSPA_Data pre_control = 0.2f;
char *pInFilePath = "/home/newmarch/Music/karaoke/nights/nightsinwhite-0.wav";
char *pOutFilePath = "tmp.wav";
void open_files() {
// using libsndfile functions for easy read/write
SF_INFO sfinfo;
sfinfo.format = 0;
pInFile = sf_open(pInFilePath, SFM_READ, &sfinfo);
if (pInFile == NULL) {
perror("can't open input file");
exit(1);
}
pOutFile = sf_open(pOutFilePath, SFM_WRITE, &sfinfo);
if (pOutFile == NULL) {
perror("can't open output file");
exit(1);
}
}
sf_count_t fill_input_buffer() {
int numread = sf_read_float(pInFile, pInStereoBuffer, 2*BUF_SIZE);
// split frames into samples for each channel
int n;
for (n = 0; n < numread; n += 2) {
pInBuffer[0][n/2] = pInStereoBuffer[n];
pInBuffer[1][n/2] = pInStereoBuffer[n+1];
}
return numread/2;
}
void empty_output_buffer(sf_count_t numread) {
// combine output samples back into frames
int n;
for (n = 0; n < 2*numread; n += 2) {
pOutStereoBuffer[n] = pOutBuffer[0][n/2];
pOutStereoBuffer[n+1] = pOutBuffer[1][n/2];
}
sf_write_float(pOutFile, pOutStereoBuffer, 2*numread);
}
gpointer run_plugin(gpointer args) {
sf_count_t numread;
// it's NULL for the amp plugin
if (psDescriptor->activate != NULL)
psDescriptor->activate(handle);
while ((numread = fill_input_buffer()) > 0) {
// reset control outside of run()
control = pre_control;
psDescriptor->run(handle, numread);
empty_output_buffer(numread);
usleep(1000);
}
printf("Plugin finished!\n");
}
void setup_ladspa() {
int lPluginIndex;
void *pvPluginHandle = loadLADSPAPluginLibrary(pcPluginFilename);
dlerror();
pfDescriptorFunction
= (LADSPA_Descriptor_Function)dlsym(pvPluginHandle, "ladspa_descriptor");
if (!pfDescriptorFunction) {
const char * pcError = dlerror();
if (pcError)
fprintf(stderr,
"Unable to find ladspa_descriptor() function in plugin file "
"\"%s\": %s.\n"
"Are you sure this is a LADSPA plugin file?\n",
pcPluginFilename,
pcError);
exit(1);
}
for (lPluginIndex = 0;; lPluginIndex++) {
psDescriptor = pfDescriptorFunction(lPluginIndex);
if (!psDescriptor)
break;
if (pcPluginLabel != NULL) {
if (strcmp(pcPluginLabel, psDescriptor->Label) != 0)
continue;
}
// got stero_amp
handle = psDescriptor->instantiate(psDescriptor, SAMPLE_RATE);
if (handle == NULL) {
fprintf(stderr, "Can't instantiate plugin %s\n", pcPluginLabel);
exit(1);
}
// get ports
int lPortIndex;
printf("Num ports %lu\n", psDescriptor->PortCount);
for (lPortIndex = 0;
lPortIndex < psDescriptor->PortCount;
lPortIndex++) {
if (LADSPA_IS_PORT_AUDIO
(psDescriptor->PortDescriptors[lPortIndex])) {
if (LADSPA_IS_PORT_INPUT
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("input %d\n", lPortIndex);
lInputPortIndex = lPortIndex;
psDescriptor->connect_port(handle,
lInputPortIndex, pInBuffer[inBufferIndex++]);
} else if (LADSPA_IS_PORT_OUTPUT
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("output %d\n", lPortIndex);
lOutputPortIndex = lPortIndex;
psDescriptor->connect_port(handle,
lOutputPortIndex, pOutBuffer[outBufferIndex++]);
}
}
if (LADSPA_IS_PORT_CONTROL
(psDescriptor->PortDescriptors[lPortIndex])) {
printf("control %d\n", lPortIndex);
psDescriptor->connect_port(handle,
lPortIndex, &control);
}
}
// we've got what we wanted, get out of this loop
break;
}
if ((psDescriptor == NULL) ||
(lInputPortIndex == -1) ||
(lOutputPortIndex == -1)) {
fprintf(stderr, "Can't find plugin information\n");
exit(1);
}
open_files();
pthread_create(&ladspa_thread, NULL, run_plugin, NULL);
}
void slider_change(GtkAdjustment *adj, gpointer data)
{
count++;
pre_control = gtk_adjustment_get_value(adj);
//gtk_label_set_text(GTK_LABEL(label), buf);
}
int main(int argc, char** argv) {
//GtkWidget *label;
GtkWidget *window;
GtkWidget *frame;
GtkWidget *slider;
GtkAdjustment *adjustment;
setup_ladspa();
gtk_init(&argc, &argv);
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
gtk_window_set_default_size(GTK_WINDOW(window), 250, 80);
gtk_window_set_title(GTK_WINDOW(window), "Volume");
frame = gtk_fixed_new();
gtk_container_add(GTK_CONTAINER(window), frame);
adjustment = gtk_adjustment_new(1.0,
0.0,
2.0,
0.1,
1.0,
0.0);
slider = gtk_scale_new(GTK_ORIENTATION_HORIZONTAL,
adjustment);
gtk_widget_set_size_request(slider, 240, 5);
gtk_fixed_put(GTK_FIXED(frame), slider, 5, 20);
//label = gtk_label_new("0");
//gtk_fixed_put(GTK_FIXED(frame), label, 190, 58);
gtk_widget_show_all(window);
g_signal_connect(window, "destroy",
G_CALLBACK (gtk_main_quit), NULL);
g_signal_connect(adjustment, "value-changed",
G_CALLBACK(slider_change), NULL);
gtk_main();
return 0;
}
它只是通过调用stereo_amp来运行,没有参数。
amp 计划
您在最后两节中调用的程序是amp程序,它在 LADSPA 源代码的文件ladspa_sdk/src/plugins/amp.c中。如果您想自己编写一个 LADSPA 插件,或者想了解其中涉及的内容,这是值得研究的。有几个关键功能。
-
DLL 加载程序调用函数
_init()。它的作用主要是为每个插件组件设置一个LADSPA_Descriptor。这是长篇大论。它包括所有可由analyseplugin打印的信息,例如:g_psMonoDescriptor->Name = strdup("Mono Amplifier");它还包含内部函数指针,例如当单声道放大器需要做一些工作时运行的函数。
g_psMonoDescriptor->run = runMonoAmplifier; -
卸载插件时,调用函数
_fini()来清理所有数据。
插件的核心是在处理样本时对样本做什么。输入样本包含在一个缓冲器中,输出样本包含在另一个缓冲器中,对于单声道放大器,每个输入样本都需要乘以增益系数才能得到输出样本。代码如下:
void
runMonoAmplifier(LADSPA_Handle Instance,
unsigned long SampleCount) {
LADSPA_Data * pfInput;
LADSPA_Data * pfOutput;
LADSPA_Data fGain;
Amplifier * psAmplifier;
unsigned long lSampleIndex;
psAmplifier = (Amplifier *)Instance;
pfInput = psAmplifier->m_pfInputBuffer1;
pfOutput = psAmplifier->m_pfOutputBuffer1;
fGain = *(psAmplifier->m_pfControlValue);
for (lSampleIndex = 0; lSampleIndex *lt; SampleCount; lSampleIndex++)
*(pfOutput++) = *(pfInput++) * fGain;
}
结论
LADSPA 是音效插件的常用框架。本章介绍了一些命令行工具和编程模型。
十五、使用 Gtk 和 FFmpeg 以叠加方式显示视频
这一章与声音无关。视频通常伴随着音频。Karaoke 经常在视频上覆盖歌词。构建一个包含视频和音频的应用会将您带入图形用户界面(GUI)的领域。这本身就是一个复杂的领域,值得(而且已经!)很多书,包括我自己多年前写的关于 X Window 系统和 Motif 的书。这一章是关于编程的视频方面,使用 FFmpeg,Gtk,Cairo 和 Pango。我假设您熟悉窗口小部件、事件、事件处理程序等概念,它们是当前所有 GUI 框架的基础。
Motif 很久以前就失去了作为 Linux/Unix 系统主要 GUI 的地位。现在有很多替代方案,包括 Gtk(Gimp 工具包)、tcl/Tk、Java Swing、KDE、XFCE 等等。每一种都有自己的追随者、使用领域、怪癖、特质等等。没有一个单一的 GUI 能满足所有人。
在这一章中,我处理 Gtk。原因有三。
- 它有一个 C 库。它也有一个 Python 库,这很好,我可能有一天会用到它。最重要的是,它不是基于 C++的。C++是我最不喜欢的语言之一。我曾经碰到过一句名言(source lost)“c++是一个逃脱的实验室实验”,我完全同意那个评价。
- 对 i18n(国际化)有很好的支持。我希望能够播放中文 Karaoke 文件,所以这对我很重要。
- 它不是基于 Java 的。不要误解我,我真的很喜欢 Java,并且已经用它编程很多年了。MIDI API 相当不错,当然其他东西比如 i18n 也很棒。但是对于 MIDI 来说,它是一个 CPU 占用率很高的设备,不能在低功耗设备上使用,比如 Raspberry Pi,而且通常音频/视频 API 已经多年没有进步了。
然而,当我努力理解 Gtk 版本 2.0 与 3.0、Cairo、Pango、Glib 等等的区别时,我认为修复 Java MIDI 引擎可能更容易!这不是一次愉快的经历,续集将会展示这一点。
FFmpeg
要播放 MPEG 文件、OGV 文件或类似文件,您需要一个解码器。主要竞争者似乎是 GStreamer 和 FFmpeg。没有特别的原因,我选择了 FFmpeg。
下面的程序读取视频文件并将前五帧存储到磁盘。直接摘自斯蒂芬·德朗格的《一个 FFmpeg 和 SDL 教程》( http://dranger.com/ffmpeg/ )。程序是play_video.c,如下图:
// tutorial01.c
// Code based on a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101
// on GCC 4.7.2 in Debian February 2015
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
/* Requires
libavcodec-dev
libavformat-dev
libswscale
*/
void SaveFrame(AVFrame *pFrame, int width, int height, int iFrame) {
FILE *pFile;
char szFilename[32];
int y;
// Open file
sprintf(szFilename, "frame%d.ppm", iFrame);
pFile=fopen(szFilename, "wb");
if(pFile==NULL)
return;
// Write header
fprintf(pFile, "P6\n%d %d\n255\n", width, height);
// Write pixel data
for(y=0; y<height; y++)
fwrite(pFrame->data[0]+y*pFrame->linesize[0], 1, width*3, pFile);
// Close file
fclose(pFile);
}
main(int argc, cha
r **argv) {
AVFormatContext *pFormatCtx = NULL;
int i, videoStream;
AVCodecContext *pCodecCtx = NULL;
AVCodec *pCodec = NULL;
AVFrame *pFrame = NULL;
AVFrame *pFrameRGB = NULL;
AVPacket packet;
int frameFinished;
int numBytes;
uint8_t *buffer = NULL;
AVDictionary *optionsDict = NULL;
struct SwsContext *sws_ctx = NULL;
if(argc < 2) {
printf("Please provide a movie file\n");
return -1;
}
// Register all formats and codecs
av_register_all();
// Open video file
if(avformat_open_input(&pFormatCtx, argv[1], NULL, NULL)!=0)
return -1; // Couldn't open file
// Retrieve stream information
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
return -1; // Couldn't find stream information
// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, argv[1], 0);
// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1)
return -1; // Didn't find a video stream
// Get a pointer to the code
c context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
if(avcodec_open2(pCodecCtx, pCodec, &optionsDict)<0)
return -1; // Could not open codec
// Allocate video frame
pFrame=avcodec_alloc_frame();
// Allocate an AVFrame structure
pFrameRGB=avcodec_alloc_frame();
if(pFrameRGB==NULL)
return -1;
// Determine required buffer size and allocate buffer
numBytes=avpicture_get_size(PIX_FMT_RGB24, pCodecCtx->width,
pCodecCtx->height);
buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
sws_ctx =
sws_getContext
(
pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtx->width,
pCodecCtx->height,
PIX_FMT_RGB24,
SWS_BILINEAR,
NULL,
NULL,
NULL
);
// Assign appropriat
e parts of buffer to image planes in pFrameRGB
// Note that pFrameRGB is an AVFrame, but AVFrame is a superset
// of AVPicture
avpicture_fill((AVPicture *)pFrameRGB, buffer, PIX_FMT_RGB24,
pCodecCtx->width, pCodecCtx->height);
// Read frames and save first five frames to disk
i=0;
while(av_read_frame(pFormatCtx, &packet)>=0) {
// Is this a packet from the video stream?
if(packet.stream_index==videoStream) {
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
&packet);
// Did we get a video frame?
if(frameFinished) {
// Convert the image from its native format to RGB
sws_scale
(
sws_ctx,
(uint8_t const * const *)pFrame->data,
pFrame->linesize,
0,
pCodecCtx->height,
pFrameRGB->data,
pFrameRGB->linesize
);
printf("Read frame\n");
// Save the frame to disk
if(++i<=5)
SaveFrame(pFrameRGB, pCodecCtx->width, pCodecCtx->height,
i);
else
break;
}
}
// Free the packet that was allocated by av_read_frame
av_free_packet(&packet);
}
// Free the RGB image
av_free(buffer);
av_free(pFrameRGB);
// Free the YUV frame
av_free(pFrame);
// Close the codec
avcodec_close(pCodecCtx);
// Close the video file
avformat_close_input(&pFormatCtx);
return 0;
}
基本 Gtk
Gtk 是一个相当标准的 GUI 工具包。简单的程序在《GTK+》(http://zetcode.com/tutorials/gtktutorial/firstprograms/)等很多教程中都有描述。有关 Gtk 编程的基础知识,请参考此类教程。
我在没有解释的情况下包括了下面的例子;它使用了三个子部件、两个按钮和一个标签。标签将保存一个整数。按钮将增加或减少这个数字。
#include <gtk/gtk.h>
gint count = 0;
char buf[5];
void increase(GtkWidget *widget, gpointer label)
{
count++;
sprintf(buf, "%d", count);
gtk_label_set_text(GTK_LABEL(label), buf);
}
void decrease(GtkWidget *widget, gpointer label)
{
count--;
sprintf(buf, "%d", count);
gtk_label_set_text(GTK_LABEL(label), buf);
}
int main(int argc, char** argv) {
GtkWidget *label;
GtkWidget *window;
GtkWidget *frame;
GtkWidget *plus;
GtkWidget *minus;
gtk_init(&argc, &argv);
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
gtk_window_set_position(GTK_WINDOW(window), GTK_WIN_POS_CENTER);
gtk_window_set_default_size(GTK_WINDOW(window), 250, 180);
gtk_window_set_title(GTK_WINDOW(window), "+-");
frame = gtk_fixed_new();
gtk_container_add(GTK_CONTAINER(window), frame);
plus = gtk_button_new_with_label("+");
gtk_widget_set_size_request(plus, 80, 35);
gtk_fixed_put(GTK_FIXED(frame), plus, 50, 20);
minus = gtk_button_new_with_label("-");
gtk_widget_set_size_request(minus, 80, 35);
gtk_fixed_put(GTK_FIXED(frame), minus, 50, 80);
label = gtk_label_new("0");
gtk_fixed_put(GTK_FIXED(frame), label, 190, 58);
gtk_widget_show_all(window);
g_signal_connect(window, "destroy",
G_CALLBACK (gtk_main_quit), NULL);
g_signal_connect(plus, "clicked",
G_CALLBACK(increase), label);
g_signal_connect(minus, "clicked",
G_CALLBACK(decrease), label);
gtk_main();
return 0;
}
Gtk 和其他 GUI 工具包一样,有大量的小部件。这些都列在 GTK+ 3 参考手册中( https://developer.gnome.org/gtk3/3.0/ )。这包括小部件 GtkImage ( https://developer.gnome.org/gtk3/3.0/GtkImage.html )。顾名思义,它可以从某个地方获取一组像素,并将它们构建成可以显示的图像。
以下示例显示了从文件加载的图像:
#include <gtk/gtk.h>
int main( int argc, char *argv[])
{
GtkWidget *window, *image;
gtk_init(&argc, &argv);
window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
image = gtk_image_new_from_file("jan-small.png");
gtk_container_add(GTK_CONTAINER(window), image);
g_signal_connect(G_OBJECT(window), "destroy", G_CALLBACK(gtk_main_quit), NULL);
gtk_widget_show(image);
gtk_widget_show(window);
gtk_main();
return 0;
}
Gtk 的版本
Gtk 目前(截至 2016 年 11 月)有主要版本 2 和 3。宏GTK_MAJOR_VERSION可用于检测版本 2 或 3。然而,Gtk 还依赖于许多其他的库,并且很难确定应该查看哪些文档页面。以下是主要库及其主要 API 页面的列表:
Gtk 3 ( https://developer.gnome.org/gtk3/3.0/
- Gdk 3 (
https://developer.gnome.org/gdk3/stable/ - 开罗 1 (
http://cairographics.org/manual/) - 庞 1 (
https://developer.gnome.org/pango/stable/ - gdk pixbuf 2()
- 油嘴滑舌 2 (
https://developer.gnome.org/glib/) - Freetype 2 (
www.freetype.org/freetype2/docs/reference/ft2-toc.html
Gtk 2
- Gdk 2 (
https://developer.gnome.org/gdk2/2.24/ - 开罗 1 (
http://cairographics.org/manual/) - 庞 1 (
https://developer.gnome.org/pango/stable/ - 油嘴滑舌 2 (
https://developer.gnome.org/glib/) - Freetype 2 (
www.freetype.org/freetype2/docs/reference/ft2-toc.html
使用 Gtk 显示视频
比如你想把 FFmpeg 产生的图像作为AVFrame s,显示在一个GtkImage里。你不希望使用从文件中读取的代码,因为以每秒 30 帧的速度读写文件是荒谬的。相反,您希望将一些内存中的帧表示加载到GtkImage中。
这里是您遇到第一个障碍的地方:在 Gtk 2.0 和 Gtk 3.0 之间,合适的内存表示发生了不兼容的变化。我只打算用 X Window 系统的语言来说,因为我不了解其他底层系统,比如微软的 Windows。
参见“从 GTK+ 2.x 迁移到 GTK+3”(https://developer.gnome.org/gtk3/3.5/gtk-migrating-2-to-3.html)了解这些版本之间的一些变化。
像素地图
X Window 系统架构模型是一个客户机-服务器模型,它让客户机(应用)与服务器(带有图形显示和输入设备的设备)进行对话。在最低层(Xlib),客户端将向服务器发送基本请求,如“从这里到那里画一条线”。服务器将使用服务器端的信息绘制线条,例如当前线条的粗细、颜色等。
如果你想用一个像素数组来表示一个图像,那么这个数组通常保存在 X Window 服务器的一个 pixmap 中。应用可以通过从客户机向服务器发送消息来创建和修改位图。即使是简单的修改,如更改单个像素的值,也需要网络往返,如果经常进行,这显然会变得非常昂贵。
Pixbufs
Pixbufs 是客户端的 pixmaps 的等价物。客户端可以操纵它们,而无需往返于 X Window 服务器。这减少了操作它们的时间和网络开销。然而,这意味着原本保存在服务器上的信息现在必须在客户端应用端构建和维护。
x、韦兰和和平号
X Window 系统已经有将近 30 年的历史了。在此期间,它已经发展到满足硬件和软件需求的变化,同时仍然保持向后兼容性。
在这 30 年中,硬件发生了重大变化:多核系统现在很普遍,GPU 带来了视频处理的变化。通常,内存量(缓存和 RAM)意味着内存不再是一个问题。
与此同时,软件方面也发生了变化。现在普遍使用 Compiz 这样的“合成窗口管理器”,这样你就可以制作出像摇晃的窗口这样的效果。这对于 X 窗口模型来说并不好:来自应用的请求发送到 X 服务器,但是请求的图像必须传递到合成窗口管理器,它将执行它的效果,然后将图像发送回 X 服务器。这是网络流量的巨大增长,X 服务器现在只是扮演显示者的角色,而不是合成器。
应用库现在已经得到了发展,以前由 X 服务器完成的许多工作现在可以由 Cairo、Pixman、Freetype、Fontconfig 和 Pango 等库在应用端完成。
所有这些变化导致了对新的后端服务器的建议,它们在这个不断发展的世界中协同工作。这是由 Wayland ( http://wayland.freedesktop.org/ )的开发引发的,但被 Ubuntu 分叉这个来开发 Mir ( https://wiki.ubuntu.com/Mir/ )有点搞砸了。不要相信这些争论。谷歌一下米尔和韦兰就知道了。
从简单的意义上来说,这意味着在未来,当 pixbufs 出现时,pixmaps 将会退出。
Gtk 3.0
随着 Gtk 3.0 的出现,像素贴图不再存在。数据结构GdkPixbuf中只有 pixbufs。要显示 FFmpeg 解码的视频,您需要在图像被转码为picture_RGB后,将其转换为GdkPixbuf,并创建GtkImage。
pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
0, 8, width, height,
picture_RGB->linesize[0],
pixmap_destroy_notify,
NULL);
gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf);
Gtk 2.0
Gtk 2.0 在结构GdkPixmap中仍然有 pixmaps。理论上,应该可以使用函数GdkPixmap *gdk_pixmap_create_from_data(GdkDrawable *drawable, const gchar *data, gint width, gint height, gint depth, const GdkColor *fg, const GdkColor *bg)编写类似于 Gtk 3.0 代码的代码,该函数在《GDK 2 参考手册》的“位图和像素图”( https://developer.gnome.org/gdk/unstable/gdk-Bitmaps-and-Pixmaps.html#gdk-pixmap-create-from-data ),然后调用void gtk_image_set_from_pixmap(GtkImage *image, GdkPixmap *pixmap, GdkBitmap *mask),该函数在 GtkImage ( www.gtk.org/api/2.6/gtk/GtkImage.html#gtk-image-set-from-pixmap )的 Gtk 2.6 参考手册中有记载。
唯一的问题是我无法让函数gdk_pixmap_create_from_data工作。无论我为 drawable 尝试什么参数,调用总是在类型或值上出错。例如,记录的值是NULL,但这总是会导致断言错误(“不应为空”)。
那么,什么有效呢?嗯,我能找到的只是 pixmap 和 pixbuf 的一点混乱:创建一个充满视频数据的 pixbuf,创建一个 pixmap,将 pix buf 数据写入 pixmap,然后用 pix map 数据填充图像。
pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
0, 8, width, height,
picture_RGB->linesize[0],
pixmap_destroy_notify,
NULL);
pixmap = gdk_pixmap_new(window->window, width, height, -1);
gdk_draw_pixbuf((GdkDrawable *) pixmap, NULL,
pixbuf,
0, 0, 0, 0, wifth, height,
GDK_RGB_DITHER_NORMAL, 0, 0);
gtk_image_set_from_pixmap((GtkImage*) image, pixmap, NULL);
螺纹和 Gtk
视频将需要在自己的线程中播放。Gtk 将在其线程中建立一个 GUI 处理循环。既然这是 Linux,你就用 Posix pthreads。视频线程需要通过以下方式明确启动:
pthread_t tid;
pthread_create(&tid, NULL, play_background, NULL);
这里函数play_background调用 FFmpeg 代码来解码视频文件。请注意,在应用实现之前,不应该启动线程,否则它会试图在不存在的窗口中绘图。
Gtk 线程将通过调用以下内容来启动:
gtk_main();
这很简单。但是现在您必须处理调用 GUI 线程的视频线程,以便绘制图像。我在这方面找到的最好的文档是“GTK+线程安全吗?我如何编写多线程 GTK+应用?”( https://developer.gnome.org/gtk-faq/stable/x481.html )。基本上,它声明影响 Gtk 线程的代码应该用一个gdk_threads_enter() … gdk_threads_leave()对括起来。
对于 Gtk 2.0 来说还可以。Gtk 3.0 呢?呜!这些调用现在已被否决。那么,你该怎么办?到目前为止(截至 2013 年 7 月),所有似乎存在的都是开发者对话,例如在 https://mail.gnome.org/archives/gtk-devel-list/2012-August/msg00020.html ,其中写道:
"We never seem to explain when gdk_threads_enter/leave is needed. Therefore, during the checkout process of my jhbuild, many key parts I saw were unnecessary. If your application does not call gdk_threads_init or gdk_threads_set_lock_functions, there is no need to use enter/leave. Of course, the library is another matter. "
实际的解决方法是不同的方向,解决方法如 https://developer.gnome.org/gdk3/stable/gdk3-Threads.html 所示:Gtk 不是线程安全的。Gtk 线程内的调用是安全的,但是大多数来自不同线程的 Gtk 调用并不安全。如果您需要从另一个线程调用 Gtk,那么调用gdk_threads_add_idle()来调用将在 Gtk 线程中运行的函数。与该延迟呼叫相关的数据可能会作为另一个参数传递给gdk_threads_add_idle()。
在本章的剩余部分,你将只考虑 Gtk 3 而不考虑 Gtk 2。
《守则》
最后,是时候看看在与 Gtk 3.0 兼容的 Gtk 应用中播放视频的代码了。我会把它打碎。
播放视频的函数作为后台线程运行。它使用 Gtk 3 读取帧并创建一个 pixbuf。内容如下:
static gboolean draw_image(gpointer user_data) {
GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;
gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
gtk_widget_queue_draw(image);
g_object_unref(pixbuf);
return G_SOURCE_REMOVE;
}
static gpointer play_background(gpointer args) {
int i;
AVPacket packet;
int frameFinished;
AVFrame *pFrame = NULL;
/* initialize packet, set data to NULL, let the demuxer fill it */
/* http://ffmpeg.org/doxygen/trunk/doc_2examples_2demuxing_8c-example.html#a80 */
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
int bytesDecoded;
GdkPixbuf *pixbuf;
AVFrame *picture_RGB;
char *buffer;
pFrame=avcodec_alloc_frame();
i=0;
picture_RGB = avcodec_alloc_frame();
buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, WIDTH, HEIGHT));
avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, WIDTH, HEIGHT);
while(av_read_frame(pFormatCtx, &packet)>=0) {
if(packet.stream_index==videoStream) {
usleep(33670); // 29.7 frames per second
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
&packet);
int width = pCodecCtx->width;
int height = pCodecCtx->height;
sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt, width, height,
PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);
if (frameFinished) {
sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, height,
picture_RGB->data, picture_RGB->linesize);
printf("old width %d new width %d\n", pCodecCtx->width, picture_RGB->width);
pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
0, 8, width, height,
picture_RGB->linesize[0], pixmap_destroy_notify,
NULL);
gdk_threads_add_idle(draw_image, pixbuf);
gtk_image_set_from_pixbuf((GtkImage*) image, pixbuf);
}
sws_freeContext(sws_ctx);
}
av_free_packet(&packet);
g_thread_yield();
}
printf("Video over!\n");
exit(0);
}
这个函数被设置为在它自己的线程中运行,并且有一个窗口供它绘制。
/* Called when the windows
are realized
*/
static void realize_cb (GtkWidget *widget, gpointer data) {
/* start the video playing in its own thread */
GThread *tid;
tid = g_thread_new("video",
play_background,
NULL);
}
main 函数负责初始化 FFmpeg 环境以读取视频,然后设置 Gtk 窗口以供其绘制。内容如下:
int main(int argc, char** argv)
{
int i;
/* FFMpeg stuff */
AVFrame *pFrame = NULL;
AVPacket packet;
AVDictionary *optionsDict = NULL;
av_register_all();
if(avformat_open_input(&pFormatCtx, "/home/httpd/html/ComputersComputing/simpson.mpg", NULL, NULL)!=0)
return -1; // Couldn't open file
// Retrieve stream information
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
return -1; // Couldn't find stream information
// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, argv[1], 0);
// Find the first video stream
videoStream=-1;
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
videoStream=i;
break;
}
if(videoStream==-1)
return -1; // Didn't find a video stream
for(i=0; i<pFormatCtx->nb_streams; i++)
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO) {
printf("Found an audio stream too\n");
break;
}
// Get a pointer to the codec context for the video stream
pCodecCtx=pFormatCtx->streams[videoStream]->codec;
// Find the decoder for the video stream
pCodec=avcodec_find_decoder(pCodecCtx->codec_id);
if(pCodec==NULL) {
fprintf(stderr, "Unsupported codec!\n");
return -1; // Codec not found
}
// Open codec
if(avcodec_open2(pCodecCtx, pCodec, &optionsDict)<0)
return -1; // Could not open codec
width = pCodecCtx->width;
height = pCodecCtx->height;
sws_ctx =
sws_getContext
(
pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtx->width,
pCodecCtx->height,
PIX_FMT_YUV420P,
SWS_BILINEAR,
NULL,
NULL,
NULL
);
/* GTK stuff now */
gtk_init (&argc, &argv);
window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
/* When the window is given the "delete-event" signal (this is given
* by the window manager, usually by the "close" option, or on the
* titlebar), we ask it to call the delete_event () function
* as defined above. The data passed to the callback
* function is NULL and is ignored in the callback function. */
g_signal_connect (window, "delete-event",
G_CALLBACK (delete_event), NULL);
/* Here we connect the "destroy" event to a signal handler.
* This event occurs when we call gtk_widget_destroy() on the window,
* or if we return FALSE in the "delete-event" callback. */
g_signal_connect (window, "destroy",
G_CALLBACK (destroy), NULL);
g_signal_connect (window, "realize", G_CALLBACK (realize_cb), NULL);
/* Sets the border width of the window. */
gtk_container_set_border_width (GTK_CONTAINER (window), 10);
image = gtk_image_new();
gtk_widget_show (image);
/* This packs the button into the window (a gtk container). */
gtk_container_add (GTK_CONTAINER (window), image);
/* and the window */
gtk_widget_show (window);
/* All GTK applications
must have a gtk_main(). Control ends here
* and waits for an event to occur (like a key press or
* mouse event). */
gtk_main ();
return 0;
}
在图像上覆盖图像
在电视电影中,通常会看到一个固定的图像叠加在视频之上。字幕可以是动态图像的一个例子,但也可以是文本覆盖。本节只考虑一个图像在另一个图像之上。
在 Gtk 2.0 中,这非常简单:将一个 pixbuf 绘制到一个 pixmap 中,然后将叠加的 pixbuf 绘制到同一个 pixmap 中。
pixmap = gdk_pixmap_new(window->window, 720, 480, -1);
gdk_draw_pixbuf((GdkDrawable *) pixmap, NULL,
pixbuf,
0, 0, 0, 0, 720, 480,
GDK_RGB_DITHER_NORMAL, 0, 0);
// overlay another pixbuf
gdk_draw_pixbuf((GdkDrawable *) pixmap, NULL,
overlay_pixbuf,
0, 0, 0, 0, overlay_width, overlay_height,
GDK_RGB_DITHER_NORMAL, 0, 0);
gtk_image_set_from_pixmap((GtkImage*) image, pixmap, NULL);
gtk_widget_queue_draw(image);
Gtk 3.0 看起来并不那么简单,因为像素地图已经消失了。许多页面建议使用 Cairo 曲面,后面的章节将会介绍这一点。但是“GdkPixbuf 结构”( https://developer.gnome.org/gdk-pixbuf/unstable/gdk-pixbuf-The-GdkPixbuf-Structure.html )建议,只要把数据类型对齐,就可以把第二个图像的像素写入第一个图像的 Pixbuf 数据中。名为“Gdk-pix buf”(http://openbooks.sourceforge.net/books/wga/graphics-gdk-pixbuf.html)的页面(虽然很老)是一个关于 Gdk pixbufs 的有用教程。您必须正确处理的一个细节是每个图像的 rowstride:二维图像存储为字节的线性数组,rowstride 告诉您一行由多少个字节组成。通常每个像素有 3 或 4 个字节(对于 RGB 或 RGB+alpha),并且这些也需要在图像之间匹配。
Gtk 3 叠加功能如下:
static void overlay(GdkPixbuf *pixbuf, GdkPixbuf *overlay_pixbuf,
int height_offset, int width_offset) {
int overlay_width, overlay_height, overlay_rowstride, overlay_n_channels;
guchar *overlay_pixels, *overlay_p;
guchar red, green, blue, alpha;
int m, n;
int rowstride, n_channels, width, height;
guchar *pixels, *p;
if (overlay_pixbuf == NULL) {
return;
}
/* get stuff out of overlay pixbuf */
overlay_n_channels = gdk_pixbuf_get_n_channels (overlay_pixbuf);
n_channels = gdk_pixbuf_get_n_channels(pixbuf);
printf("Overlay has %d channels, destination has %d channels\n",
overlay_n_channels, n_channels);
overlay_width = gdk_pixbuf_get_width (overlay_pixbuf);
overlay_height = gdk_pixbuf_get_height (overlay_pixbuf);
overlay_rowstride = gdk_pixbuf_get_rowstride (overlay_pixbuf);
overlay_pixels = gdk_pixbuf_get_pixels (overlay_pixbuf);
rowstride = gdk_pixbuf_get_rowstride (pixbuf);
width = gdk_pixbuf_get_width (pixbuf);
pixels = gdk_pixbuf_get_pixels (pixbuf);
printf("Overlay: width %d str8ide %d\n", overlay_width, overlay_rowstride);
printf("Dest: width str8ide %d\n", rowstride);
for (m = 0; m < overlay_width; m++) {
for (n = 0; n < overlay_height; n++) {
overlay_p = ove
rlay_pixels + n * overlay_rowstride + m * overlay_n_channels;
red = overlay_p[0];
green = overlay_p[1];
blue = overlay_p[2];
if (overlay_n_channels == 4)
alpha = overlay_p[3];
else
alpha = 0;
p = pixels + (n+height_offset) * rowstride + (m+width_offset) * n_channels;
p[0] = red;
p[1] = green;
p[2] = blue;
if (n_channels == 4)
p[3] = alpha;
}
}
}
阿尔法通道
叠加图像中可能有一些“透明”部分。你不希望这样的部分被覆盖到下面的图像。但是这些部分需要在像素阵列中有一个值。连零都是一个值:黑!一些图像会为每个像素分配另一个字节作为 alpha 通道。这有一个显示像素透明度的值。值 255 表示完全不透明,值 0 表示完全透明。
将透明像素与底层像素合并的最简单方法就是不要这样做:不要动底层像素。维基百科“阿尔法合成”( http://en.wikipedia.org/wiki/Alpha_compositing )页面指出了更复杂的算法。
使用函数gdk_pixbuf_add_alpha可以将没有 alpha 通道的图像转换成有 alpha 通道的图像。这也可以用于通过匹配颜色来设置 alpha 通道的值。例如,下面的代码应该将任何白色像素的 alpha 值设置为 0,将所有其他像素的 alpha 值设置为 255:
pixbuf = gdk_pixbuf_add_alpha(pixbuf, TRUE, 255, 255, 255);
不幸的是,它似乎想留下一个像素的“边缘”,应该标记为透明。
有了 alpha 标记,可以在覆盖功能中使用一个简单的测试来决定是否执行覆盖。
if (alpha < 128) {
continue;
}
仅仅为了几行改动就给出一个完整的程序是不值得的。是gtk_play_video_overlay_alpha.c。
使用 Cairo 绘制图像
随着 Gtk 3.0 中 pixmaps 的消失,Cairo 现在是将多个组件组装成一个图像的唯一真正的方法。您可以在 http://cairographics.org/documentation/ 找到开罗的一般信息,在 http://zetcode.com/gfx/cairo/ 找到教程,在 http://zetcode.com/gfx/cairo/cairoimg/ 找到叠加到图像上的信息。
开罗需要来源和目的地。源可以改变,通常是从图像源到颜色源,等等。目的地是画出来的东西的最终目的地。
目的地可以在内存中,也可以在各种后端。您需要一个内存中的目的地,以便可以从中提取 pixbuf,所有操作都在客户端完成。您创建一个目的地作为类型为cairo_surface_t的表面,并使用以下内容将其设置到类型为cairo_t的 Cairo 上下文中:
cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
width, height);
cairo_t *cr = cairo_create(surface);
Cairo 上下文cr随后用于设置源、执行绘制等等。最后,您将从surface中提取一个位图。
第一步是将视频的每一帧的源设置为 pixbuf,并使用以下代码将其绘制到目标:
gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
cairo_paint (cr);
您可以在此基础上叠加另一个图像,方法是将源更改为叠加图像,并绘制:
gdk_cairo_set_source_pixbuf(cr, overlay_pixbuf, 300, 200);
cairo_paint (cr);
请注意,如果覆盖图有“透明”像素,Cairo 将进行任何所需的 alpha 混合。
要绘制文本,您需要将源重置为 RGB 表面,设置文本的所有参数,并将文本绘制到目标中。这是通过以下方式完成的:
// white text
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
// this is a standard font for Cairo
cairo_select_font_face (cr, "cairo:serif",
CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_BOLD);
cairo_set_font_size (cr, 20);
cairo_move_to(cr, 10.0, 50.0);
cairo_show_text (cr, "hello");
最后,您要从目的地提取最终图像,并将其设置到GdkImage中进行显示。这里还有一个 Gtk 2.0 和 Gtk 3.0 的区别:Gtk 3.0 有一个函数gdk_pixbuf_get_from_surface,会返回一个GdKPixbuf;Gtk 2.0 没有这个功能。你会在这里看到 Gtk 3.0 版本。
pixbuf = gdk_pixbuf_get_from_surface(surface,
0,
0,
width,
height);
gdk_threads_add_idle(draw_image, pixbuf);
使用 Cairo 播放视频的修改函数如下:
static gboolean draw_image(gpointer user_data) {
GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;
gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
gtk_widget_queue_draw(image);
g_object_unref(pixbuf);
return G_SOURCE_REMOVE;
}
static void *play_background(void *args) {
int i;
AVPacket packet;
int frameFinished;
AVFrame *pFrame = NULL;
int bytesDecoded;
GdkPixbuf *pixbuf;
GdkPixbuf *overlay_pixbuf;
AVFrame *picture_RGB;
char *buffer;
GError *error = NULL;
overlay_pixbuf = gdk_pixbuf_new_from_file(OVERLAY_IMAGE, &error);
if (!overlay_pixbuf) {
fprintf(stderr, "%s\n", error->message);
g_error_free(error);
exit(1);
}
// add an alpha layer for a white background
overlay_pixbuf = gdk_pixbuf_add_alpha(overlay_pixbuf, TRUE, 255, 255, 255);
int overlay_width = gdk_pixbuf_get_width(overlay_pixbuf);
int overlay_height = gdk_pixbuf_get_height(overlay_pixbuf);
pFrame=avcodec_alloc_frame();
i=0;
picture_RGB = avcodec_alloc_frame();
buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, 720, 576));
avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, 720, 576);
while(av_read_frame(pFormatCtx, &packet)>=0) {
if(packet.stream_index==videoStream) {
usleep(33670); // 29.7 frames per second
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
&packet);
int width = pCodecCtx->width;
int height = pCodecCtx->height;
sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt, width, height,
PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);
if (frameFinished) {
printf("Frame %d\n", i++);
sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, height, picture_RGB->data, picture_RGB->linesize);
printf("old width %d new width %d\n", pCodecCtx->width, picture_RGB->width);
pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
0, 8, width, height,
picture_RGB->linesize[0], pixmap_destroy_notify,
NULL);
// Create the destination surface
cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
width, height);
cairo_t *cr = cairo_create(surface);
// draw the background image
gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
cairo_paint (cr);
// overlay an image on top
// alpha blending will be done by Cairo
gdk_cairo_set_source_pixbuf(cr, overlay_pixbuf, 300, 200);
cairo_paint (cr);
// draw some white text on top
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
// this is a standard font for Cairo
cairo_select_font_face (cr, "cairo:serif",
CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_BOLD);
cairo_set_font_size (cr, 20);
cairo_move_to(cr, 10.0, 50.0);
cairo_show_text (cr, "hello");
pixbuf = gdk_pixbuf_get_from_surface(surface,
0,
0,
width,
height);
gdk_threads_add_idle(draw_image, pixbuf);
sws_freeContext(sws_ctx);
cairo_surface_destroy(surface);
cairo_destroy(cr);
}
}
av_free_packet(&packet);
}
printf("Video over!\n");
exit(0);
}
使用 Pango 绘制文本
虽然 Cairo 可以绘制任何形式的文本,但像cairo_show_text这样的函数没有太大的灵活性。比如说,画多种颜色会涉及很多工作。Pango 是一个处理文本所有方面的库。在 https://developer.gnome.org/pango/stable/ 有一本盘古参考手册。好的教程在 www.ibm.com/developerworks/library/l-u-pango2/ 。
给文本着色(和一些其他效果)的最简单方法是创建用 HTML 标记的文本,如下所示:
gchar *markup_text = "<span foreground=\"red\">hello </span><span foreground=\"black\">world</span>";
红色的是“你好”,黑色的是“世界”。这然后被解析成文本“红黑”和一组属性标记。
gchar *markup_text = "<span foreground=\"red\">hello </span><span foreground=\"black\">world</span>";
PangoAttrList *attrs;
gchar *text;
pango_parse_markup (markup_text, -1,0, &attrs, &text, NULL, NULL);
这可以通过从 Cairo 上下文创建一个PangoLayout来呈现到 Cairo 上下文中,在 Pango 布局中布置文本及其属性,然后在 Cairo 上下文中显示这个布局。
PangoLayout *layout;
PangoFontDescription *desc;
cairo_move_to(cr, 300.0, 50.0);
layout = pango_cairo_create_layout (cr);
pango_layout_set_text (layout, text, -1);
pango_layout_set_attributes(layout, attrs);
pango_cairo_update_layout (cr, layout);
pango_cairo_show_layout (cr, layout);
(是的,在所有这些中,有很多在库之间跳来跳去!)
和前面一样,一旦所有内容都被绘制到 Cairo 上下文中,就可以从 Cairo 表面目的地提取出一个 pixbuf,设置到GtkImage中,并添加到 Gtk 事件队列中。
使用 Pango 绘制视频的修改函数如下:
static gboolean draw_image(gpointer user_data) {
GdkPixbuf *pixbuf = (GdkPixbuf *) user_data;
gtk_image_set_from_pixbuf((GtkImage *) image, pixbuf);
gtk_widget_queue_draw(image);
g_object_unref(pixbuf);
return G_SOURCE_REMOVE;
}
static void *play_background(void *args) {
int i;
AVPacket packet;
int frameFinished;
AVFrame *pFrame = N
ULL;
/* initialize packet, set data to NULL, let the demuxer fill it */
/* http://ffmpeg.org/doxygen/trunk/doc_2examples_2demuxing_8c-example.html#a80 */
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
int bytesDecoded;
GdkPixbuf *pixbuf;
GdkPixbuf *overlay_pixbuf;
AVFrame *picture_RGB;
char *buffer;
// Pango marked up text, half red, half black
gchar *markup_text = "<span foreground=\"red\">hello</span><span foreground=\"black\">world</span>";
PangoAttrList *attrs;
gchar *text;
pango_parse_markup (markup_text, -1,0, &attrs, &text, NULL, NULL);
GError *error = NULL;
overlay_pixbuf = gdk_pixbuf_new_from_file(OVERLAY_IMAGE, &error);
if (!overlay_pixbuf) {
fprintf(stderr, "%s\n", error->message);
g_error_free(error);
exit(1);
}
// add an alpha lay
er for a white background
overlay_pixbuf = gdk_pixbuf_add_alpha(overlay_pixbuf, TRUE, 255, 255, 255);
int overlay_width = gdk_pixbuf_get_width(overlay_pixbuf);
int overlay_height = gdk_pixbuf_get_height(overlay_pixbuf);
pFrame=avcodec_alloc_frame();
i=0;
picture_RGB = avcodec_alloc_frame();
buffer = malloc (avpicture_get_size(PIX_FMT_RGB24, 720, 576));
avpicture_fill((AVPicture *)picture_RGB, buffer, PIX_FMT_RGB24, 720, 576);
while(av_read_frame(pFormatCtx, &packet)>=0) {
if(packet.stream_index==videoStream) {
usleep(33670); // 29.7 frames per second
// Decode video frame
avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished,
&packet);
int width = pCodecCtx->width;
int height = pCodecCtx->height;
sws_ctx = sws_getContext(width, height, pCodecCtx->pix_fmt, width, height,
PIX_FMT_RGB24, SWS_BICUBIC, NULL, NULL, NULL);
if (frameFinished) {
printf("Frame %d\n", i++);
sws_scale(sws_ctx, (uint8_t const * const *) pFrame->data, pFrame->linesize, 0, height,
picture_RGB->data, picture_RGB->linesize);
printf("old width %d new width %d\n", pCodecCtx->width, picture_RGB->width);
pixbuf = gdk_pixbuf_new_from_data(picture_RGB->data[0], GDK_COLORSPACE_RGB,
0, 8, width, height,
picture_RGB->linesize[0], pixmap_destroy_notify,
NULL);
// Create the destination surface
cairo_surface_t *surface = cairo_image_surface_create (CAIRO_FORMAT_ARGB32,
width, height);
cairo_t *cr = cairo_create(surface);
// draw the background image
gdk_cairo_set_source_pixbuf(cr, pixbuf, 0, 0);
cairo_paint (cr);
// overlay an image on top
// alpha blending will be done by Cairo
gdk_cairo_set_source_pixbuf(cr, overlay_pixbuf, 300, 200);
cairo_paint (cr);
// draw some white text on top
cairo_set_source_rgb(cr, 1.0, 1.0, 1.0);
// this is a standard font for Cairo
cairo_select_font_face (cr, "cairo:serif",
CAIRO_FONT_SLANT_NORMAL,
CAIRO_FONT_WEIGHT_BOLD);
cairo_set_font_size (cr, 20);
cairo_move_to(cr, 10.0, 50.0);
cairo_show_text (cr, "hello");
// draw Pango text
PangoLayout *layout;
PangoFontDescription *desc;
cairo_move_to(cr, 300.0, 50.0);
layout = pango_cairo_create_layout (cr);
pango_layout_set_text (layout, text, -1);
pango_layout_set_attributes(layout, attrs);
pango_cairo_update_layout (cr, layout);
pango_cairo_show_layout (cr, layout);
pixbuf = gdk_pixbuf_get_from_surface(surface,
0,
0,
width,
height);
gdk_threads_add
_idle(draw_image, pixbuf);
sws_freeContext(sws_ctx);
g_object_unref(layout);
cairo_surface_destroy(surface);
cairo_destroy(cr);
}
}
av_free_packet(&packet);
}
printf("Vi
deo over!\n");
exit(0);
}
结论
掌握 Gtk 工具包的某些方面并不容易。你将在后面的章节中用到这些材料,这也是为什么它被从本书的声音章节中抽出,放在它自己的章节中。那些对 Linux sound 不感兴趣的人可能会发现它很有用。
十六、MIDI
MIDI 是电子版的乐谱。它基本上是一组指令,告诉 MIDI 演奏者演奏哪些音符,声音有多大,使用哪些乐器,使用什么效果,以及何时停止演奏音符。
MIDI 有两种形式:一种是“wire”格式,在这种格式中,MIDI 命令通过流发送,并在接收时进行处理;另一种是文件格式,在这种格式中,MIDI 命令存储在文件中,并从文件中读取和播放。
发明 MIDI 是为了让乐器能够交流,让一种乐器能够控制另一种乐器。它在电子音乐中被大量使用,但通常适用于任何电子乐器。当然,如果有合适的软件,计算机可以被认为是 MIDI 乐器。
资源
以下是一些资源:
- 计算机音乐导论:第一卷:第三章,【迷笛】(
www.indiana.edu/~emusic/etext/MIDI/chapter3_MIDI.shtml) - MIDI 制造商协会:教程(
www.midi.org/aboutmidi/tutorials.php) - Ted 的 Linux MIDI 指南(
http://tedfelix.com/linux/linux-midi.html - 标准 MIDI 文件格式规范。1.1 (
www.cs.cmu.edu/~music/cmsip/readings/Standard-MIDI-file-format-updated.pdf
MIDI 系统的组件
一个 MIDI 系统可以是一个单独的乐器,既产生又消耗 MIDI 事件。通常所说的合成器通常有一个键盘和多个控制来生成 MIDI 事件,也有硬件来从这些控制中产生声音。
合成器
抽象地说,合成器是 MIDI 事件的消费者,也是声音的生产者,通过扬声器或耳机。合成器可以在硬件中实现这一点,但也可以使用被称为声音字体的表格在软件中实现。声音字体有很多种,我们将在下一章讨论。
顺序
合成器对 MIDI 事件做出实时反应。其中最重要的是演奏音符的事件。现在,活页乐谱使用不同种类的音符(钩针、八分音符等等)来表示持续时间。MIDI 改为使用NOTE ON和NOTE OFF事件。
这些音符事件不能一次全部发送到合成器,否则它会尝试一次播放它们。比方说,如果 MIDI 事件是由一个人在键盘上产生的,那么他们就控制它们如何被发送。
在这本书里,你将会看到 MIDI 事件存储在一个文件里的情况。因此,文件阅读器必须控制事件发送到合成器的时间。定序器的作用是在适当的时间向合成器发送事件。
其他组件
一个最小的系统将由一个音序器(一个人或一个组件)在正确的时间发送 MIDI 到合成器。不过,可能还有其他组件,包括架子鼓、产生混响或延迟等声音效果的设备,或者预先录制或数字化音频并可以回放的采样器。
MIDI 事件
MIDI 事件有几种类别。对我们来说,主要的事件是程序变更事件、注释事件和元事件。
程序变更事件
乐器或“声音”与通道相关联。声音和频道之间的建立通常在播放开始时完成,但是可以通过节目改变事件来改变。
记录事件
注意事件不是NOTE ON就是NOTE OFF。它们包括一个选择演奏乐器的频道。他们有一个代表音符的数字。从 0 到 127 共有 128 个,它们对应于音符 C0(8.175 赫兹)到 G10(12543.854 赫兹)。这些值是可以改变的,例如,对于微型音乐,但这超出了本书的范围。音符事件也包含力度,它给出音符的音量。
元事件
有一组元事件给出了关于 MIDI 系统演奏的信息。这些包括版权声明和序列或曲目名称,但对 Karaoke 最重要的是歌词和文本事件。
这些元事件不通过网络发送。比方说,合成器不知道如何处理版权声明。元事件包含在 MIDI 文件中,可以被从文件中读取的任何东西解释。
这导致了序列器的行为差异,当您查看 Karaoke 系统时会遇到这种差异:一些序列器提供元信息,如 Java 序列器。其他的没有,比如fluidsynth。
结论
MIDI 系统早在 20 世纪 80 年代就已经出现了。从这个意义上说,它们是“旧的”技术,并且经常提出替换建议。然而,MIDI 仍然是一种持久的电子格式。本章介绍了 MIDI 系统的组成部分,并给出了 MIDI 信息的抽象视图。
十七、MIDI 的用户级工具
本章概述了用于播放 MIDI 文件的主要工具。它不包括 MIDI 编辑器、MIDI 制作人员等。
资源
查看以下资源:
- Ted 的 Linux MIDI 指南(
http://tedfelix.com/linux/linux-midi.html
声音字体
本章中描述的每个工具都包括一个软件合成器,它将输入的 MIDI 数据生成音频作为 PCM 数据。MIDI 数据包含关于演奏每个音符的乐器的信息,当然,每个乐器听起来都不一样。因此,合成器必须利用从 MIDI notes +乐器到 PCM 数据的映射信息。
映射通常使用声音字体文件来完成。这有各种格式。初级的是.sf2格式( http://connect.creativelabs.com/developer/SoundFont/Forms/AllItems.aspx/ )。有些合成器(如 TiMidity)也可以使用 Gravis 超声贴片,这是录制的真实乐器。
已经创建了许多声音字体文件。例如,请参见“SoundFonts 和其他类似文件的链接”( www.synthfont.com/links_to_soundfonts.html )(尽管许多链接已断开)。
- 一种常见的声音字体来自 FluidSynth,名为
/usr/share/sounds/sf2/FluidR3_GM.sf2。这个文件将近 150Mb。声音字体不小! - Java Sound 有一个声音字体叫
soundbank-emg.sf2。这相当小,只有 1.9Mb! - 另一种流行的声音字体是 S. Christian Collins 的 at general user _ GS _ 1.44-MuseScore(
www.schristiancollins.com/soundfonts/GeneralUser_GS_1.44-MuseScore.zip)。这个不算大,31Mb。 - 可以找 Tim Brechbill 的小音字体;6Mb(链接自
http://musescore.org/en/handbook/soundfont) - 您可以在“TiMidity++配置文件包 v2004/8/3”页面(
http://timidity.s11.xrea.com/files/readme_cfgp.htm)找到声音字体列表
可能令人惊讶的是,使用不同的声音字体似乎对 CPU 的使用没有太大的影响。对于 FluidSynth 来说,它们在一首歌曲上使用大约 60%到 70%的 CPU。当然,它们听起来确实不同。
TiMidity
TiMidity 是一个“软件声音渲染器(MIDI 音序器和 MOD 播放器)”。它的主页是 Maemo.org(http://maemo.org/packages/view/timidity/)。
TiMidity 可以用来播放 MIDI 文件,方法是在命令行上给出它们,就像这样:
timidity rehab.mid
TiMidity 使用的默认声音字体是 Gravis 超声波补丁,来自/usr/share/midi/freepats/目录。这些声音字体是许多乐器所缺少的,因此应该被另一种字体所取代,例如 FluidSynth 字体。在配置文件/etc/timidity/timidity.cfg中进行设置。
作为服务器的 TiMidity
TiMidity 也可以作为监听端口的 ALSA 服务器运行(参见 http://wiki.winehq.org/MIDI “在 UNIX 上使用 MIDI”)。
timidity -iAD -B2,8 -Os1l -s 44100
-iAD选项将它作为后台守护进程作为 ALSA 序列器客户端运行。-B2,8选项选择缓冲区碎片的数量。-Os1l选项选择 ALSA 输出作为 PCM。-s选项是样本大小。(对于树莓派,我发现-B0,12比-B2,8好用。)
在这种模式下,ALSA 可以向它发送信息。命令
aconnect -0
将显示如下输出:
client 14: 'Midi Through' [type=kernel]
0 'Midi Through Port-0'
laptop:/home/httpd/html/LinuxSound/MIDI/Python/pyPortMidi-0.0.3$aconnect -o
client 14: 'Midi Through' [type=kernel]
0 'Midi Through Port-0'
client 128: 'TiMidity' [type=user]
0 'TiMidity port 0 '
1 'TiMidity port 1 '
2 'TiMidity port 2 '
3 'TiMidity port 3 '
Midi 直通端口没有用,但是 TiMidity 端口有用。然后,MIDI 文件可以由 ALSA 音序器播放,如下所示:
aplaymidi -p128:0 rehab.mid
设置 TiMidity 输出设备
您可以使用-O选项更改 TiMidity 的默认输出。TiMidity 帮助(timidity -h)显示如下内容:
Available output modes (-O, --output-mode option):
-Os ALSA pcm device
-Ow RIFF WAVE file
-Or Raw waveform data
-Ou Sun audio file
-Oa AIFF file
-Ol List MIDI event
-Om Write MIDI file
-OM MOD -> MIDI file conversion
对于其中一些模式,也可以使用-o选项设置设备名称。例如,要使用hw:2 ALSA 设备播放文件,请使用:
timidity -Os -o hw:2 ...
TiMidity 和 Jack
TiMidity 可通过使用-Oj选项的插孔输出运行。在 Ubuntu 等基于用户的环境中,可能需要停止或暂停 PulseAudio,启动 Jack 服务器,然后运行 Timothy。例如,在一个终端中,可以通过以下方式暂停 PulseAudio:
pasuspender cat
在另一个例子中,使用 ALSA 输入和输出启动 Jack 守护程序。
jackd -dalsa
在第三个终端,运行 TiMidity。
timidity -Oj 54154.mid
也可以通过运行qjackctl以图形方式显示链接。
GStreamer
GStreamer 允许您构建可以使用gst-launch播放的“管道”。它可以用这个播放 MIDI 文件,例如:
gst-launch filesrc location="rehab.mid" ! decodebin ! alsasink
流体合成
fluidsynth是一个命令行 MIDI 播放器。它通过命令行在 ALSA 下运行,如下所示:
fluidsynth -a alsa -l <sound font> <files...>
声音字体是在命令行上显式设置的,因此可以设置为另一种声音字体。
qsynth是fluidsynth的 GUI 界面。
您可以使用fluidsynth将 MIDI 文件转换成 WAV 文件:
fluidsynth -F out.wav /usr/share/sounds/sf2/FluidR3_GM.sf2 myfile.mid
作为服务器的 fluidsynth
fluidsynth可以像 TiMidity 一样作为服务器运行。用这个:
fluidsynth --server --audio-driver=alsa /usr/share/sounds/sf2/FluidR3_GM.sf2
然后a connect -o将显示端口,可以播放以下内容:
amidi -p 128:0 <midi-file>
玫瑰花园
Rosegarden 是一个全面的音频和 MIDI 音序器、乐谱编辑器和通用音乐创作和编辑环境。它的主页在 www.rosegardenmusic.com/ 。它不是一个独立的合成器;它用fluidsynth举例。
维尔德米迪
这个序列发生器/合成器的目标是体积小。它在这方面取得了成功。
比较
在不同的系统上播放同一首歌曲时,我观察到以下 CPU 模式:
TiMidity+脉冲音频(带有 GUS 或 SF2 声音字体)
- 12%到 20%的 CPU
fluidsynth +脉冲
- 65%到 72%的 CPU
维尔德米迪
- 6%的 CPU
Java 声音
- 52%到 60%
GStreamer
- 15%到 20%的 CPU
可见光通讯
VLC 是一个通用的媒体播放器。有一个 VLC 模块( https://wiki.videolan.org/Midi )使用fluidsynth处理 MIDI 文件。为了在 Debian 系统上运行,你首先需要安装vlc-plugin-fluidsynth包。然后在 VLC 的高级选项中,选择编解码器-音频编解码器-FluidSynth。例如,您需要将声音字体设置为/usr/share/sounds/sf2/FluidR3_GM.sf2。
结论
本章介绍了各种用于操纵 MIDI 的用户级工具。它主要包括播放器,但也有大量的 MIDI 编辑器,生产者,等等。