package audio.processing.dynamic; import audio.processing.tools.NormaliserEffect; /** * **************************************************************************** A Hard-Knee Expander Effect which allows * the user to lower the level of signal which is under a threshold set by the user. The attenuation is decided by the * 'expansion ratio', and the speed at which the signal is attenuated is decided by the attack and release times. An * expander is the opposite of a compressor. * * @author Eddie Al-Shakarchi - e.alshakarchi@cs.cf.ac.uk * @version $Revision: 4052 $ * @see Expander */ public class ExpanderEffect { float samplingFrequency; float thresholdLevel; // In (negative) Decibels double attackIncrement; double releaseIncrement; double currentExpansionRatio = 1.0; // start at 1.0 double thresholdValue; double expansionRatio; double gainLevel; // In Samples int attackTime; // In Samples int releaseTime; // In Samples int threshWidth; boolean autoGain; String detection; NormaliserEffect normalise = null; // Create buffer arrays short[] output; short[] outputData; /** * Creates an Expander effect * * @param threshold the threshold level in (negative) decibels. When the signal goes under this threshold, * expansion is applied * @param ratio the expansion ratio is the ratio of change of the input signal versus the change in the * output signal * @param attack the time taken before the expander kicks into action. In milliseconds. * @param release the time taken for the 'expansion' to stop after the signal falls below the threshold. In * milliseconds. * @param gain make up gain to boost the signal level after expansion * @param autoGain boolean value to indicate if the user wishes to automatically normalise the audio after * compression * @param detectionType multiple choice string input which chooses the type of choices by process method to choose * suitable method. Chooses between Peak, Region Average, Region Max, and RMS. * @param thresholdWidth the size of the window which is the 'region' being examined too be able to set the * threshold * @param sampleRate the sample rate of the incoming audio */ public ExpanderEffect(float threshold, double ratio, int attack, int release, double gain, boolean autoGain, String detectionType, int thresholdWidth, float sampleRate) { samplingFrequency = sampleRate; setThreshold(threshold); setRatio(ratio); setAttackTime(attack); setReleaseTime(release); setGain(gain); setDetectionType(detectionType); setThresholdWidth(thresholdWidth); setAutoGain(autoGain); } /** * Sets the threshold level in DECIBELS. 0dB is the nominal/maximum value, the threshold is therefore relative to this, * in negative decibels. This works for short (a maximum of 32767) * * @param thresh this is an integer value, which is converted to a decibel value, relative to the maximum short value * of 32767 */ public void setThreshold(float thresh) { thresholdLevel = (int) (Math.pow(10, (thresh / 20)) * 32767); } /** * Sets the compression ratio. * * @param rat this is compression ratio value. Any samples which are over the threshold level are divided by this * value, but first taking into account the attack/release times */ public void setRatio(double rat) { expansionRatio = rat; } /** * Sets the compressor's attack time in samples, after converting from milliseconds * * @param att this is the size of the attack 'delay'. This is used to set delay before the compression is applied, and * create the compression envelope */ public void setAttackTime(int att) { attackTime = ((int) (att * (float) samplingFrequency / (float) 1000)); } /** * Sets the compressors release time in samples, after converting from milliseconds * * @param rel this is the size of the release 'delay'. This is used before the compression is stopped being applied, * and creates the compression release envelope */ public void setReleaseTime(int rel) { releaseTime = ((int) (rel * (float) samplingFrequency / (float) 1000)); } /** * Sets the make up gain. After the signal is compressed, the audio may be quieter and so the user may wish to * increase the volume to compensate. This value is ignored if 'auto gain' is selected. * * @param vol this can be used to increase the volume of the signal, after compression. This is in decibels * (logarithmic). */ public void setGain(double vol) { gainLevel = (double) (Math.pow(10, (vol / 20))); } /** * Sets the type of threshold calculator that will be used (Peak, RMS, Region Average or Region Max) * * @param detect this is the type of threshold calculator which will be used. */ public void setDetectionType(String detect) { detection = detect; } /** * Sets the threshold width that will be used in samples, after converting from milliseconds. This is used when region * max, region average, and RMS threshold calculators are used. This is essentially a 'window' where only a small part * of the audio file is examined. If the user chooses 'Region Average' and sets a threshold width of 10ms, then the * average value of the samples in the regions' 'window' is taken * * @param tWidth this is the threshold width, in milliseconds. This method is used to convert tWidth to samples. */ public void setThresholdWidth(int tWidth) { threshWidth = ((int) (tWidth * (float) samplingFrequency / (float) 1000)); } /** * Selects if the signal will be normalised before outputting * * @param auto this is a boolean variable. This is used to to decide if the signal will be normalised to 100%, or if * the make up gain is used */ public void setAutoGain(boolean auto) { autoGain = auto; } /************************************ * The Three Threshold Calculators...* *************************************/ /** * Method to calculate the threshold when 'Regional Max' is selected. This calculates the threshold by finding the * maximum value in the window of the array which is being considered. The window size is set by the thresholdWidth * variable * * @param currentChunk the current short array or 'audio chunk' being considered. If chunking is not being used, * this will stay constant. * @param currentPosition the current position of the sample being considered in for loop in the process method. */ public short getRegionMax(short[] currentChunk, int currentPosition) { short max = 0; int startPos = currentPosition - (threshWidth / 2); int endPos = startPos + threshWidth; // This makes sure that the area being examined is not bigger than the chunk size if (endPos > currentChunk.length) { endPos = currentChunk.length; startPos = startPos - (endPos - currentChunk.length); } // This makes sure that for the start of the entire wave, that StartPos does not // give ArrayOutOfBounds at the beginning of the wave if (startPos < 0) { startPos = 0; } // For each sample in the little segment for (int i = startPos; i < endPos; i++) { if (Math.abs(currentChunk[i]) > Math.abs(max)) { max = currentChunk[i]; } } return max; } /** * Method to calculate the threshold when 'Regional Average' is selected. This calculates the threshold by finding * the average value in the window of the array which is being considered. This can help to give a more subtle and * natural compressor sound. The window size is set by the thresholdWidth variable * * @param currentChunk the current short array or 'audio chunk' being considered. If chunking is not being used, * this will stay constant. * @param currentPosition the current position of the sample being considered in for loop in the process method. */ public short getRegionAverage(short[] currentChunk, int currentPosition) { short temp = 0; short average; int startPos = currentPosition - (threshWidth / 2); int endPos = startPos + threshWidth; // This makes sure that the area being examined is not bigger than the chunk size if (endPos > currentChunk.length) { endPos = currentChunk.length; startPos = startPos - (endPos - currentChunk.length); } // This makes sure that for the start of the entire wave, that StartPos does not // give ArrayOutOfBounds at the beginning of the wave if (startPos < 0) { startPos = 0; } // For each sample in the little segment for (int i = startPos; i < endPos; ++i) { temp += (short) currentChunk[i]; } average = (short) (temp / currentChunk.length); return average; } /** * Method to calculate the threshold when 'Regional RMS' is selected. This calculates the threshold by finding the * value which is the 'Root Mean Square' in the window of the array which is being considered. This models human * hearing more accurately to human hearing. The window size is set by the thresholdWidth variable. * * @param currentChunk the current short array or 'audio chunk' being considered. If chunking is not being used, * this will stay constant. * @param currentPosition the current position of the sample being considered in for loop in the process method. */ public short getRegionRMS(short[] currentChunk, int currentPosition) { short temp = 0; short rms; int startPos = currentPosition - (threshWidth / 2); int endPos = startPos + threshWidth; // This makes sure that the area being examined is not bigger than the chunk size if (endPos > currentChunk.length) { endPos = currentChunk.length; startPos = startPos - (endPos - currentChunk.length); } // This makes sure that for the start of the entire wave, that StartPos does not // give ArrayOutOfBounds at the beginning of the wave if (startPos < 0) { startPos = 0; } // For each sample in the little segment for (int i = startPos; i < endPos; ++i) { temp += (short) currentChunk[i]; } rms = (short) (temp / currentChunk.length); return rms; } /** * Process method that compresses the audio. A threshold calculator is chosen and the attack and release envelopes are * created, to create a subtle compression effect. The values that are over the threshold are divided by the current * compression ratio, which is worked out depending on the attack/release times.Works in 32bit (ints) to avoid any * (unintentional) clipping from using shorts. * * @param input short array containing the input data to be manipulated by algorithm * @return a short array which has been manipulated. */ public short[] process(short input[]) { short[] output = new short[input.length]; int[] temp = new int[input.length]; int outputSample = 0; int finalSample = 0; int maxValue = 0; attackIncrement = (double) expansionRatio / (double) attackTime; releaseIncrement = (double) expansionRatio / (double) releaseTime; // // For each sample, find the maximum // for (int n = 0; n < input.length; n++) { // // outputSample = (int)(input[n]); // // if (Math.abs(outputSample) > Math.abs(maxValue)){ // maxValue = outputSample; // } // } // thresholdValue = maxValue * ((double)thresholdLevel); // System.out.println("ThresholdValue = " + thresholdValue); System.out.println("ThresholdLevel = " + thresholdLevel); // For each sample in the array... for (int n = 0; n < input.length; n++) { if (detection.equals("Peak")) { if (Math.abs(input[n]) < (int) thresholdLevel) //switch on { currentExpansionRatio = currentExpansionRatio + attackIncrement; } else { currentExpansionRatio = currentExpansionRatio - releaseIncrement; } } else if (detection.equals("RMS")) { if (getRegionRMS(input, n) < (int) thresholdLevel) //switch on { currentExpansionRatio = currentExpansionRatio + attackIncrement; } else { currentExpansionRatio = currentExpansionRatio - releaseIncrement; } } else if (detection.equals("Region Max")) { if (getRegionMax(input, n) < (int) thresholdLevel) //switch on { currentExpansionRatio = currentExpansionRatio + attackIncrement; } else { currentExpansionRatio = currentExpansionRatio - releaseIncrement; } } else { // If detection = "Region Average" if (getRegionAverage(input, n) < (int) thresholdLevel) //switch on { currentExpansionRatio = currentExpansionRatio + attackIncrement; } else { currentExpansionRatio = currentExpansionRatio - releaseIncrement; } } // Stops the Expansion ratio from going out of range if (currentExpansionRatio > expansionRatio) { currentExpansionRatio = expansionRatio; } // Stops the Expansion ratio from going out of range if (currentExpansionRatio < 1.0) { currentExpansionRatio = 1.0; } // Actually compress the values over the set threshold if (Math.abs(input[n]) < (int) thresholdLevel) { outputSample = (int) (input[n] / currentExpansionRatio); } else { outputSample = input[n]; } temp[n] = outputSample; } // End of main for loop // If autogain is not selected, then attentuate the signal level and limit // so that there is no signal overflow if (autoGain == false) { for (int n = 0; n < input.length; n++) { temp[n] = (int) (temp[n] * gainLevel); // Limits outputSample to max 16bit (short) value int limit = temp[n]; if (limit > 32767) { limit = 32767; } else if (limit < -32767) { limit = -32767; } // Turns int back into short value in output array after manipulation // and limiting output[n] = (short) limit; }// End of for loop return output; }// End of if // Post processing -> sorting out the normalisation. If autogain is chosen, // then the signal is normalised to 100% and the make up gain slider is ignored. // Otherwise, the signal is always passed throught the make up gain slider (above) else { short[] finalOutput = new short[output.length]; if (normalise == null){ normalise = new NormaliserEffect(100); } finalOutput = normalise.process(temp); return finalOutput; } } }