/* * LimiterDlg.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 */ package de.sciss.fscape.gui; import java.awt.GridBagConstraints; import java.awt.Insets; import java.io.File; import java.io.IOException; import javax.swing.*; 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.Constants; 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; /** * Processing module for peak limiting. * * TODO: Use `FastLog` to improve speed */ public class LimiterDlg extends ModulePanel { // Properties (defaults) private static final int PR_INPUTFILE = 0; // pr.text private static final int PR_OUTPUTFILE = 1; private static final int PR_OUTPUTTYPE = 0; // pr.intg private static final int PR_OUTPUTRES = 1; private static final int PR_SYNC = 0; // pr.bool private static final int PR_BOOST = 0; // pr.para private static final int PR_CEILING = 1; private static final int PR_ATTACK = 2; private static final int PR_RELEASE = 3; private static final String PRN_INPUTFILE = "InputFile"; 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_SYNC = "Sync"; private static final String PRN_BOOST = "Boost"; private static final String PRN_CEILING = "Ceiling"; private static final String PRN_ATTACK = "Attack"; private static final String PRN_RELEASE = "Release"; private static final String prText[] = { "", "" }; private static final String prTextName[] = { PRN_INPUTFILE, PRN_OUTPUTFILE }; private static final int prIntg[] = { 0, 0 }; private static final String prIntgName[] = { PRN_OUTPUTTYPE, PRN_OUTPUTRES }; private static final boolean prBool[] = { true }; private static final String prBoolName[] = { PRN_SYNC }; private static final Param prPara[] = { null, null, null, null }; private static final String prParaName[] = { PRN_BOOST, PRN_CEILING, PRN_ATTACK, PRN_RELEASE }; private static final int GG_INPUTFILE = GG_OFF_PATHFIELD + PR_INPUTFILE; 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_SYNC = GG_OFF_CHECKBOX + PR_SYNC; private static final int GG_BOOST = GG_OFF_PARAMFIELD + PR_BOOST; private static final int GG_CEILING = GG_OFF_PARAMFIELD + PR_CEILING; private static final int GG_ATTACK = GG_OFF_PARAMFIELD + PR_ATTACK; private static final int GG_RELEASE = GG_OFF_PARAMFIELD + PR_RELEASE; private static PropertyArray static_pr = null; private static Presets static_presets = null; public LimiterDlg() { super("Limiter"); init2(); } protected void buildGUI() { 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.paraName = prParaName; static_pr.para[PR_BOOST] = new Param( 3.0, Param.DECIBEL_AMP); static_pr.para[PR_CEILING] = new Param( -0.2, Param.DECIBEL_AMP); static_pr.para[PR_ATTACK] = new Param( 20.0, Param.ABS_MS ); static_pr.para[PR_RELEASE] = new Param(200.0, Param.ABS_MS ); fillDefaultAudioDescr(static_pr.intg, PR_OUTPUTTYPE, PR_OUTPUTRES); static_presets = new Presets(getClass(), static_pr.toProperties(true)); } presets = static_presets; pr = (PropertyArray) static_pr.clone(); // -------- build GUI -------- GridBagConstraints con; PathField ggInputFile, ggOutputFile; PathField[] ggInputs; ParamField ggParam; JCheckBox ggCheck; gui = new GUISupport(); con = gui.getGridBagConstraints(); con.insets = new Insets(1, 2, 1, 2); // -------- input gadgets -------- con.fill = GridBagConstraints.BOTH; con.gridwidth = GridBagConstraints.REMAINDER; gui.addLabel(new GroupLabel("Waveform Input", GroupLabel.ORIENT_HORIZONTAL, GroupLabel.BRACE_NONE)); ggInputFile = new PathField(PathField.TYPE_INPUTFILE + PathField.TYPE_FORMATFIELD, "Select input file"); ggInputFile.handleTypes(GenericFile.TYPES_SOUND); con.gridwidth = 1; con.weightx = 0.1; gui.addLabel(new JLabel("File Name:", SwingConstants.RIGHT)); con.gridwidth = GridBagConstraints.REMAINDER; con.weightx = 0.9; gui.addPathField(ggInputFile, GG_INPUTFILE, null); // -------- output gadgets -------- gui.addLabel(new GroupLabel("Output", GroupLabel.ORIENT_HORIZONTAL, GroupLabel.BRACE_NONE)); 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] = ggInputFile; ggOutputFile.deriveFrom(ggInputs, "$D0$F0Lim$E"); con.gridwidth = 1; con.weightx = 0.1; gui.addLabel(new JLabel("File Name:", 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); // -------- settings gadgets -------- gui.addLabel(new GroupLabel("Settings", GroupLabel.ORIENT_HORIZONTAL, GroupLabel.BRACE_NONE)); final ParamSpace[] spcAtkRls = new ParamSpace[] { Constants.spaces[Constants.absMsSpace] }; final ParamSpace[] spcBoost = new ParamSpace[] { Constants.spaces[Constants.decibelAmpSpace], Constants.spaces[Constants.factorAmpSpace ] }; final ParamSpace[] spcCeiling = new ParamSpace[] { new ParamSpace(Constants.minDecibel, 0.0, 0.01, Param.DECIBEL_AMP), Constants.spaces[Constants.ratioAmpSpace] }; ggParam = new ParamField(spcBoost); con.weightx = 0.1; con.gridwidth = 1; gui.addLabel(new JLabel("Boost:", SwingConstants.RIGHT)); con.weightx = 0.4; gui.addParamField(ggParam, GG_BOOST, null); con.ipadx = 8; ggParam = new ParamField(spcAtkRls); con.weightx = 0.1; gui.addLabel(new JLabel("Attack [-60 dB]:", SwingConstants.RIGHT)); con.ipadx = 0; con.weightx = 0.4; con.gridwidth = GridBagConstraints.REMAINDER; gui.addParamField(ggParam, GG_ATTACK, null); ggParam = new ParamField(spcCeiling); con.weightx = 0.1; con.gridwidth = 1; gui.addLabel(new JLabel("Ceiling:", SwingConstants.RIGHT)); con.weightx = 0.4; gui.addParamField(ggParam, GG_CEILING, null); con.ipadx = 8; ggParam = new ParamField(spcAtkRls); con.weightx = 0.1; gui.addLabel(new JLabel("Release [-60 dB]:", SwingConstants.RIGHT)); con.ipadx = 0; con.weightx = 0.4; con.gridwidth = GridBagConstraints.REMAINDER; gui.addParamField(ggParam, GG_RELEASE, null); con.ipady = 4; ggCheck = new JCheckBox("Synchronize Channels"); con.weightx = 0.5; con.gridwidth = GridBagConstraints.REMAINDER; gui.addCheckbox(ggCheck, GG_SYNC, null); initGUI(this, FLAGS_PRESETS | FLAGS_PROGBAR, gui); } public void fillGUI() { super.fillGUI(); super.fillGUI(gui); } public void fillPropertyArray() { super.fillPropertyArray(); super.fillPropertyArray(gui); } // -------- Processor Interface -------- protected void process() { AudioFile inF = null; AudioFile outF = null; final AudioFileDescr inSpec, outSpec; final int numChannels; long numFrames; final PathField ggOutput; final int atkSize, rlsSize; // sample frames re -150 dB final int lapSize, inBufSize, grainBufSize, envSize; final float[][] inBuf; final double[][] gainBuf; final double[] env; final boolean sync; final Param ampRef; final double ceil, boost; final int gainChans; final double atk60, rls60, atkCoef, rlsCoef; topLevel: try { // ---- open input ---- inF = AudioFile.openAsRead(new File(pr.text[PR_INPUTFILE])); inSpec = inF.getDescr(); numChannels = inSpec.channels; numFrames = inSpec.length; // ---- open output ---- ggOutput = (PathField) gui.getItemObj(GG_OUTPUTFILE); if (ggOutput == null) throw new IOException(ERR_MISSINGPROP); outSpec = new AudioFileDescr(inSpec); ggOutput.fillStream(outSpec); outF = AudioFile.openAsWrite(outSpec); // .... check running .... if (!threadRunning) break topLevel; // attack and release durations in sample frames re -60 dB atk60 = Math.max(1, (int) AudioFileDescr.millisToSamples(inSpec, pr.para[PR_ATTACK ].value + 0.5)); rls60 = Math.max(1, (int) AudioFileDescr.millisToSamples(inSpec, pr.para[PR_RELEASE].value + 0.5)); // damping coefficients atkCoef = Math.pow(1.0e-3, 1.0 / atk60); rlsCoef = Math.pow(1.0e-3, 1.0 / rls60); // instead of -60 dB, we calculate the number of frames necessary to fall down to -60*2.5 = -150 dB // which is the internal noise floor of the attack/release envelope atkSize = (int) (atk60 * 2.5); rlsSize = (int) (rls60 * 2.5); envSize = atkSize + rlsSize; env = new double[envSize]; env[atkSize] = 1.0; for (int i = 1; i < atkSize; i++) env[atkSize - i] = Math.pow(atkCoef, i); for (int i = 1; i < rlsSize; i++) env[atkSize + i] = Math.pow(rlsCoef, i); ampRef = new Param(1.0, Param.ABS_AMP); boost = (Param.transform(pr.para[PR_BOOST ], Param.ABS_AMP, ampRef, null)).value; ceil = (Param.transform(pr.para[PR_CEILING], Param.ABS_AMP, ampRef, null)).value; lapSize = Math.max(atkSize, 8192); grainBufSize = envSize + lapSize; sync = pr.bool[PR_SYNC]; gainChans = sync ? 1 : numChannels; gainBuf = new double[gainChans][grainBufSize]; inBufSize = atkSize + lapSize; inBuf = new float[numChannels][inBufSize]; Util.fill(gainBuf, 0, grainBufSize, boost); int outSkip = atkSize; for (long framesRead = 0L, framesWritten = 0L; (framesWritten < numFrames) && threadRunning; ) { // ---- read input ---- final int readLen = (int) Math.min(lapSize, numFrames - framesRead); inF.readFrames(inBuf, atkSize, readLen); // ---- adjust gain buffer ---- for (int i = atkSize, ii = i + readLen; i < ii; i++) { for (int ch = 0; ch < gainChans; ch++) { float max = 0.0f; if (sync) { for (float[] chBuf : inBuf) { max = Math.max(max, Math.abs(chBuf[i])); } } else { max = Math.abs(inBuf[ch][i]); } final double[] gainBufCh = gainBuf[ch]; final double gain0 = gainBufCh[i]; final double amp0 = max * gain0; // if the cumulative amplitude exceeds the // threshold by now, multiply the gain buffer // with the attack/release envelope for that // particular frame if (amp0 > ceil) { // lin-lin: // (in - inLow) / (inHigh - inLow) * (outHigh - outLow) + outLow // thus env.linlin(0, 1, 1.0, ceil/amp0) becomes // (env - 0) / (1 - 0) * (ceil/amp0 - 1) + 1 // = env * (ceil/amp0 - 1) + 1 // // E.g. if we're overshooting by 3 dB, // then ceil/amp0 = 0.7. At peak we'll multiply by -0.3 + 1.0 = 0.7 final double mul = ceil / amp0 - 1.0; for (int j = 0, k = i - atkSize; j < envSize; j++, k++) { gainBufCh[k] *= env[j] * mul + 1.0; } } } } // ---- calculate output ---- final int writeLen = (int) Math.min(lapSize, numFrames - framesWritten) - outSkip; final int outStop = outSkip + writeLen; for (int ch = 0; ch < numChannels; ch++) { final double[] gainBufCh = gainBuf[ch % gainChans]; final float[] inBufCh = inBuf[ch]; for (int i = outSkip; i < outStop; i++) { final float gain = (float) gainBufCh[i]; if (gain != 1f) inBufCh[i] *= gain; } } // ---- write output ---- outF.writeFrames(inBuf, outSkip, writeLen); // ---- buffer rotation ---- for (double[] gainBufCh : gainBuf) { System.arraycopy(gainBufCh, outStop, gainBufCh, 0, grainBufSize - outStop); } Util.fill(gainBuf, grainBufSize - outStop, outStop, boost); for (float[] inBufCh : inBuf) { System.arraycopy(inBufCh, outStop, inBufCh, 0, inBufSize - outStop); } outSkip = 0; framesRead += readLen; framesWritten += writeLen; // .... progress .... setProgression((float) ((double) (framesRead + framesWritten) / (numFrames + numFrames))); } // .... check running .... if (!threadRunning) break topLevel; // ---- finish ---- inF.close(); inF = null; outF.close(); outF = null; } catch (IOException e1) { setError(e1); } // ---- clean up (topLevel) ---- if (inF != null) inF .cleanUp(); if (outF != null) outF.cleanUp(); } }