kshoji / javax.sound.midi-for-Android

Package javax.sound.midi porting for Android
Apache License 2.0
75 stars 34 forks source link

"MidiUnavailableException: Transmitter not found" when using val sequencer = MidiSystem.getSequencer() #17

Open chriscoomber opened 4 years ago

chriscoomber commented 4 years ago

I'm trying to write code which plays MIDI notes from file.

I want to be able to intercept packets sent by the sequencer, and then process them, before sending them on to the synthesizer.

Sequencer -> Custom processing -> Synthesizer

I don't want the sequencer to send events directly to the Synthesizer!

Based on reading the JavaX docs, I think I should be able to do the following:

// get sequencer
Sequencer seq = MidiSystem.getSequencer(false);
seq.open();
seq.setSequence((new StandardMidiFileReader()).getSequence(getActivity().getAssets().open("test.mid")));

// get synth
Synthesizer synth = new SoftSynthesizer();
synth.open();
synth.loadAllInstruments(SF2Soundbank(getActivity().getAssets().open("test.sf2")));

// get middleware - this implements Transmitter and Receiver
PacketInterceptor intercept = new PacketInterceptor();

// connect everything together
sequencer.getTransmitter().setReceiver(packetInterceptor);
packetInterceptor.setReceiver(synth.getReceiver());

However, I'm seeing jp.kshoji.javax.sound.midi.MidiUnavailableException: Transmitter not found. From looking at the code, it seems like the Sequencer isn't created with any Transmitters. Bizarrely, it seems to get its transmitters from other devices, using MidiSystemUtils.getTransmitters(). This doesn't seem right. It is contrary to the documentation at https://docs.oracle.com/javase/7/docs/api/javax/sound/midi/MidiSystem.html#getSequencer(boolean), which indicates that the Sequencer should have no open transmitters, but that there should still be transmitters that I can get so that I can set their receivers.

This issue seems related to #2, but the resolution of that doesn't seem applicable to me, because I want to control which device is connected to which other device, using transmitters/receivers.

chriscoomber commented 4 years ago

From looking a bit more, it looks like upon open(), SequencerImpl does two things (among other things):

SequencerImpl doesn't have its own Transmitters, and I think it should.

The bits in bold are bugs. There's no way I can see that you could configure a collection of MIDI devices that includes the SequencerImpl to talk to each other in any way other then all of them talking to all of them. You can't finely tune who the SequencerImpl sends events to.

For anyone else seeing this issue, I've managed to hack around it for my use-case (Sequencer -> Custom Processing -> Synth) by doing the following (kotlin code):

        // create synth
        synth = SoftSynthesizer()
        synth.open()
        synth.loadAllInstruments(SF2Soundbank(activity!!.assets.open("test.sf2")))

        // get sequencer
        val sequencer = MidiSystem.getSequencer(false)

        // create custom processor
        val customEventProcessor = CustomEventProcessor()

        // Sequencer -> custom processor. Due to a bug, we have to go via the MidiSystem in a
        // special way.
        MidiSystem.addMidiDevice(customEventProcessor)

        // Custom processor -> synth
        customEventProcessor.transmitter.receiver = synth.receiver

        // Play
        sequencer.open()
        val fileReader = StandardMidiFileReader()
        val sequence = fileReader.getSequence(activity!!.assets.open("test.mid"))
        sequencer.sequence = sequence
        sequencer.start()

where your custom processing (between the sequencer and the synth) goes in this class:

    class CustomEventProcessor : MidiDevice {

        private val recv = object : Receiver {
            override fun send(message: MidiMessage, ms: Long) {
                // Custom message processing goes here
                Log.d("test", "got message: $message")

                // Pass on to the transmitter
                trans.receiver?.send(message, ms)
            }

            override fun close() {}
        }

        private val trans = WeakTransmitter()

        override fun getDeviceInfo(): MidiDevice.Info {
            return MidiDevice.Info("test", "example", "custom processor", "0.1")
        }

        // Register one receiver with the MidiSystem. This is for the sequencer to connect to us.
        override fun getMaxReceivers() = 1
        override fun getReceiver(): Receiver = recv
        override fun getReceivers(): MutableList<Receiver> = MutableList(1) { recv }

        // Don't register the transmitter with MidiSystem - advertise a count of 0. The sequencer
        // implementation is bugged and will point this at itself, stealing it from legitimate
        // devices. We can still use the transmitter manually.
        override fun getMaxTransmitters() = 0
        override fun getTransmitter(): Transmitter = trans
        override fun getTransmitters(): MutableList<Transmitter> = mutableListOf()

        override fun open() {}
        override fun isOpen() = true
        override fun close() {}
        override fun getMicrosecondPosition(): Long { TODO("Not yet implemented") }
    }

    // Convenient simple implementation of a Transmitter
    class WeakTransmitter : Transmitter {
        private var destination: WeakReference<Receiver>? = null

        override fun getReceiver(): Receiver? {
            return destination?.get()
        }

        override fun setReceiver(recv: Receiver?) {
            destination = recv?.let { WeakReference(recv) }
        }

        override fun close() {
        }
    }
lordsxrom commented 2 years ago

From looking a bit more, it looks like upon open(), SequencerImpl does two things (among other things):

  • Find all the receivers of all devices that have been added to MidiSystem. It will send events (during playback) to all of these. You cannot control which of these receive the events.
  • Finds all the transmitters of all devices that have been added to MidiSystem. It sets itself as the destination receiver (for recording) for those transmitter, potentially stealing that transmitter from another device that was set up properly (with device.getTransmitter().setReceiver(foo)).

SequencerImpl doesn't have its own Transmitters, and I think it should.

The bits in bold are bugs. There's no way I can see that you could configure a collection of MIDI devices that includes the SequencerImpl to talk to each other in any way other then all of them talking to all of them. You can't finely tune who the SequencerImpl sends events to.

For anyone else seeing this issue, I've managed to hack around it for my use-case (Sequencer -> Custom Processing -> Synth) by doing the following (kotlin code):

        // create synth
        synth = SoftSynthesizer()
        synth.open()
        synth.loadAllInstruments(SF2Soundbank(activity!!.assets.open("test.sf2")))

        // get sequencer
        val sequencer = MidiSystem.getSequencer(false)

        // create custom processor
        val customEventProcessor = CustomEventProcessor()

        // Sequencer -> custom processor. Due to a bug, we have to go via the MidiSystem in a
        // special way.
        MidiSystem.addMidiDevice(customEventProcessor)

        // Custom processor -> synth
        customEventProcessor.transmitter.receiver = synth.receiver

        // Play
        sequencer.open()
        val fileReader = StandardMidiFileReader()
        val sequence = fileReader.getSequence(activity!!.assets.open("test.mid"))
        sequencer.sequence = sequence
        sequencer.start()

where your custom processing (between the sequencer and the synth) goes in this class:

    class CustomEventProcessor : MidiDevice {

        private val recv = object : Receiver {
            override fun send(message: MidiMessage, ms: Long) {
                // Custom message processing goes here
                Log.d("test", "got message: $message")

                // Pass on to the transmitter
                trans.receiver?.send(message, ms)
            }

            override fun close() {}
        }

        private val trans = WeakTransmitter()

        override fun getDeviceInfo(): MidiDevice.Info {
            return MidiDevice.Info("test", "example", "custom processor", "0.1")
        }

        // Register one receiver with the MidiSystem. This is for the sequencer to connect to us.
        override fun getMaxReceivers() = 1
        override fun getReceiver(): Receiver = recv
        override fun getReceivers(): MutableList<Receiver> = MutableList(1) { recv }

        // Don't register the transmitter with MidiSystem - advertise a count of 0. The sequencer
        // implementation is bugged and will point this at itself, stealing it from legitimate
        // devices. We can still use the transmitter manually.
        override fun getMaxTransmitters() = 0
        override fun getTransmitter(): Transmitter = trans
        override fun getTransmitters(): MutableList<Transmitter> = mutableListOf()

        override fun open() {}
        override fun isOpen() = true
        override fun close() {}
        override fun getMicrosecondPosition(): Long { TODO("Not yet implemented") }
    }

    // Convenient simple implementation of a Transmitter
    class WeakTransmitter : Transmitter {
        private var destination: WeakReference<Receiver>? = null

        override fun getReceiver(): Receiver? {
            return destination?.get()
        }

        override fun setReceiver(recv: Receiver?) {
            destination = recv?.let { WeakReference(recv) }
        }

        override fun close() {
        }
    }

Thanks a lot. It was really helpful :)