/*
* Copyright (C) 2013-2017 たんらる
*/
package fourthline.mabiicco.midi;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.sound.midi.*;
import javax.sound.sampled.LineUnavailableException;
import com.sun.media.sound.DLSInstrument;
import com.sun.media.sound.DLSRegion;
import com.sun.media.sound.SoftSynthesizer;
import fourthline.mabiicco.AppErrorHandler;
import fourthline.mmlTools.MMLEventList;
import fourthline.mmlTools.MMLNoteEvent;
import fourthline.mmlTools.MMLScore;
import fourthline.mmlTools.MMLTempoEvent;
import fourthline.mmlTools.MMLTrack;
import fourthline.mmlTools.core.MMLTickTable;
/**
* MabinogiのDLSファイルを使ってMIDIを扱います.
*/
public final class MabiDLS {
private static MabiDLS instance = null;
private Synthesizer synthesizer;
private Sequencer sequencer;
private MidiChannel channel[];
private ArrayList<MMLNoteEvent[]> playNoteList = new ArrayList<>();
private static final int MAX_CHANNEL_PLAY_NOTE = 4;
public static final int MAX_MIDI_PART = 16;
private ArrayList<InstClass> insts = new ArrayList<>();
private static final int DLS_BANK = (0x79 << 7);
public static final String DEFALUT_DLS_PATH = "Nexon/Mabinogi/mp3/MSXspirit.dls";
private ArrayList<Runnable> notifier = new ArrayList<>();
private boolean muteState[] = new boolean[ MAX_MIDI_PART ];
private WavoutDataLine wavout;
public static MabiDLS getInstance() {
if (instance == null) {
instance = new MabiDLS();
}
return instance;
}
private MabiDLS() {}
/**
* initialize
*/
public void initializeMIDI() throws MidiUnavailableException, InvalidMidiDataException, IOException, LineUnavailableException {
this.synthesizer = MidiSystem.getSynthesizer();
((SoftSynthesizer)this.synthesizer).open(wavout = new WavoutDataLine(), null);
addTrackEndNotifier(() -> wavout.stopRec());
long latency = this.synthesizer.getLatency();
int maxPolyphony = this.synthesizer.getMaxPolyphony();
System.out.printf("Latency: %d\nMaxPolyphony: %d\n", latency, maxPolyphony);
this.sequencer = MidiSystem.getSequencer();
this.sequencer.open();
this.sequencer.addMetaEventListener(meta -> {
int type = meta.getType();
if (type == MMLTempoEvent.META) {
// テンポイベントを処理します.
byte metaData[] = meta.getData();
sequencer.setTempoInMPQ(ByteBuffer.wrap(metaData).getInt());
} else if (type == 0x2f) {
// トラック終端
if (loop) {
sequenceStart();
} else {
notifier.forEach(t -> t.run());
}
}
});
// シーケンサとシンセサイザの初期化
initializeSynthesizer();
Transmitter transmitter = this.sequencer.getTransmitters().get(0);
transmitter.setReceiver(this.synthesizer.getReceiver());
}
// ループ再生時にも使用するパラメータ.
private boolean loop = false;
private long startTick;
private int startTempo;
public void setLoop(boolean b) {
this.loop = b;
}
public boolean isLoop() {
return this.loop;
}
/**
* MMLScoreからMIDIシーケンスに変換して, 再生を開始する.
* @param mmlScore
* @param startTick
*/
public void createSequenceAndStart(MMLScore mmlScore, long startTick) {
try {
MabiDLS.getInstance().loadRequiredInstruments(mmlScore);
Sequencer sequencer = MabiDLS.getInstance().getSequencer();
Sequence sequence = createSequence(mmlScore);
sequencer.setSequence(sequence);
this.startTick = startTick;
this.startTempo = mmlScore.getTempoOnTick(startTick);
updatePanpot(mmlScore);
sequenceStart();
} catch (InvalidMidiDataException e) {
e.printStackTrace();
}
}
public IWavoutState getWavout() {
return wavout;
}
public void startWavout(MMLScore mmlScore, File outFile, Runnable endNotify) {
createSequenceAndStart(mmlScore, 0);
try {
wavout.startRec(new FileOutputStream(outFile), endNotify);
} catch (FileNotFoundException e) {
wavout.stopRec();
e.printStackTrace();
}
}
public void stopWavout() {
wavout.stopRec();
sequencer.stop();
}
private void allNoteOff() {
for (MidiChannel ch : this.channel) {
ch.allNotesOff();
}
}
private void sequenceStart() {
allNoteOff();
midiSetMuteState();
sequencer.setTickPosition(startTick);
sequencer.setTempoInBPM(startTempo);
sequencer.start();
}
public void addTrackEndNotifier(Runnable n) {
notifier.add(n);
}
public void loadingDLSFile(File file) throws InvalidMidiDataException, IOException {
System.out.println("["+file.getName()+"]");
if (file.getName().equals("")) {
return;
}
if (!file.exists()) {
// 各Rootディレクトリを探索します.
for (Path path : FileSystems.getDefault().getRootDirectories()) {
File aFile = new File(path.toString() + file.getPath());
if (aFile.exists()) {
file = aFile;
break;
}
};
}
if (file.exists()) {
List<InstClass> loadList = InstClass.loadDLS(file);
for (InstClass inst : loadList) {
if (!insts.contains(inst)) {
insts.add(inst);
}
}
}
}
public synchronized void loadRequiredInstruments(MMLScore score) {
ArrayList<InstClass> requiredInsts = new ArrayList<>();
ArrayList<MMLTrack> trackList = new ArrayList<>(score.getTrackList());
for (MMLTrack track : trackList) {
InstClass inst1 = getInstByProgram( track.getProgram() );
InstClass inst2 = getInstByProgram( track.getSongProgram() );
if ( (inst1 != null) && (!requiredInsts.contains(inst1)) ) {
requiredInsts.add(inst1);
}
if ( (inst2 != null) && (!requiredInsts.contains(inst2)) ) {
requiredInsts.add(inst2);
}
}
// load required Instruments
List<Instrument> loadedList = Arrays.asList(synthesizer.getLoadedInstruments());
for (InstClass inst : requiredInsts) {
try {
Instrument instrument = inst.getInstrument();
if (!loadedList.contains(instrument)) {
synthesizer.loadInstrument(instrument);
}
} catch (OutOfMemoryError e) {
AppErrorHandler.getInstance().exec();
System.exit(1);
}
}
}
public Sequencer getSequencer() {
return sequencer;
}
public Synthesizer getSynthesizer() {
return synthesizer;
}
private void initializeSynthesizer() throws InvalidMidiDataException, IOException, MidiUnavailableException {
this.channel = this.synthesizer.getChannels();
for (int i = 0; i < this.channel.length; i++) {
this.playNoteList.add(new MMLNoteEvent[MAX_CHANNEL_PLAY_NOTE]);
}
for (MidiChannel ch : this.channel) {
ch.programChange(DLS_BANK, 0);
/* ctrl 91 汎用エフェクト 1(リバーブ) */
ch.controlChange(91, 0);
}
this.synthesizer.unloadAllInstruments(this.synthesizer.getDefaultSoundbank());
all();
}
public InstClass[] getAvailableInstByInstType(List<InstType> e) {
return insts.stream()
.filter(inst -> e.contains(inst.getType()))
.toArray(size -> new InstClass[size]);
}
public InstClass getInstByProgram(int program) {
for (InstClass inst : insts) {
if (inst.getProgram() == program) {
return inst;
}
}
return null;
}
/**
* 単音再生
*/
public void playNote(int note, int program, int channel, int velocity) {
MMLNoteEvent playNote = this.playNoteList.get(channel)[0];
if ( (playNote == null) || (playNote.getNote() != note) ) {
playNote = new MMLNoteEvent(note, 0, 0, velocity);
}
playNotes(new MMLNoteEvent[] { playNote }, program, channel);
}
/** 和音再生 */
public void playNotes(MMLNoteEvent noteList[], int program, int channel) {
/* シーケンサによる再生中は鳴らさない */
if (sequencer.isRunning()) {
return;
}
midiMuteOff();
changeProgram(program, channel);
setChannelPanpot(channel, 64);
MidiChannel midiChannel = this.channel[channel];
MMLNoteEvent[] playNoteEvents = this.playNoteList.get(channel);
for (int i = 0; i < playNoteEvents.length; i++) {
MMLNoteEvent note = null;
if ( (noteList != null) && (i < noteList.length) ) {
note = noteList[i];
}
if ( (note == null) || (!note.equals(playNoteEvents[i])) ) {
if (playNoteEvents[i] != null) {
midiChannel.noteOff( convertNoteMML2Midi(playNoteEvents[i].getNote()) );
playNoteEvents[i] = null;
}
}
if ( (note != null) && (!note.equals(playNoteEvents[i]))) {
InstClass instClass = getInstByProgram(program);
int velocity = convertVelocityOnAtt(instClass, note.getNote(), note.getVelocity());
int midiNote = convertNoteMML2Midi(note.getNote());
if (midiNote >= 0) {
midiChannel.noteOn(midiNote, velocity);
}
playNoteEvents[i] = note;
}
}
}
public void changeProgram(int program, int ch) {
if (channel[ch].getProgram() != program) {
channel[ch].programChange(DLS_BANK, program);
}
}
/**
* 指定したチャンネルのパンポットを設定します.
* @param ch
* @param panpot
*/
public void setChannelPanpot(int ch, int panpot) {
channel[ch].controlChange(10, panpot);
}
public void toggleMute(int ch) {
muteState[ch] = !muteState[ch];
midiSetMuteState();
}
public void setMute(int ch, boolean mute) {
muteState[ch] = mute;
midiSetMuteState();
}
public boolean getMute(int ch) {
return muteState[ch];
}
public void solo(int ch) {
for (int i = 0; i < muteState.length; i++) {
muteState[i] = (i != ch);
}
midiSetMuteState();
}
public void all() {
for (int i = 0; i < muteState.length; i++) {
muteState[i] = false;
}
midiSetMuteState();
}
/** MIDIにMuteStateを反映する. */
private void midiSetMuteState() {
for (int i = 0; i < muteState.length; i++) {
channel[i].setMute(muteState[i]);
}
}
/** MIDIのMute解除する. */
private void midiMuteOff() {
for (MidiChannel c : channel) {
c.setMute(false);
}
}
public void updatePanpot(MMLScore score) {
int trackCount = 0;
for (MMLTrack mmlTrack : score.getTrackList()) {
int panpot = mmlTrack.getPanpot();
this.setChannelPanpot(trackCount, panpot);
trackCount++;
}
}
/**
* MIDIシーケンスを作成します。
* @throws InvalidMidiDataException
*/
public Sequence createSequence(MMLScore score) throws InvalidMidiDataException {
Sequence sequence = new Sequence(Sequence.PPQ, MMLTickTable.TPQN);
int trackCount = 0;
for (MMLTrack mmlTrack : score.getTrackList()) {
convertMidiTrack(sequence.createTrack(), mmlTrack, trackCount);
trackCount++;
if (trackCount >= MAX_MIDI_PART) {
break;
}
}
// グローバルテンポ
Track track = sequence.getTracks()[0];
List<MMLTempoEvent> globalTempoList = score.getTempoEventList();
for (MMLTempoEvent tempoEvent : globalTempoList) {
byte tempo[] = tempoEvent.getMetaData();
int tickOffset = tempoEvent.getTickOffset();
MidiMessage message = new MetaMessage(MMLTempoEvent.META,
tempo, tempo.length);
track.add(new MidiEvent(message, tickOffset));
}
if (trackCount <= 13) {
// コーラスパートの作成
createVoiceMidiTrack(sequence, score, 13, 100); // 男声コーラス
createVoiceMidiTrack(sequence, score, 14, 110); // 女声コーラス
}
return sequence;
}
private void createVoiceMidiTrack(Sequence sequence, MMLScore score, int channel, int program) throws InvalidMidiDataException {
Track track = sequence.createTrack();
ShortMessage pcMessage = new ShortMessage(ShortMessage.PROGRAM_CHANGE,
channel,
program,
0);
track.add(new MidiEvent(pcMessage, 0));
for (MMLTrack mmlTrack : score.getTrackList()) {
if (mmlTrack.getSongProgram() != program) {
continue;
}
InstClass instClass = getInstByProgram(program);
convertMidiPart(track, mmlTrack.getMMLEventAtIndex(3).getMMLNoteEventList(), channel, instClass);
}
}
/**
* トラックに含まれるすべてのMMLEventListを1つのMIDIトラックに変換します.
* @param track
* @param channel
* @throws InvalidMidiDataException
*/
private void convertMidiTrack(Track track, MMLTrack mmlTrack, int channel) throws InvalidMidiDataException {
int program = mmlTrack.getProgram();
ShortMessage pcMessage = new ShortMessage(ShortMessage.PROGRAM_CHANGE,
channel,
program,
0);
track.add(new MidiEvent(pcMessage, 0));
boolean enablePart[] = InstClass.getEnablePartByProgram(program);
InstClass instClass = getInstByProgram(mmlTrack.getProgram());
MMLMidiTrack midiTrack = new MMLMidiTrack(mmlTrack.getGlobalTempoList());
for (int i = 0; i < enablePart.length; i++) {
if (enablePart[i]) {
MMLEventList eventList = mmlTrack.getMMLEventAtIndex(i);
midiTrack.add(eventList.getMMLNoteEventList());
}
}
convertMidiPart(track, midiTrack.getNoteEventList(), channel, instClass);
}
private void convertMidiPart(Track track, List<MMLNoteEvent> eventList, int channel, InstClass inst) {
int velocity = MMLNoteEvent.INIT_VOL;
// Noteイベントの変換
for ( MMLNoteEvent noteEvent : eventList ) {
int note = noteEvent.getNote();
int tick = noteEvent.getTick();
int tickOffset = noteEvent.getTickOffset() + 1;
int endTickOffset = tickOffset + tick - 1;
// ボリュームの変更
if (noteEvent.getVelocity() >= 0) {
velocity = convertVelocityOnAtt(inst, note, noteEvent.getVelocity());
}
try {
// ON イベント作成
MidiMessage message1 = new ShortMessage(ShortMessage.NOTE_ON,
channel,
convertNoteMML2Midi(note),
velocity);
track.add(new MidiEvent(message1, tickOffset));
// Off イベント作成
MidiMessage message2 = new ShortMessage(ShortMessage.NOTE_OFF,
channel,
convertNoteMML2Midi(note),
0);
track.add(new MidiEvent(message2, endTickOffset));
} catch (InvalidMidiDataException e) {
e.printStackTrace();
}
}
}
private int convertNoteMML2Midi(int mml_note) {
return (mml_note + 12);
}
/**
* 音源のAttenuationをVelocityに反映する.
* @param inst 音源
* @param note 変換前のNote
* @param velocity 変換前のVelocity
* @return 変換後のVelocity
*/
private int convertVelocityOnAtt(InstClass inst, int note, int velocity) {
velocity = inst.getType().convertVelocityMML2Midi(velocity);
if (velocity == 0) {
return 0;
}
note = convertNoteMML2Midi(note);
Instrument instrument = inst.getInstrument();
if (instrument instanceof DLSInstrument) {
DLSInstrument dlsinst = (DLSInstrument) instrument;
for (DLSRegion region : dlsinst.getRegions()) {
if ( (note >= region.getKeyfrom())
&& (note <= region.getKeyto()) ) {
double attenuation = region.getSampleoptions().getAttenuation() / 655360.0;
velocity = (int) Math.sqrt( Math.pow(10.0, attenuation/20) * (double)(velocity * velocity) );
break;
}
}
}
return velocity;
}
public List<MidiDevice.Info> getMidiInDevice() {
ArrayList<MidiDevice.Info> midiDeviceList = new ArrayList<>();
for (MidiDevice.Info info : MidiSystem.getMidiDeviceInfo()) {
try {
MidiDevice device = MidiSystem.getMidiDevice(info);
if ( (device.getMaxTransmitters() != 0) && (device.getMaxReceivers() == 0) ) {
// -1は制限なし.
midiDeviceList.add(device.getDeviceInfo());
}
} catch (MidiUnavailableException e) {
e.printStackTrace();
}
}
return midiDeviceList;
}
public static void main(String args[]) {
try {
InstClass.debug = true;
MabiDLS midi = new MabiDLS();
midi.initializeMIDI();
for (MidiDevice.Info info : MidiSystem.getMidiDeviceInfo()) {
System.out.println(info);
}
midi.loadingDLSFile(new File(DEFALUT_DLS_PATH));
System.exit(0);
} catch (Exception e) {
e.printStackTrace();
}
}
}