协议:CC BY-NC-SA 4.0
十八、MIDI Java 声音
Java Sound 有一个开发良好的 MIDI 系统,有干净分离的组件,如音序器和合成器,它允许元事件和普通 MIDI 事件挂钩。本章考虑使用 MIDI API 编程。
资源
许多资源可用于 Java Sound。
- Java 平台标准版 7 API 规范(
http://docs.oracle.com/javase/7/docs/api/
)是所有标准 Java APIs 的参考点,包括javax.sound.sampled
。 - Java 教程(
http://docs.oracle.com/javase/tutorial/sound/index.html
)中的“Trail: Sound”教程很好地概述了 sampled 和 MIDI 包。 - Java Sound Resources (
www.jsresources.org/faq_audio.html
)的音频编程常见问题解答了很多关于 Java Sound 的问题。 - Java Sound 程序员指南(
http://docs.oracle.com/javase/7/docs/technotes/guides/sound/programmer_guide/contents.html
)是一本来自甲骨文(前 Sun MicroSystems)的关于 Java Sound 的完整书籍。 - 声音组(
http://openjdk.java.net/groups/sound/
)由设计、实现和维护各种 OpenJDK 声音组件的开发人员组成。您可以在开源社区中找到更多关于 Java Sound 正在进行的开发的信息。 - 查看 Gervill 软件声音合成器源(
https://java.net/projects/gervill/sources/Mercurial/show
)。
主要的 Java 声音 MIDI 类
Java Sound 依靠一组类来支持 MIDI。这些是 Java 中的标准。以下是主要类别:
MidiSystem
类是所有 MIDI 类的入口点。MidiDevice
包括合成器、音序器、MIDI 输入端口和 MIDI 输出端口。- 一个
Transmitter
向一个Receiver
发送MidiEvent
个对象。一个Transmitter
是 MIDI 事件的来源,一个Receiver
是事件的消费者。 Sequencer
是用于采集和回放 MIDI 事件序列的设备。它有发射器,因为它通常会将存储在序列中的 MIDI 信息发送到另一个设备,如合成器或 MIDI 输出端口。它也有接收器,因为它可以采集 MIDI 信息并将它们存储在序列中。(http://docs.oracle.com/javase/7/docs/technotes/guides/sound/programmer_guide/chapter8.html#118852
)。- 一个
Synthesizer
是用来产生声音的装置。它是javax.sound.midi
包中唯一产生音频数据的对象(http://docs.oracle.com/javase/7/docs/technotes/guides/sound/programmer_guide/chapter8.html#118852
)。
设备信息
通过查询MidiSystem
的DeviceInfo
对象列表,可以找到设备信息。每个信息对象包含诸如Name
和Vendor
的字段。您可以通过MidiSystem.getMidiDevice(info)
使用该信息对象找到实际设备。然后,可以向该设备查询其接收器和发送器以及其类型,如序列发生器或合成器。
一个恼人的部分是,你无法获得所有设备的发射机和接收机的列表,只有那些开放的。您可以请求默认的发送器和接收器,这将隐式地打开它们。因此,您可以看到,在请求缺省值之前,列表可能是空的,但是如果有缺省值,那么之后它就不是空的了!如果没有默认值,将抛出一个M
idiUnavailableException
异常。
该计划如下:
import javax.sound.midi.*;
import java.util.*;
public class DeviceInfo {
public static void main(String[] args) throws Exception {
MidiDevice.Info[] devices;
/*
MidiDevice.Info[] info = p.getDeviceInfo();
for (int m = 0; m < info.length; m++) {
System.out.println(info[m].toString());
}
*/
System.out.println("MIDI devices:");
devices = MidiSystem.getMidiDeviceInfo();
for (MidiDevice.Info info: devices) {
System.out.println(" Name: " + info.toString() +
", Decription: " +
info.getDescription() +
", Vendor: " +
info.getVendor());
MidiDevice device = MidiSystem.getMidiDevice(info);
if (! device.isOpen()) {
device.open();
}
if (device instanceof Sequencer) {
System.out.println(" Device is a sequencer");
}
if (device instanceof Synthesizer) {
System.out.println(" Device is a synthesizer");
}
System.out.println(" Open receivers:");
List<Receiver> receivers = device.getReceivers();
for (Receiver r: receivers) {
System.out.println(" " + r.toString());
}
try {
System.out.println("\n Default receiver: " +
device.getReceiver().toString());
System.out.println("\n Open receivers now:");
receivers = device.getReceivers();
for (Receiver r: receivers) {
System.out.println(" " + r.toString());
}
} catch(MidiUnavailableException e) {
System.out.println(" No default receiver");
}
System.out.println("\n Open transmitters:");
List<Transmitter> transmitters = device.getTransmitters();
for (Transmitter t: transmitters) {
System.out.println(" " + t.toString());
}
try {
System.out.println("\n Default transmitter: " +
device.getTransmitter().toString());
System.out.println("\n Open transmitters now:");
transmitters = device.getTransmitters();
for (Transmitter t: transmitters) {
System.out.println(" " + t.toString());
}
} catch(MidiUnavailableException e) {
System.out.println(" No default transmitter");
}
device.close();
}
Sequencer sequencer = MidiSystem.getSequencer();
System.out.println("Default system sequencer is " +
sequencer.getDeviceInfo().toString() +
" (" + sequencer.getClass() + ")");
Synthesizer synthesizer = MidiSystem.getSynthesizer();
System.out.println("Default system synthesizer is " +
synthesizer.getDeviceInfo().toString() +
" (" + synthesizer.getClass() + ")");
}
}
我的系统上的输出如下:
MIDI devices:
Name: Gervill, Decription: Software MIDI Synthesizer, Vendor: OpenJDK
Device is a synthesizer
Open receivers:
Default receiver: com.sun.media.sound.SoftReceiver@72f2a824
Open receivers now:
com.sun.media.sound.SoftReceiver@72f2a824
Open transmitters:
No default transmitter
Name: Real Time Sequencer, Decription: Software sequencer, Vendor: Oracle Corporation
Device is a sequencer
Open receivers:
Default receiver: com.sun.media.sound.RealTimeSequencer$SequencerReceiver@c23c5ff
Open receivers now:
com.sun.media.sound.RealTimeSequencer$SequencerReceiver@c23c5ff
Open transmitters:
Default transmitter: com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@4e13aa4e
Open transmitters now:
com.sun.media.sound.RealTimeSequencer$SequencerTransmitter@4e13aa4e
Default system sequencer is Real Time Sequencer
Default system synthesizer is Gervill
转储 MIDI 文件
这两个程序从jsresources.org
转储一个 MIDI 文件到控制台。MidiSystem
从文件中创建一个Sequence
。序列的每个轨道都循环播放,每个轨道中的每个事件都被检查。虽然可以就地打印,但是每个事件都被传递给一个Receiver
对象,在本例中是DumpReceiver
。该对象可以做任何事情,但在本例中只是将事件打印到stdout
。
The
DumpSequence.java
程序读取一个作为命令行参数给出的 MIDI 文件,并以可读形式将其内容列表转储到标准输出。它首先获取一个Sequence
并打印出关于序列的信息,然后依次获取每个轨道,打印出轨道的内容。
/*
* DumpSequence.java
*
* This file is part of jsresources.org
*/
/*
* Copyright (c) 1999, 2000 by Matthias Pfisterer
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.File;
import java.io.IOException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.Sequence;
import javax.sound.midi.Track;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Receiver;
public class DumpSequence
{
private static String[] sm_astrKeyNames = {"C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"};
private static Receiver sm_receiver = new DumpReceiver(System.out, true);
public static void main(String[] args) {
/*
* We check that there is exactely one command-line
* argument. If not, we display the usage message and
* exit.
*/
if (args.length != 1) {
out("DumpSequence: usage:");
out("\tjava DumpSequence <midifile>");
System.exit(1);
}
/*
* Now, that we're shure there is an argument, we take it as
* the filename of the soundfile we want to play.
*/
String strFilename = args[0];
File midiFile = new File(strFilename);
/*
* We try to get a Sequence object, which the content
* of the MIDI file.
*/
Sequence sequence = null;
try {
sequence = MidiSystem.getSequence(midiFile);
} catch (InvalidMidiDataException e) {
e.printStackTrace();
System.exit(1);
} catch (IOException e) {
e.printStackTrace();
System.exit(1);
}
/*
* And now, we output the data.
*/
if (sequence == null) {
out("Cannot retrieve Sequence.");
} else {
out("------------------------------------------------------------------------");
out("File: " + strFilename);
out("------------------------------------------------------------------------");
out("Length: " + sequence.getTickLength() + " ticks");
out("Duration: " + sequence.getMicrosecondLength() + " microseconds");
out("------------------------------------------------------------------------");
float fDivisionType = sequence.getDivisionType();
String strDivisionType = null;
if (fDivisionType == Sequence.PPQ) {
strDivisionType = "PPQ";
} else if (fDivisionType == Sequence.SMPTE_24) {
strDivisionType = "SMPTE, 24 frames per second";
} else if (fDivisionType == Sequence.SMPTE_25) {
strDivisionType = "SMPTE, 25 frames per second";
} else if (fDivisionType == Sequence.SMPTE_30DROP) {
strDivisionType = "SMPTE, 29.97 frames per second";
} else if (fDivisionType == Sequence.SMPTE_30) {
strDivisionType = "SMPTE, 30 frames per second";
}
out("DivisionType: " + strDivisionType);
String strResolutionType = null;
if (sequence.getDivisionType() == Sequence.PPQ) {
strResolutionType = " ticks per beat";
} else {
strResolutionType = " ticks per frame";
}
out("Resolution: " + sequence.getResolution() + strResolutionType);
out("------------------------------------------------------------------------");
Track[] tracks = sequence.getTracks();
for (int nTrack = 0; nTrack < tracks.length; nTrack++) {
out("Track " + nTrack + ":");
out("-----------------------");
Track track = tracks[nTrack];
for (int nEvent = 0; nEvent < track.size(); nEvent++) {
MidiEvent event = track.get(nEvent);
output(event);
}
out("--------------------------------------------------------------------");
}
}
}
public static void output(MidiEvent event) {
MidiMessage message = event.getMessage();
long lTicks = event.getTick();
sm_receiver.send(message, lTicks);
}
private static void out(String strMessage) {
System.out.println(strMessage);
}
}
/*** DumpSequence.java ***/
有几个网站有合法的免费 MIDI 文件。文件 http://files.mididb.com/amy-winehouse/rehab.mid
给出了结果。
---------------------------------------------------------------------------
File: rehab.mid
---------------------------------------------------------------------------
Length: 251475 ticks
Duration: 216788738 microseconds
---------------------------------------------------------------------------
DivisionType: PPQ
Resolution: 480 ticks per beat
---------------------------------------------------------------------------
Track 0:
-----------------------
tick 0: Time Signature: 4/4, MIDI clocks per metronome tick: 24, 1/32 per 24 MIDI clocks: 8
tick 0: Key Signature: C major
tick 0: SMTPE Offset: 32:0:0.0.0
tick 0: Set Tempo: 145.0 bpm
tick 0: End of Track
---------------------------------------------------------------------------
Track 1:
-----------------------
tick 0: Sequence/Track Name: amy winehouse - rehab
tick 0: Instrument Name: GM Device
tick 40: Sysex message: F0 7E 7F 09 01 F7
tick 40: End of Track
---------------------------------------------------------------------------
Track 2:
-----------------------
tick 0: MIDI Channel Prefix: 1
tick 0: Sequence/Track Name: amy winehouse - rehab
tick 0: Instrument Name: GM Device 2
tick 480: [B1 79 00] channel 2: control change 121 value: 0
tick 485: [B1 0A 40] channel 2: control change 10 value: 64
tick 490: [B1 5D 14] channel 2: control change 93 value: 20
tick 495: [B1 5B 00] channel 2: control change 91 value: 0
tick 500: [B1 0B 7F] channel 2: control change 11 value: 127
tick 505: [B1 07 69] channel 2: control change 7 value: 105
tick 510: [E1 00 40] channel 2: pitch wheel change 8192
tick 515: [B1 00 00] channel 2: control change 0 value: 0
tick 520: [C1 22] channel 2: program change 34
...
播放 MIDI 文件
要播放 MIDI 文件,您可以使用MidiSystem
从File
创建一个Sequence
。您还可以从MidiSystem
中创建一个Sequencer
,并将序列传递给它。音序器将通过其Transmitter
输出 MIDI 信息。这就完成了系统的 MIDI 事件发生端的设置。
游戏面是通过从MidiSystem
中获取一个Synthesizer
来构建的。从合成器中找到Receiver
,并将其提供给 MIDI 事件的发送器。通过调用音序器上的start()
开始播放,音序器从文件中读取并将 MIDI 事件传递给它的发送器。这些被传递到合成器的接收器并播放。图 18-1 显示了相关类的 UML 类图。
图 18-1。
Class diagram for the SimpleMidiPlayer
这段代码来自播放一个音频文件(简单)。原文被大量评论,但我已经删除了它的大部分印刷书籍。逻辑是从文件中加载一个序列,获得默认的序列器,并将该序列设置到序列器中。音序器不一定是合成器,但默认音序器通常是合成器。如果没有,你得到默认的合成器,然后将音序器的发射器连接到合成器的接收器。然后通过调用音序器上的start()
来播放 MIDI 文件。
/*
* SimpleMidiPlayer.java
*
* This file is part of jsresources.org
*/
/*
* Copyright (c) 1999 - 2001 by Matthias Pfisterer
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* - Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
* - Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
import java.io.File;
import java.io.IOException;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.MetaEventListener;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.Receiver;
import javax.sound.midi.Transmitter;
import javax.sound.midi.MidiChannel;
import javax.sound.midi.ShortMessage;
public class SimpleMidiPlayer {
private static Sequencer sm_sequencer = null;
private static Synthesizer sm_synthesizer = null;
public static void main(String[]args) {
if (args.length == 0 || args[0].equals("-h")) {
printUsageAndExit();
}
String strFilename = args[0];
File midiFile = new File(strFilename);
/*
* We read in the MIDI file to a Sequence object.
*/
Sequence sequence = null;
try {
sequence = MidiSystem.getSequence(midiFile);
}
catch(InvalidMidiDataException e) {
e.printStackTrace();
System.exit(1);
}
catch(IOException e) {
e.printStackTrace();
System.exit(1);
}
/*
* Now, we need a Sequencer to play the sequence.
* Here, we simply request the default sequencer.
* With an argument of false, it does not create
* a default syntesizer.
*/
try {
sm_sequencer = MidiSystem.getSequencer(false);
}
catch(MidiUnavailableException e) {
e.printStackTrace();
System.exit(1);
}
if (sm_sequencer == null) {
out("SimpleMidiPlayer.main(): can't get a Sequencer");
System.exit(1);
}
try {
sm_sequencer.open();
}
catch(MidiUnavailableException e) {
e.printStackTrace();
System.exit(1);
}
/*
* Next step is to tell the Sequencer which
* Sequence it has to play.
*/
try {
sm_sequencer.setSequence(sequence);
}
catch(InvalidMidiDataException e) {
e.printStackTrace();
System.exit(1);
}
Receiver synthReceiver = null;
if (!(sm_sequencer instanceof Synthesizer)) {
/*
* We try to get the default synthesizer, open()
* it and chain it to the sequencer with a
* Transmitter-Receiver pair.
*/
try {
sm_synthesizer = MidiSystem.getSynthesizer();
sm_synthesizer.open();
synthReceiver = sm_synthesizer.getReceiver();
Transmitter seqTransmitter = sm_sequencer.getTransmitter();
seqTransmitter.setReceiver(synthReceiver);
}
catch(MidiUnavailableException e) {
e.printStackTrace();
}
}
/*
* Now, we can start playing
*/
sm_sequencer.start();
try {
Thread.sleep(5000);
}
catch(InterruptedException e) {
e.printStackTrace();
}
}
private static void printUsageAndExit() {
out("SimpleMidiPlayer: usage:");
out("\tjava SimpleMidiPlayer <midifile>");
System.exit(1);
}
private static void out(String strMessage) {
System.out.println(strMessage);
}
}
Playing a file to an external MIDI synthesizer
我有一台 Edirol Studio Canvas SD-20 合成器,是花了几百澳币买的。它通过 USB 接口插入电脑。ALSA 通过以下几点认识到这一点:
$ amidi -l
Dir Device Name
IO hw:2,0,0 SD-20 Part A
IO hw:2,0,1 SD-20 Part B
I hw:2,0,2 SD-20 MIDI
MidiDevice.Info
设备信息列出hw:2,0,0
两次,一次用于输入,一次用于输出,其他值类似。设备信息可以通过toString
方法识别,该方法返回值如"SD20 [hw:2,0,0]"
。从设备信息中,可以发现该设备与使用MidiSystem.getMidiDevice(info)
前一样。输入和输出设备可以通过它支持的maxOutputReceivers
的数量来区分:零表示无,而任何其他值(包括–1!)表示它有一个 MIDI 接收器。选择外部接收器是通过代码来完成的,用以下代码替换合成器的先前设置:
Receiver synthReceiver = null;
MidiDevice.Info[] devices;
devices = MidiSystem.getMidiDeviceInfo();
for (MidiDevice.Info info: devices) {
System.out.println(" Name: " + info.toString() +
", Decription: " +
info.getDescription() +
", Vendor: " +
info.getVendor());
if (info.toString().equals("SD20 [hw:2,0,0]")) {
MidiDevice device = MidiSystem.getMidiDevice(info);
if (device.getMaxReceivers() != 0) {
try {
device.open();
System.out.println(" max receivers: " + device.getMaxReceivers());
receiver = device.getReceiver();
System.out.println("Found a receiver");
break;
} catch(Exception e) {}
}
}
}
if (receiver == null) {
System.out.println("Receiver is null");
System.exit(1);
}
try {
Transmitter seqTransmitter = sm_sequencer.getTransmitter();
seqTransmitter.setReceiver(receiver);
}
catch(MidiUnavailableException e) {
e.printStackTrace();
}
/*
* Now, we can start playing as before
*/
改变音色库
音色库是一组以某种方式编码的“声音”,用于生成播放的音乐。Java 的默认声音合成器是 Gervill 合成器,它在$HOME/.gervill/soundbank-emg.sf2
中寻找默认的声音库。这个默认的声音库很小;只有 1.9MB 大小。而且听起来,嗯,质量很差。
DaWicked1 在“Better Java-midi instrument sounds for Linux”(www.minecraftforum.net/forums/mapping-and-modding/mapping-and-modding-tutorials/1571330-better-java-midi-instrument-sounds-for-linux
)中提供了两种方法来改善这一点:更简单的方法是用更好的字体(如 FluidSynth 字体)替换声音字体,使用默认名称。
第二种方法是编程式的,可能更好,因为它允许在运行时有更多的灵活性和选择。
改变音调和速度
更改 MIDI 文件的回放速度意味着更改从音序器发送 MIDI 信息的速率。Java 序列器有控制这种情况的方法,比如setTempoFactor
。序列器将通过以不同的速率发送消息来响应此方法。
可以通过改变NOTE_ON
和NOTE_OFF
消息的音调来改变音符的音调。这不仅要为将来的音符做,也要为当前正在播放的音符做。幸运的是,有一个名为弯音的 MIDI 命令,它可以被发送到合成器,以改变所有当前播放和未来音符的音高。弯音值 0x2000 对应于没有音高变化;高达 0x4000 的值表示音高增加,低于 0x 4000 的值表示音高减少。有许多网站给出了复杂的公式,但最简单的似乎是 MIDI 弯音范围( www.ultimatemetal.com/forum/threads/midi-pitch-bend-range.680677/
),它指出 683 的音高变化大致是一个半音。因此,您更改音高值并向接收器发送一个新的弯音事件。
你寻找来自ʿ的用户的输入。然后这些调用适当的方法。说明这一点的程序是对本章前面给出的SimpleMidiPlayer
的改编,称为AdjustableMidiPlayer.java
。在正文中,您将对sleep
的调用替换为以下内容:
BufferedReader br = new BufferedReader(new
InputStreamReader(System.in));
String str = null;
System.out.println("Enter lines of text.");
System.out.println("Enter 'stop' to quit.");
do {
try {
str = br.readLine();
if (str.length() >= 2) {
byte[] bytes = str.getBytes();
if (bytes[0] == 27 && bytes[1] == 91) {
if (bytes[2] == 65) {
// up
increasePitch();
} else if (bytes[2] == 66) {
// down
decreasePitch();
} else if (bytes[2] == 67) {
//right
faster();
} else if (bytes[2] == 68) {
//left
slower();
}
}
}
} catch(java.io.IOException e) {
}
} while(!str.equals("stop"));
}
where the new functions are given by
private void increasePitch() {
// 683 from www.ultimatemetal.com/forum/threads/midi-pitch-bend-range.680677/
pitch += 683;
for (int n = 0; n < 16; n++) {
try {
MidiMessage msg =
new ShortMessage(ShortMessage.PITCH_BEND,
n,
pitch & 0x7F, pitch >> 7);
synthReceiver.send(msg, 0);
} catch (Exception e) {
}
}
}
private void decreasePitch() {
// 683 from www.ultimatemetal.com/forum/threads/midi-pitch-bend-range.680677/
pitch -= 683;
for (int n = 0; n < 16; n++) {
try {
MidiMessage msg =
new ShortMessage(ShortMessage.PITCH_BEND,
n,
pitch & 0x7F, pitch >> 7);
synthReceiver.send(msg, 0);
} catch (Exception e) {
}
}
}
float speed = 1.0f;
private void faster() {
speed *= 1.2f;
sm_sequencer.setTempoFactor(speed);
}
private void slower() {
speed /= 1.2f;
sm_sequencer.setTempoFactor(speed);
}
使用 TiMidity 代替默认的格维尔合成器
软合成器 TiMidity 可以作为后端合成器运行,使用 ALSA 序列器,如下所示:
$timidity -iA -B2,8 -Os -EFreverb=0
Opening sequencer port: 128:0 128:1 128:2 128:3
(对于 FluidSynth 来说也差不多。)这是在端口 128:0 上打开的,依此类推。
不幸的是,这对于 Java Sound 来说是不可见的,Java Sound 要么需要默认的 Gervill 合成器,要么需要原始的 MIDI 合成器,比如硬件合成器。正如在第十九章中所讨论的,你可以通过使用 ALSA 原始 MIDI 端口来解决这个问题。
您可以通过以下方式添加原始 MIDI 端口:
modprobe snd-seq snd-virmidi
这将把虚拟设备带入 ALSA 原始 MIDI 和 ALSA 音序器空间:
$amidi -l
Dir Device Name
IO hw:3,0 Virtual Raw MIDI (16 subdevices)
IO hw:3,1 Virtual Raw MIDI (16 subdevices)
IO hw:3,2 Virtual Raw MIDI (16 subdevices)
IO hw:3,3 Virtual Raw MIDI (16 subdevices)
$aplaymidi -l
Port Client name Port name
14:0 Midi Through Midi Through Port-0
28:0 Virtual Raw MIDI 3-0 VirMIDI 3-0
29:0 Virtual Raw MIDI 3-1 VirMIDI 3-1
30:0 Virtual Raw MIDI 3-2 VirMIDI 3-2
31:0 Virtual Raw MIDI 3-3 VirMIDI 3-3
虚拟原始 MIDI 端口 3-0 可以通过以下方式连接到 TiMidity 端口 0:
aconnect 28:0 128:0
玩到 TiMidity 的最后一步是把AdaptableMidiPlayer.java
的一行字由此改过来:
if (info.toString().equals("SD20 [hw:2,0,0]")) {
对此:
if (info.toString().equals("VirMIDI [hw:3,0,0]")) {
结论
本章使用 MIDI API 构建了一些程序,并讨论了如何使用外部硬件合成器和软合成器,如 TiMidity。
十九、MIDI ALSA
ALSA 通过音序器 API 为 MIDI 设备提供了一些支持。客户端可以向音序器发送 MIDI 事件,音序器将根据事件的时间播放它们。然后,其他客户端可以接收这些有序事件,例如,合成它们。
资源
以下是一些资源:
- ALSA 序列器”(
www.alsa-project.org/~frank/alsa-sequencer/index.html
),一份设计文件。 - 《ALSA 编程 how to》(
www.suse.de/~mana/alsa090_howto.html
)包括编写音序器客户端、MIDI 路由器、PCM 和 MIDI 结合(miniFMsynth)、调度 MIDI 事件(miniArp)。 - MIDI 序列器 API (
http://alsa-project.org/alsa-doc/alsa-lib/group___sequencer.html
。 - 序列器接口(
http://alsa-project.org/alsa-doc/alsa-lib/seq.html
)。 - ALSA 音序系统(
www.alsa-project.org/~tiwai/lk2k/lk2k.html
)是岩井隆之对音序系统的深入见解。
ALSA 音序器客户端
ALSA 提供了一个音序器,可以从一组客户端接收 MIDI 事件,并根据事件中的定时信息播放给其他客户端。能够发送此类事件的客户端是文件读取器,如aplaymidi
或其他序列器。客户端也可以读取应该播放的事件。可能的消费客户端包括分离器、路由器或软合成器,如 TiMidity。
TiMidity 可以运行一个 ALSA 音序器客户端,它会消耗 MIDI 事件并合成,根据 http://linux-audio.com/TiMidity-howto.html
。
timidity -iA -B2,8 -Os -EFreverb=0
在我的计算机上,这产生了以下内容:
Requested buffer size 2048, fragment size 1024
ALSA pcm 'default' set buffer size 2048, period size 680 bytes
TiMidity starting in ALSA server mode
Opening sequencer port: 129:0 129:1 129:2 129:3
然后它坐在那里等待连接。
FluidSynth 也可以用作服务器(参见http://tedfelix.com/linux/linux-midi.html
Ted 的 Linux MIDI 指南)。
fluidsynth --server --audio-driver=alsa -C0 -R1 -l /usr/share/soundfonts/FluidR3_GM.sf2
ALSA 音序器发送 MIDI“连线”事件。这不包括 MIDI 文件事件,如文本或歌词元事件。这使得它对于 Karaoke 播放器来说毫无用处。可以修改文件阅读器aplaymid
来将元事件发送给一个监听器(比如 Java MetaEventListener
),但是由于这些事件来自文件阅读器而不是序列发生器,它们通常会在被排序播放之前到达。可惜。
像pykaraoke
这样的程序利用了 ALSA 序列器。然而,为了获得正确的歌词定时,它包括一个 MIDI 文件解析器,基本上作为第二个序列器,只提取和显示文本/歌词事件。
connect(连接)
程序aconnect
可以用来列出 sequencer 服务器和客户机,比如 sequencer。我已经设置了两个客户端运行:TiMidity 和 seqdemo(稍后讨论)。这个命令
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 '
client 129: 'ALSA Sequencer Demo' [type=user]
0 'ALSA Sequencer Demo'
当使用-i
选项运行时,它会产生以下结果:
$aconnect -i
client 0: 'System' [type=kernel]
0 'Timer '
1 'Announce '
client 14: 'Midi Through' [type=kernel]
0 'Midi Through Port-0'
程序aconnect
可以通过以下方式在输入和输出客户端之间建立连接:
aconnect in out
seqdemo
Matthias Nagorni 的“ALSA 编程指南”中的程序seqdemo.c
是一个基本的序列器客户端。它打开一个 MIDI 声音序列器客户端,然后进入一个轮询循环,打印关于收到的 MIDI 事件的信息。它简单介绍了 ALSA MIDI API。
seqdemo.c
的代码如下:
/* seqdemo.c by Matthias Nagorni */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <alsa/asoundlib.h>
snd_seq_t *open_seq();
void midi_action(snd_seq_t *seq_handle);
snd_seq_t *open_seq() {
snd_seq_t *seq_handle;
int portid;
if (snd_seq_open(&seq_handle, "default", SND_SEQ_OPEN_INPUT, 0) < 0) {
fprintf(stderr, "Error opening ALSA sequencer.\n");
exit(1);
}
snd_seq_set_client_name(seq_handle, "ALSA Sequencer Demo");
if ((portid = snd_seq_create_simple_port(seq_handle, "ALSA Sequencer Demo",
SND_SEQ_PORT_CAP_WRITE|SND_SEQ_PORT_CAP_SUBS_WRITE,
SND_SEQ_PORT_TYPE_APPLICATION)) < 0) {
fprintf(stderr, "Error creating sequencer port.\n");
exit(1);
}
return(seq_handle);
}
void midi_action(snd_seq_t *seq_handle) {
snd_seq_event_t *ev;
do {
snd_seq_event_input(seq_handle, &ev);
switch (ev->type) {
case SND_SEQ_EVENT_CONTROLLER:
fprintf(stderr, "Control event on Channel %2d: %5d \r",
ev->data.control.channel, ev->data.control.value);
break;
case SND_SEQ_EVENT_PITCHBEND:
fprintf(stderr, "Pitchbender event on Channel %2d: %5d \r",
ev->data.control.channel, ev->data.control.value);
break;
case SND_SEQ_EVENT_NOTEON:
fprintf(stderr, "Note On event on Channel %2d: %5d \r",
ev->data.control.channel, ev->data.note.note);
break;
case SND_SEQ_EVENT_NOTEOFF:
fprintf(stderr, "Note Off event on Channel %2d: %5d \r",
ev->data.control.channel, ev->data.note.note);
break; ALSA Programming HOWTO
}
snd_seq_free_event(ev);
} while (snd_seq_event_input_pending(seq_handle, 0) > 0);
}
int main(int argc, char *argv[]) {
snd_seq_t *seq_handle;c
int npfd;
struct pollfd *pfd;
seq_handle = open_seq();
npfd = snd_seq_poll_descriptors_count(seq_handle, POLLIN);
pfd = (struct pollfd *)alloca(npfd * sizeof(struct pollfd));
snd_seq_poll_descriptors(seq_handle, pfd, npfd, POLLIN);
while (1) {
if (poll(pfd, npfd, 100000) > 0) {
midi_action(seq_handle);
}
}
}
阿普莱米迪
该程序aplaymidi
将发挥后端 MIDI 合成器,如 TiMidity。它需要一个端口名,可以通过以下内容找到:
aplaymidi -l
输出将如下所示:
Port Client name Port name
14:0 Midi Through Midi Through Port-0
128:0 TiMidity TiMidity port 0
128:1 TiMidity TiMidity port 1
128:2 TiMidity TiMidity port 2
128:3 TiMidity TiMidity port 3
131:0 aseqdump aseqdump
端口号与aconnect
使用的端口号相同。这些不是 ALSA 设备名(hw:0
等等),而是 ALSA 序列器 API 所特有的。
然后,它可以向其中一个端口播放 MIDI 文件,如下所示:
aplaymidi -p 128:0 54154.mid
代码可以在 SourceArchive.com(http://alsa-utils.sourcearchive.com/documentation/1.0.8/aplaymidi_8c-source.html
)找到。
原始 MIDI 端口
根据 RawMidi 接口( www.alsa-project.org/alsa-doc/alsa-lib/rawmidi.html
),RawMidi 接口“被设计为通过 Midi 线路写入或读取原始(未改变的)Midi 数据,而没有在接口中定义的任何时间戳。”
原始 MIDI 物理设备
raw MIDI 接口通常用于管理硬件 MIDI 设备。例如,如果我将 Edirol SD-20 合成器插入 USB 端口,它会在amidi
下显示如下:
$amidi -l
Dir Device Name
IO hw:2,0,0 SD-20 Part A
IO hw:2,0,1 SD-20 Part B
I hw:2,0,2 SD-20 MIDI
这些名称使用与hw:...
的 ALSA 回放和记录设备相同的模式。
原始 MIDI 虚拟设备
Linux 内核模块snd_virmidi
可以创建虚拟的原始 MIDI 设备。首先添加模块(参见 https://wiki.allegro.cc/index.php?title=Using_TiMidity%2B%2B_with_ALSA_raw_MIDI
和AlsaMidiOverview [
http://alsa.opensrc.org/AlsaMidiOverview
)。
modprobe snd-seq snd-virmidi
这将把虚拟设备带入 ALSA 原始 MIDI 和 ALSA 音序器空间:
$amidi -l
Dir Device Name
IO hw:3,0 Virtual Raw MIDI (16 subdevices)
IO hw:3,1 Virtual Raw MIDI (16 subdevices)
IO hw:3,2 Virtual Raw MIDI (16 subdevices)
IO hw:3,3 Virtual Raw MIDI (16 subdevices)
$aplaymidi -l
Port Client name Port name
14:0 Midi Through Midi Through Port-0
28:0 Virtual Raw MIDI 3-0 VirMIDI 3-0
29:0 Virtual Raw MIDI 3-1 VirMIDI 3-1
30:0 Virtual Raw MIDI 3-2 VirMIDI 3-2
31:0 Virtual Raw MIDI 3-3 VirMIDI 3-3
将 MIDI 客户端映射到 MIDI 原始空间
一些程序/API 使用 ALSA 序列器空间;其他人使用 ALSA raw MIDI 空间。虚拟端口允许使用一个空间的客户端使用不同空间的客户端。
例如,TiMidity 可以作为 sequencer 客户端运行,如下所示:
timidity -iA -B2,8 -Os -EFreverb=0
这仅显示在音序器空间中,不显示在原始 MIDI 空间中,并显示给aconnect -o
如下:
$aconnect -o
client 14: 'Midi Through' [type=kernel]
0 'Midi Through Port-0'
client 28: 'Virtual Raw MIDI 3-0' [type=kernel]
0 'VirMIDI 3-0 '
client 29: 'Virtual Raw MIDI 3-1' [type=kernel]
0 'VirMIDI 3-1 '
client 30: 'Virtual Raw MIDI 3-2' [type=kernel]
0 'VirMIDI 3-2 '
client 31: 'Virtual Raw MIDI 3-3' [type=kernel]
0 'VirMIDI 3-3 '
client 128: 'TiMidity' [type=user]
0 'TiMidity port 0 '
1 'TiMidity port 1 '
2 'TiMidity port 2 '
3 'TiMidity port 3 '
aconnect -i
显示虚拟端口如下:
$aconnect -i
client 0: 'System' [type=kernel]
0 'Timer '
1 'Announce '
client 14: 'Midi Through' [type=kernel]
0 'Midi Through Port-0'
client 28: 'Virtual Raw MIDI 3-0' [type=kernel]
0 'VirMIDI 3-0 '
client 29: 'Virtual Raw MIDI 3-1' [type=kernel]
0 'VirMIDI 3-1 '
client 30: 'Virtual Raw MIDI 3-2' [type=kernel]
0 'VirMIDI 3-2 '
client 31: 'Virtual Raw MIDI 3-3' [type=kernel]
0 'VirMIDI 3-3 '
然后,虚拟原始 MIDI 3-0 可以通过以下方式连接到 TiMidity 端口 0:
aconnect 28:0 128:0
然后客户可以发送 MIDI 信息到原始的 MIDI 设备hw:3,0
,TiMidity 会合成它们。在前一章中,我通过展示如何用 TiMidity 替换默认的 Java 合成器来使用它。
关闭所有笔记
如果你在一个设备或软合成器上播放某个东西,那么如果这个东西被打断,它可能无法干净地播放完。例如,它可能在某个通道上启动了一个NOTE ON
,但由于中断,它不会发送一个通知。合成器将继续播放音符。
要停止播放,使用amidi
发送“原始”MIDI 命令。十六进制序列 00 B0 7B 00 将发送“通道 0 上的所有音符关闭”类似地,命令 00 B1 7B 00 将发送“通道 1 上的所有音符关闭”,并且只有 16 个可能的通道。
端口hw:1,0
上原始设备的相关命令如下:
amidi -p hw:1,0 -S "00 B0 7B 00"
...
结论
本章简要讨论了 ALSA 下可用的 MIDI 模型。虽然这背后有一个重要的编程 API,但您主要使用了命令amidi
、aplaymidi
和aconnect
,并且看到了使用seqdemo.c
程序的 API。
二十、FluidSynth
FluidSynth 是一个播放 MIDI 文件的应用,也是一个 MIDI 应用库。
资源
以下是一些资源:
- FluidSynth 主页(
http://sourceforge.net/apps/trac/fluidsynth/
) - FluidSynth 下载页面(
http://sourceforge.net/projects/fluidsynth/
) - FluidSynth 1.1 开发者文档(
http://fluidsynth.sourceforge.net/api/
)
- Sourcearchive.com
fluidsynth
文档(http://fluidsynth.sourcearchive.com/documentation/1.1.5-1/main.html
)
演员
fluidsynth
是一个命令行 MIDI 播放器。它在 ALSA 的命令行下运行,如下所示:
fluidsynth -a alsa -l <soundfont> <files...>
一种常用的声音字体是/usr/share/sounds/sf2/FluidR3_GM.sf2
。
Qsynth 是fluidsynth
的 GUI 界面。看起来像图 20-1 。
图 20-1。
Qsynth
播放 MIDI 文件
FluidSynth API 包括以下内容:
- 使用
new_fluid_player
创建的音序器 - 使用
new_fluid_synth
创建的合成器 - 使用
new_fluid_audio_driver
创建的音频播放器,它运行在一个单独的线程中 - 一个“设置”对象,可用于控制其他组件的许多功能,由
new_fluid_settings
创建,并由fluid_settings_setstr
等调用修改
使用 ALSA 播放 MIDI 文件序列的典型程序如下。它创建各种对象,设置音频播放器使用 ALSA,然后将每个声音字体和 MIDI 文件添加到播放器中。然后对fluid_player_play
的调用依次播放每个 MIDI 文件。
#include <fluidsynth.h>
#include <fluid_midi.h>
int main(int argc, char** argv)
{
int i;
fluid_settings_t* settings;
fluid_synth_t* synth;
fluid_player_t* player;
fluid_audio_driver_t* adriver;
settings = new_fluid_settings();
fluid_settings_setstr(settings, "audio.driver", "alsa");
synth = new_fluid_synth(settings);
player = new_fluid_player(synth);
adriver = new_fluid_audio_driver(settings, synth);
/* process command line arguments */
for (i = 1; i < argc; i++) {
if (fluid_is_soundfont(argv[i])) {
fluid_synth_sfload(synth, argv[1], 1);
} else {
fluid_player_add(player, argv[i]);
}
}
/* play the midi files, if any */
fluid_player_play(player);
/* wait for playback termination */
fluid_player_join(player);
/* cleanup */
delete_fluid_audio_driver(adriver);
delete_fluid_player(player);
delete_fluid_synth(synth);
delete_fluid_settings(settings);
return 0;
}
计算机编程语言
pyFluidSynth
是一个绑定到 FluidSynth 的 Python,允许你向 FluidSynth 发送 MIDI 命令。
结论
本章简要讨论了 FluidSynth 的编程模型和 API。
二十一、TiMidity
TiMidity 被设计成一个独立的应用。为此,您应该构建一个新的“接口”它也可以被颠覆,就好像它是一个可以被调用的库。本章解释了这两种方式。
TiMidity 设计
TiMidity 被设计成一个独立的应用。当它被构建时,你得到了一个可执行文件,但是没有一个可以被调用的函数库,不像 FluidSynth。
TiMidity 你能做的就是增加不同的接口。例如,有 ncurses、Xaw 和哑接口可以在运行时调用,例如:
timidity -in ...
timidity -ia ...
timidity -id ...
还有其他更专业的应用,如 WRD、emacs、ALSA 和远程接口。
例如,Xaw 接口看起来如图 21-1 所示。
图 21-1。
TiMidity with Xaw interface
这个想法似乎是,如果你想要额外的东西,也许你应该建立一个自定义界面,并驱动它从 TiMidity。
这并不总是适合我,因为我更喜欢能够以一种简单的方式将 TiMidity 嵌入到我自己的应用中。本章的其余部分从两方面来看。
- 将 TiMidity 转化为一个库,并将其包含在您自己的代码中
- 构建您自己的界面
把 TiMidity 变成图书馆
TiMidity 不是被设计成一个库,所以你必须说服它。这并不难;你只需要摆弄一下构建系统。
托管环境挂钩
一个由应用控制的系统在一个受管理的环境中工作得不是很好,比如 Windows(或者更近一些的环境,比如 Android)。在这样的环境中,你不能调用 TiMidity 的main
,而是调用属于框架的main
函数。这将依次调用应用中的适当函数。
要使用这样的钩子,你需要下载 TiMidity 的源代码,要么从包管理器,要么从 TiMidity++网站( http://timidity.sourceforge.net/
)。
出于谨慎,main
函数的变体在文件timidity/timidity.c
中。被各种define
控制,可以有main
或者win_main
。更有趣的定义之一是ANOTHER_MAIN
。如果这样定义,那么main
函数的任何版本都不会被编译,你会得到一个自由主对象模块。
如果从顶级源目录按以下方式构建 TiMidity,将产生一个错误,即main
函数未定义:
CFLAGS="-DANOTHER_MAIN" ./configure
make
这是一个钩子,你需要把 TiMidity 从一个独立的应用变成能够被另一个应用调用的库。请注意,您不能仅仅从构建中删除timidity/timidity.c
。该文件包含太多其他关键功能!
建造图书馆
要将 TiMidity 构建为一个静态库,移除前面所示的main
函数,并尝试构建 TiMidity。我发现我还需要指定我想要使用的输出系统,比如 ALSA。
CFLAGS="-DANOTHER_MAIN" ./configure --enable-audio=alsa
nake clean
make
这构建了几个.ar
文件和许多对象.o
模块,但无法构建最终的timidity
可执行文件,因为(当然)没有main
函数。它还会在timidity
子目录中留下一堆未链接的文件。
通过从 TiMidity 源目录的顶部运行以下命令,可以将所有的目标模块收集到一个归档文件中:
ar cru libtimidity.a */*.o
ranlib libtimidity.a
因为您必须从源代码开始构建 TiMidity,所以在您尝试构建这个备选库版本之前,请检查它是否在正常模式下工作。通过这种方式,你可以发现你需要,比如说,libasound-dev
库来使用 ALSA,在你被其他东西弄混之前!
图书馆入口点
用ANOTHER_MAIN
构建的 TiMidity 暴露了这些公共入口点:
void timidity_start_initialize(void);
int timidity_pre_load_configuration(void);
int timidity_post_load_configuration(void);
void timidity_init_player(void);
int timidity_play_main(int nfiles, char **files);
int got_a_configuration;
它们似乎没有在任何方便的头文件中定义。
最小的应用
真正 TiMidity 的应用被编码为在许多不同的操作系统上使用许多不同版本的库。如前所述,在构建目标文件和库的过程中,大部分依赖项都被考虑进去了。
一个最小的应用只是在my_main.c
中的库入口点周围包装你自己的main
。
#include <stdio.h>
extern void timidity_start_initialize(void);
extern int timidity_pre_load_configuration(void);
extern int timidity_post_load_configuration(void);
extern void timidity_init_player(void);
extern int timidity_play_main(int nfiles, char **files);
extern int got_a_configuration;
int main(int argc, char **argv)
{
int err, main_ret;
timidity_start_initialize();
if ((err = timidity_pre_load_configuration()) != 0)
return err;
err += timidity_post_load_configuration();
if (err) {
printf("couldn't load configuration file\n");
exit(1);
}
timidity_init_player();
main_ret = timidity_play_main(argc, argv);
return main_ret;
}
compile
命令需要引入 TiMidity 库和任何其他需要的库,用于 ALSA 应用。
my_timidity: my_main.o
gcc -g -o my_timidity my_main.o libtimidity.a -lasound -lm
向 MIDI 文件播放背景视频
作为一个更复杂的例子,让我们看看在播放 MIDI 文件的同时播放一个视频文件。假设视频文件没有音频成分,并且没有尝试在两个流之间执行任何同步——这是额外的复杂性!
要播放视频文件,您可以使用 FFmpeg 库将视频流解码为视频帧。然后,您需要在某种 GUI 对象中显示这些帧,有许多工具包可以做到这一点。我选择了 Gtk 工具包,因为它是 Gnome 的基础,是 C 语言的,支持许多其他东西,比如 i18n 等等。我的代码是基于斯蒂芬·德朗格的“一个 ffmpeg 和 SDL 教程”( http://dranger.com/ffmpeg/
),它使用了 SDL 工具包来显示。
这使用pthreads
包在单独的线程中运行视频和 MIDI。我通过硬编码文件名和固定视频帧的大小来作弊。让它在 Gtk 3.0 下工作真的很糟糕,因为 Gtk 3.0 已经移除了像素图,而且花了太多太长的时间才发现发生了什么。
我将代码分成了两个文件,一个使用 Gtk 播放视频,另一个播放 TiMidity 库并调用视频。视频播放文件为video_code.c
。此处省略了代码,因为它本质上是第十五章中描述的代码。
文件video_player.c
设置 TiMidity 环境,调用视频在后台播放,然后调用play_midi
。内容如下:
#include <string.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
void timidity_start_initialize(void);
int timidity_pre_load_configuration(void);
int timidity_post_load_configuration(void);
void timidity_init_player(void);
void *init_gtk(void *args);
void init_ffmpeg();
#define MIDI_FILE "54154.mid"
static void *play_midi(void *args) {
char *argv[1];
argv[0] = MIDI_FILE;
int argc = 1;
timidity_play_main(argc, argv);
printf("Audio finished\n");
exit(0);
}
int main(int argc, char** argv)
{
int i;
/* Timidity stuff */
int err;
timidity_start_initialize();
if ((err = timidity_pre_load_configuration()) == 0) {
err = timidity_post_load_configuration();
}
if (err) {
printf("couldn't load configuration file\n");
exit(1);
}
timidity_init_player();
init_ffmpeg();
pthread_t tid_gtk;
pthread_create(&tid_gtk, NULL, init_gtk, NULL);
play_midi(NULL);
return 0;
}
构建新界面
前面的部分播放了 MIDI 和背景视频,本质上是作为独立的应用,作为独立的非交互线程。TiMidity 允许一个用户界面的更大集成,可以动态地添加到 TiMidity 中。
共享对象
你可以构建自己的接口,添加到 TiMidity 中,而不用改变或重新编译 TiMidity。这样的接口被构建为可动态加载的共享库,当 TiMidity 开始时被加载。
你必须小心编译和链接标志来构建这些库(参见 http://stackoverflow.com/questions/7252550/loadable-bash-builtin
“在 Linux 中构建共享对象”)。为了从my_interface.c
构建共享对象if_my_interface.so
,我使用了以下代码:
gcc -fPIC $(CFLAGS) -c -o my_interface.o my_interface.c
gcc -shared -o if_my_interface.so my_interface.o
TiMidity 只会加载以if_
开头的文件。它们可以驻留在任何目录中,默认为类似于/usr/lib/timidity
或/usr/local/lib/timidity
(参见timidity -h
中的“支持的动态加载接口”目录)。
加载动态模块的默认目录可以用选项-d
覆盖,如下所示:
timidity -d. -im --trace 54154.mid
入口点
每个接口必须有一个可以被动态加载器调用的唯一函数。回想一下,使用命令行选项-i
选择接口,例如timidity -iT ...
,以选择 VT100 接口。你的接口必须有一个不被任何其他接口使用的 ASCII 字母标识符,比如说m
代表“我的接口”然后加载程序将寻找一个函数,如下所示,其中函数名中的m
是标识符:
ControlMode *interface_m_loader(void)
这个函数很简单:它只是返回类型为ControlMode
的结构的地址,该结构在接口代码的其他地方定义。
ControlMode *interface_m_loader(void)
{
return &ctl;
}
控制方式
ControlMode
结构如下:
typedef struct {
char *id_name, id_character;
char *id_short_name;
int verbosity, trace_playing, opened;
int32 flags;
int (*open)(int using_stdin, int using_stdout);
void (*close)(void);
int (*pass_playing_list)(int number_of_files, char *list_of_files[]);
int (*read)(int32 *valp);
int (*write)(char *buf, int32 size);
int (*cmsg)(int type, int verbosity_level, char *fmt, ...);
void (*event)(CtlEvent *ev); /* Control events */
} ControlMode;
这定义了关于接口和一组函数的信息,这些函数由 TiMidity 调用,以响应 TiMidity 内的事件和动作。例如,对于“我的界面”,该结构如下:
ControlMode ctl=
{
"my interface", 'm',
"my iface",
1, /* verbosity */
0, /* trace playing */
0, /* opened */
0, /* flags */
ctl_open,
ctl_close,
pass_playing_list,
ctl_read,
NULL, /* write */
cmsg,
ctl_event
};
这些领域有些是显而易见的,但有些则不那么明显。
open
- 调用这个函数来设置哪些文件用于 I/O。
close
- 这叫做关闭它们。
pass_playing_list
- 这个函数被传递了一个要播放的文件列表。最有可能的操作是遍历这个列表,对每个列表调用
play_midi_file
。
read
- 我还不确定这是干什么用的。
write
- 我还不确定这是干什么用的。
cmsg
- 这被称为信息消息。
event
- 这是处理 MIDI 控制事件的主要功能。通常,对于每种类型的控制事件,它都是一个大开关。
包括文件
这太乱了。一个典型的接口将需要知道 TiMidity 使用的一些常数和函数。虽然这些是为 TiMidity 而有逻辑地组织起来的,但是对于一个新的界面来说,它们不是方便地组织起来的。所以,你必须不断地引入额外的包含,这些额外的包含指向其他外部的,需要更多的包含,等等。这些可能在不同的目录中,比如timidity
和utils
,所以你必须指向许多不同的包含目录。
请注意,您将需要 TiMidity 的源代码来获得这些包含文件;你可以从 SourceForge TiMidity++下载( http://sourceforge.net/projects/timidity/?source=dlp
)。
我的简单界面
这基本上和 TiMidity 中内置的“哑”接口做的是一样的。它是从当前目录加载的,并通过以下命令调用:
timidity -im -d. 54154.mid
代码在一个文件中,my_interface.c
。
下面的代码中有两个主要的函数,其余的都省略了。重要的功能有ctl_event
和ctl_lyric
。函数ctl_event
处理 TiMidity 产生的事件。对于这个接口,您只想在播放时打印歌词,所以当一个CTLE_LYRIC
事件发生时,调用ctl_lyric
。ctl_lyric
功能使用 TiMidity 功能event2string
查找歌词,并将其打印到输出,如果需要,根据歌词文本打印换行符。接口文件如下所示:
/*
my_interface.c
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif /* HAVE_CONFIG_H */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>
#ifndef NO_STRING_H
#include <string.h>
#else
#include <strings.h>
#endif
#include "support.h"
#include "timidity.h"
#include "output.h"
#include "controls.h"
#include "instrum.h"
#include "playmidi.h"
#include "readmidi.h"
static int ctl_open(int using_stdin, int using_stdout);
static void ctl_close(void);
static int ctl_read(int32 *valp);
static int cmsg(int type, int verbosity_level, char *fmt, ...);
static void ctl_total_time(long tt);
static void ctl_file_name(char *name);
static void ctl_current_time(int ct);
static void ctl_lyric(int lyricid);
static void ctl_event(CtlEvent *e);
static int pass_playing_list(int number_of_files, char *list_of_files[]);
#define ctl karaoke_control_mode
ControlMode ctl=
{
"my interface", 'm',
"my iface",
1, /* verbosity */
0, /* trace playing */
0, /* opened */
0, /* flags */
ctl_open,
ctl_close,
pass_playing_list,
ctl_read,
NULL, /* write */
cmsg,
ctl_event
};
static FILE *outfp;
int karaoke_error_count;
static char *current_file;
struct midi_file_info *current_file_info;
static int pass_playing_list(int number_of_files, char *list_of_files[]) {
int n;
for (n = 0; n < number_of_files; n++) {
printf("Playing list %s\n", list_of_files[n]);
current_file = list_of_files[n];
play_midi_file( list_of_files[n]);
}
return 0;
}
/*ARGSUSED*/
static int ctl_open(int using_stdin, int using_stdout)
{
// sets output channel and prints info about the file
}
static void ctl_close(void)
{
// close error channel
}
/*ARGSUSED*/
static int ctl_read(int32 *valp)
{
return RC_NONE;
}
static int cmsg(int type, int verbosity_level, char *fmt, ...)
{
// prints an error message
return 0;
}
static void ctl_total_time(long tt)
{
// counts playing time
}
static void ctl_file_name(char *name)
{
// prints playing status
}
static void ctl_current_time(int secs)
{
// keeps track of current time
}
static void ctl_lyric(int lyricid)
{
char *lyric;
current_file_info = get_midi_file_info(current_file, 1);
lyric = event2string(lyricid);
if(lyric != NULL)
{
if(lyric[0] == ME_KARAOKE_LYRIC)
{
if(lyric[1] == '/' || lyric[1] == '\\')
{
fprintf(outfp, "\n%s", lyric + 2);
fflush(outfp);
}
else if(lyric[1] == '@')
{
if(lyric[2] == 'L')
fprintf(outfp, "\nLanguage: %s\n", lyric + 3);
else if(lyric[2] == 'T')
fprintf(outfp, "Title: %s\n", lyric + 3);
else
fprintf(outfp, "%s\n", lyric + 1);
}
else
{
fputs(lyric + 1, outfp);
fflush(outfp);
}
}
else
{
if(lyric[0] == ME_CHORUS_TEXT || lyric[0] == ME_INSERT_TEXT)
fprintf(outfp, "\r");
fputs(lyric + 1, outfp);
fflush(outfp);
}
}
}
static void ctl_event(CtlEvent *e)
{
switch(e->type)
{
case CTLE_NOW_LOADING:
ctl_file_name((char *)e->v1);
break;
case CTLE_LOADING_DONE:
// MIDI file is loaded, about to play
current_file_info = get_midi_file_info(current_file, 1);
if (current_file_info != NULL) {
printf("file info not NULL\n");
} else {
printf("File info is NULL\n");
}
break;
case CTLE_PLAY_START:
ctl_total_time(e->v1);
break;
case CTLE_CURRENT_TIME:
ctl_current_time((int)e->v1);
break;
#ifndef CFG_FOR_SF
case CTLE_LYRIC:
ctl_lyric((int)e->v1);
break;
#endif
}
}
/*
* interface_<id>_loader();
*/
ControlMode *interface_m_loader(void)
{
return &ctl;
}
它被编译成接口文件if_my_interface.so
,如下所示:
gcc -fPIC -c -o my_interface.o my_interface.c
gcc -shared -o if_my_interface.so my_interface.o
运行我的简单界面
当我试图使用标准包 TiMidity v2.13.2-40.1 运行该接口时,它在一个内存释放调用中崩溃。代码被剥离了,所以追踪原因并不容易,我还没有费心去做——我不确定软件包发行版是针对什么库、代码版本等等进行编译的。
我从源头上建立了自己的 TiMidity 的副本。这工作得很好。请注意,当您从源代码构建 TiMidity 时,您需要指定它可以加载动态模块,例如,使用以下代码:
congfigure --enable-audio=alsa --enable-vt100 --enable-debug –enable-dynamic
与源建立在子目录 TiMidity+±2.14.0,玩使用这个界面是由
TiMidity++-2.14.0/timidity/timidity -d. -im 54154.mid
向 MIDI 文件播放背景视频
你可以从播放之前给出的视频中获取代码,并把它作为 TiMidity 系统的“后端”作为“视频”接口。本质上所有需要做的就是从简单的接口改变ctl_open
来调用 Gtk 代码播放视频,改变接口的身份。
新的“视频”界面是video_player_interface.c
。唯一重要的变化是对ctl_open
的修改,现在内容如下:
extern void init_gtk(void *args);
/*ARGSUSED*/
static int ctl_open(int using_stdin, int using_stdout)
{
outfp=stdout;
ctl.opened=1;
init_ffmpeg();
/* start Gtk in its own thread */
pthread_t tid_gtk;
pthread_create(&tid_gtk, NULL, init_gtk, NULL);
return 0;
}
if_video_player.so
构建命令如下所示:
CFLAGS = -ITiMidity++-2.14.0/timidity -ITiMidity++-2.14.0 -ITiMidity++-2.14.0/utils $(shell pkg-config --cflags gtk+-3.0 libavformat libavcodec libswscale libavutil )
LIBS3 = $(shell pkg-config --libs gtk+-3.0 libavformat libavcodec libswscale libavutil )
video_code.o: video_code.c
gcc -fPIC $(CFLAGS) -c -o video_code.o video_code.c
if_video_player.so: video_player_interface.c video_code.o
gcc -fPIC $(CFLAGS) -c -o video_player_interface.o video_player_interface.c
gcc -shared -o if_video_player.so video_player_interface.o video_code.o \
$(LIBS3)
它使用以下命令运行:
TiMidity++-2.14.0/timidity/timidity -d. -iv
54154.mid
结论
TiMidity 不是为其他应用设计的。要么你添加一个新的接口,要么你绕开 TiMidity 的设计去生产一个库。本章展示了这两种机制,并用简单和更复杂的例子进行了说明。
二十二、Karaoke 系统概述
这一章简要总结了连续的几章。
从我的角度来看,这本书的全部目的是记录在我构建 Linux Karaoke 系统的过程中 Linux sound 发生了什么。这一章着眼于我利用前几章的材料所做的各种探索。
首先,我的目标是什么?
- 能够播放 KAR 文件(一种可能的 Karaoke 文件格式)
- 每次至少显示一行歌词,突出显示应该唱的字符
- 对于中文歌曲,显示歌词的拼音(英文)形式以及中文字符
- 在背景中播放电影
- 以某种形式展示旋律
- 显示与旋律相对唱的音符
- 以某种方式给结果打分
我所做的一切都没有接近这些目标。让我挑选出我迄今为止探索的亮点:
- 最简单的“现成”系统是 PyKaraoke,kmid 是它的忠实追随者。这些播放 KAR 文件和突出显示歌词,但仅此而已。
- 向这种系统添加麦克风输入的最简单方法是使用外部混音器。这些也可以做混响和其他效果。
- Jack 和 PulseAudio 可以轻松地用于添加麦克风输入作为播放,但效果需要更多的工作。
- Java 在几乎所有方面都很酷——除了延迟最终会毁了它。
- FluidSynth 可以被黑客攻击以提供挂钩来悬挂 Karaoke。但是它是 CPU 密集型的,没有为其他处理留出空间。
- TiMidity 是一个独立的系统,具有可配置的后端。它可以被配置成一个简陋的 Karaoke 系统。但是可以通过黑客攻击使其成为一个库,这赋予了它更多的潜力。它不像 FluidSynth 那样占用大量 CPU 资源。
- 播放背景电影可以使用 FFmpeg 和 Gtk 之类的 GUI 来完成。Gtk 也有在视频上叠加高亮歌词的机制,但是 Gtk 2 和 Gtk 3 的机制不同。
- TiMidity 可以与 FFmpeg 和 Gtk 结合使用,在电影背景下显示突出显示的歌词。
- 尽管 Java 库 TarsosDSP 可以提供大量信息,但目前还看不到计分。
以下章节涵盖了这些主题:
用户级工具
- Karaoke 是一种“观众参与”的音响系统,在这种系统中,配乐和通常的旋律随着歌词的移动显示一起播放。本章考虑了播放 Karaoke 的功能、格式和用户级工具。
解码松肯卡拉 DVD 上的 DKD 文件
- 这一章是关于从我的 Songken 卡拉 DVD 中获取信息,这样我就可以开始编写播放歌曲的程序。它不直接参与在 Linux 下播放声音,作为附录给出。
Java 声音
- Java 声音对 Karaoke 没有直接支持。本章着眼于如何将 Java 声音库与其他库(如 Swing)结合起来,为 MIDI 文件提供一个 Karaoke 播放器。
副标题
- 许多 Karaoke 系统使用加在某种电影上的字幕。本章着眼于如何在 Linux 系统上做到这一点。选择有限,但有可能。
流体合成
- FluidSynth 是一个播放 MIDI 文件的应用,也是一个 MIDI 应用库。它没有播放 Karaoke 文件的挂钩。本章讨论 FluidSynth 的一个扩展,它添加了适当的钩子,然后使用这些钩子来构建各种各样的 Karaoke 系统。
TiMidity
- TiMidity 被设计成一个独立的应用,具有一种特殊的可扩展性。开箱后,它可以播放 Karaoke,但不是很好。这一章着眼于如何与 TiMidity 建立一个 Karaoke 系统。
二十三、Karaoke 用户级工具
Karaoke 是一种“观众参与”的音响系统,在这种系统中,配乐和通常的旋律随着歌词的移动显示一起播放。在此范围内,可以有变化或不同的特征。
- 歌词可以一次全部显示,而音乐按顺序播放。
- 歌词可以与旋律线同步突出显示。
- 旋律线可能会一直播放或可以关闭。
- 一些球员还将包括一个歌手唱这首歌。
- 有些有人声的玩家会在有人唱歌的时候关掉人声。
- 有些演奏者会给出旋律音符的图形显示。
- 一些演奏者会给出旋律的图形显示,还会显示歌手所唱的音符。
- 有些选手会根据对歌手准确性的一些评价来打分。这种评分的基础通常是未知的。
- 有些播放器允许你改变播放速度和播放音高。
- 大多数播放器将接受两个麦克风,并可以为歌手的声音添加混响效果。
- 很多播放器会让你提前选择歌曲,建立动态播放列表。
Karaoke 在亚洲很受欢迎,在欧洲国家也有追随者。Karaoke 系统被认为起源于亚洲,尽管根据维基百科( http://en.wikipedia.org/wiki/Karaoke
)记载的历史有点模糊。
在 www.karawin.fr/defenst.php
中描述了 Karaoke 的各种文件格式。本章考虑了播放 Karaoke 的功能、格式和用户级工具。
视频光盘系统
视频 CD 是光盘上一种较老的视频存储形式。分辨率相当低,一般为 352×240 像素,帧率为每秒 25 帧。虽然它们被一些电影使用,但是它们已经被 DVD 电影所取代。然而,它们曾一度被广泛用于 Karaoke 唱片。
来自亚洲的较便宜的 CD/DVD 播放器通常有麦克风输入,可以与 VCD 光盘一起用作 Karaoke 播放器。通常文件是 AVI 或 MPEG 格式的简单电影,所以你可以跟着唱。虽然歌词通常会随着旋律及时突出显示,但没有诸如乐谱或旋律显示之类的高级功能。
如果您有 VCD 光盘,它们可以作为 IC9660 文件安装在您的计算机上,但是在 Linux 系统上,您不能直接提取这些文件。像 VLC、MPlayer 和图腾这样的玩家可以播放它们的文件。
你需要使用类似vcdimager
的东西从 VCD 光盘中提取文件。这可能在你的包系统中,或者你可以从 GNU 开发者网站( www.gnu.org/software/vcdimager/
)下载并从源代码编译它。然后,可以使用以下内容将视频文件提取为 MPEG 或 AVI 文件:
vcdxrip --cdrom-device=/dev/cdrom --rip
(在我的系统上,我不得不将/dev/cdrom
替换为/dev/sr1
,因为我无法从默认的 DVD 播放器中提取。我通过运行mount
找到了它是什么设备,然后用umount
卸载了它。)
CD+G 光盘
根据维基百科的“CD+G”页面( https://en.wikipedia.org/wiki/CD%2BG
),“CD+G(也称为 CD+Graphics)是光盘标准的扩展,当在兼容设备上播放时,可以在光盘上的音频数据旁边呈现低分辨率的图形。CD+G 光盘通常用于 Karaoke 机,Karaoke 机利用这一功能在屏幕上显示光盘上歌曲的歌词。
每首歌曲由两个文件组成:一个音频文件和一个包含歌词的视频文件(可能还有一些背景场景)。
您可以使用这种格式购买许多光盘。你不能在电脑上直接播放它们。Rhythmbox 将播放音频,但不播放视频。VLC 和图腾不喜欢他们。
将文件翻录到电脑上存储在硬盘上并不那么简单。音频光盘没有正常意义上的文件系统。例如,您不能使用 Unix mount
命令挂载它们;它们甚至不是 ISO 格式的。相反,你需要使用一个类似于cdrdao
的程序将文件解压成一个二进制文件,然后对其进行处理。
$ cdrdao read-cd --driver generic-mmc-raw --device /dev/cdroms/cdrom0 --read-subchan rw_raw mycd.toc
前面的代码创建了一个数据文件和一个目录文件。
CDG 文件的格式显然还没有公开发布,但是由 Jim Bumgardner(早在 1995 年!)在“CD+G 揭示:在软件中回放 Karaoke 曲目”( http://jbum.com/cdg_revealed.html
)。
声音榨汁机等程序会提取音轨,但留下视频。
MP3+G 文件
MP3+G 文件是适用于普通电脑的 CD+G 文件。它们由包含音频的 MP3 文件和包含歌词的 CDG 文件组成。通常它们是用拉链拉在一起的。
很多卖 CD+G 文件的网站也卖 MP3+G 文件。各种网站给出了如何创建自己的 MP3+G 文件的说明。免费网站不多。
来自cdgtools-0.3.2
的程序cgdrip.py
可以从音频光盘中抓取 CD+G 文件,并将其转换为一对 MP3+G 文件。(Python)源代码中的指令如下:
# To start using cdgrip immediately, try the following from the
# command-line (replacing the --device option by the path to your
# CD device):
#
# $ cdrdao read-cd --driver generic-mmc-raw --device /dev/cdroms/cdrom0 --read-subchan rw_raw mycd.toc
# $ python cdgrip.py --with-cddb --delete-bin-toc mycd.toc
#
# You may need to use a different --driver option or --read-subchan mode
# to cdrdao depending on your CD device. For more in depth details, see
# the usage instructions below.
购买 CD+G 或 MP3+G 文件
有很多卖 CD+G 和 MP3+G 歌曲的网站。只要在谷歌上搜索一下。然而,每首歌的平均价格约为 3 美元,如果你想建立一个大的收藏,这可能会变得昂贵。一些网站会对大量购买给予折扣,但即使 100 首歌曲 30 美元,费用也可能很高。
拥有大量收藏的网站来来去去。在撰写本文时,aceume.com 为我们提供了 14000 首英文歌曲,售价 399 美元。但你可以花 600 美元购买他们的 AK3C Android 一体机云 Karaoke 播放器,其中包含 21,000 首英文歌曲和 35,000 首中文歌曲。这使得建造自己的 Karaoke 播放器的经济性变得不稳定。我将忽略这个问题,这是你的选择!
将 MP3+G 转换为视频文件
工具ffmpeg
可以将音频和视频合并成一个视频文件,例如:
ffmpeg -i Track1.cdg -i Track1.mp3 -y Track1.avi
使用以下内容创建包含视频和音频的 AVI 文件:
avconv -i Track1.cdg -i Track1.mp3 test.avi
avconv -i test.avi -c:v libx264 -c:a copy outputfile.mp4
这个可以用 VLC,MPlayer,Rhythmbox 等等来玩。
有一个程序叫做 cdg2video
。它的最后日期是 2011 年 2 月,FFmpeg 内部的变化意味着它不再编译。即使您修复了这些明显的变化,C 编译器也会对不推荐使用的 FFmpeg 函数提出大量的抱怨。
MPEG-4 文件
使用 MPEG-4 视频播放器的 Karaoke 系统越来越普遍。这些将所有信息嵌入到视频中。这些档案的球员没有计分系统。
有些人认为它们的音质更好;例如 http://boards.straightdope.com/sdmb/showthread.php?t=83441
见。我认为与其说是格式的问题,不如说是合成器的问题。当然高端合成器制造商如雅马哈不会同意!
MPEG-4 文件肯定比相应的 MIDI 文件要大,你需要一个大容量的磁盘来存放它们。
有很多卖 MP4 歌曲的网站。只要在谷歌上搜索一下。然而,每首歌的平均价格约为 3 美元,如果你想建立一个大的收藏,这可能会变得非常昂贵。
在撰写本文时,似乎还没有一个网站出售大量的 MPEG-4 歌曲。但是,过去有过,将来也可能有。
Karaoke 机
有许多 Karaoke 机都配有 DVD。在大多数情况下,歌曲存储为 MIDI 文件,歌曲轨道在一个 MIDI 文件中,歌词在另一个文件中。一些较新的系统将使用 WMA 文件的配乐,这使得一个轨道有声乐供应和其他没有声乐。这种系统通常包括评分机制,尽管评分的依据并不明确。最新的是基于硬盘的,通常是 MP4 文件。他们似乎没有计分系统。这些系统的供应商定期更换,即使系统本身只是重新贴牌。我拥有 Malata 和 Sonken 的系统,但它们是多年前购买的。我不相信更新的型号一定是改进的。
我拥有的两个系统表现出不同的特点。Sonken MD-388 1 可播放多种语言的歌曲,如中文、韩语、英语等。我妻子是中国人,但我看不懂汉字。有一个英文化的脚本叫做拼音,Sonken 显示了汉字和拼音,所以我也可以跟着唱。看起来像图 23-1 。
图 23-1。
Screen dump of Sonken player
万利达 MDVD-6619 2 播放中文歌曲不显示拼音。但它确实显示了你应该唱的音符和你实际唱的音符。图 23-2 显示我跑调了。
图 23-2。
Screen dump of Malata player
MIDI 播放器
MIDI 格式的 Karaoke 文件可以从几个站点找到,通常以.kar
结尾。任何 MIDI 播放器如 TiMidity 都可以播放这样的文件。然而,它们并不总是显示与旋律同步的歌词。
查找 MIDI 文件
网上有几个网站提供 MIDI 格式的文件。
- MIDIZone (
www.free-midi.org/
) - midworld(
www.midiworld.com/
- CoolMIDI (
www.cool-midi.com/
- 电鲜(
http://electrofresh.com/
) - Freemidi (
http://freemidi.org/
- Karaoke 版(
www.karaoke-version.com/
)——比如第四轨的《旭日之家》旋律 - MIDaoke(
www.midaoke.com/
)——比如平克·弗洛伊德第二轨的《希望你在这里》旋律 - 家庭音乐家(
http://karaoke.homemusician.net/
)——例如,老鹰乐队的《加州旅馆》在第四轨的单簧管上标注了“旋律”
KAR 文件格式
卡拉 MIDI 文件没有正式的标准。有一种被广泛接受的行业格式,称为 MIDI Karaoke 类型 1 文件格式。
以下来自迷笛 Karaoke FAQ(http://gnese.free.fr/Projects/KaraokeTime/Fichiers/karfaq.html
):
- 什么是 MIDI 卡拉 Type 1(。KAR)文件格式?MIDI Karaoke 文件是一种标准的 MIDI 文件类型 1,它包含一个单独的轨道,其中歌曲的歌词作为文本事件输入。将一个 MIDI Karaoke 文件加载到音序器中,以检查该文件曲目的内容。第一个轨道包含文本事件,用于将文件识别为 MIDI Karaoke 文件。
@KMIDI KARAOKE FILE
文本事件就是为此目的而使用的。可选文本事件@V0100
表示格式版本号。以@I
开头的任何内容都是您想要包含在文件中的任何信息。 - 第二个轨道包含歌曲歌词的文本元事件。第一个事件是
@LENGL
。它标识了歌曲的语言,在本例中是英语。接下来的几个事件从@T
开始,它标识了歌曲的标题。您最多可以有三个这样的事件。第一个事件应该包含歌曲的标题。一些程序(如 Soft Karaoke)读取此事件以获取要在文件打开对话框中显示的歌曲名称。第二个事件通常包含歌曲的表演者或作者。第三个事件可以包含任何版权信息或任何其他内容。 - 第二音轨的剩余部分包含歌曲的歌词。每个事件都是事件发生时应该唱的音节。如果文字以\开头,则表示清空屏幕,在屏幕顶部显示文字。如果文本以/开头,则表示转到下一行。
- 重要提示:一个
.kar
文件中每个屏幕只能有三行,以便软 Karaoke 正确播放该文件。换句话说,一行歌词的每一行开头只能有两个正斜杠。下一行必须以反斜杠开始。
这种格式有几个缺点,列举如下:
- 没有指定可能的语言列表,只有英语。
- 未指定文本的编码(例如,Unicode UTF-8)。
- 没有办法识别传送旋律的通道。
皮卡拉奥克
PyKaraoke 是用 Python 写的专用 Karaoke 播放器,使用了 Pygame、WxPython 等多种库。它会播放歌曲并显示你在歌词中的位置。一个“烟雾进入你的眼睛”( www.midikaraoke.com/cgi-bin/songdir/jump.cgi?ID=1280
)的屏幕截图看起来像图 23-3 。
图 23-3。
Screen dump of PyKaraoke
PyKaraoke 播放配乐并显示歌词。它不能通过播放歌手的输入来充当合适的 Karaoke 系统。但是 PyKaraoke 使用 PulseAudio 系统,所以你可以同时播放其他节目。特别是,你可以让 PyKaraoke 在一个窗口运行,而pa-mic-2-speaker
在另一个窗口运行。PulseAudio 将混合两个输出流,并一起播放两个源。当然,没有额外的工作,在这样的系统中不可能得分。
kmid【3】
是 KDE 的一名 Karaoke 歌手。它会播放歌曲并显示你在歌词中的位置。一个“烟雾进入你的眼睛”的屏幕截图看起来像图 23-4 。
图 23-4。
kmid
screen dump. kmid
uses either TiMidity or FluidSynth as a MIDI back end.
kmid
播放音轨并显示歌词。它不能通过播放歌手的输入来充当合适的 Karaoke 系统。但是kmid
可以使用 PulseAudio 系统,所以可以同时播放其他节目。特别是,你可以让kmid
在一个窗口中运行,而pa-mic-2-speaker
在另一个窗口中运行。PulseAudio 将混合两个输出流,并一起播放两个源。当然,没有额外的工作,在这样的系统中不可能得分。
麦克风输入和混响效果
几乎所有的个人电脑和笔记本电脑都有声卡来播放音频。虽然几乎所有这些都有麦克风输入,但有些没有。例如,我的戴尔笔记本电脑没有,Raspberry Pi 没有,许多 Android 电视媒体盒也没有。
那些没有麦克风输入的电脑通常有 USB 端口。他们通常会接受 USB 声卡,如果 USB 有麦克风输入,那么这是公认的。
如果你想支持两个或更多的麦克风,那么你需要相应数量的声卡或混音设备。我见过百灵达 MX-400 MicroMix,四通道紧凑型低噪声混音器,20 美元,或者你可以在电子产品网站上找到电路图(音频混音器的谷歌电路图)。
混响是一种效果,通过添加具有不同延迟的(人工)回声,赋予声音更饱满的“身体”。百灵达还生产 MIX800 MiniMix,它可以混合两个具有混响效果的麦克风,还具有一个用于线路输入的直通端口(因此您可以播放音乐并控制麦克风)。(我没有百灵达的链接。)一个类似的单元是 UNIFY K9 混响电脑 Karaoke 混音器。
中国的 DVD 播放器通常有双麦克风输入,具有混音和混响功能。因为它们的价格只有 13 美元。诚然,对于 1000 台来说,说明混音和混响应该不会太贵。我猜他们用的是三菱 M65845AFP ( www.datasheetcatalog.org/datasheet/MitsubishiElectricCorporation/mXuuvys.pdf
),“带麦克风混音电路的数字回声”数据手册显示了许多可能的配置,适合喜欢自己构建的用户。
结论
有各种各样的 Karaoke 系统,使用 VCD 光盘或专用系统。MIDI 格式的 Karaoke 文件可以用普通的 MIDI 软件来播放,还有几个 Linux 的 Karaoke 播放器。
Footnotes 1
这个 Sonken 已经不卖了。然而,也有不同品牌下销售的类似型号。
2
较新的型号在中国销售,但目前只有非常有限的英语曲目。
3
似乎已经从当前的 KDE 版本中消失了。这是一个真正的耻辱,因为它非常好。