package jass.generators; import jass.engine.*; import java.io.*; import java.util.*; /** UG that maintains a list of sources and QuenchableModalObjectWithOneContact's and estimates excitations and turns off inaudible modes according to a masking analysis. @author Kees van den Doel (kvdoel@cs.ubc.ca) */ public class ModalQuencher extends InOut { /** This is the level in dB SPL of loudest mode (as hear by listener) Should be estimated with a mike, if possible. Here we just assume 60 dB as a "reference" level. */ protected float dbLevelLoudestMode = 60; /** Masking curve offset (how high about masker) */ protected float av = 20; /** maximum excitation occurred */ protected float maximumExdBThatOccurred = -100000; /** Sampling rate in Hertz. */ public float srate; /** Registered QuenchableModalObjectWithOneContact's */ private Vector<QuenchableModalObjectWithOneContact> modalObjectsContainer; /** Registered QuenchableModalObjectWithOneContact's */ protected QuenchableModalObjectWithOneContact[] mobs; /** Estimated excitations of inputs to the above QuenchableModalObjectWithOneContact */ protected float[] sourceExcitations; /** Modal data array */ protected ModeData[] modeData; /** How many frames to skip before quenching again */ protected int nFramesToSkip = 1; private int nMobs = 0; /** For monitoring purpose */ protected int nKilledModes = 0; protected int nTotalModes = 0; /** @return number of modes that where pruned */ public int getNKilledModes() { return nKilledModes; } /** @return total number of modes */ public int getTotalModes() { return nTotalModes; } /** Constructor. @param bufferSize internal buffersize @param nFramesToSkip how many frames to skip before quenching again */ public ModalQuencher(int bufferSize, int nFramesToSkip) { super(bufferSize); setFramesToSkip(nFramesToSkip); modalObjectsContainer = new Vector<QuenchableModalObjectWithOneContact>(); } /** To be called after all sources and mobs have been added */ public void init() { mobs = new QuenchableModalObjectWithOneContact[nMobs]; for(int i=0;i<nMobs;i++) { mobs[i] = modalObjectsContainer.get(i); } makeModeDataStructure(); } /** Set masking curve height. @param val offset in dB (20 is safe, 1 is agressive) */ public void setAv(float val) { av = val; } /** Set level in dB SPL at observer of loudest mode. This will be the loudest value during a run. @param val loudest mode at loudest moment in dB */ public void setDbLevelLoudestMode(float val) { dbLevelLoudestMode = val; } /** Reset level */ public void resetLevel() { maximumExdBThatOccurred = 0; } /** How many frames (buffers) to skip before recomputing modal quenching @param n nFramesToSkip */ public void setFramesToSkip(int n) { nFramesToSkip = n; } /** When this is called, all sources have already been queried (available through getSources(). Estimate excitations from sources for each QuenchableModalObjectWithOneContact, enter them in ModeData[], then turn on/off appropriate modes. */ protected void computeBuffer() { if(getTime()%nFramesToSkip == 0) { doQuenching(); } } /** Sort estimate excitations, sort ModeData[], quench */ protected void doQuenching() { estimateExcitations(); sortModeData(); quench(); } // estimate levels of inputs private void getSourceLevels() { Object[] arrayOfSources = getSources(); for(int is=0;is<nMobs;is++) { sourceExcitations[is] = 0; Out currentSource = (Out) arrayOfSources[is]; float[] sourceBuffer = currentSource.peekAtBuffer(); for(int i=0;i<bufferSize;i++) { sourceExcitations[is] += sourceBuffer[i] * sourceBuffer[i]; } // System.out.println("exc["+is+"] = "+ sourceExcitations[is]); } } /** Estimate exitations. Load protected float[] sourceExcitations with sum_i sourceBuf^2[i], for the source buffers. */ protected void estimateExcitations() { getSourceLevels(); int totalnmodes = modeData.length; int point = 0; // works only for 1 location for(int i=0;i<totalnmodes;i++) { ModeData md = modeData[i]; QuenchableModalObjectWithOneContact qmob = md.parent; int indexOfModeInQMOB = md.indexInParent; float currentExcitation = qmob.getModeExcitation(indexOfModeInQMOB); float a_mode = qmob.modalModel.a[point][indexOfModeInQMOB]; float sourceExcitation = a_mode * sourceExcitations[md.indexOfModalObject]; // set excitation md.ex = currentExcitation + sourceExcitation; md.exdB = BarkScale.decibel(md.ex); md.exMOBdB = BarkScale.decibel(currentExcitation); // use exMOB as we use this to set the masker level if(md.exMOBdB > maximumExdBThatOccurred) { maximumExdBThatOccurred = md.exdB; //System.out.println("max level = " + maximumExdBThatOccurred); } } } /** */ protected void quench() { MaskingCurve.av = this.av; // first set all flag to on int ntotalmodes = modeData.length; for(int i=0;i<ntotalmodes;i++) { modeData[i].on = true; //float lev = modeData[i].exdB+ dbLevelLoudestMode - maximumExdBThatOccurred; //System.out.println("ex["+i+"] = " + lev); } // add this to all excitations to normalize absolute levels to the // actual level (which may be true or "reference"). I.e., we // pretend maximumExdBThatOccurred = dbLevelLoudestMode float addToEx = dbLevelLoudestMode - maximumExdBThatOccurred; // for every mode, starting with loudest kill all modes it masks. If lies below threshold kill self and continue for(int i=0;i<ntotalmodes-1;i++) { if(modeData[i].on == true) { // skip off ones float levelMasker = modeData[i].exMOBdB + addToEx; if((modeData[i].exdB + addToEx) < modeData[i].ath) { modeData[i].on = false; } else { float bMasker = modeData[i].b; for(int k=i+1;k<ntotalmodes;k++) { if(modeData[k].on == true) { // skip off ones float levelMasked = modeData[k].exdB + addToEx; if(levelMasked < modeData[i].ath) { modeData[k].on = false; } else { float bMaskee = modeData[k].b; if(levelMasked < MaskingCurve.masker(bMaskee,bMasker,levelMasker)) { modeData[k].on = false; } } } } } } } // loaded ModeData[], now kill modes int nkilled=0; for(int i=0;i<ntotalmodes;i++) { modeData[i].parent.setOnBit(modeData[i].indexInParent,modeData[i].on); if(!modeData[i].on) { nkilled++; } } double perc = 100*(ntotalmodes-nkilled)/((double)ntotalmodes); //System.out.println("killed: "+nkilled +"/"+ntotalmodes+" using: "+perc+"%"); this.nKilledModes = nkilled; this.nTotalModes = ntotalmodes; } /** Add QuenchableModalObjectWithOneContact to Sink. @param s QuenchableModalObjectWithOneContact to add. @return object representing Source in Sink (may be null). */ public synchronized void addModalObject(QuenchableModalObjectWithOneContact s) { modalObjectsContainer.addElement(s); nMobs++; } /** Get array of QuenchableModalObjectWithOneContact's @return array of QuenchableModalObjectWithOneContact's, null if there are none. */ public QuenchableModalObjectWithOneContact[] getModalObjects() { return modalObjectsContainer.toArray(new QuenchableModalObjectWithOneContact[0]); } protected void sortModeData() { QSort.sort(modeData); } /** Make data structure (to be called after all Sources and QuenchableModalObjectWithOneContact's have been added). Also allocate the array of excitations. Each source is attached to 1 ModalObject. */ private void makeModeDataStructure() { int nmodes = 0; for(int i=0;i<mobs.length;i++) { nmodes += mobs[i].modalModel.nfUsed; } modeData = new ModeData[nmodes]; for(int i =0;i<nmodes;i++) { modeData[i] = new ModeData(); } sourceExcitations = new float[mobs.length]; int imode = 0; for(int i=0;i<mobs.length;i++) { sourceExcitations[i] = 0; ModalModel mm = mobs[i].modalModel; int nfInHere = mm.nfUsed; for(int k=0;k<nfInHere;k++) { modeData[imode].f = mm.f[k] * mm.fscale; modeData[imode].b = BarkScale.bark(mm.f[k]); modeData[imode].d = mm.d[k] * mm.dscale; modeData[imode].a = mm.a[0][k] * mm.ascale; if(modeData[imode].d != 0) { modeData[imode].a2_d = modeData[imode].a * modeData[imode].a / modeData[imode].d; } else { modeData[imode].a2_d = 0; } modeData[imode].ath = 0; // simple model for this for now modeData[imode].ex = 0; modeData[imode].exdB = BarkScale.decibel(modeData[imode].ex); modeData[imode].exMOBdB = BarkScale.decibel(0); modeData[imode].parent = mobs[i]; modeData[imode].indexOfModalObject = i; modeData[imode].indexInParent = k; modeData[imode].on = true; imode++; } } } } class ModeData implements Comparable { float f; // freq in Hz float b; //freq in Barks float d; // damping in 1/s float a; // gain float a2_d; // a^2/d float ath; // absolute threshold for this mode (in terms of y^2, y audio signal) float ex; // current excitation (energy) float exdB; // current excitation in dB (10log_10(energy)) float exMOBdB; // current excitation of ModalObject only (not including source) in dB (10log_10(energy)) QuenchableModalObjectWithOneContact parent; // ModalObject it belongs to int indexOfModalObject; // 0,.., n-1 for n objects, refers to order in patch int indexInParent; // index of mode in parent boolean on; // true if this mode is on, false otherwise public int compareTo(Object o) { ModeData m = (ModeData)o; if(this.ex < m.ex) { return 1; } else if(this.ex > m.ex) { return -1; // this.ex == m.ex: } else if(this.a2_d < m.a2_d) { return 1; } else if(this.a2_d > m.a2_d) { return -1; // this.a2_d == m.a2_d } else if(this.f < m.f) { return 1; } else if(this.f > m.f) { return -1; } else { return 0; } } } // IDEA: since excitation is overestimated, may have 2 much masking, so correct for this // by using only the ACTUAL excitation in ModalObject to set masker, but use SUM // for maskees /** Defines masking curve for source at given freq. in Barks. */ class MaskingCurve { static private final float sl = 25; // lower slope; constant /** magic number defining level of masker. 20dB suggested by literature, experimentally 65dB is OK */ static public float av = 5; /** float lm = level in dB SPL of source */ static private final float su(float lm) { return (float)(22 - lm/5); // upper slope } /** Masking curve (function of maskee bMaskee in Barks) for masker of level lm (dB SPL) and Bark bMasker. @param bMaskee freq. in Barks of masked freq. @param bMasker freq. in Barks of masking freq. @param lm dB SPL loudness of masker @return dB SPL value of masking curve */ public static final float masker(float bMaskee, float bMasker, float lm) { if(bMaskee < bMasker) { return (float)(lm - av + sl * (bMaskee - bMasker)); } else { return (float)(lm - av + su(lm) * (bMasker - bMaskee)); } } } /** Convert to and from Barkscale Use: Schroeder (1977): bark = 7*asinh(f/650); */ class BarkScale { static private final double FACTOR = 10/Math.log(10); static private final double MIN = 1.e-10; static private final double asinh(double x) { return Math.log(Math.sqrt(x*x+1) + x); } static private final double sinh(double x) { return (Math.exp(x) - Math.exp(-x))/2; } static public final float bark(float hertz) { return (float)(7*asinh((float)(hertz/650))); } static public final float hertz(float bark) { return (float)(650 * sinh(bark/7)); } static public final float decibel(float energy) { return (float)(FACTOR * Math.log(Math.max(MIN,Math.abs(energy)))); } } class QSort { static final public void sort(ModeData[] list) { // System.out.println("sorting "+list.length +" items"); quicksort(list, 0, list.length-1); } static final private void quicksort(ModeData[] list, int p, int r) { if (p < r) { int q = partition(list,p,r); if(q == r) { q--; } quicksort(list,p,q); quicksort(list,q+1,r); } } static final private int partition (ModeData[] list, int p, int r) { ModeData pivot = list[p]; int lo = p; int hi = r; while (true) { while (list[hi].compareTo(pivot) >= 0 && lo < hi) { hi--; } while (list[lo].compareTo(pivot) < 0 && lo < hi) { lo++; } if (lo < hi) { ModeData T = list[lo]; list[lo] = list[hi]; list[hi] = T; } else { return hi; } } } }