停止MIDI音序器,以便我可以播放其他内容

时间:2017-04-07 01:36:52

标签: java multithreading swing midi javax.sound.midi

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

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.midi API的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。基本上,这种方法的工作方式是tasksLinkedBlockingQueue的{​​{1}}。这允许我做的是在外部类(Consumer<MidiLatte>,我的自定义MPThread,是一个内部类)中有一个方法,它将Thread添加到队列中。这样可以很容易地安排这样的MIDI任务:

Consumer<MidiLatte>

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

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

然而,后者很难,因为MIDI任务已经发送并且掌握在MIDI API手中。我尝试过使用tasks.clear(); ,但我得到了奇怪的结果,如下例所示:

sequencer.stop()

player.addAction(midiLatte -> { //midi stuff }); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } player.cancelPending(); player.addAction(midiLatte -> { //Midi stuff }); 调用cancelPending()并清除队列。最终发生的事情如下(我从经验中知道,当用户不清楚地描述他们的错误时,它可能会令人讨厌和混乱,所以我将逐步展示它。请告诉我你是否仍然不明白什么发生):

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

为了说明这一点,假设第一个序列的注释为sequencer.stop(),第二个序列为A B C D E F G A。应该发生的情况是,如果第一个序列中的{m}介于1 2 3 4 5 6 7 8C之间,则整个回放为D。但是,实际发生的是A B C 1 2 3 4 5 6 7 8

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

1 个答案:

答案 0 :(得分:1)

好吧,所以,尽管可以说,问题在于除了无法实际控制基础Sequencer之外的任何事情,因为它继续播放它的缓存数据。

很遗憾,您无法访问Sequencer ... yippy skippy,MidiLatte也没有为您提供修改代码的扩展点。

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

!!这是一个黑色!!

如果您无法修改MidiLatte的原始来源,那么您别无选择,为了达到您想要实现的目标,您必须能够访问Sequencer < / p>

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已经在使用它自己的线程来播放笔记而且并发性只会使这种情况下更加困难< / p>