Linux 声音编程教程(六)

原文:Linux Sound Programming

协议:CC BY-NC-SA 4.0

十八、MIDI Java 声音

Java Sound 有一个开发良好的 MIDI 系统,有干净分离的组件,如音序器和合成器,它允许元事件和普通 MIDI 事件挂钩。本章考虑使用 MIDI API 编程。

资源

许多资源可用于 Java Sound。

主要的 Java 声音 MIDI 类

Java Sound 依靠一组类来支持 MIDI。这些是 Java 中的标准。以下是主要类别:

设备信息

通过查询MidiSystemDeviceInfo对象列表,可以找到设备信息。每个信息对象包含诸如NameVendor的字段。您可以通过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 文件,您可以使用MidiSystemFile创建一个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_ONNOTE_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 音序器客户端

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.htmlTed 的 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_MIDIAlsaMidiOverview [ 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,但您主要使用了命令amidiaplaymidiaconnect,并且看到了使用seqdemo.c程序的 API。

二十、FluidSynth

FluidSynth 是一个播放 MIDI 文件的应用,也是一个 MIDI 应用库。

资源

以下是一些资源:

演员

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 而有逻辑地组织起来的,但是对于一个新的界面来说,它们不是方便地组织起来的。所以,你必须不断地引入额外的包含,这些额外的包含指向其他外部的,需要更多的包含,等等。这些可能在不同的目录中,比如timidityutils,所以你必须指向许多不同的包含目录。

请注意,您将需要 TiMidity 的源代码来获得这些包含文件;你可以从 SourceForge TiMidity++下载( http://sourceforge.net/projects/timidity/?source=dlp )。

我的简单界面

这基本上和 TiMidity 中内置的“哑”接口做的是一样的。它是从当前目录加载的,并通过以下命令调用:

timidity -im -d. 54154.mid

代码在一个文件中,my_interface.c

下面的代码中有两个主要的函数,其余的都省略了。重要的功能有ctl_eventctl_lyric。函数ctl_event处理 TiMidity 产生的事件。对于这个接口,您只想在播放时打印歌词,所以当一个CTLE_LYRIC事件发生时,调用ctl_lyricctl_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 格式的文件。

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 版本中消失了。这是一个真正的耻辱,因为它非常好。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值