package com.xenoage.zong.desktop.io.midi.out;
import com.sun.media.sound.AudioSynthesizer;
import com.xenoage.utils.exceptions.InvalidFormatException;
import com.xenoage.utils.jse.settings.Settings;
import lombok.val;
import javax.sound.midi.*;
import javax.sound.midi.MidiDevice.Info;
import javax.sound.sampled.*;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.util.*;
/**
* This class shares a {@link Synthesizer}, {@link Sequencer}
* and other objects needed for MIDI and audio playback.
*
* It should be initialized at the start of the program by
* calling the <code>init()</code>method and
* when closing the program, the <code>close()</code> method
* should be called.
*
* Some code is based on the SimpleMidiPlayer demo
* application from Gervill.
*
* @author Andreas Wenger
*/
public class SynthManager {
public static final String configFile = "synth";
private static SynthManager instance = null;
private Synthesizer synthesizer = null;
private Sequencer sequencer = null;
private Soundbank soundbank = null;
private Mixer mixer = null;
private SourceDataLine line = null;
private AudioFormat format = null;
private Set<ControllerEventListener> controllerEventListeners = new HashSet<>();
/**
* Initializes the manager. Must be called at the beginning one time
* and each time when the audio settings should be reloaded.
* @param readSettings true, to load the settings from the configuration file.
* false, to use default settings (e.g. for an applet)
*/
public static void init(boolean readSettings)
throws MidiUnavailableException {
//TIDY method (especially default settings)
if (instance == null) {
instance = new SynthManager();
}
if (readSettings) {
//load settings (or use default ones)
Settings s = Settings.getInstance();
String file = configFile;
String deviceName = s.getSetting("devicename", file);
float sampleRate = s.getSetting("samplerate", file, 44100);
int sampleSizeInBits = s.getSetting("bits", file, 16);
int channels = s.getSetting("channels", file, 2);
int latency = s.getSetting("latency", file, 100);
int polyphony = s.getSetting("polyphony", file, 64);
String interpolation = s.getSetting("interpolation", file, "linear");
String soundbank = s.getSetting("soundbank", file, null);
//init midi and soundbank
instance.initMidi(sampleRate, sampleSizeInBits, channels, latency, polyphony, deviceName,
interpolation);
if (soundbank != null && soundbank.length() > 0) {
try {
instance.loadSoundbank(new File(soundbank));
} catch (InvalidFormatException ex) {
//TODO
}
}
}
else {
instance.initMidi(44100, 16, 2, 100, 64, null, "linear");
}
}
/**
* Gets the single instance of this manager.
* Throws an IllegalStateEx when init was not called before
* (this additional method was created so that there is no
* need for throwing a MidiUnavail here).
*/
private static SynthManager getInstance() {
if (instance == null)
throw new IllegalStateException("init() must be called first");
return instance;
}
/**
* For the parameters, see {@link Sequencer#addControllerEventListener}.
* Call this method instead, because this class will remember the registered
* listeners so you can call removeAllControllerEventListeners.
*/
public static void addControllerEventListener(ControllerEventListener listener, int... controllers) {
SynthManager instance = getInstance();
instance.controllerEventListeners.add(listener);
instance.sequencer.addControllerEventListener(listener, controllers);
}
/**
* Removes all addeed ControllerEventListeners.
*/
public static void removeAllControllerEventListeners() {
SynthManager instance = getInstance();
for (ControllerEventListener listener : instance.controllerEventListeners)
instance.sequencer.removeControllerEventListener(listener, null);
instance.controllerEventListeners.clear();
}
/**
* Closes all MIDI objects and frees the resources.
*/
public static void close() {
SynthManager t = instance;
if (t.sequencer != null) {
t.sequencer.stop();
t.sequencer.close();
}
if (t.synthesizer != null) {
t.synthesizer.close();
}
if (t.mixer != null) {
t.mixer.close();
}
instance = null;
}
/**
* Gets the synthesizer.
*/
public static Synthesizer getSynthesizer() {
return getInstance().synthesizer;
}
/**
* Gets the sequencer.
*/
public static Sequencer getSequencer() {
return getInstance().sequencer;
}
public static Soundbank getSoundbank() {
return getInstance().soundbank;
}
/**
* Loads the soundbank from the given file.
* If it fails, an {@link InvalidFormatException} is thrown.
*/
public void loadSoundbank(File file)
throws InvalidFormatException {
try {
Soundbank newSB;
try (FileInputStream fis = new FileInputStream(file)) {
newSB = MidiSystem.getSoundbank(new BufferedInputStream(fis));
}
if (soundbank != null)
synthesizer.unloadAllInstruments(soundbank);
soundbank = newSB;
synthesizer.loadAllInstruments(soundbank);
} catch (Exception ex) {
throw new InvalidFormatException("Invalid soundbank: " + file, ex);
}
}
/**
* (Re)initializes the MIDI objects and (re)configures the audio settings.
* If currently playback is running, it is stopped.
* TIDY
* @param sampleRate the number of samples per second, e.g. 44100
* @param sampleSizeInBits the number of bits in each sample, e.g. 16
* @param channels the number of channels (1 for mono, 2 for stereo, and so on)
* @param latency the latency in ms
* @param polyphony maximum number of concurrent notes
* @param deviceName name of the device, or null for default
* @param interpolation linear, cubic, sinc or point
*/
public void initMidi(float sampleRate, int sampleSizeInBits, int channels, int latency,
int polyphony, String deviceName, String interpolation)
throws MidiUnavailableException {
Sequence sequence = null;
if (sequencer != null) {
sequencer.stop();
sequence = sequencer.getSequence();
}
if (synthesizer != null) {
synthesizer.close();
}
if (mixer != null) {
mixer.close();
}
format = new AudioFormat(sampleRate, sampleSizeInBits, channels, true, false);
if (deviceName != null) {
Mixer.Info selinfo = null;
for (Mixer.Info info : AudioSystem.getMixerInfo()) {
Mixer mixer = AudioSystem.getMixer(info);
boolean hassrcline = false;
for (Line.Info linfo : mixer.getSourceLineInfo()) {
if (linfo instanceof javax.sound.sampled.DataLine.Info) {
hassrcline = true;
break;
}
}
if (hassrcline) {
if (info.getName().equals(deviceName)) {
selinfo = info;
break;
}
}
}
if (selinfo != null) {
mixer = AudioSystem.getMixer(selinfo);
try {
mixer.open();
int bufferSize = (int) (format.getFrameSize() * format.getFrameRate() * latency / 1000f);
if (bufferSize < 500)
bufferSize = 500;
DataLine.Info dataLineInfo = new DataLine.Info(SourceDataLine.class, format, bufferSize);
if (mixer.isLineSupported(dataLineInfo)) {
line = (SourceDataLine) mixer.getLine(dataLineInfo);
line.open(format, bufferSize);
line.start();
}
} catch (Throwable t) {
mixer = null;
}
}
}
Map<String, Object> ainfo = new HashMap<>();
ainfo.put("format", format);
ainfo.put("max polyphony", polyphony);
ainfo.put("latency", latency * 1000L);
ainfo.put("interpolation", interpolation);
ainfo.put("large mode", true);
AudioSynthesizer synth = findAudioSynthesizer();
if (synth == null)
return; //no audio synthesizer
synth.open(line, ainfo);
synthesizer = synth;
if (soundbank == null) {
soundbank = synth.getDefaultSoundbank();
}
if (sequencer == null) {
try {
sequencer = MidiSystem.getSequencer(false);
} catch (MidiUnavailableException ex) {
//sequencer already open. no problem.
}
}
if (sequencer.isOpen()) {
sequencer.close();
}
sequencer.getTransmitter().setReceiver(synthesizer.getReceiver());
sequencer.open();
if (sequence != null) {
try {
sequencer.setSequence(sequence);
} catch (InvalidMidiDataException ex) {
}
}
}
private AudioSynthesizer findAudioSynthesizer()
throws MidiUnavailableException {
//first check if default synthesizer is AudioSynthesizer.
Synthesizer synth = MidiSystem.getSynthesizer();
if (synth instanceof AudioSynthesizer)
return (AudioSynthesizer) synth;
//if default synthesizer is not AudioSynthesizer, check others.
Info[] infos = MidiSystem.getMidiDeviceInfo();
for (val info : infos) {
MidiDevice dev = MidiSystem.getMidiDevice(info);
if (dev instanceof AudioSynthesizer)
return (AudioSynthesizer) dev;
}
//no AudioSynthesizer was found, return null.
return null;
}
/**
* Gets a list with the names of the available audio mixers.
* The mixers are in the order provided by the platform, so
* their indices can be used to identify them.
*/
public static List<String> getAudioMixers() {
ArrayList<String> mixerNames = new ArrayList<>();
for (Mixer.Info info : AudioSystem.getMixerInfo()) {
Mixer mixer = AudioSystem.getMixer(info);
boolean hassrcline = false;
for (Line.Info linfo : mixer.getSourceLineInfo())
if (linfo instanceof javax.sound.sampled.DataLine.Info)
hassrcline = true;
if (hassrcline) {
mixerNames.add(info.getName());
}
}
return mixerNames;
}
}