-1

EDIT: Here's a self-contained example:

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();

I was able to find the source code of MidiLatte here. Sorry for having do deal with it; I'm required to use it for my class. (If it were up to me, I would use javax.sound.midi directly…) What this is supposed to do is to start playing a sequence of 12 notes (first for loop) but stop it after 3000 milliseconds so it can play a different sequence (the second for loop). However, what ends up happening is that when the second sequence starts playing, it does not do so from the beginning; it starts several notes in.


I am making a Java GUI application that uses the javax.sound.midi API. Specifically, it is a sequencer which allows users to input notes and play them in sequence. The issue I'm having is that I need a way, after I have sent my MIDI messages, to stop them so that I can play something else. For instance, let's say the user hits play. Then, obviously, the program plays the notes. What I want to have happen is that if the user presses play while the sequence is playing, it stops the current playback and starts over.

The way I have gotten MIDI to work with Swing—it is not trivial to do so without ruining the event queue—is by having a separate Thread which deals with MIDI stuff. Aside from the problem which I presented in the previous paragraph, this works fine. Here is the run method of my Thread implementation:

@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 is a utility class that makes it easier to send midi messages and wraps a Sequencer. Basically, the way that this works is that tasks is a LinkedBlockingQueue of Consumer<MidiLatte>s. What this allows me to do is to have a method in the outer class (MPThread, my custom Thread, is an inner class) which adds a Consumer<MidiLatte> to the queue. This makes it easy to schedule MIDI tasks like this:

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

However, as stated above, I need to be able to cancel these tasks and stop MIDI playback once they already have been sent. The former is easy—I can just clear the queue:

tasks.clear();

However, the latter is difficult because the MIDI tasks are already sent and are in the hands of the MIDI API. I have tried using sequencer.stop() but I am getting weird results from that, as seen in this example:

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

Where cancelPending() invokes sequencer.stop() and clears the queue. What ends up happening is as follows (I know from experience that it can be annoying and confusing when a user describes their bug unclearly, so I'm going to show it step by step. Please tell me if you still don't understand what happens.):

  • The first sequence starts playing correctly.
  • After 3000 (more or less) milliseconds, the first sequence stops.
  • The second sequence immediately starts playing, but does not start from the beginning; rather it starts playing as if it had been playing the whole time.

To illustrate this, imagine the notes of the first sequence are A B C D E F G A and the second sequence goes 1 2 3 4 5 6 7 8. What should happen is that if 3000 mils falls between C and D in the first sequence, the whole playback is A B C 1 2 3 4 5 6 7 8. However, what actually happens is A B C 4 5 6 7 8.

How can I achieve the behavior that I want? If there's an inherent problem with the way I've set up my concurrency, please tell me. Just to reiterate the behavior that I want, here it is. When the play button is pressed, the MIDI sequence plays. If I press it again while the sequence is still playing, the first playback stops and the sequence plays again from the start. Please tell me if I am unclear in any way. Thank you!

ostrichofevil
  • 749
  • 7
  • 19
  • 1
    For better help sooner, post a [MCVE] or [Short, Self Contained, Correct Example](http://www.sscce.org/). – Andrew Thompson Apr 07 '17 at 01:40
  • 1
    I'm with @AndrewThompson -- that the best way to get us to understand your code and your problem will be to create your [MCVE](http://stackoverflow.com/help/mcve) and/or [SSCCE](http://www.sscce.org/) and to post it with your question, but this won't be an easy thing to do, since you're going to have to mock the libraries that we don't have access to. – Hovercraft Full Of Eels Apr 07 '17 at 01:43
  • 2
    I'm not sure if this has anything to do with Swing per say, so you might just be able to generate an example from your custom player – MadProgrammer Apr 07 '17 at 01:44
  • @MadProgrammer The reason that this has to do with Swing is that without Swing, I wouldn't have do deal with concurrency. Because doing the midi stuff on the main thread messes up Swing's event queue, I have to use multiple threads and this is introducing the complexity that you see here. – ostrichofevil Apr 07 '17 at 01:46
  • 2
    @ostrichofevil I understand and respect that Swing has caused you to work down a given path, but the problem you're having is related to Swing, it's more of a general concurrency issue. I'm just suggesting that a MVCE wouldn't have to have a Swing component, as you could possibly replicate the issue without it. If, in the process of generating an example it works as expected without the UI (but using the threading model), then by all means, wrap a simple UI around, I'm just suggesting that you might be able to generate a simpler example without it – MadProgrammer Apr 07 '17 at 01:48
  • 1
    @MadProgrammer I'll try to do so. – ostrichofevil Apr 07 '17 at 01:49
  • @MadProgrammer I've posted another example; I was able to recreate the problem. Part of the issue is that I'm in a really basic class, hence the simple but not-so-robust `MidiLatte` provided to us, but I already have a lot of experience with Java (my school doesn't let us skip), so I'm doing more advanced work within the assignment such as using Swing, which hasn't been covered (neither has concurrency). Unfortunately, `MidiLatte` wasn't really meant to accommodate what I'm trying to do (let alone a Swing app, hence the concurrency), and I think that's part of why I'm having problems. – ostrichofevil Apr 07 '17 at 02:21
  • The best I can tell (having no experience with MIDI) is the `Sequencer` is playing out it's queue, I've noted that even after I call `close` it continues to play. I don't think the problem lies with your code but with the `MidiLatte` class as you just don't have enough access to control the `Sequencer` - what you really want to do is all `stop` on the `Sequencer` – MadProgrammer Apr 07 '17 at 02:44
  • @MadProgrammer *"I'm not sure if this has anything to do with Swing per say,"* I find it handy to through a `JOptionPane` up when playing a sound clip or MIDI sequences, as that will keep the daemon thread on which they run, alive. Of course, there are ways around that, like.. OP: Create a non-daemon thread. – Andrew Thompson Apr 07 '17 at 02:44
  • *"**EDIT: Here's a self-contained example:**"* That's an uncompilable code snippet. An MCVE needs a class definition, imports and a `main` method to put it on screen. Have you actually ***read*** the links I offered on the first comment? – Andrew Thompson Apr 07 '17 at 02:46
  • 1
    @AndrewThompson The MIDI `Sequencer` is already playing through a non-daemon thread ... had to kill the process to stop it when testing :P – MadProgrammer Apr 07 '17 at 02:47
  • @MadProgrammer Huh.. OK not as sure about the MIDI side of it, but a `Clip` at least, **used to** use a daemon thread. :P – Andrew Thompson Apr 07 '17 at 02:48
  • @AndrewThompson I'm such a hypocrite—when I'm commenting upon others' posts, I'm perfectly capable of telling them to post a MVCE (and sharing [this link](http://stackoverflow.com/help/mcve), but I can't seem to do so myself! – ostrichofevil Apr 07 '17 at 03:53

1 Answers1

1

Okay, so, as much as can tell, the problem is with anything but the inability to actually controller the underlying Sequencer, as it's continuing to play it's cached data.

Unfortunately, you don't have access to the Sequencer ... yippy skippy, nor does the MidiLatte provide any extension point for you to modify the code.

A HACK solution would be to do what you're already doing, using reflection to access the private field, for example...

!! THIS IS A HACK !!

If you can't modify the original source of MidiLatte, then you have little choice, in order to achieve what you seem to be wanting to achieve, you must have access to the 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();
        }
    }

}

As to the need for concurrency, I'd say you don't need it, as the Sequencer is already using it's own thread to play the notes and concurrency would just make it more difficult in this case

Community
  • 1
  • 1
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Sorry, this is my fault for not choosing what to put in my example well, but I was already doing that! That's how I was able to call `stop` and had my problem. Even when I was doing that, the second sequence wasn't starting from the beginning. I think I'm going to try to deal with this by just taking a closer look at `MidiLatte` and looking more into daemon threads. I also might just try to get permission to ditch `MidiLatte` and use the Sound API myself. Thank you though for your time! – ostrichofevil Apr 07 '17 at 03:50
  • One of the things I also do is call `removeAll`, not sure if it made a difference, but in my testing I was able to get it to work just fine – MadProgrammer Apr 07 '17 at 03:52