Android 6.0 (Marshmallow):如何演奏 midi 音符?

Android 6.0 (Marshmallow): How to play midi notes?

我正在创建一个生成现场乐器声音的应用程序,并且我计划使用 Android Marshmallow(6.0 版)中的新 Midi API。我已经阅读了此处的包概述文档 http://developer.android.com/reference/android/media/midi/package-summary.html 并且我知道如何生成 Midi 音符,但我仍然不确定:在生成它们的 Midi 数据后我如何实际播放这些音符?

我需要合成器程序来播放 Midi 音符吗?如果是这样,我必须自己制作还是由 Android 或第三方提供?

我是 Midi 的新手,所以请尽可能描述您的回答。

到目前为止我尝试过的: 我创建了一个 Midi 管理器对象并打开了一个输入端口

MidiManager m = (MidiManager)context.getSystemService(Context.MIDI_SERVICE); 
MidiInputPort inputPort = device.openInputPort(index);

然后,我向端口

发送了一条测试noteOn midi消息
byte[] buffer = new byte[32];
int numBytes = 0;
int channel = 3; // MIDI channels 1-16 are encoded as 0-15.
buffer[numBytes++] = (byte)(0x90 + (channel - 1)); // note on
buffer[numBytes++] = (byte)60; // pitch is middle C
buffer[numBytes++] = (byte)127; // max velocity
int offset = 0;
// post is non-blocking
inputPort.send(buffer, offset, numBytes);

我还设置了一个 class 来接收 MIDI 音符消息

class MyReceiver extends MidiReceiver {
    public void onSend(byte[] data, int offset,
            int count, long timestamp) throws IOException {
        // parse MIDI or whatever
    }
}
MidiOutputPort outputPort = device.openOutputPort(index);
outputPort.connect(new MyReceiver());

现在,这是我最困惑的地方。我的应用程序的用例是成为用于制作音乐的一体化作曲和播放工具。换句话说,我的应用程序需要包含或使用虚拟 MIDI 设备(例如另一个应用程序的 MIDI 合成器的意图)。除非有人已经制作了这样的合成器,否则我必须在我的应用程序生命周期内自己创建一个。我如何实际将接收到的 midi noteOn() 转换为从我的扬声器发出的声音?我特别困惑,因为还必须有一种方法来以编程方式确定音符听起来像是来自哪种类型的乐器:这也是在合成器中完成的吗?

Android Marshmallow 中的 Midi 支持相当新,所以我无法在线找到任何教程或示例合成器应用程序。任何见解表示赞赏。

Do I need a synthesizer program to play Midi notes? If so, do I have to make my own or is one provided by Android or a 3rd party?

不,幸运的是您不需要制作自己的合成器。 Android 已经内置了一个:SONiVOX 嵌入式音频合成器。 Android docs on SONiVOX JETCreator 中的状态:

JET works in conjunction with SONiVOX's Embedded Audio Synthesizer (EAS) which is the MIDI playback device for Android.

不清楚您是想要实时播放,还是想先创建乐曲然后在同一个应用程序中播放。您还声明要播放 MIDI 音符,而不是文件。但是,请注意,Midi 播放是 supported on android devices. So playing a .mid file should be done the same way you would play a .wav file using MediaPlayer.

老实说,我没用过midi包,也没做过midi播放,不过如果你能建一个.mid文件存盘,直接用MediaPlayer.

现在,如果你想直接播放 midi notesnot 文件,那么你可以使用 this mididriver package。使用此包,您应该能够将 midi 数据写入嵌入式合成器:

/**
* Writes midi data to the Sonivox synthesizer. 
* The length of the array should be the exact length 
* of the message or messages. Returns true on success, 
* false on failure.
*/

boolean write(byte buffer[])  

如果你想进一步降低,你甚至可以使用AudioTrack直接玩PCM。

有关其他信息,这是 blog post 我从一位似乎与您有类似问题的人那里找到的。他说:

Personally I solved the dynamic midi generation issue as follows: programmatically generate a midi file, write it to the device storage, initiate a mediaplayer with the file and let it play. This is fast enough if you just need to play a dynamic midi sound. I doubt it’s useful for creating user controlled midi stuff like sequencers, but for other cases it’s great.

希望我涵盖了所有内容。

我还没有找到任何 "official" 方法来从 Java 代码控制内部合成器。

可能最简单的选择是使用 Android midi driver for the Sonivox synthesizer

获取它 as an AAR package(解压缩 *.zip)并将 *.aar 文件存储在您工作区的某个位置。路径并不重要,它不需要位于您自己的应用程序的文件夹结构中,但项目中的 "libs" 文件夹可能是一个合乎逻辑的位置。

在 Android Studio 中打开您的 Android 项目:

File -> New -> New Module -> Import .JAR/.AAR Package -> Next -> Find and select the "MidiDriver-all-release.aar" and change the subproject name if you want. -> Finish

等待 Gradle 发挥它的魔力,然后转到 "app" 模块的设置(您自己的应用程序项目的设置)到 "Dependencies" 选项卡并添加(带有绿色“ +" 符号)作为模块依赖项的 MIDI 驱动程序。现在您可以访问 MIDI 驱动程序了:

import org.billthefarmer.mididriver.MidiDriver;
   ...
MidiDriver midiDriver = new MidiDriver();

无需担心 NDK 和 C++,您可以使用这些 Java 方法:

// Not really necessary. Receives a callback when/if start() has succeeded.
midiDriver.setOnMidiStartListener(listener);
// Starts the driver.
midiDriver.start();
// Receives the driver's config info.
midiDriver.config();
// Stops the driver.
midiDriver.stop();
// Just calls write().
midiDriver.queueEvent(event);
// Sends a MIDI event to the synthesizer.
midiDriver.write(event);

一个非常基本的 "proof of concept" 用于播放和停止音符可能是这样的:

package com.example.miditest;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.widget.Button;

import org.billthefarmer.mididriver.MidiDriver;

public class MainActivity extends AppCompatActivity implements MidiDriver.OnMidiStartListener,
        View.OnTouchListener {

    private MidiDriver midiDriver;
    private byte[] event;
    private int[] config;
    private Button buttonPlayNote;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        buttonPlayNote = (Button)findViewById(R.id.buttonPlayNote);
        buttonPlayNote.setOnTouchListener(this);

        // Instantiate the driver.
        midiDriver = new MidiDriver();
        // Set the listener.
        midiDriver.setOnMidiStartListener(this);
    }

    @Override
    protected void onResume() {
        super.onResume();
        midiDriver.start();

        // Get the configuration.
        config = midiDriver.config();

        // Print out the details.
        Log.d(this.getClass().getName(), "maxVoices: " + config[0]);
        Log.d(this.getClass().getName(), "numChannels: " + config[1]);
        Log.d(this.getClass().getName(), "sampleRate: " + config[2]);
        Log.d(this.getClass().getName(), "mixBufferSize: " + config[3]);
    }

    @Override
    protected void onPause() {
        super.onPause();
        midiDriver.stop();
    }

    @Override
    public void onMidiStart() {
        Log.d(this.getClass().getName(), "onMidiStart()");
    }

    private void playNote() {

        // Construct a note ON message for the middle C at maximum velocity on channel 1:
        event = new byte[3];
        event[0] = (byte) (0x90 | 0x00);  // 0x90 = note On, 0x00 = channel 1
        event[1] = (byte) 0x3C;  // 0x3C = middle C
        event[2] = (byte) 0x7F;  // 0x7F = the maximum velocity (127)

        // Internally this just calls write() and can be considered obsoleted:
        //midiDriver.queueEvent(event);

        // Send the MIDI event to the synthesizer.
        midiDriver.write(event);

    }

    private void stopNote() {

        // Construct a note OFF message for the middle C at minimum velocity on channel 1:
        event = new byte[3];
        event[0] = (byte) (0x80 | 0x00);  // 0x80 = note Off, 0x00 = channel 1
        event[1] = (byte) 0x3C;  // 0x3C = middle C
        event[2] = (byte) 0x00;  // 0x00 = the minimum velocity (0)

        // Send the MIDI event to the synthesizer.
        midiDriver.write(event);

    }

    @Override
    public boolean onTouch(View v, MotionEvent event) {

        Log.d(this.getClass().getName(), "Motion event: " + event);

        if (v.getId() == R.id.buttonPlayNote) {
            if (event.getAction() == MotionEvent.ACTION_DOWN) {
                Log.d(this.getClass().getName(), "MotionEvent.ACTION_DOWN");
                playNote();
            }
            if (event.getAction() == MotionEvent.ACTION_UP) {
                Log.d(this.getClass().getName(), "MotionEvent.ACTION_UP");
                stopNote();
            }
        }

        return false;
    }
}

布局文件只有一个按钮,按住时播放预定义的音符,松开时停止:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="com.example.miditest.MainActivity"
    android:orientation="vertical">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Play a note"
        android:id="@+id/buttonPlayNote" />
</LinearLayout>

其实就是这么简单。上面的代码很可能是触摸钢琴应用程序的起点,它具有 128 种可选乐器、非常不错的延迟和许多应用程序所缺乏的适当 "note off" 功能。

至于选择乐器:您只需要发送一个 MIDI "program change" 消息到您打算演奏的频道,以从通用 MIDI 音色集中的 128 种声音中选择一种。但这与 MIDI 的细节有关,与库的使用无关。

同样,您可能希望抽象出 MIDI 的低级细节,以便您可以轻松地在特定通道上使用特定乐器以特定速度在特定时间播放特定音符,为此您可能从迄今为止制作的所有开源 Java 和 MIDI 相关应用程序和库中找到一些线索。

顺便说一句,此方法不需要 Android 6.0。目前 only 4.6 % of devices visiting the Play Store run Android 6.x,因此您的应用不会有太多受众。

当然,如果您想使用 android.media.midi 包,您可以使用该库来实现 android.media.midi.MidiReceiver 以接收 MIDI 事件并在内部合成器上播放它们。 Google 已经有一些 demo code that plays notes with square and saw waves。只需将其替换为内部合成器即可。

其他一些选项可能是查看将 FluidSynth 移植到 Android 的状态。我想可能会有一些可用的东西。

编辑: 其他可能有趣的库:

要使用 Android MIDI API 生成声音,您需要一个接受 MIDI 输入的合成器应用程序。不幸的是,这是我在 Google Play 上找到的唯一此类应用程序: https://play.google.com/store/apps/details?id=com.mobileer.midisynthexample

我可以通过向此应用发送音符打开和音符关闭消息来播放音乐。但是程序更改效果不佳。除非我在我的代码中做错了什么,否则该应用程序似乎只有两种乐器。

但是,有些人正在开发其他合成器应用程序,所以我希望很快会有更多应用程序可用。这个应用程序看起来很有前途,尽管我还没有亲自测试过它: https://github.com/pedrolcl/android/tree/master/NativeGMSynth