/*
* Copyright (C) 2012 Klaus Reimer <k@ailis.de>
* See LICENSE.txt for licensing information.
*/
package de.ailis.midi4js;
import java.applet.Applet;
import java.util.HashMap;
import java.util.Map;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiDevice.Info;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Transmitter;
import netscape.javascript.JSObject;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONStringer;
/**
* Applet which exposes access to MIDI hardware to JavaScript.
*
* @author Klaus Reimer (k@ailis.de)
*/
public class Midi4JS extends Applet
{
/** Serial version UID. */
private static final long serialVersionUID = 1L;
/** Map from device handle to device. */
private transient Map<Integer, MidiDevice> deviceMap;
/** Map from receiver handle to receiver. */
private transient Map<Integer, Receiver> receiverMap;
/** Map from transmitter handle to transmitter. */
private transient Map<Integer, Transmitter> transmitterMap;
/**
* Returns the JavaScript namespace of Midi4JS.
*
* @return The JavaScript namespace.
*/
JSObject getJSNamespace()
{
try
{
final JSObject window = JSObject.getWindow(this);
final JSObject namespace = (JSObject) window.getMember("midi4js");
return namespace;
}
catch (final Exception e)
{
System.err.println("Unable to get midi4js JavaScript " +
"namespace. Maybe JavaScript context is already destroyed? " +
"Original exception: ");
e.printStackTrace(System.err);
return null;
}
}
/**
* Executes a method on the JavaScript namespace of Midi4JS.
*
* @param methodName
* The name of the method to execute.
* @param arguments
* Optional arguments.
*/
void execJSMethod(final String methodName, final Object... arguments)
{
// Get the JavaScript midi4js namespace. Do nothing if not found.
final JSObject namespace = getJSNamespace();
if (namespace == null) return;
try
{
namespace.call(methodName, arguments);
}
catch (final Exception e)
{
System.err.println("Unable to call method '" + methodName + "' " +
"on midi4js namespace. Original exception: ");
e.printStackTrace(System.err);
}
}
/**
* @see java.applet.Applet#start()
*/
@Override
public void start()
{
System.out.println("Started midi4js applet (Instance #" +
System.identityHashCode(this) + ")");
this.deviceMap = new HashMap<Integer, MidiDevice>();
this.receiverMap = new HashMap<Integer, Receiver>();
this.transmitterMap = new HashMap<Integer, Transmitter>();
execJSMethod("appletStarted");
}
/**
* @see java.applet.Applet#stop()
*/
@Override
public void stop()
{
for (final Receiver receiver : this.receiverMap.values())
receiver.close();
for (final Transmitter transmitter : this.transmitterMap.values())
transmitter.close();
for (final MidiDevice device : this.deviceMap.values())
if (device.isOpen()) device.close();
this.deviceMap = null;
this.receiverMap = null;
this.transmitterMap = null;
System.out.println("Stopped midi4js applet (Instance #" +
System.identityHashCode(this) + ")");
}
/**
* Resolves a device handle into a MIDI device. If this fails then an
* exception is thrown.
*
* @param handle
* The device handle.
* @return The MIDI device.
*/
private MidiDevice resolveDeviceHandle(final int handle)
{
final MidiDevice device = this.deviceMap.get(handle);
if (device == null)
throw new RuntimeException("No MIDI device with handle " + handle +
" found");
return device;
}
/**
* Resolves a receiver handle into a receiver. If this fails then an
* exception is thrown.
*
* @param handle
* The receiver handle.
* @return The receiver.
*/
private Receiver resolveReceiverHandle(final int handle)
{
final Receiver receiver = this.receiverMap.get(handle);
if (receiver == null)
throw new RuntimeException("No receiver with handle " + handle +
" found");
return receiver;
}
/**
* Resolves a transmitter handle into a transmitter. If this fails then an
* exception is thrown.
*
* @param handle
* The transmitter handle.
* @return The transmitter.
*/
private Transmitter resolveTransmitterHandle(final int handle)
{
final Transmitter transmitter = this.transmitterMap.get(handle);
if (transmitter == null)
throw new RuntimeException("No transmitter with handle " + handle +
" found");
return transmitter;
}
/**
* Writes MIDI device info into the specified JSON stringer.
*
* @param json
* The JSON stringer.
* @param info
* The MIDI device info.
* @throws JSONException
* When JSON output fails.
*/
private void deviceInfo(final JSONStringer json, final Info info)
throws JSONException
{
json.object();
json.key("name").value(info.getName());
json.key("description").value(info.getDescription());
json.key("vendor").value(info.getVendor());
json.key("version").value(info.getVersion());
json.endObject();
}
/**
* Returns an array of information objects representing the set of all MIDI
* devices available on the system.
*
* @return The array of information objects about all available MIDI
* devices.
* @throws JSONException
* When JSON string could not be constructed.
*/
public String getMidiDeviceInfo() throws JSONException
{
final JSONStringer js = new JSONStringer();
js.array();
for (final Info info : MidiSystem.getMidiDeviceInfo())
{
js.object();
js.key("name").value(info.getName());
js.key("description").value(info.getDescription());
js.key("vendor").value(info.getVendor());
js.key("version").value(info.getVersion());
js.endObject();
}
js.endArray();
return js.toString();
}
/**
* Returns the handle for the midi device with the specified index. The
* device must be released by calling the releaseMidiDevice() method!
*
* @param index
* The device index.
* @return The device handle.
* @throws MidiUnavailableException
* If the midi device is not available.
*/
public int getMidiDevice(final int index) throws MidiUnavailableException
{
final Info[] infos = MidiSystem.getMidiDeviceInfo();
final MidiDevice device = MidiSystem.getMidiDevice(infos[index]);
final int handle = System.identityHashCode(device);
this.deviceMap.put(handle, device);
return handle;
}
/**
* Releases the specified device handle.
*
* @param deviceHandle
* The device handle to release.
*/
public void releaseMidiDevice(final int deviceHandle)
{
this.deviceMap.remove(deviceHandle);
}
/**
* Checks how many transmitters the specified device supports. -1 means
* unlimited number of transmitters.
*
* @param deviceHandle
* The device handle.
* @return The maximum number of transmitters. -1 for unlimited.
*/
public int getMaxTransmitters(final int deviceHandle)
{
return resolveDeviceHandle(deviceHandle).getMaxTransmitters();
}
/**
* Checks how many receivers the specified device supports. -1 means
* unlimited number of receivers.
*
* @param deviceHandle
* The device handle.
* @return The maximum number of receivers. -1 for unlimited.
*/
public int getMaxReceivers(final int deviceHandle)
{
return resolveDeviceHandle(deviceHandle).getMaxReceivers();
}
/**
* Returns MIDI IN receiver for the specified device. A receiver must
* be closed when no longer needed.
*
* @param deviceHandle
* The handle of the MIDI device.
* @return The handle of the receiver.
* @throws MidiUnavailableException
* When MIDI device is unavailable.
*/
public int getReceiver(final int deviceHandle)
throws MidiUnavailableException
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
final Receiver receiver = device.getReceiver();
final int receiverHandle = System.identityHashCode(receiver);
this.receiverMap.put(receiverHandle, receiver);
return receiverHandle;
}
/**
* Releases the specifeid receiver handle.
*
* @param receiverHandle
* The receiver handle to release.
*/
public void closeReceiver(final int receiverHandle)
{
final Receiver receiver = resolveReceiverHandle(receiverHandle);
receiver.close();
this.receiverMap.remove(receiverHandle);
}
/**
* Returns all currently open receivers.
*
* @param deviceHandle
* The device handle.
* @return All currently open receivers in form of a JSON-encoded string
* which describes an array of receiver handles.
* @throws JSONException
* When JSON data could not be constructed.
*/
public String getReceivers(final int deviceHandle) throws JSONException
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
final JSONStringer json = new JSONStringer();
json.array();
for (final Receiver receiver : device.getReceivers())
{
json.value(System.identityHashCode(receiver));
}
json.endArray();
return json.toString();
}
/**
* Returns MIDI IN transmitter for the specified device. A transmitter must
* be closed when no longer needed.
*
* @param deviceHandle
* The handle of the MIDI device.
* @return The handle of the transmitter.
* @throws MidiUnavailableException
* When MIDI device is unavailable.
*/
public int getTransmitter(final int deviceHandle)
throws MidiUnavailableException
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
final Transmitter transmitter = device.getTransmitter();
final int transmitterHandle = System.identityHashCode(transmitter);
this.transmitterMap.put(transmitterHandle, transmitter);
return transmitterHandle;
}
/**
* Releases the specifeid transmitter handle.
*
* @param transmitterHandle
* The transmitter handle to release.
*/
public void closeTransmitter(final int transmitterHandle)
{
final Transmitter transmitter =
resolveTransmitterHandle(transmitterHandle);
transmitter.close();
this.transmitterMap.remove(transmitterHandle);
}
/**
* Returns all currently open transmitters.
*
* @param deviceHandle
* The device handle.
* @return All currently open transmitters in form of a JSON-encoded string
* which describes an array of transmitter handles.
* @throws JSONException
* When JSON data could not be constructed.
*/
public String getTransmitters(final int deviceHandle) throws JSONException
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
final JSONStringer json = new JSONStringer();
json.array();
for (final Transmitter transmitter : device.getTransmitters())
{
json.value(System.identityHashCode(transmitter));
}
json.endArray();
return json.toString();
}
/**
* Sends a MIDI message to a receiver.
*
* @param receiverHandle
* The handle of the receiver.
* @param jsonMessageStr
* Then message encoded as a JSON string
* @param timeStamp
* The message timestamp
* @throws InvalidMidiDataException
* When the midi data is invalid.
* @throws JSONException
* When JSON data could not be parsed.
*/
public void sendMessage(final int receiverHandle,
final String jsonMessageStr,
final long timeStamp)
throws InvalidMidiDataException, JSONException
{
final Receiver receiver = resolveReceiverHandle(receiverHandle);
final JSONObject json = new JSONObject(jsonMessageStr);
final JSONArray jsonData = json.getJSONArray("data");
final int length = jsonData.length();
final byte[] data = new byte[length];
for (int i = 0; i < length; i++)
data[i] = (byte) (jsonData.getInt(i) & 0xff);
final RawMidiMessage message = new RawMidiMessage(data);
receiver.send(message, timeStamp);
}
/**
* Sets the receiver of a transmitter.
*
* @param transmitterHandle
* The handle of the transmitter.
* @param receiverHandle
* The handle of the receiver. 0 to unset.
*/
public void setTransmitterReceiver(final int transmitterHandle,
final int receiverHandle)
{
final Transmitter transmitter =
resolveTransmitterHandle(transmitterHandle);
final Receiver receiver =
receiverHandle == 0 ? null : resolveReceiverHandle(receiverHandle);
transmitter.setReceiver(receiver);
}
/**
* Returns the handle of the receiver which is connected with the
* specified transmitter.
*
* @param transmitterHandle
* The handle of the transmitter.
* @return The handle of the receiver or 0 if none.
*/
public int getTransmitterReceiver(final int transmitterHandle)
{
final Transmitter transmitter =
resolveTransmitterHandle(transmitterHandle);
final Receiver receiver = transmitter.getReceiver();
return System.identityHashCode(receiver);
}
/**
* Creates a new receiver and returns the handle on it.
*
* @return The handle for the receiver.
*/
public int createReceiver()
{
final Receiver receiver = new MessageReceiver(this);
final int handle = System.identityHashCode(receiver);
this.receiverMap.put(handle, receiver);
return handle;
}
/**
* Opens the specified MIDI device.
*
* @param deviceHandle
* The device handle.
* @throws MidiUnavailableException
* When MIDI device is not available.
*/
public void openMidiDevice(final int deviceHandle)
throws MidiUnavailableException
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
device.open();
}
/**
* Checks if the specified MIDI device is open.
*
* @param deviceHandle
* The device handle.
* @return True if device is, open false if not.
*/
public boolean isMidiDeviceOpen(final int deviceHandle)
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
return device.isOpen();
}
/**
* Closes the specified MIDI device.
*
* @param deviceHandle
* The device handle.
* @throws MidiUnavailableException
* When MIDI device is not available.
*/
public void closeMidiDevice(final int deviceHandle)
throws MidiUnavailableException
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
device.close();
}
/**
* Returns the device info of the specified device.
*
* @param deviceHandle
* The device handle.
* @return The device info as a JSON string.
* @throws JSONException
* When JSON output fails.
*/
public String getMidiDeviceInfo(final int deviceHandle)
throws JSONException
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
final JSONStringer json = new JSONStringer();
deviceInfo(json, device.getDeviceInfo());
return json.toString();
}
/**
* Returns the current timestamp of the device.
*
* @param deviceHandle
* The device handle.
* @return The current timestamp of the device.
*/
public long getMidiDeviceMicrosecondPosition(final int deviceHandle)
{
final MidiDevice device = resolveDeviceHandle(deviceHandle);
return device.getMicrosecondPosition();
}
}