/*
* Created on Feb 10, 2005
*
* 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.voiceserver;
import java.util.Vector;
import javax.swing.JFrame;
import com.frinika.project.FrinikaAudioSystem;
import com.frinika.global.FrinikaConfig;
/**
* The VoiceServer terminates the audio output of all voices, by polling them sequentially for small buffers
* of data. The buffer size also determines the latency of the server, thus also for all the VoiceGenerators
* (softsynths) it is hosting. The VoiceServer is an abstract class which should be extended and integrated
* with a sound hardware interface. The voiceserver however provides all functionality needed for timing and
* synchronization of the voice generators.
*
* The VoiceServer has a realtime and a non-realtime mode. Realtime mode is used when playing live - and timing
* mechanisms has to be accurate. Non realtime mode is used when creating a rendered version of a sequence of
* sound. For example when exporting a song to a wav file.
*
* Routing mechanisms are available using Voice.nextVoice
* @author Peter Johan Salomonsen
*
* PJL - changed call to FrinikaConfig.getOSLatency() to FrinikaAUdioSYstem.getOutputLatency()
*/
public abstract class VoiceServer
{
private int bufferSize = 512; // Float buffer size - adjusts automatically - do not modify
private int sampleRate = (int) FrinikaConfig.sampleRate;
private long audioStartTime = System.nanoTime();
private long frameBufferPos = 0;
private boolean isRealtime = true;
protected Vector<Voice> audioOutputGenerators = new Vector<Voice>();
Vector<Voice> removedTransmitters = new Vector<Voice>();
Vector<Voice> addedTransmitters = new Vector<Voice>();
/**
* Returns the current sample rate
* @return samplerate
*/
public final int getSampleRate()
{
return(sampleRate);
}
/**
* Change current sample rate
* @param sampleRate
*/
public final void setSampleRate(int sampleRate)
{
this.sampleRate = sampleRate;
}
/**
* @return Returns the bufferSize in number of frames
*/
public final int getBufferSize() {
return bufferSize;
}
/**
* Get position of where in frame to start a generator
*
* PJL made public so AudioRecording can see the framePos
* @return
*/
private final long getFramePos()
{
return((long) (
( (System.nanoTime() - audioStartTime) *
(sampleRate / 1000000000.0) )
) );
}
/**
* Update framebuffer pos for each frame
*
*/
private final void updateFrameBufferPos()
{
frameBufferPos = getFramePos()-(getBufferSize()/2);
}
/**
* Used by synths to add a new generator (midi note)
* @param transmitter - the generator to add
*/
public final void addTransmitter(Voice transmitter)
{
if(isRealtime)
transmitter.startFramePos = getFramePos();
addedTransmitters.add(transmitter);
}
/**
* Used by synths to trigger a change in an existing generator. Ie. a release of a midi note.
*
* @param transmitter
* @param interrupt
*/
public final void interruptTransmitter(Voice transmitter, VoiceInterrupt interrupt)
{
if(isRealtime)
interrupt.interruptFramePos = getFramePos();
transmitter.interrupts.add(interrupt);
}
/**
* Called by the generator itself when it knows that it has finished its processing.
* @param transmitter
*/
public final void removeTransmitter(Voice transmitter)
{
removedTransmitters.add(transmitter);
}
/**
* Before each frame generator tables must be updated. Remove transmitters that are
* finished, and add pending.
*
*/
private final void updateGenerators()
{
while(removedTransmitters.size()>0)
audioOutputGenerators.remove(removedTransmitters.remove(0));
while(addedTransmitters.size()>0)
{
Voice transmitter = addedTransmitters.remove(0);
if(transmitter.nextVoice!=null)
{
int nIndex = audioOutputGenerators.indexOf(transmitter.nextVoice);
if(nIndex>-1)
audioOutputGenerators.add(nIndex,transmitter);
else
audioOutputGenerators.add(transmitter);
}
else
audioOutputGenerators.add(transmitter);
}
}
/**
* Read a frame into the given buffer of floats
* @param floatBuffer
*/
private final void floatRead(float[] floatBuffer)
{
bufferSize = floatBuffer.length;
updateGenerators();
for(Voice audioGen : audioOutputGenerators)
{
int bufferPos = 0;
if(audioGen.startFramePos > 0) // Don't need this on wav export
{
bufferPos = (int)(2 * (audioGen.startFramePos - frameBufferPos));
if(bufferPos < 0)
bufferPos = 0;
}
if(bufferPos<floatBuffer.length)
{
int endBufferPos = 0;
while(audioGen.interrupts.size()>0)
{
VoiceInterrupt interrupt = audioGen.interrupts.get(0);
if(interrupt.interruptFramePos > 0)
{
endBufferPos = (int)(2 * (interrupt.interruptFramePos - frameBufferPos));
if(endBufferPos < 0)
endBufferPos = 0;
}
if(endBufferPos<=floatBuffer.length)
{
audioGen.fillBuffer(bufferPos,endBufferPos,floatBuffer);
interrupt.doInterrupt();
bufferPos=endBufferPos;
audioGen.interrupts.remove(0);
}
else
break;
}
audioGen.fillBuffer(bufferPos,floatBuffer.length,floatBuffer);
audioGen.startFramePos = 0;
}
}
processFinalOutput(floatBuffer);
}
/**
* Override this method to add processing to the final output
* @param buffer
*/
public void processFinalOutput(float[] buffer) {
}
/**
* Read a frame into the given buffers of bytes, and floats
* @param outBuffer
* @param floatBuffer
*/
private final void byteRead(byte[] outBuffer, float[] floatBuffer)
{
floatRead(floatBuffer);
for(int n = 0;n<outBuffer.length;)
{
//Left
float floatSample = floatBuffer[n/2];
short sample;
if(floatSample>=1.0f)
sample=0x7fff;
else if(floatSample<=-1.0f)
sample=-0x8000;
else
sample = (short)(floatSample*0x8000);
outBuffer[n++] = (byte)((sample & 0xff00) >> 8);
outBuffer[n++] =(byte)(sample & 0xff);
//Right
floatSample = floatBuffer[n/2];
if(floatSample>=1.0f)
sample=0x7fff;
else if(floatSample<=-1.0f)
sample=-0x8000;
else
sample = (short)(floatSample*0x8000);
outBuffer[n++] = (byte)((sample & 0xff00) >> 8);
outBuffer[n++] =(byte)(sample & 0xff);
}
}
/**
* Read a frame into the given buffer of floats
* @param floatBuffer
*/
public final void read(float[] floatBuffer)
{
if(isRealtime)
{
updateFrameBufferPos();
floatRead(floatBuffer);
}
else
floatRead(floatBuffer);
}
/**
* Read a frame into the given buffers of bytes, and floats
* @param outBuffer
* @param floatBuffer
*/
public final void read(byte[] outBuffer, float[] floatBuffer)
{
if(isRealtime)
{
updateFrameBufferPos();
byteRead(outBuffer,floatBuffer);
}
}
/**
* Read a frame into the given buffers of bytes, and floats. Used in non realtime mode
* i.e. when exporting audio to a file.
* @param outBuffer
* @param floatBuffer
*/
public final void readNonRealtime(byte[] outBuffer, float[] floatBuffer)
{
if(!isRealtime)
byteRead(outBuffer,floatBuffer);
}
public final void realtimeOn()
{
isRealtime=true;
}
public final void realtimeOff()
{
isRealtime=false;
}
public abstract void configureAudioOutput(JFrame frame);
/**
* Returns the latency in microseconds
* @return
*/
public final long getLatency() {
return (1000000L * (bufferSize/2)) / sampleRate;
}
/**
* Returns the latency in frames
* @return
*/
public final int getLatencyAsFrames() {
return (bufferSize/2);
}
/**
* Returns the voiceServer latency plus the operating system latency in microseconds
* @return
*/
public final long getTotalLatency()
{
// return getLatency()+(FrinikaConfig.getOSLatencyMillis()*1000);
return (long) (getLatency()+(FrinikaAudioSystem.getAudioServer().getOutputLatencyMillis()*1000));
}
/**
* Returns the total latency in frames
* @return
*/
public final int getTotalLatencyAsFrames()
{
return (int)(getSampleRate()*(getTotalLatency())/1000000);
}
}