/* Nord Modular Midi Protocol 3.03 Library Copyright (C) 2003-2006 Marcus Andersson This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ package net.sf.nmedit.jnmprotocol2; import java.awt.EventQueue; import java.util.NoSuchElementException; import java.util.concurrent.locks.ReentrantLock; import javax.sound.midi.Receiver; import javax.sound.midi.SysexMessage; import javax.sound.midi.Transmitter; import net.sf.nmedit.jnmprotocol2.utils.QueueBuffer; import net.sf.nmedit.jpdl2.stream.BitStream; /** * Receives and sends javax.sound.midi.MidiMessage. * Incoming javax.sound.midi.MidiMessages are * transformed to net.sf.nmedit.jnmprotocol2.MidiMessages. * * This class is thread safe. */ public abstract class AbstractNmProtocol { // empty byte array private static final byte[] NO_BYTES = new byte[0]; // lock for getLock().wait() / getLock().notify() / waitForActivity() calls private final Object lock = new Object(); // lock for the sendQueue private final Object sendLock = new Object(); // lock for the receivedQueue private final Object receiveLock = new Object(); // lock for the event queue private final Object eventsLock = new Object(); // lock for heartbeat() calls private ReentrantLock heartbeatLock = new ReentrantLock(false); // queue containing the outgoing packets private QueueBuffer<EnqueuedPacket> sendQueue = new QueueBuffer<EnqueuedPacket>(); // queue containing the data of incoming javax.sound.midi.MidiMessages private QueueBuffer<byte[]> receivedQueue = new QueueBuffer<byte[]>(); // queue containing incoming net.sf.nmedit.jnmprotocol2.MidiMessages private QueueBuffer<MidiMessage> eventQueue = new QueueBuffer<MidiMessage>(); private QueueBuffer<byte[]> sysexReceiveQueue = new QueueBuffer<byte[]>(); private byte[] tmpSysexData = null; // remembers when activity() was called the last time private volatile long recentActivity = 0; // transmitter (output) private Transmitter transmitter = new ProtocolTransmitter(); // receiver (input) private Receiver receiver = new ProtocolReceiver(this); // message handler to process messages from the event queue private MessageHandler messageHandler; public AbstractNmProtocol() { super(); } /** * Clears all message queues. */ public void reset() { synchronized (sendLock) { sendQueue.clear(); } synchronized (receiveLock) { receivedQueue.clear(); } synchronized (eventsLock) { eventQueue.clear(); } } /** * Sets the message handler. * @param messageHandler the message handler */ public synchronized void setMessageHandler(MessageHandler messageHandler) { this.messageHandler = messageHandler; } /** * Returns the message handler. * @return the message handler */ public synchronized MessageHandler getMessageHandler() { return messageHandler; } /** * Returns the current timeout value. * @return the current timeout value */ protected long getTimeout() { return 0; } /** * Processes all pending messages and dispatches incoming events. * * If heartbeat() is invoked while another thread holds * the lock then heartbeat() returns immediatelly. * * @throws MidiException a midi exception occured */ public final void heartbeat() throws MidiException { // return when heartbeat() is invoked recursively if (heartbeatLock.isHeldByCurrentThread()) return; // the reentrant lock is not necessary but // it avoids that invokations of heartbeat() // from multiple thread block each other unecessarily // see if the lock is held by another thread // if so then the heartbeat implementation is not called if (heartbeatLock.tryLock()) { // no other thread holds the lock try { // call the heartbeat implementation heartbeatImpl(); } finally { // release the lock - event if an exception occured heartbeatLock.unlock(); } } } /** * Processes all pending messages and dispatches incoming events. * * @throws MidiException a midi exception occured */ protected abstract void heartbeatImpl() throws MidiException; public final void waitForActivity() { waitForActivity(0); } /** * Waits at most 'timeout' milliseconds until some activity is observed, * an InterrupedException is thrown due to another reason or * one of the expected reply messages timeout is reached. * * Activity is observed when {@link #activity()} was called. * * @param timeout in milliseconds * @see #activity() */ public final void waitForActivity(long timeout) { // enssure that we do not wait longer than the reply message timeout long msgtimeout = getTimeout(); // reply message timeout is set ... if (msgtimeout>0) { // adjust timeout if necessary if (timeout == 0 || timeout > msgtimeout) timeout = msgtimeout; } // wait try { synchronized (getLock()) { getLock().wait(timeout); } } catch (InterruptedException e) { // caused by activity() or due to another reason } } public final Object getLock() { return lock; } /** * Sets the {@link #getRecentActivity() recent activity time} * and notifies all threads waiting on the {@link #getLock() lock}. */ public void activity() { recentActivity = System.currentTimeMillis(); synchronized (getLock()) { getLock().notify(); getLock().notifyAll(); } } /** * Returns the recent activity time in milliseconds. * The value is set each time {@link #activity()} is invoked. * @return recent activity */ public long getRecentActivity() { return recentActivity; } /** * Removes a message from the send queue. * @throws NoSuchElementException if this send queue is empty. */ protected void removeFromSendQueue() { synchronized (sendLock) { sendQueue.remove(); } } /** * Returns true if the send queue is empty. * @return true if the send queue is empty */ protected boolean isSendQueueEmpty() { synchronized (sendLock) { return sendQueue.isEmpty(); } } /** * Returns the next message in the send queue or null if the sendqueue is empty. * @return the next message in the send queue or null if the sendqueue is empty. */ protected EnqueuedPacket peekSendQueue() { synchronized (sendLock) { return sendQueue.peek(); } } /** * Clears the send queue. */ protected void clearSendQueue() { synchronized(sendLock) { sendQueue.clear(); } } private int indexOfByte(byte[] data, byte search, int start) { for (int i = start; i < data.length; i++) { if (data[i] == search) return i; } return -1; } private int indexOfByte(byte[] data, byte search) { return indexOfByte(data, search, 0); } protected void dumpByteArray(String s, byte []arr) { System.out.print(s + " ["); int i = 0; for (byte b : arr) { if (i++ == 16) { System.out.println(); i = 0; } System.out.print(Integer.toHexString(b) + ", "); } System.out.println("]"); } /* * with mmj under macosx, when a sysex is so long that it has to be split across multiple messages, sysex handling is not correct. * We get split sysex across receivedBytes() boundaries, and the following receivedBytes() start with 0xF7 * Handle this until there is a mmj workaround */ protected byte[] getNextReceivedSysexBytes() { byte[] data = null; if (sysexReceiveQueue.isEmpty()) { byte bytes[]; synchronized (receiveLock) { bytes= getReceivedBytes(); } // if (bytes!= null) { // System.out.println("getReceived "); // Hexdump.printHex(bytes); // } // if (bytes != null && bytes.length > 0) // dumpByteArray("receivedBytes ", bytes); if (bytes == NO_BYTES) return NO_BYTES; if (tmpSysexData != null) { byte newSysex[] = new byte[tmpSysexData.length + bytes.length]; System.arraycopy(tmpSysexData, 0, newSysex, 0, tmpSysexData.length); System.arraycopy(bytes, 0, newSysex, tmpSysexData.length, bytes.length); tmpSysexData = newSysex; } else { byte newSysex[] = new byte[bytes.length]; tmpSysexData = newSysex; System.arraycopy(bytes, 0, tmpSysexData, 0, bytes.length); } if (tmpSysexData == null) return NO_BYTES; // if (bytes != null && bytes.length > 0) // dumpByteArray("tmpSysexData ", tmpSysexData); int start = 0; do { int startIdx = indexOfByte(tmpSysexData, (byte)0xF0, start); if (startIdx < 0) { // discard tmpSysexData = null; break; } int endIdx = indexOfByte(tmpSysexData, (byte)0xF7, startIdx); if (endIdx < 0) { start = startIdx; break; } else { byte result[] = new byte[endIdx + 1 - startIdx]; System.arraycopy(tmpSysexData, startIdx, result, 0, endIdx - startIdx + 1); // dumpByteArray("result ", result); sysexReceiveQueue.offer(result); } start = endIdx + 1; } while ((tmpSysexData != null) && (start < tmpSysexData.length)); if (tmpSysexData != null) { if (start < tmpSysexData.length) { byte newSysex[] = new byte[tmpSysexData.length - start]; System.arraycopy(tmpSysexData, start, newSysex, 0, tmpSysexData.length - start); tmpSysexData = newSysex; } else if (start == tmpSysexData.length) { tmpSysexData = null; } } } data = sysexReceiveQueue.poll(); return data != null ? data : NO_BYTES; } /** * Removes and returns the next message in the received queue. * If the queue was empty then an empty byte array is returned. * @return the next message int the received queue */ protected byte[] getReceivedBytes() { byte[] data = null; synchronized(receiveLock) { byte[] tmpData = null; while ((tmpData = receivedQueue.poll()) != null) { if (data == null) { data = tmpData; } else { byte []tmpData2= new byte[data.length + tmpData.length]; System.arraycopy(data, 0, tmpData2, 0, data.length); System.arraycopy(tmpData, 0, tmpData2, data.length, tmpData.length); data = tmpData2; } } } return (data != null) ? data : NO_BYTES; } /** * Sends a javax.sound.midi.MidiMessage to the transmitter/device. * @param message the message */ protected void send(javax.sound.midi.MidiMessage message) { transmitter.getReceiver().send(message, -1); } public void send(MidiMessage midiMessage) throws MidiException { BitStream stream = midiMessage.getBitStream(); if (stream == null) throw new MidiException("stream is null in "+midiMessage, MidiException.INVALID_MIDI_DATA); EnqueuedPacket packet = EnqueuedPacket.create(stream.toByteArray(), midiMessage.expectsReply()); synchronized (sendLock) { sendQueue.offer(packet); } activity(); } /** * Data of a javax.sound.midi.MidiMessage received by the receiver. * @param data midi message */ protected void received(byte[] data) { synchronized (receiveLock) { receivedQueue.offer(data); } activity(); } /** * Adds an incoming midi message to the event queue. * The messages will be passed to the {@link #getMessageHandler() message handler} * by {@link #dispatchEvents()}. * @param message incoming midi message */ protected void eventQueue_offer(MidiMessage message) { synchronized (eventsLock) { eventQueue.offer(message); } } /** * Returns the transmitter. * @return the transmitter */ public Transmitter getTransmitter() { return transmitter; } /** * Returns the receiver. * @return the receiver */ public Receiver getReceiver() { return receiver; } /** * Removes and returns all events which are currently in the event queue. * If the event queue is empty, then null is returned. * @return events */ private QueueBuffer<MidiMessage> releaseEvents() { synchronized (eventsLock) { if (!eventQueue.isEmpty()) return eventQueue.release(); } return null; } /** * Dispatches the events returned by {@link #releaseEvents()} * in the current thread. */ public void dispatchEventsImmediatelly() { QueueBuffer<MidiMessage> events = releaseEvents(); if (events != null) dispatchEvents(events); } /** * Dispatches the events on the AWT event dispatch thread. */ public void dispatchEvents() { QueueBuffer<MidiMessage> events = releaseEvents(); if (events == null) return; if (EventQueue.isDispatchThread()) { // we are in the AWT event dispatch thread dispatchEvents(events); } else { // post the event to the AWT event dispatch thread EventQueue.invokeLater(new DispatchLater(events)); } } /** * Dispatches the events. * @param events the events */ protected void dispatchEvents(QueueBuffer<MidiMessage> events) { MessageHandler mh = getMessageHandler(); if (mh == null) return; for (MidiMessage message: events) { mh.processMessage(message); } } private class DispatchLater implements Runnable { private QueueBuffer<MidiMessage> events; public DispatchLater(QueueBuffer<MidiMessage> events) { this.events = events; } public void run() { dispatchEvents(events); } } private static class ProtocolReceiver implements Receiver { // private boolean closed = false; private AbstractNmProtocol receiver; public ProtocolReceiver(AbstractNmProtocol receiver) { this.receiver = receiver; } public void send(javax.sound.midi.MidiMessage message, long timeStamp) {/* if (closed) throw new IllegalStateException("receiver closed");*/ if (message instanceof SysexMessage) { // System.out.println("sysex message status " + message.getStatus()); // Hexdump.printHex(((SysexMessage)message).getMessage()); if (message.getStatus() == 0xf7){ receiver.received(((SysexMessage)message).getData()); } else { receiver.received(message.getMessage()); } } else { receiver.received(message.getMessage()); } } public void close() { // closed = true; } } /** * The transmitter */ private static class ProtocolTransmitter implements Transmitter { private Receiver receiver; public synchronized void setReceiver( Receiver receiver ) { this.receiver = receiver; } public synchronized Receiver getReceiver() { return receiver; } public void close() { // no op } } }