/*
* BleachDlg.java
* (FScape)
*
* Copyright (c) 2001-2016 Hanns Holger Rutz. All rights reserved.
*
* This software is published under the GNU General Public License v3+
*
*
* For further information, please contact Hanns Holger Rutz at
* contact@sciss.de
*
*
* Changelog:
* 18-Feb-10 created
*/
package de.sciss.fscape.gui;
import de.sciss.fscape.io.GenericFile;
import de.sciss.fscape.prop.Presets;
import de.sciss.fscape.prop.PropertyArray;
import de.sciss.fscape.session.ModulePanel;
import de.sciss.fscape.util.Param;
import de.sciss.fscape.util.ParamSpace;
import de.sciss.fscape.util.Util;
import de.sciss.io.AudioFile;
import de.sciss.io.AudioFileDescr;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.io.EOFException;
import java.io.File;
import java.io.IOException;
/**
* Adaptive whitening filter.
*/
public class BleachDlg
extends ModulePanel {
// -------- private variables --------
// properties
private static final int PR_ANAINFILE = 0; // pr.text
private static final int PR_FLTINFILE = 1;
private static final int PR_OUTPUTFILE = 2;
private static final int PR_OUTPUTTYPE = 0; // pr.intg
private static final int PR_OUTPUTRES = 1;
private static final int PR_GAINTYPE = 2;
private static final int PR_GAIN = 0; // pr.para
private static final int PR_FILTERLENGTH = 1;
private static final int PR_FEEDBACKGAIN = 2;
private static final int PR_FILTERCLIP = 3;
private static final int PR_USEANAASFLT = 0; // pr.bool
private static final int PR_INVERSE = 1;
private static final int PR_TWOWAYS = 2;
private static final String PRN_ANAINFILE = "AnaInFile";
private static final String PRN_FLTINFILE = "FltInFile";
private static final String PRN_OUTPUTFILE = "OutputFile";
private static final String PRN_OUTPUTTYPE = "OutputType";
private static final String PRN_OUTPUTRES = "OutputReso";
private static final String PRN_FILTERLENGTH = "FilterLength";
private static final String PRN_FILTERCLIP = "FilterClip";
private static final String PRN_FEEDBACKGAIN = "FeedbackGain";
private static final String PRN_USEANAASFLT = "UseAnaAsFilter";
private static final String PRN_INVERSE = "Inverse";
private static final String PRN_TWOWAYS = "TwoWays";
private static final String prText[] = { "", "", "" };
private static final String prTextName[] = { PRN_ANAINFILE, PRN_FLTINFILE, PRN_OUTPUTFILE };
private static final int prIntg[] = { 0, 0, GAIN_UNITY };
private static final String prIntgName[] = { PRN_OUTPUTTYPE, PRN_OUTPUTRES, PRN_GAINTYPE };
private static final Param prPara[] = { null, null, null, null };
private static final String prParaName[] = { PRN_GAIN, PRN_FILTERLENGTH, PRN_FEEDBACKGAIN, PRN_FILTERCLIP };
private static final boolean prBool[] = { true, false, false };
private static final String prBoolName[] = { PRN_USEANAASFLT, PRN_INVERSE, PRN_TWOWAYS };
private static final int GG_ANAINFILE = GG_OFF_PATHFIELD + PR_ANAINFILE;
private static final int GG_FLTINFILE = GG_OFF_PATHFIELD + PR_FLTINFILE;
private static final int GG_OUTPUTFILE = GG_OFF_PATHFIELD + PR_OUTPUTFILE;
private static final int GG_OUTPUTTYPE = GG_OFF_CHOICE + PR_OUTPUTTYPE;
private static final int GG_OUTPUTRES = GG_OFF_CHOICE + PR_OUTPUTRES;
private static final int GG_GAINTYPE = GG_OFF_CHOICE + PR_GAINTYPE;
private static final int GG_GAIN = GG_OFF_PARAMFIELD + PR_GAIN;
private static final int GG_FILTERLENGTH = GG_OFF_PARAMFIELD + PR_FILTERLENGTH;
private static final int GG_FILTERCLIP = GG_OFF_PARAMFIELD + PR_FILTERCLIP;
private static final int GG_FEEDBACKGAIN = GG_OFF_PARAMFIELD + PR_FEEDBACKGAIN;
private static final int GG_USEANAASFLT = GG_OFF_CHECKBOX + PR_USEANAASFLT;
private static final int GG_INVERSE = GG_OFF_CHECKBOX + PR_INVERSE;
private static final int GG_TWOWAYS = GG_OFF_CHECKBOX + PR_TWOWAYS;
private static PropertyArray static_pr = null;
private static Presets static_presets = null;
private static final String ERR_CHANNELS = "Inputs must share\nsame # of channels!";
// -------- public methods --------
public BleachDlg()
{
super( "Bleach" );
init2();
}
protected void buildGUI()
{
// initialize PropertyArray once
if( static_pr == null ) {
static_pr = new PropertyArray();
static_pr.text = prText;
static_pr.textName = prTextName;
static_pr.intg = prIntg;
static_pr.intgName = prIntgName;
static_pr.bool = prBool;
static_pr.boolName = prBoolName;
static_pr.para = prPara;
static_pr.para[ PR_FILTERLENGTH ] = new Param( 441.0, Param.NONE );
static_pr.para[ PR_FEEDBACKGAIN ] = new Param( -60.0, Param.DECIBEL_AMP );
static_pr.para[ PR_FILTERCLIP ] = new Param( 18.0, Param.DECIBEL_AMP );
static_pr.paraName = prParaName;
// static_pr.superPr = DocumentFrame.static_pr;
fillDefaultAudioDescr( static_pr.intg, PR_OUTPUTTYPE, PR_OUTPUTRES );
fillDefaultGain( static_pr.para, PR_GAIN );
static_presets = new Presets( getClass(), static_pr.toProperties( true ));
}
presets = static_presets;
pr = (PropertyArray) static_pr.clone();
// -------- build GUI --------
final GridBagConstraints con;
final PathField ggAnaInFile, ggOutputFile, ggFltInFile;
final PathField[] ggInputs;
final JCheckBox ggUseAnaAsFlt;
final JCheckBox ggInverse, ggTwoWays;
final Component[] ggGain;
final ParamField ggFilterLength, ggFeedbackGain, ggFilterClip;
final ParamSpace spcFeedbackGain, spcFilterClip;
gui = new GUISupport();
con = gui.getGridBagConstraints();
con.insets = new Insets( 1, 2, 1, 2 );
final ItemListener il = new ItemListener() {
public void itemStateChanged( ItemEvent e )
{
final int id = gui.getItemID( e );
switch( id ) {
case GG_USEANAASFLT:
pr.bool[ id - GG_OFF_CHECKBOX ] = ((JCheckBox) e.getSource()).isSelected();
reflectPropertyChanges();
break;
}
}
};
// -------- Input-Gadgets --------
con.fill = GridBagConstraints.BOTH;
con.gridwidth = GridBagConstraints.REMAINDER;
gui.addLabel( new GroupLabel( "Waveform I/O", GroupLabel.ORIENT_HORIZONTAL,
GroupLabel.BRACE_NONE ));
ggAnaInFile = new PathField( PathField.TYPE_INPUTFILE + PathField.TYPE_FORMATFIELD,
"Select analysis input file" );
ggAnaInFile.handleTypes( GenericFile.TYPES_SOUND );
con.gridwidth = 1;
con.weightx = 0.1;
gui.addLabel( new JLabel( "Analysis Input:", SwingConstants.RIGHT ));
con.gridwidth = GridBagConstraints.REMAINDER;
con.weightx = 0.9;
gui.addPathField( ggAnaInFile, GG_ANAINFILE, null );
ggFltInFile = new PathField( PathField.TYPE_INPUTFILE + PathField.TYPE_FORMATFIELD,
"Select filter input file" );
ggFltInFile.handleTypes( GenericFile.TYPES_SOUND );
con.gridwidth = 1;
con.weightx = 0.1;
gui.addLabel( new JLabel( "Filter Input:", SwingConstants.RIGHT ));
con.gridwidth = GridBagConstraints.REMAINDER;
con.weightx = 0.9;
ggUseAnaAsFlt = new JCheckBox( "Use Analysis File" );
gui.addCheckbox( ggUseAnaAsFlt, GG_USEANAASFLT, il );
con.weightx = 0.1;
con.gridwidth = 1;
gui.addLabel( new JLabel() );
con.weightx = 0.9;
con.gridwidth = GridBagConstraints.REMAINDER;
gui.addPathField( ggFltInFile, GG_FLTINFILE, null );
ggOutputFile = new PathField( PathField.TYPE_OUTPUTFILE + PathField.TYPE_FORMATFIELD +
PathField.TYPE_RESFIELD, "Select output file" );
ggOutputFile.handleTypes( GenericFile.TYPES_SOUND );
ggInputs = new PathField[ 1 ];
ggInputs[ 0 ] = ggAnaInFile;
// ggInputs[ 1 ] = ggFltInFile;
ggOutputFile.deriveFrom( ggInputs, "$D0$F0White$E" );
con.gridwidth = 1;
con.weightx = 0.1;
// con.gridx = 0;
gui.addLabel( new JLabel( "Whitened Output:", SwingConstants.RIGHT ));
con.gridwidth = GridBagConstraints.REMAINDER;
con.weightx = 0.9;
gui.addPathField( ggOutputFile, GG_OUTPUTFILE, null );
gui.registerGadget( ggOutputFile.getTypeGadget(), GG_OUTPUTTYPE );
gui.registerGadget( ggOutputFile.getResGadget(), GG_OUTPUTRES );
ggGain = createGadgets( GGTYPE_GAIN );
con.weightx = 0.1;
con.gridwidth = 1;
gui.addLabel( new JLabel( "Gain:", SwingConstants.RIGHT ));
con.weightx = 0.4;
gui.addParamField( (ParamField) ggGain[ 0 ], GG_GAIN, null );
con.weightx = 0.5;
con.gridwidth = GridBagConstraints.REMAINDER;
gui.addChoice( (JComboBox) ggGain[ 1 ], GG_GAINTYPE, null );
// -------- Plot Settings --------
gui.addLabel( new GroupLabel( "Filter Settings", GroupLabel.ORIENT_HORIZONTAL,
GroupLabel.BRACE_NONE ));
ggFilterLength = new ParamField( new ParamSpace[] {
new ParamSpace( 2, 1000000, 1, Param.NONE )});
con.weightx = 0.1;
con.gridwidth = 1;
gui.addLabel( new JLabel( "Filter Length:", SwingConstants.RIGHT ));
con.weightx = 0.4;
// con.gridwidth = GridBagConstraints.REMAINDER;
gui.addParamField( ggFilterLength, GG_FILTERLENGTH, null );
// ggIterations = new ParamField( new ParamSpace[] {
// new ParamSpace( 1, 1000000, 1, Param.NONE )});
// con.weightx = 0.1;
//// con.gridwidth = 1;
// gui.addLabel( new JLabel( "Iterations:", SwingConstants.RIGHT ));
// con.weightx = 0.4;
// con.gridwidth = GridBagConstraints.REMAINDER;
// gui.addParamField( ggIterations, GG_ITERATIONS, null );
spcFeedbackGain = new ParamSpace( Double.NEGATIVE_INFINITY, 0.0, 0.1, Param.DECIBEL_AMP );
ggFeedbackGain = new ParamField( spcFeedbackGain );
con.weightx = 0.1;
gui.addLabel( new JLabel( "Feedback Gain:", SwingConstants.RIGHT ));
con.weightx = 0.4;
con.gridwidth = GridBagConstraints.REMAINDER;
gui.addParamField( ggFeedbackGain, GG_FEEDBACKGAIN, null );
spcFilterClip = new ParamSpace( 0.0, Double.POSITIVE_INFINITY, 0.1, Param.DECIBEL_AMP );
ggFilterClip = new ParamField( spcFilterClip );
con.gridwidth = 1;
con.weightx = 0.1;
gui.addLabel( new JLabel( "Filter Clip:", SwingConstants.RIGHT ));
con.weightx = 0.4;
// con.gridwidth = GridBagConstraints.REMAINDER;
gui.addParamField( ggFilterClip, GG_FILTERCLIP, null );
ggTwoWays = new JCheckBox();
con.gridwidth = 1;
con.weightx = 0.1;
gui.addLabel( new JLabel( "Two Ways (Backward-Forward):", SwingConstants.RIGHT ));
con.weightx = 0.4;
con.gridwidth = GridBagConstraints.REMAINDER;
gui.addCheckbox( ggTwoWays, GG_TWOWAYS, null );
con.gridwidth = 2;
gui.addLabel( new JLabel());
ggInverse = new JCheckBox();
con.gridwidth = 1;
con.weightx = 0.1;
gui.addLabel( new JLabel( "Inverse Operation (Colorize):", SwingConstants.RIGHT ));
con.weightx = 0.4;
con.gridwidth = GridBagConstraints.REMAINDER;
gui.addCheckbox( ggInverse, GG_INVERSE, null );
reflectPropertyChanges();
initGUI( this, FLAGS_PRESETS | FLAGS_PROGBAR, gui );
}
/**
* Transfer values from prop-array to GUI
*/
public void fillGUI()
{
super.fillGUI();
super.fillGUI( gui );
}
/**
* Transfer values from GUI to prop-array
*/
public void fillPropertyArray()
{
super.fillPropertyArray();
super.fillPropertyArray( gui );
}
// -------- Processor Interface --------
protected void process()
{
AudioFile anaInF = null;
AudioFile outF = null;
AudioFile fltInF = null;
final AudioFile tmpF, tmpF2;
final AudioFileDescr anaInDescr, outDescr;
final int numCh;
final long anaInLength, fltInLength, numFrames;
final boolean useAnaAsFilter = pr.bool[ PR_USEANAASFLT ];
final PathField ggOutput;
final int fltLength = (int) pr.para[ PR_FILTERLENGTH ].value;
// final int iterations = (int) pr.para[ PR_ITERATIONS ].value;
final Param ampRef = new Param( 1.0, Param.ABS_AMP ); // transform-Referenz
final double feedbackGain = (Param.transform( pr.para[ PR_FEEDBACKGAIN ], Param.ABS_AMP, ampRef, null )).value;
final double[][] fltKernel;
final float[][] anaBuf, outBuf, fltBuf;
final boolean absGain = pr.intg[ PR_GAINTYPE ] == GAIN_ABSOLUTE;
final double filterMax = (Param.transform( pr.para[ PR_FILTERCLIP ], Param.ABS_AMP, ampRef, null )).value;
final double filterMin = -filterMax;
// final boolean preCalc = pr.bool[ PR_PRECALC ];
final boolean inverse = pr.bool[ PR_INVERSE ];
final boolean twoWays = pr.bool[ PR_TWOWAYS ];
long framesRead, framesWritten, progOff, progLen;
int chunkLen;
float[] anaChanBuf, outChanBuf, fltChanBuf;
double[] fltChanKernel;
double d1, errNeg;
float gain;
float maxAmp = 0.0f;
topLevel: try {
// ---- open input, output; init ----
// analysis input
anaInF = AudioFile.openAsRead( new File( pr.text[ PR_ANAINFILE ]));
anaInDescr = anaInF.getDescr();
numCh = anaInDescr.channels;
anaInLength = anaInDescr.length;
// this helps to prevent errors from empty files!
if( (anaInLength < 1) || (numCh < 1) ) throw new EOFException( ERR_EMPTY );
// .... check running ....
if( !threadRunning ) break topLevel;
// filter input
if( !useAnaAsFilter ) {
fltInF = AudioFile.openAsRead( new File( pr.text[ PR_FLTINFILE ]));
final AudioFileDescr fltInDescr = fltInF.getDescr();
// .... check running ....
if( !threadRunning ) break topLevel;
if( numCh != fltInDescr.channels ) {
throw new IOException( ERR_CHANNELS );
}
fltInLength = fltInDescr.length;
} else {
fltInLength = anaInLength;
}
numFrames = Math.min( anaInLength, fltInLength );
// output
ggOutput = (PathField) gui.getItemObj( GG_OUTPUTFILE );
if( ggOutput == null ) throw new IOException( ERR_MISSINGPROP );
outDescr = new AudioFileDescr( anaInDescr );
ggOutput.fillStream( outDescr );
outF = AudioFile.openAsWrite( outDescr );
// .... check running ....
if( !threadRunning ) break topLevel;
// ---- further inits ----
if( absGain ) {
gain = (float) (Param.transform( pr.para[ PR_GAIN ], Param.ABS_AMP, ampRef, null )).value;
tmpF = null;
} else {
gain = 1f;
tmpF = createTempFile( outDescr );
}
if (twoWays) {
tmpF2 = createTempFile( outDescr );
} else {
tmpF2 = null;
}
fltKernel = new double[ numCh ][ fltLength ];
// FFFFFBBBBBBBBB
anaBuf = new float[ numCh ][ fltLength + 8192 ];
outBuf = new float[ numCh ][ 8192 ];
if( useAnaAsFilter ) {
fltBuf = anaBuf;
} else {
fltBuf = new float[ numCh ][ fltLength + 8192 ];
}
// ---- main loop ----
progLen = numFrames * 3 + (absGain ? 0L : numFrames) + (twoWays ? numFrames << 1 : 0L);
progOff = 0L;
framesWritten = 0L;
framesRead = 0L;
AudioFile readAnaInF = anaInF;
AudioFile readFltInF = fltInF;
AudioFile writeOutF = twoWays ? tmpF2 : (absGain ? outF : tmpF);
boolean isSecondPass = false;
while (threadRunning && (framesWritten < numFrames)) {
chunkLen = (int) Math.min(8192, numFrames - framesRead);
// two ways + separate filter: (1) read reversed ana (2) read _forward_ ana
// two ways + same file : (1) read reversed ana (2) read _forward_ ana
// one way + separate filter: (1) read ana
// one way + same file : (1) read ana
if (twoWays && (/* useAnaAsFilter || */ !isSecondPass)) {
readAnaInF.seekFrame(numFrames - framesRead - chunkLen);
readAnaInF.readFrames(anaBuf, fltLength, chunkLen);
Util.reverse(anaBuf, fltLength, chunkLen);
} else {
readAnaInF.readFrames(anaBuf, fltLength, chunkLen);
}
// two ways + separate filter: (1) read reversed flt (2) read reversed flt/prev-out
// two ways + same file : (1) do not read (2) read reversed flt/prev-out
// one way + separate filter: (1) read filter
// one way + same file : (1) don't read
if (twoWays && (isSecondPass || !useAnaAsFilter)) {
readFltInF.seekFrame(numFrames - framesRead - chunkLen);
readFltInF.readFrames(fltBuf, fltLength, chunkLen);
Util.reverse(fltBuf, fltLength, chunkLen);
} else if (!useAnaAsFilter) {
readFltInF.readFrames(fltBuf, fltLength, chunkLen);
}
framesRead += chunkLen;
progOff += chunkLen;
// process
for (int ch = 0; ch < numCh; ch++) {
anaChanBuf = anaBuf[ch];
fltChanKernel = fltKernel[ch];
outChanBuf = outBuf[ch];
fltChanBuf = fltBuf[ch];
for (int i = 0; i < chunkLen; i++) {
// calc error
d1 = 0.0;
int k = i;
for (int j = 0; j < fltLength; j++, k++) {
d1 += anaChanBuf[k] * fltChanKernel[j];
}
// err = d1 - anaChanBuf[ k ];
errNeg = anaChanBuf[k] - d1;
if (useAnaAsFilter) {
// use straight as output...
outChanBuf[i] = inverse ? (float) d1 : (float) errNeg;
} else {
// ...or calc output according to filter buffer
k = i;
for (int j = 0; j < fltLength; j++, k++) {
d1 += fltChanBuf[k] * fltChanKernel[j];
}
outChanBuf[i] = inverse ? (float) d1 : (float) (fltChanBuf[k] - d1);
}
// update kernel
d1 = errNeg * feedbackGain;
k = i;
for( int j = 0; j < fltLength; j++, k++ ) {
fltChanKernel[ j ] = Math.max( filterMin, Math.min( filterMax,
fltChanKernel[ j ] + d1 * anaChanBuf[ k ]));
}
}
}
progOff += chunkLen;
// handle overlap
// Util.copy( anaBuf, fltLength, anaBuf, 0, chunkLen );
Util.copy( anaBuf, 8192, anaBuf, 0, fltLength );
if( !useAnaAsFilter ) {
// Util.copy( fltBuf, fltLength, fltBuf, 0, chunkLen );
Util.copy( fltBuf, 8192, fltBuf, 0, fltLength );
}
// write output
maxAmp = Math.max( maxAmp, Util.maxAbs( outBuf, 0, chunkLen ));
if( absGain ) {
Util.mult( outBuf, 0, chunkLen, gain );
}
writeOutF.writeFrames( outBuf, 0, chunkLen );
framesWritten += chunkLen;
progOff += chunkLen;
// .... progress ....
setProgression( (float) progOff / (float) progLen );
if (twoWays && framesRead == numFrames && !isSecondPass) {
assert (framesWritten == numFrames);
writeOutF.flush();
anaInF.seekFrame(0L);
Util.clear(fltKernel);
Util.clear(fltBuf);
Util.clear(anaBuf);
readFltInF = writeOutF;
// if (useAnaAsFilter) readAnaInF = writeOutF;
writeOutF = absGain ? outF : tmpF;
framesRead = 0L;
framesWritten = 0L;
isSecondPass = true;
}
}
// .... check running ....
if( !threadRunning ) break topLevel;
// ---- finish ----
// adjust gain
if( !absGain ) {
gain = (float) (Param.transform( pr.para[ PR_GAIN ], Param.ABS_AMP,
new Param( 1.0 / maxAmp, Param.ABS_AMP ), null )).value;
normalizeAudioFile( writeOutF, outF, anaBuf, gain, 1.0f );
}
if (tmpF != null) deleteTempFile(tmpF );
if (tmpF2 != null) deleteTempFile(tmpF2);
outF.close();
outF = null;
// inform about clipping/ low level
maxAmp *= gain;
handleClipping( maxAmp );
}
catch( IOException e1 ) {
setError( e1 );
}
catch( OutOfMemoryError e2 ) {
setError( new Exception( ERR_MEMORY ));
}
// ---- cleanup (topLevel) ----
if( anaInF != null ) anaInF.cleanUp();
if( fltInF != null ) fltInF.cleanUp();
if( outF != null ) outF.cleanUp();
} // process()
protected void reflectPropertyChanges()
{
super.reflectPropertyChanges();
final PathField ggFltInFile = (PathField) gui.getItemObj( GG_FLTINFILE );
if( ggFltInFile != null ) {
ggFltInFile.setEnabled( !pr.bool[ PR_USEANAASFLT ]);
}
}
}