package com.nerdscentral.audio.midi;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Stack;
import java.util.concurrent.ConcurrentHashMap;
import javax.sound.midi.InvalidMidiDataException;
import javax.sound.midi.MetaMessage;
import javax.sound.midi.MidiDevice;
import javax.sound.midi.MidiDevice.Info;
import javax.sound.midi.MidiEvent;
import javax.sound.midi.MidiMessage;
import javax.sound.midi.MidiSystem;
import javax.sound.midi.MidiUnavailableException;
import javax.sound.midi.Receiver;
import javax.sound.midi.Sequence;
import javax.sound.midi.Sequencer;
import javax.sound.midi.ShortMessage;
import javax.sound.midi.Synthesizer;
import javax.sound.midi.SysexMessage;
import javax.sound.midi.Track;
import javax.sound.midi.Transmitter;
import com.nerdscentral.sython.SFPL_RuntimeException;
public class MidiFunctions
{
public static final int MODWHEEL = 0x01;
public static final int BREATH = 0x02;
public static final int FOOT = 0x04;
public static final int PORTAMENTO_TIME = 0x05;
public static final int VOLUME = 0x07;
public static final int BALANCE = 0x08;
public static final int PAN = 0x0A;
public static final int PORTAMENTO = 0x41;
public static final int SOSTENUTO = 0x42;
public static final int RESET = 0x79;
public static final int NOTE_ON = 0x90;
public static final int NOTE_OFF = 0x80;
private static HashMap<Integer, String> controllerLookup= new HashMap<>();
static
{
controllerLookup.put(MODWHEEL, "modwheel"); //$NON-NLS-1$
controllerLookup.put(BREATH, "breath"); //$NON-NLS-1$
controllerLookup.put(FOOT, "foot"); //$NON-NLS-1$
controllerLookup.put(VOLUME, "volume"); //$NON-NLS-1$
controllerLookup.put(BALANCE, "balance"); //$NON-NLS-1$
controllerLookup.put(PAN, "pan"); //$NON-NLS-1$
controllerLookup.put(PORTAMENTO,"portamento"); //$NON-NLS-1$
controllerLookup.put(SOSTENUTO, "sostenuto"); //$NON-NLS-1$
controllerLookup.put(RESET, "reset"); //$NON-NLS-1$
}
private static String getController(int numb)
{
String ret=controllerLookup.get(numb);
if(ret==null)
{
ret="unknown"; //$NON-NLS-1$
}
return ret;
}
private static HashMap<Integer, String> smLookup = new HashMap<>();
static
{
smLookup.put(ShortMessage.ACTIVE_SENSING, "active_sensing"); //$NON-NLS-1$
smLookup.put(ShortMessage.CHANNEL_PRESSURE, "channel_pressure"); //$NON-NLS-1$
smLookup.put(ShortMessage.CONTINUE, "continue"); //$NON-NLS-1$
smLookup.put(ShortMessage.CONTROL_CHANGE, "control_change"); //$NON-NLS-1$
smLookup.put(ShortMessage.END_OF_EXCLUSIVE, "end_of_exclusive"); //$NON-NLS-1$
smLookup.put(ShortMessage.MIDI_TIME_CODE, "midi_time_code"); //$NON-NLS-1$
smLookup.put(ShortMessage.NOTE_OFF, "note_off"); //$NON-NLS-1$
smLookup.put(ShortMessage.NOTE_ON, "note_on"); //$NON-NLS-1$
smLookup.put(ShortMessage.PITCH_BEND, "pitch_bend"); //$NON-NLS-1$
smLookup.put(ShortMessage.POLY_PRESSURE, "poly_pressure"); //$NON-NLS-1$
smLookup.put(ShortMessage.PROGRAM_CHANGE, "program_change"); //$NON-NLS-1$
smLookup.put(ShortMessage.SONG_POSITION_POINTER, "song_position_pointer"); //$NON-NLS-1$
smLookup.put(ShortMessage.SONG_SELECT, "song_select"); //$NON-NLS-1$
smLookup.put(ShortMessage.START, "start"); //$NON-NLS-1$
smLookup.put(ShortMessage.STOP, "stop"); //$NON-NLS-1$
smLookup.put(ShortMessage.SYSTEM_RESET, "system_reset"); //$NON-NLS-1$
smLookup.put(ShortMessage.TIMING_CLOCK, "timing_clock"); //$NON-NLS-1$
smLookup.put(ShortMessage.TUNE_REQUEST, "tune_request"); //$NON-NLS-1$
}
private static HashMap<Integer, String> metaLookup = new HashMap<>();
static
{
metaLookup.put(0x00,"sequence_number"); //$NON-NLS-1$
metaLookup.put(0x01,"text"); //$NON-NLS-1$
metaLookup.put(0x02,"copyright"); //$NON-NLS-1$
metaLookup.put(0x03,"squency_name"); //$NON-NLS-1$
metaLookup.put(0x04,"instrument_name"); //$NON-NLS-1$
metaLookup.put(0x05,"lyric"); //$NON-NLS-1$
metaLookup.put(0x06,"marker"); //$NON-NLS-1$
metaLookup.put(0x07,"cue"); //$NON-NLS-1$
metaLookup.put(0x20,"channel"); //$NON-NLS-1$
metaLookup.put(0x2F,"end"); //$NON-NLS-1$
metaLookup.put(0x51,"tempo"); //$NON-NLS-1$
metaLookup.put(0x54,"smpte"); //$NON-NLS-1$
metaLookup.put(0x58,"time_signature"); //$NON-NLS-1$
metaLookup.put(0x59,"key_signature"); //$NON-NLS-1$
metaLookup.put(0x7F,"other"); //$NON-NLS-1$
}
public static Sequence preProcessChannels(Sequence seqIn) throws InvalidMidiDataException
{
// If there are multiple channels per track we move the channels into separate
// tracks so the output numer of tracks might be bigger than the input so first
// work out how many tracks we need.
int nTracks = 0;
// Create the output sequence
Sequence sqlOut = new Sequence(seqIn.getDivisionType(), seqIn.getResolution(), nTracks);
return sqlOut;
}
public static Sequence readMidiFile(String fileName) throws InvalidMidiDataException, IOException
{
return MidiSystem.getSequence(new File(fileName));
}
public static void setChannel(ShortMessage sm, int channel) throws InvalidMidiDataException
{
sm.setMessage(sm.getCommand(), channel, sm.getData1(), sm.getData2());
}
public static void setChannel(MidiEvent ev, int channel) throws InvalidMidiDataException
{
if (ev.getMessage() instanceof ShortMessage)
{
ShortMessage sm = (ShortMessage) ev.getMessage();
sm.setMessage(sm.getCommand(), channel, sm.getData1(), sm.getData2());
}
}
public static List<Object> processSequence(Sequence sequence)
{
return processSequence(sequence, "INSIDE"); //$NON-NLS-1$
}
// DELETE - remove the overlapping (second etc) note
// INSIDE - make the second note inside the first
// OUTSIDE - break into two sequential notes
public static List<Object> processSequence(Sequence sequence, String overlapMode)
{
if (overlapMode == "OUTSIDE") throw new RuntimeException("Not yet supported"); //$NON-NLS-1$ //$NON-NLS-2$
List<Object> table = new ArrayList<>();
int trackNumber = -1;
for (Track track : sequence.getTracks())
{
++trackNumber;
List<Object> column = new ArrayList<>();
table.add(column);
HashMap<Integer, Stack<Map<String, Object>>> onMap = new HashMap<>();
for (int i = 0; i < track.size(); i++)
{
MidiEvent event = track.get(i);
MidiMessage message = event.getMessage();
if (message instanceof ShortMessage)
{
ShortMessage sm = (ShortMessage) message;
if (sm.getCommand() == NOTE_ON)
{
int key = sm.getData1();
int velocity = sm.getData2();
if (!onMap.containsKey(key))
{
onMap.put(key, new Stack<Map<String, Object>>());
}
Stack<Map<String, Object>> stack = onMap.get(key);
if (velocity > 0)
{
Map<String, Object> row = new ConcurrentHashMap<>();
row.put("command", "note"); //$NON-NLS-1$//$NON-NLS-2$
row.put("tick", (double) event.getTick()); //$NON-NLS-1$
row.put("key", (double) key); //$NON-NLS-1$
row.put("velocity", (double) velocity); //$NON-NLS-1$
row.put("track", (double) trackNumber); //$NON-NLS-1$
row.put("channel", (double) sm.getChannel()); //$NON-NLS-1$
stack.push(row);
if (stack.size() > 1)
{
System.err.println("Warning: Overlapping Notes Detected:"); //$NON-NLS-1$
for (Map<String, Object> s : stack)
{
StringBuilder out = new StringBuilder();
for (Entry<String, Object> kv : s.entrySet())
{
out.append(kv.getKey());
out.append("="); //$NON-NLS-1$
out.append(kv.getValue());
out.append(" "); //$NON-NLS-1$
}
System.err.println(" " + out); //$NON-NLS-1$
}
if (overlapMode == "DELETE") //$NON-NLS-1$
{
System.err.println(" DELETE - remove second"); //$NON-NLS-1$
stack.pop();
}
}
}
else
{
Map<String, Object> row = stack.pop();
if (stack.size() == 0)
{
System.err.println("Deleted or missmatch note silence"); //$NON-NLS-1$
}
else
{
row.put("tick_off", (double) event.getTick()); //$NON-NLS-1$
column.add(row);
}
}
}
else if (sm.getCommand() == NOTE_OFF)
{
int key = sm.getData1();
Stack<Map<String, Object>> stack = onMap.get(key);
if (stack.size() == 0)
{
System.err.println("Deleted or missmatch end note"); //$NON-NLS-1$
}
else
{
Map<String, Object> row = stack.pop();
row.put("tick_off", (double) event.getTick()); //$NON-NLS-1$
column.add(row);
}
}
else
{
Map<String, Object> row = new ConcurrentHashMap<>();
String command = smLookup.get(sm.getCommand());
if (command == null) command = "small-unknown"; //$NON-NLS-1$
row.put("command", "command"); //$NON-NLS-1$ //$NON-NLS-2$
row.put("type", command); //$NON-NLS-1$
row.put("track", (double) trackNumber); //$NON-NLS-1$
row.put("channel", (double) sm.getChannel()); //$NON-NLS-1$
if(command.equals("control_change")) //$NON-NLS-1$
{
row.put("controller",getController(sm.getData1())); //$NON-NLS-1$
row.put("amount",sm.getData2()); //$NON-NLS-1$
}else if(command.equals("pitch_bend")){ //$NON-NLS-1$
row.put("amount",sm.getData1()<<8 | sm.getData2()); //$NON-NLS-1$
// TODO interpret more commands completely rather than leaving
// raw
}else{
row.put("data1", (double) sm.getData1()); //$NON-NLS-1$
row.put("data2", (double) sm.getData2()); //$NON-NLS-1$
}
row.put("tick", (double) event.getTick()); //$NON-NLS-1$
column.add(row);
}
}
else if (message instanceof SysexMessage)
{
SysexMessage sxm = (SysexMessage) message;
Map<String, Object> row = new ConcurrentHashMap<>();
row.put("command", "sysex"); //$NON-NLS-1$ //$NON-NLS-2$
row.put("data", sxm.getData()); //$NON-NLS-1$
row.put("length", sxm.getLength()); //$NON-NLS-1$
row.put("status", sxm.getStatus()); //$NON-NLS-1$
row.put("track", (double) trackNumber); //$NON-NLS-1$
row.put("tick", (double) event.getTick()); //$NON-NLS-1$
column.add(row);
}
else if (message instanceof MetaMessage)
{
MetaMessage mm = (MetaMessage) message;
String type=metaLookup.get(mm.getType());
if(type==null)
{
type="unknown"; //$NON-NLS-1$
}
Map<String, Object> row = new ConcurrentHashMap<>();
row.put("command", "meta"); //$NON-NLS-1$ //$NON-NLS-2$
row.put("type",type) ;//$NON-NLS-1$
row.put("data", mm.getData()); //$NON-NLS-1$
row.put("length", mm.getLength()); //$NON-NLS-1$
row.put("status", mm.getStatus()); //$NON-NLS-1$
row.put("tick", (double) event.getTick()); //$NON-NLS-1$
column.add(row);
}
else
{
System.out.println("Other message: " + message.getClass()); //$NON-NLS-1$
}
}
}
return table;
}
/**
* Makes a new sequence with just the headers copied over
*
* @throws InvalidMidiDataException
*
*/
public static Sequence blankSequence(Sequence in) throws InvalidMidiDataException
{
return new Sequence(in.getDivisionType(), in.getResolution());
}
/**
* Generates a midi sequence with the tick at 1 millisecond to fit with the SFPL.
*
* @return the sequence
* @throws InvalidMidiDataException
*/
public static Sequence createSequence() throws InvalidMidiDataException
{
Sequence ret = new Sequence(Sequence.PPQ, 500);
return ret;
}
public static Track createTrack(Sequence sequence)
{
return sequence.createTrack();
}
public static Track getSequenceTrack(Sequence sequence, int trackNo)
{
return sequence.getTracks()[trackNo];
}
/** Saves a sequence as a type one file */
public static void writeMidiFile(String fileName, Sequence sequence) throws IOException
{
try (FileOutputStream fs = new FileOutputStream(fileName);)
{
MidiSystem.write(sequence, 1, fs);
}
}
/**
* Returns a sequencer attached to the device with the passed device name.
*
* @throws MidiUnavailableException
* @throws InvalidMidiDataException
*/
@SuppressWarnings("resource")
public static Sequencer getPlayableSequencer(Sequence sequence, int deviceNo) throws MidiUnavailableException,
InvalidMidiDataException
{
Info deviceInfo = MidiSystem.getMidiDeviceInfo()[deviceNo];
Sequencer sequencer = MidiSystem.getSequencer(false);
sequencer.setSequence(sequence);
MidiDevice device = MidiSystem.getMidiDevice(deviceInfo);
Receiver receiver = device.getReceiver();
sequencer.getTransmitter().setReceiver(receiver);
return sequencer;
}
/** Closes all receivers attached to this sequencer */
public static void closeSequencer(Sequencer sequencer)
{
for (Transmitter transmitter : sequencer.getTransmitters())
{
transmitter.getReceiver().close();
transmitter.close();
}
}
public static List<Map<String, Object>> getMidiDeviceNames() throws MidiUnavailableException
{
Info[] infos = MidiSystem.getMidiDeviceInfo();
List<Map<String, Object>> ret = new ArrayList<>();
int i = 0;
for (Info info : infos)
{
Map<String, Object> l = new HashMap<>();
l.put("name", info.getName()); //$NON-NLS-1$
l.put("number", i++); //$NON-NLS-1$
l.put("vendor", info.getVendor()); //$NON-NLS-1$
l.put("version", info.getVersion()); //$NON-NLS-1$
l.put("description", info.getDescription()); //$NON-NLS-1$
l.put("synthesiser", MidiSystem.getMidiDevice(info) instanceof Synthesizer); //$NON-NLS-1$
l.put("sequencer", MidiSystem.getMidiDevice(info) instanceof Sequencer); //$NON-NLS-1$
l.put("class", MidiSystem.getMidiDevice(info).getClass()); //$NON-NLS-1$
try (MidiDevice dev = MidiSystem.getMidiDevice(info);)
{
l.put("max-reveivers", dev.getMaxReceivers()); //$NON-NLS-1$
l.put("max-transmitters", dev.getMaxTransmitters()); //$NON-NLS-1$
ret.add(l);
}
}
return ret;
}
public static MidiPlayer getPlayer()
{
return new MidiPlayer();
}
public static MidiPlayer getPlayer(int sequNo, int synthNo) throws SFPL_RuntimeException
{
return new MidiPlayer(sequNo, synthNo);
}
public static void addNote(Track track, int channel, int on, int off, int key, int velocity)
throws InvalidMidiDataException
{
ShortMessage sm1 = new ShortMessage(ShortMessage.NOTE_ON, channel, key, velocity);
ShortMessage sm2 = new ShortMessage(ShortMessage.NOTE_OFF, channel, key, velocity);
MidiEvent ev1 = new MidiEvent(sm1, on);
MidiEvent ev2 = new MidiEvent(sm2, off);
track.add(ev1);
track.add(ev2);
}
private static void addControl(Track track, int channel, int at, int amount, int control) throws InvalidMidiDataException
{
ShortMessage sm1 = new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, amount);
MidiEvent ev1 = new MidiEvent(sm1, at);
track.add(ev1);
}
public static void addPan(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, PAN);
}
public static void addModWheel(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, MODWHEEL);
}
public static void addBreath(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, BREATH);
}
public static void addBFoot(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, FOOT);
}
public static void addPortamentoTime(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, PORTAMENTO_TIME);
}
public static void addVolume(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, VOLUME);
}
public static void addBalance(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, BALANCE);
}
public static void addPortamento(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, PORTAMENTO);
}
public static void addSostenuto(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, SOSTENUTO);
}
public static void addReset(Track track, int channel, int at, int amount) throws InvalidMidiDataException
{
addControl(track, channel, at, amount, RESET);
}
private static MidiEvent makeControl(int channel, int at, int amount, int control) throws InvalidMidiDataException
{
ShortMessage sm1 = new ShortMessage(ShortMessage.CONTROL_CHANGE, channel, control, amount);
return new MidiEvent(sm1, at);
}
public static MidiEvent makePan(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, PAN);
}
public static MidiEvent makeModWheel(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, MODWHEEL);
}
public static MidiEvent makeBreath(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, BREATH);
}
public static MidiEvent makeBFoot(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, FOOT);
}
public static MidiEvent makePortamentoTime(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, PORTAMENTO_TIME);
}
public static MidiEvent makeVolume(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, VOLUME);
}
public static MidiEvent makeBalance(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, BALANCE);
}
public static MidiEvent makePortamento(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, PORTAMENTO);
}
public static MidiEvent makeSostenuto(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, SOSTENUTO);
}
public static MidiEvent makeReset(int channel, int at, int amount) throws InvalidMidiDataException
{
return makeControl(channel, at, amount, RESET);
}
public static MidiEvent makePitchBend(int channel, int at, int amount) throws InvalidMidiDataException
{
int am = amount + 8192;
ShortMessage sm1 = new ShortMessage(ShortMessage.PITCH_BEND, channel, am >> 7, am & 127);
return new MidiEvent(sm1, at);
}
}