/* * Needlehole.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: * 06-Feb-05 added standard deviation + minimum filter + improved speed * 17-Mar-05 added center clipping * 18-Sep-05 refurbished as processing plug-in */ package de.sciss.fscape.render; import de.sciss.gui.PrefCheckBox; import de.sciss.gui.PrefComboBox; import de.sciss.gui.PrefNumberField; import de.sciss.gui.SpringPanel; import de.sciss.gui.StringItem; import de.sciss.util.NumberSpace; import javax.swing.*; import java.io.IOException; import java.util.prefs.Preferences; /** * Processing module for moving window * based filtering of a sound. */ public class Needlehole extends AbstractRenderPlugIn { private Preferences prefs; private static final String KEY_GAINTYPE = "gaintype"; private static final String KEY_GAIN = "gain"; private static final String KEY_FILTER = "filter"; private static final String KEY_LENGTH = "length"; private static final String KEY_THRESH = "thresh"; private static final String KEY_SUBDRY = "subdry"; private static final String GAIN_ABSOLUTE = "abs"; private static final String GAIN_NORMALIZED = "norm"; private static final String FILTER_MEDIAN = "median"; private static final String FILTER_STDDEV = "stddev"; private static final String FILTER_MINIMUM = "minimum"; private static final String FILTER_CENTER = "center"; public boolean hasUserParameters() { return true; } public boolean shouldDisplayParameters() { return true; } public void init(Preferences p) { super.init(p); prefs = p; } public JComponent getSettingsView(RenderContext context) { final SpringPanel p = new SpringPanel(); final PrefNumberField ggGain = new PrefNumberField(); final PrefComboBox ggGainType = new PrefComboBox(); final PrefNumberField ggLength = new PrefNumberField(); final PrefComboBox ggFilter = new PrefComboBox(); final PrefNumberField ggThresh = new PrefNumberField(); final PrefCheckBox ggSubDry = new PrefCheckBox(); ggGain.setSpace(NumberSpace.genericDoubleSpace); ggGainType.addItem(new StringItem(GAIN_NORMALIZED, "normalized")); ggGainType.addItem(new StringItem(GAIN_ABSOLUTE, "immediate")); ggLength.setSpace(NumberSpace.genericDoubleSpace); // XXX ggThresh.setSpace(NumberSpace.genericDoubleSpace); // XXX ggFilter.addItem(new StringItem(FILTER_MEDIAN, "Median")); ggFilter.addItem(new StringItem(FILTER_STDDEV, "Standard Deviation")); ggFilter.addItem(new StringItem(FILTER_MINIMUM, "Minimum")); ggFilter.addItem(new StringItem(FILTER_CENTER, "Center Clipping")); p.gridAdd(new JLabel("Gain", SwingConstants.RIGHT), 0, 0); p.gridAdd(ggGain, 1, 0); p.gridAdd(ggGainType, 1, 0); p.gridAdd(new JLabel("Window length", SwingConstants.RIGHT), 0, 1); p.gridAdd(ggLength, 1, 1); p.gridAdd(new JLabel("Filter", SwingConstants.RIGHT), 0, 2); p.gridAdd(ggFilter, 1, 2); p.gridAdd(new JLabel("Clip thresh", SwingConstants.RIGHT), 0, 3); p.gridAdd(ggThresh, 1, 3); p.gridAdd(new JLabel("Subtract dry signal", SwingConstants.RIGHT), 0, 4); p.gridAdd(ggSubDry, 1, 4); ggGain .setPreferences(this.prefs, KEY_GAIN); ggGainType.setPreferences(this.prefs, KEY_GAINTYPE); ggLength .setPreferences(this.prefs, KEY_LENGTH); ggFilter .setPreferences(this.prefs, KEY_FILTER); ggThresh .setPreferences(this.prefs, KEY_THRESH); ggSubDry .setPreferences(this.prefs, KEY_SUBDRY); p.makeCompactGrid(); return p; } public String getName() { return "Needlehole Cherry Blossom"; } private RunningWindowFilter prFilter; private boolean prSubDry; private boolean prNormalize; private float prGain; private float[][] prInBuf; private int prInBufSize; // private float prMaxAmp; private int prOffStart; // private long prFramesRead; private long prFramesWritten; private long prRenderLength; private int prWinSize; public boolean producerBegin(RenderContext context, RenderSource source) throws IOException { final double winSizeMillis = prefs.getDouble(KEY_LENGTH, 1.0); final String filterType = prefs.get(KEY_FILTER, FILTER_MEDIAN); final double threshAmp = Math.exp(prefs.getDouble(KEY_THRESH, -3.0) / 20 * Math.log(10)); // XXX final int outBufSize; final Integer outBufSizeI; prSubDry = prefs.getBoolean(KEY_SUBDRY, false); prNormalize = prefs.get(KEY_GAINTYPE, "").equals(GAIN_NORMALIZED); prWinSize = Math.max(1, (int) (winSizeMillis * context.getSourceRate() + 0.5) & ~1); outBufSize = Math.max(8192, prWinSize); outBufSizeI = new Integer(outBufSize); prInBufSize = outBufSize + prWinSize; prOffStart = prWinSize >> 1; prInBuf = new float[source.numChannels][prInBufSize]; // cannot have other buffer sizes than this context.setOption(RenderContext.KEY_MINBLOCKSIZE , outBufSizeI); context.setOption(RenderContext.KEY_PREFBLOCKSIZE, outBufSizeI); context.setOption(RenderContext.KEY_MAXBLOCKSIZE , outBufSizeI); if (filterType.equals(FILTER_MEDIAN)) { prFilter = new MedianFilter(prWinSize, source.numChannels); } else if (filterType.equals(FILTER_STDDEV)) { prFilter = new StdDevFilter(prWinSize, source.numChannels); } else if (filterType.equals(FILTER_MINIMUM)) { prFilter = new MinimumFilter(prWinSize, source.numChannels); } else if (filterType.equals(FILTER_CENTER)) { prFilter = new CenterClippingFilter(prWinSize, source.numChannels, threshAmp); } else { throw new IOException("Unknown filter type : " + filterType); } if (prNormalize) { prGain = 1.0f; } else { prGain = (float) Math.exp(prefs.getDouble(KEY_GAIN, 0.0) / 20 * Math.log(10)); // XXX } prRenderLength = context.getTimeSpan().getLength(); prFramesWritten = 0; return context.getConsumer().consumerBegin(context, source); } public boolean producerRender(RenderContext context, RenderSource source) throws IOException { final int transLen = (int) Math.min(prInBufSize - prWinSize, prRenderLength - prFramesWritten); for (int ch = 0; ch < source.numChannels; ch++) { System.arraycopy(source.blockBuf[ch], source.blockBufOff, prInBuf, prOffStart, source.blockBufLen); } // zero-padding last chunk if (prOffStart + source.blockBufLen < prInBufSize) { fill(prInBuf, prOffStart + source.blockBufLen, prInBufSize - prOffStart - source.blockBufLen, 0f); } prFilter.process(prInBuf, source.blockBuf, 0, source.blockBufOff, transLen); if (prSubDry) { subtract(source.blockBuf, source.blockBufOff, prInBuf, prWinSize >> 1, transLen); } // shift buffers for (int ch = 0; ch < source.numChannels; ch++) { System.arraycopy(prInBuf[ch], prInBufSize - prWinSize, prInBuf[ch], 0, prWinSize); } prOffStart = prWinSize; prFramesWritten += transLen; if (prGain != 1.0f) { multiply(source.blockBuf, source.blockBufOff, transLen, prGain); } return context.getConsumer().consumerRender(context, source); } private static void fill(float[][] buf, int off, int len, float value) { float[] convBuf; for (int ch = 0; ch < buf.length; ch++) { convBuf = buf[ch]; for (int i = off, j = off + len; i < j; ) { convBuf[i++] = value; } } } private static void multiply(float[][] buf, int off, int len, float value) { float[] convBuf; for (int ch = 0; ch < buf.length; ch++) { convBuf = buf[ch]; for (int i = off, j = off + len; i < j; ) { convBuf[i++] *= value; } } } private static void subtract(float[][] bufA, int offA, float[][] bufB, int offB, int len) { float[] convBuf1, convBuf2; for (int ch = 0; ch < bufA.length; ch++) { convBuf1 = bufA[ch]; convBuf2 = bufB[ch]; for (int i = offA, j = offB, k = offA + len; i < k; ) { convBuf1[i++] -= convBuf2[j++]; } } } // -------- Window Filter -------- private interface RunningWindowFilter { public void process(float[][] inBuf, float[][] outBuf, int inOff, int outOff, int len) throws IOException; } private static class StdDevFilter implements RunningWindowFilter { final int winSize; final int channels; final double[][] dcMem; final int winSizeM1; public StdDevFilter(int winSize, int channels) { this.winSize = winSize; this.channels = channels; winSizeM1 = winSize - 1; dcMem = new double[channels][2]; } public void process(float[][] inBuf, float[][] outBuf, int inOff, int outOff, int len) throws IOException { int ch, i, j, k, m, n; float[] convBuf2, convBuf3; double[] convBuf4; double d1, d2, mu, mus, omus, sum; for (ch = 0; ch < channels; ch++) { convBuf4 = dcMem[ch]; convBuf2 = inBuf[ch]; convBuf3 = outBuf[ch]; // calc first full window sum mus = 0.0; for( i = 0, m = inOff; i < winSizeM1; i++, m++ ) { mus += convBuf2[ m ]; // sum all but last one in window } omus = 0.0; for (j = 0, m = inOff, n = outOff; j < len; j++, m++, n++) { // shift by one : remove obsolete sample // and add new last window sample mus = mus - omus + convBuf2[m + winSizeM1]; mu = mus / winSize; // mean now sum = 0.0; for (i = 0, k = m; i < winSize; i++, k++) { d1 = convBuf2[k] - mu; sum += d1 * d1; // variance } d1 = Math.sqrt(sum); // standard deviation // ---- remove DC ---- d2 = d1 - convBuf4[0] + 0.99 * convBuf4[1]; convBuf3[n] = (float) d2; convBuf4[0] = d1; convBuf4[1] = d2; omus = convBuf2[m]; } } // for channels } // process } // class StdDevFilter private static class MinimumFilter implements RunningWindowFilter { final int winSize; final int channels; final int winSizeM1; public MinimumFilter(int winSize, int channels) { this.winSize = winSize; this.channels = channels; winSizeM1 = winSize - 1; } public void process(float[][] inBuf, float[][] outBuf, int inOff, int outOff, int len) throws IOException { int ch, i, j, k, m, n, minidx; float[] convBuf2, convBuf3; float f1, f2, min; for (ch = 0; ch < channels; ch++) { convBuf2 = inBuf[ch]; convBuf3 = outBuf[ch]; minidx = -1; min = 0.0f; for (j = 0, m = inOff, n = outOff; j < len; j++, m++, n++) { if (minidx < m) { // need to find again f1 = Math.abs(convBuf2[m]); minidx = m; for (i = 1, k = m + 1; i < winSize; i++, k++) { f2 = Math.abs(convBuf2[k]); if (f2 < f1) { f1 = f2; minidx = k; } } min = convBuf2[minidx]; } else { f1 = convBuf2[m + winSizeM1]; if (Math.abs(f1) < Math.abs(min)) { min = f1; minidx = m + winSizeM1; } } convBuf3[n] = min; minidx--; } } // for channels } // process } // class MinimumFilter private static class MedianFilter implements RunningWindowFilter { final int winSize, medianOff, winSizeM; final int channels; final float[][] buf; final int[][] idxBuf; protected MedianFilter(int winSize, int channels) { this.winSize = winSize; this.channels = channels; buf = new float[channels][winSize]; idxBuf = new int [channels][winSize]; medianOff = winSize >> 1; winSizeM = winSize - 1; } public void process(float[][] inBuf, float[][] outBuf, int inOff, int outOff, int len) throws IOException { int ch, i, j, k, m, n; float[] convBuf1, convBuf2, convBuf3; int[] convBuf4; float f1; for (ch = 0; ch < channels; ch++) { convBuf1 = buf[ch]; convBuf2 = inBuf[ch]; convBuf3 = outBuf[ch]; convBuf4 = idxBuf[ch]; m = inOff; n = outOff; convBuf1[0] = convBuf2[m++]; convBuf4[0] = 0; // --- calculate the initial median by sorting inBuf content of length 'winSize --- // XXX this is a really slow sorting algorithm and should be replaced by a fast one // e.g. by exchanging the j-loop by a step-algorithm (stepping right into // i/2 and if f1 < convBuf1[i/2] steppping to i/4 else i*3/4 etc. for (i = 1; i < winSize; i++) { f1 = convBuf2[m++]; for (j = 0; j < i; j++) { if (f1 < convBuf1[j]) { System.arraycopy(convBuf1, j, convBuf1, j + 1, i - j); for (k = 0; k < i; k++) { if (convBuf4[k] >= j) convBuf4[k]++; } break; } } convBuf1[j] = f1; convBuf4[i] = j; } // now the median is approx. (for winSize >> 1) the sample in convBuf1[winSize/2] //System.err.println( "A---unsorted---" ); //for( int p = 0; p < winSize; p++ ) { // System.err.println( p + " : "+convBuf2[inOff+p] ); //} //System.err.println( " --sorted---" ); //for( int p = 0; p < winSize; p++ ) { // System.err.println( p + " : "+convBuf1[p] ); //} // XXX this is a really slow sorting algorithm and should be replaced by a fast one // e.g. by exchanging the j-loop by a step-algorithm (stepping right into // i/2 and if f1 < convBuf1[i/2] steppping to i/4 else i*3/4 etc. // ; also the two arraycopies could be collapsed into one or two shorter ones for (i = 0; i < len; i++) { convBuf3[n++] = convBuf1[medianOff]; j = convBuf4[i % winSize]; // index of the element to be removed (i.e. shifted left out of the win) System.arraycopy(convBuf1, j + 1, convBuf1, j, winSizeM - j); for (k = 0; k < winSize; k++) { if (convBuf4[k] > j) convBuf4[k]--; } f1 = convBuf2[m++]; for (j = 0; j < winSizeM; j++) { if (f1 < convBuf1[j]) { System.arraycopy(convBuf1, j, convBuf1, j + 1, winSizeM - j); for (k = 0; k < winSize; k++) { if (convBuf4[k] >= j) convBuf4[k]++; } break; } } // j = index of the element to be inserted (i.e. coming from the right side of the win) convBuf1[j] = f1; convBuf4[i % winSize] = j; } //System.err.println( "B---unsorted---" ); //for( int p = 0; p < winSize; p++ ) { // System.err.println( p + " : "+convBuf2[inOff+len+p] ); //} //System.err.println( " ---sorted---" ); //for( int p = 0; p < winSize; p++ ) { // System.err.println( p + " : "+convBuf1[p] ); //} } // for channels } // process } // class MedianFilter // Center Clipping for a variable threshold // which is determined by a running histogram // and a percentage threshold value // // this only works if a) process() is // called on successive chunks; b) samples don't exceed +12 dBFS private static class CenterClippingFilter implements RunningWindowFilter { final int channels; final int winSizeM1; final int[][] histogram; final int threshSum; boolean init = false; public CenterClippingFilter(int winSize, int channels, double threshAmp) { this.channels = channels; winSizeM1 = winSize - 1; histogram = new int[channels][16384]; threshSum = (int) (threshAmp * winSize + 0.5); } public void process(float[][] inBuf, float[][] outBuf, int inOff, int outOff, int len) throws IOException { float[] convBuf2, convBuf3; int[] convBuf4; int histoIdx, histoSum; float f1, clip; for (int ch = 0; ch < channels; ch++) { convBuf4 = histogram[ch]; convBuf2 = inBuf[ch]; convBuf3 = outBuf[ch]; // calc first maximum // max = 0.0f; // for( int i = 0, m = inOff; i < len; i++, m++ ) { // f1 = Math.abs( convBuf2[ m ]); // if( f1 > max ) max = f1; // } // then calc initial histo // for( int i = 0; i < 8192; i++ ) { // convBuf4[ i ] = 0; // } if (!init) { for (int i = 0, j = inOff; i < winSizeM1; i++, j++) { f1 = convBuf2[j]; histoIdx = (int) (Math.sqrt(Math.min(1.0f, Math.abs(f1 / 4))) * 16383.5); // histoIdx = 8191 - (int) (Math.log( Math.max( 4.656613e-10, Math.min( 1.0f, Math.abs( f1 / 4)))) * -381.2437); convBuf4[histoIdx]++; } } for (int j = 0, m = inOff, n = outOff; j < len; j++, m++, n++) { // shift by one : remove obsolete sample // and add new last window sample f1 = convBuf2[m + winSizeM1]; histoIdx = (int) (Math.sqrt(Math.min(1.0f, Math.abs(f1 / 4))) * 16383.5); // histoIdx = 8191 - (int) (Math.log( Math.max( 4.656613e-10, Math.min( 1.0f, Math.abs( f1 / 4)))) * -381.2437); convBuf4[histoIdx]++; // find thresh for (histoIdx = 0, histoSum = 0; histoIdx < 8192 && histoSum < threshSum; histoIdx++) { histoSum += convBuf4[histoIdx]; } clip = (float) histoIdx / 16383; clip = clip*clip*4; // clip = (float) (Math.exp( (histoIdx - 8191) / 381.2437 ) * 4); f1 = convBuf2[m]; if (f1 >= 0.0f) { convBuf3[n] = Math.max(0.0f, f1 - clip); } else { convBuf3[n] = Math.min(0.0f, f1 + clip); } f1 = convBuf2[m]; // now obsolete histoIdx = (int) (Math.sqrt(Math.min(1.0f, Math.abs(f1 / 4))) * 16383.5); // histoIdx = 8191 - (int) (Math.log( Math.max( 4.656613e-10, Math.min( 1.0f, Math.abs( f1 / 4)))) * -381.2437); convBuf4[histoIdx]--; } } // for channels init = true; } // process } // class CenterClippingFilter }