-1

编辑:这是一个独立的例子:

MidiLatte midiLatte = new MidiLatte();

for (int i = 60; i <= 72; i++) {
    midiLatte.addNote(i, 4);
}
midiLatte.playAndRemove();

try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}

try {
    Field f = MidiLatte.class.getDeclaredField("player");
    f.setAccessible(true);
    Sequencer s = (Sequencer) f.get(midiLatte);
    s.stop();
} catch (NoSuchFieldException | IllegalAccessException e) {
    e.printStackTrace();
}

for (int i = 48; i <= 60; i++) {
    midiLatte.addNote(i, 4);
}
midiLatte.playAndRemove();

我能够找到MidiLatte here的源代码。很抱歉不得不处理它;我需要在课堂上使用它。(如果由我决定,我会javax.sound.midi直接使用......)这应该做的是开始播放 12 个音符的序列(第一个for循环),但在 3000 毫秒后停止它,以便它可以播放不同的序列(第二个for循环)。然而,最终发生的事情是,当第二个序列开始播放时,它并没有从头开始播放;它开始了几个音符。


我正在制作一个使用javax.sound.midiAPI 的 Java GUI 应用程序。具体来说,它是一个音序器,允许用户输入音符并按顺序播放它们。我遇到的问题是我需要一种方法,在我发送我的 MIDI 消息之后,阻止它们,以便我可以播放其他东西。例如,假设用户点击播放。然后,显然,程序播放音符。我想要发生的是,如果用户在播放序列时按下播放,它会停止当前播放并重新开始。

我让 MIDI 与 Swing 一起工作的方式——在不破坏事件队列的情况下这样做并非易事——是通过单独Thread处理 MIDI 内容。除了我在上一段中提出的问题之外,这工作正常。这是run我的Thread实现方法:

@Override
public void run() {
    midiLatte = new MidiLatte();
    //This loop waits for tasks to become available
    while (true) {
        try {
            tasks.take().accept(midiLatte);
        } catch (InterruptedException e) {
            System.out.println("MPThread interrupted");
        }
    }
}

MidiLatte是一个实用程序类,可以更轻松地发送 midi 消息并包装一个Sequencer. 基本上,它的工作方式tasks是 a LinkedBlockingQueueof Consumer<MidiLatte>s。这允许我做的是在外部类中有一个方法(MPThread我的自定义Thread,是一个内部类),它将 a 添加Consumer<MidiLatte>到队列中。这样可以很容易地安排这样的 MIDI 任务:

midiPlayer.addAction(midiLatte -> {
    // do midi stuff
});

但是,如上所述,我需要能够取消这些任务并在它们已经发送后停止 MIDI 播放。前者很简单——我可以清空队列:

tasks.clear();

但是,后者很困难,因为 MIDI 任务已经发送并且在 MIDI API 手中。我试过使用sequencer.stop(),但我得到了奇怪的结果,如本例所示:

player.addAction(midiLatte -> {
    //midi stuff
});
try {
    Thread.sleep(3000);
} catch (InterruptedException e) {
    e.printStackTrace();
}
player.cancelPending();
player.addAction(midiLatte -> {
    //Midi stuff
});

WherecancelPending()调用sequencer.stop()并清除队列。最终发生的情况如下(我从经验中知道,当用户不清楚地描述他们的错误时,它可能会令人烦恼和困惑,所以我将一步一步地展示它。如果你仍然不明白什么,请告诉我发生。):

  • 第一个序列开始正确播放。
  • 3000(或多或少)毫秒后,第一个序列停止。
  • 第二个序列立即开始播放,但不是从头开始;相反,它开始播放就好像它一直在播放一样。

为了说明这一点,假设第一个序列的音符是A B C D E F G A,第二个序列的音符是1 2 3 4 5 6 7 8。应该发生的是,如果 3000 mils 落在第一个序列之间C和之间D,则整个播放是A B C 1 2 3 4 5 6 7 8. 然而,实际发生的是A B C 4 5 6 7 8.

我怎样才能实现我想要的行为?如果我设置并发的方式存在固有问题,请告诉我。只是为了重申我想要的行为,就是这样。当按下播放按钮时,MIDI 序列播放。如果我在序列仍在播放时再次按下它,第一次播放将停止并且序列从头开始再次播放。如果我有任何不清楚的地方,请告诉我。谢谢!

4

1 回答 1

1

好的,所以,据我所知,问题在于除了无法实际控制底层之外的任何东西Sequencer,因为它继续播放它的缓存数据。

不幸的是,您无权访问Sequencer... yippy skippy,也没有MidiLatte提供任何扩展点供您修改代码。

一个 HACK 解决方案是做你已经在做的事情,使用反射来访问私有字段,例如......

!!这是一个黑客!

如果您无法修改 的原始来源MidiLatte,那么您别无选择,为了实现您似乎想要实现的目标,您必须有权访问Sequencer

public static class StoppableMidiLatte extends MidiLatte {

    public void stop() {
        removeAll();
        try {
            Field f = MidiLatte.class.getDeclaredField("player");
            f.setAccessible(true);
            Sequencer s = (Sequencer) f.get(this);
            s.stop();
        } catch (NoSuchFieldException | IllegalAccessException e) {
            e.printStackTrace();
        }
    }

}

至于并发的需要,我想说你不需要它,因为Sequencer已经在使用它自己的线程来播放音符,而在这种情况下,并发只会让它变得更加困难

于 2017-04-07T02:56:51.827 回答