/*
* Created on Sep 11, 2004
*
* Copyright (c) 2005 Peter Johan Salomonsen (http://www.petersalomonsen.com)
*
* http://www.frinika.com
*
* This file is part of Frinika.
*
* Frinika 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.
*
* Frinika 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 Frinika; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package com.frinika.benchmark.audio;
import javax.sound.sampled.*;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JSlider;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import com.frinika.voiceserver.Voice;
import com.frinika.voiceserver.VoiceServer;
import java.util.Vector;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.lang.Thread;
/**
* An integration of the Voice server with the sound hardware interfaces of the java sound api.
*
* Brief description of the latency schemes
*
* Frinika latency schemes is more or less all about how to pause after data is processed and written to the audio output. A typical (buffer) cycle is this:
*
* 1. Process audio data
* 2. Write data to audioOut
* 3. Pause until time for processing new buffer
* The pause time is the total buffer time (represented by number of samples in the buffer) minus the time used in step 1 and 2.
*
* It's very important that the time used for all these three steps is as constant as possible, and that the total time equals the time represented by a buffer.
*
* To obtain this constant time different "blocking" schemes are implemented, as alternative to the blocking provided by sourceDataLine.write. What scheme to use depends
* on your system, try yourself and choose the scheme that is most stable. My experience is that the sdl.write blocking sometimes blocks too long, resulting in glitches.
* The two main alternatives in Frinika makes sure that sd.write doesn't block at all (by setting a very large buffer size), and then takes care of the blocking manually.
*
* - Standard latency cheme (no checkboxes checked in the audio device conf - this is the scheme that works best on most systems)
* - Blocking is done using Thread.sleepNanos
*
* - "UltraLowLatency"
* - Blocking is done using a loop and Thread.yield() while measuring sdl.getLongFramePosition(), very CPU intensive, but very accurate on when it stops blocking
*
* - "UltraLowLatency" with "Frinika estimated framepos"
* - Same as above but instead of using sdl.getLongFramePosition(), Frinika calculates an ideal Frame position by measuring System.nanoTime()
*
* @author Peter Johan Salomonsen
*/
public class PJVoiceServer extends VoiceServer implements Runnable {
Mixer.Info currentMixer;
AudioFormat format = new AudioFormat(getSampleRate(),16,2,true,true);
DataLine.Info infoOut = new DataLine.Info(SourceDataLine.class, format);
SourceDataLine lineOut;
boolean isRunning = false;
boolean hasStopped = false;
// 512 frames by default
int bufferSize = 2048;
/**
* Ultra low latency mode can be used for small buffer sizes to obtain better latency -
* BUT it eats all your CPU.
*/
protected boolean ultraLowLatency = false;
/**
* Use Java standard way of latency control (blocking on sdl.write)
*/
protected boolean standardLatency = false;
/**
* Use Frinika estimated frame pos (using System.nanoTime) or SDL.getLongFramePosition
*/
private boolean useEstimatedFramePos = true;
public PJVoiceServer(int bufferSize)
{
if(System.getProperty("os.name").equals("Mac OS X"))
{
System.out.println("Detected Mac OS X. Automatically tuning audio device settings. ");
// These are the settings working best on a G5 iMac 2Ghz
useEstimatedFramePos = false;
ultraLowLatency = true;
}
currentMixer = AudioSystem.getMixerInfo()[0];
this.bufferSize=4*bufferSize;
startAudioOutput();
}
public void startAudioOutput()
{
try {
lineOut = (SourceDataLine)AudioSystem.getMixer(currentMixer).getLine(infoOut);
if(standardLatency)
lineOut.open(format,bufferSize);
else
lineOut.open(format);
lineOut.start();
System.out.println("Buffersize: "+bufferSize+" / "+lineOut.getBufferSize());
} catch (Exception e) {
lineOut = null;
System.out.println("No audio output available. Use Audio Devices dialog to reconfigure.");
}
Thread thread = new Thread(this);
// thread.setPriority(Thread.MAX_PRIORITY);
thread.start();
}
public void stopAudioOutput() throws Exception
{
isRunning = false;
while(!hasStopped)
Thread.yield();
hasStopped = false;
if(lineOut!=null)
{
lineOut.drain();
lineOut.stop();
lineOut.close();
}
}
public void run()
{
try
{
isRunning = true;
byte[] outBuffer = new byte[bufferSize];
float[] floatBuffer = new float[bufferSize/2];
long totalTimeNanos = (long)(((float)(bufferSize / 4f) / (float)getSampleRate()) * 1000000000);
// nanoTime when buffer expires
long expireNanos=0;
long framesWritten = 0;
while(isRunning)
{
long startTimeNanos = System.nanoTime();
for(int n = 0;n<(floatBuffer.length);n++)
floatBuffer[n] = 0;
read(outBuffer,floatBuffer);
long endTimeNanos = System.nanoTime();
if(lineOut!=null){
System.out.println(lineOut.getBufferSize()-lineOut.available());
lineOut.write(outBuffer,0,outBuffer.length);
}
/**
* If we use standard latency model, the write above will do the blocking.
* Else we'll have to do the blocking manually below....
*/
if(!standardLatency)
{
if(expireNanos<System.nanoTime())
expireNanos = System.nanoTime()+totalTimeNanos;
else
expireNanos += totalTimeNanos;
if(ultraLowLatency)
{
/**
* Ultra low latency mode can be used for small buffer sizes to obtain better latency -
* BUT it eats all your CPU. Be careful.
*/
if(useEstimatedFramePos)
{
//Frinika Estimated frame position
while(expireNanos-System.nanoTime()>totalTimeNanos)
Thread.yield(); // Keeps your CPU as busy as possible
}
else
{
//Use lineOut frameposition
while(lineOut.getLongFramePosition()<framesWritten)
Thread.yield();
}
}
else
{
long sleepNanos = expireNanos - totalTimeNanos - System.nanoTime();
if(sleepNanos>0)
Thread.sleep(sleepNanos / 1000000, (int)(sleepNanos % 1000000));
}
}
// This is used when not using the Frinika estimated position
framesWritten+=(outBuffer.length/4);
// cpuMeter.setCpuPercent((int)(((float)(endTimeNanos - startTimeNanos) / (float)totalTimeNanos) * 100));
}
}
catch(Exception e)
{
e.printStackTrace();
}
hasStopped = true;
}
public void configureAudioOutput(JFrame frame)
{
JDialog dialog = new JDialog(frame,"Audio devices");
dialog.setLayout(new GridLayout(8,1));
Vector<Mixer.Info> mixers = new Vector<Mixer.Info>();
for(Mixer.Info mInfo : AudioSystem.getMixerInfo())
{
System.out.println(mInfo);
if(AudioSystem.getMixer(mInfo).isLineSupported(infoOut))
mixers.add(mInfo);
}
final JComboBox cb = new JComboBox(mixers);
dialog.add(cb);
dialog.add(new JLabel("Slide to adjust latency:"));
final JSlider sl = new JSlider(64,8192);
sl.setToolTipText("Slide to adjust latency");
dialog.add(sl);
final JLabel lb = new JLabel();
dialog.add(lb);
class LatencyListener implements ChangeListener {
int bufferSize = 0;
public void stateChanged(ChangeEvent e) {
bufferSize = ((int)(sl.getValue() / 64))*64;
lb.setText("Latency = "+bufferSize+" frames");
}
}
final LatencyListener latencyListener = new LatencyListener();
sl.addChangeListener(latencyListener);
sl.setValue(bufferSize/4);
final JCheckBox ultraLowLatencyCheckBox = new JCheckBox("Ultra-low latency support (CAREFUL - eats all CPU it can get)",ultraLowLatency);
final JCheckBox useEstimatedFramePosCheckBox = new JCheckBox("Use Frinika estimated framepos",useEstimatedFramePos);
ultraLowLatencyCheckBox.setToolTipText("If you set a low latency (typical below 1024) above, you might need to turn this on as well. ");
ultraLowLatencyCheckBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent evt) {
ultraLowLatency = ultraLowLatencyCheckBox.isSelected();
useEstimatedFramePosCheckBox.setEnabled(!standardLatency & ultraLowLatency);
}});
ultraLowLatencyCheckBox.setEnabled(!standardLatency);
dialog.add(ultraLowLatencyCheckBox);
useEstimatedFramePosCheckBox.setToolTipText("On some systems Frinika does a better estimation of the audio position, on others not... ");
useEstimatedFramePosCheckBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent evt) {
useEstimatedFramePos = useEstimatedFramePosCheckBox.isSelected();
}});
useEstimatedFramePosCheckBox.setEnabled(!standardLatency & ultraLowLatency);
dialog.add(useEstimatedFramePosCheckBox);
final JCheckBox standardLatencyCheckBox = new JCheckBox("Use standard javasound latency control",standardLatency);
standardLatencyCheckBox.setToolTipText("This is the standard javasound method for latency control, may be good on some systems - but not on others.. ");
standardLatencyCheckBox.addItemListener(new ItemListener() {
public void itemStateChanged(ItemEvent evt) {
standardLatency = standardLatencyCheckBox.isSelected();
// Cannot use ultra low latency if using standard latency
useEstimatedFramePosCheckBox.setEnabled(!standardLatency & ultraLowLatency);
ultraLowLatencyCheckBox.setEnabled(!standardLatency);
}});
dialog.add(standardLatencyCheckBox);
final JButton applyButton = new JButton("Apply");
applyButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
try
{
stopAudioOutput();
PJVoiceServer.this.bufferSize = latencyListener.bufferSize * 4;
// FrinikaConfig.setAudioBufferLength(latencyListener.bufferSize);
PJVoiceServer.this.currentMixer = (Mixer.Info)cb.getSelectedItem();
startAudioOutput();
}
catch(Exception ex) {
ex.printStackTrace();}
}});
dialog.add(applyButton);
dialog.setSize(600,200);
dialog.setVisible(true);
}
public void setBufferSize(int len) throws Exception {
stopAudioOutput();
bufferSize = len * 4;
startAudioOutput();
}
/**
*
*/
public void printStats() {
if(false)
{
System.out.println("Audio Output State");
try
{
for(Voice gen : audioOutputGenerators)
System.out.println(gen);
System.out.println("Num of generators: "+audioOutputGenerators.size());
}
catch(Exception e) {}
}
}
}