package ch.retorte.intervalmusiccompositor.util; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioInputStream; import javax.sound.sampled.AudioSystem; import ch.retorte.intervalmusiccompositor.spi.audio.ByteArrayConverter; import org.tritonus.dsp.ais.AmplitudeAudioInputStream; import org.tritonus.sampled.convert.SampleRateConversionProvider; import ch.retorte.intervalmusiccompositor.messagebus.DebugMessage; import ch.retorte.intervalmusiccompositor.spi.audio.AudioStandardizer; import ch.retorte.intervalmusiccompositor.spi.messagebus.MessageProducer; /** * Some static methods for easing the handling of sound data. * * @author nw */ public class SoundHelper implements AudioStandardizer, ByteArrayConverter { private MessageProducer messageProducer; public SoundHelper(MessageProducer messageProducer) { this.messageProducer = messageProducer; } private void addDebugMessage(String message) { messageProducer.send(new DebugMessage(this, message)); } @Override public byte[] convert(AudioInputStream audioInputStream) throws IOException { return getByteArray(audioInputStream); } /** * Determines the largest average frame value of a audio stream. Uses a 16 * bytes window as smallest unit. May be expanded through the windows * argument. * * @param inputBuffer * The to be inspected audio {@link AudioInputStream} buffer * @param sampleWindow * How many single samples are to be averaged * @return The largest average of the stream * @throws IOException if there was trouble */ public int getAvgAmplitude(AudioInputStream inputBuffer, int sampleWindow) throws IOException { int sample; int sampleWindowCounter = 0; long sampleWindowSum = 0; int maxSample = 0; int bytesPerFrame = inputBuffer.getFormat().getFrameSize(); // Add factor 4 to speed up int numberOfBytes = 2 * bytesPerFrame * 8; byte[] audioBytes = new byte[numberOfBytes]; int numberOfBytesRead; while ((numberOfBytesRead = inputBuffer.read(audioBytes)) != -1) { if (numberOfBytesRead == numberOfBytes) { // Here we do a roll out to speed up the whole thing sample = Math.abs((audioBytes[0] & 0xFF) | (audioBytes[1] << 8)); sample += Math.abs((audioBytes[2] & 0xFF) | (audioBytes[3] << 8)); sample += Math.abs((audioBytes[4] & 0xFF) | (audioBytes[5] << 8)); sample += Math.abs((audioBytes[6] & 0xFF) | (audioBytes[7] << 8)); sample += Math.abs((audioBytes[8] & 0xFF) | (audioBytes[9] << 8)); sample += Math.abs((audioBytes[10] & 0xFF) | (audioBytes[11] << 8)); sample += Math.abs((audioBytes[12] & 0xFF) | (audioBytes[13] << 8)); sample += Math.abs((audioBytes[14] & 0xFF) | (audioBytes[15] << 8)); sample = (sample / 8); } else { // If there is not enough data anymore we don't bother // and discard it sample = 0; } if (sampleWindowCounter < sampleWindow) { sampleWindowSum += sample; sampleWindowCounter++; } else { if (maxSample <= (sampleWindowSum / sampleWindow)) { maxSample = (int) (sampleWindowSum / sampleWindow); } sampleWindowCounter = 0; sampleWindowSum = 0; } } return maxSample; } public AudioInputStream getLeveledStream(AudioInputStream audioInputStream, float desiredRelativeAmplitude) { AmplitudeAudioInputStream amplitudeAudioInputStream = new AmplitudeAudioInputStream(audioInputStream); amplitudeAudioInputStream.setAmplitudeLinear(desiredRelativeAmplitude); return amplitudeAudioInputStream; } public AudioInputStream getStreamFromInputStream(InputStream inputStream, long streamSize) { return new AudioInputStream(inputStream, TARGET_AUDIO_FORMAT, (streamSize / TARGET_AUDIO_FORMAT.getFrameSize())); } public byte[] getStreamPart(AudioInputStream inputStream, Long startTimeMS, Long durationMS) throws IOException { // First skip to the desired position long bytesToSkip = Math.round((startTimeMS / 1000) * inputStream.getFormat().getSampleRate() * inputStream.getFormat().getFrameSize()); long skippedBytes = inputStream.skip(bytesToSkip); if (bytesToSkip != skippedBytes) { addDebugMessage("Tried to skip " + bytesToSkip + " bytes but only skipped " + skippedBytes + " bytes."); } byte[] result = new byte[Math.round((durationMS / 1000) * inputStream.getFormat().getSampleRate() * inputStream.getFormat().getFrameSize())]; // Choose a buffer of 100 KB byte[] audioBytes = new byte[102400]; int numberOfBytesRead; int totalNumberOfBytesRead = 0; while ((numberOfBytesRead = inputStream.read(audioBytes)) != -1) { if (result.length <= totalNumberOfBytesRead + numberOfBytesRead) { // Write down the last bytes System.arraycopy(audioBytes, 0, result, totalNumberOfBytesRead, result.length - totalNumberOfBytesRead); // Interrupt break; } System.arraycopy(audioBytes, 0, result, totalNumberOfBytesRead, numberOfBytesRead); totalNumberOfBytesRead += numberOfBytesRead; } return result; } private byte[] getByteArray(AudioInputStream inputStream) throws IOException { byte[] result = new byte[(int) getStreamSizeInBytes(inputStream)]; // Choose a buffer of 100 KB byte[] buffer = new byte[102400]; int len; int writtenBytes = 0; while ((len = inputStream.read(buffer)) > 0) { System.arraycopy(buffer, 0, result, writtenBytes, len); writtenBytes += len; } return result; } public double getStreamLengthInSeconds(AudioInputStream inputStream) { return (inputStream.getFrameLength() / inputStream.getFormat().getFrameRate()); } private long getStreamSizeInBytes(AudioInputStream inputStream) { // Calculate length of wav input stream AudioFormat af = inputStream.getFormat(); long frameLength = inputStream.getFrameLength(); long byteLength; int frameSize = af.getFrameSize(); byteLength = frameLength * frameSize; return byteLength; } public byte[] generateSilenceOfLength(double lengthInSeconds) { int samples = getSamplesFromSeconds(lengthInSeconds); byte[] silenceBuffer = new byte[samples]; for (int i = 0; i < silenceBuffer.length; i = i + 2) { silenceBuffer[i] = (byte) 0x80; silenceBuffer[i + 1] = (byte) 0x00; } return silenceBuffer; } public int getSamplesFromSeconds(double seconds) { return (int) (seconds * SAMPLE_RATE * TARGET_AUDIO_FORMAT.getFrameSize()); } public double getSecondsFromSamples(int samples) { return samples / SAMPLE_RATE / TARGET_AUDIO_FORMAT.getFrameSize(); } public AudioInputStream getStreamExtract(AudioInputStream ais, int start, int length) { long startMs = start * 1000; long durationMs = length * 1000; AudioInputStream result = null; try { long streamLengthMs = (long) (getStreamLengthInSeconds(ais) * 1000); if (streamLengthMs < startMs + durationMs) { if (streamLengthMs < durationMs) { startMs = 0; durationMs = (int) streamLengthMs; } else { startMs = (int) ((streamLengthMs - durationMs) / 2); } } byte[] streamExtract = getStreamPart(ais, startMs, durationMs); int extractFrameLength = (int) (TARGET_AUDIO_FORMAT.getSampleRate() * TARGET_AUDIO_FORMAT.getFrameSize() * (durationMs / 1000)); result = new AudioInputStream(new ByteArrayInputStream(streamExtract), TARGET_AUDIO_FORMAT, extractFrameLength); } catch (IOException e) { // nop } return result; } public byte[] linearBlend(byte[] sampleByteArray, double blendTime) { // Since the fading caused clicking noises in the beginning and the end // of the fading process (where the sound intensity exceeded the limit) // we enlarge the fade duration by 0.05 seconds. int clickPreventDuration = 8820; // Determine how many samples there are to alter // There are 176400 bytes per second int corrLength = sampleByteArray.length - 1; int samples = (int) ((TARGET_AUDIO_FORMAT.getSampleRate() * TARGET_AUDIO_FORMAT.getFrameSize() * blendTime) + clickPreventDuration); if (((double) sampleByteArray.length / 2) < samples) { if (clickPreventDuration <= ((double) sampleByteArray.length / 2)) { samples = (int) ((double) sampleByteArray.length / 2) + clickPreventDuration; } else { samples = (int) ((double) sampleByteArray.length / 2); } } int valueFront; int valueEnd; for (int i = 0; i < samples; i = i + 2) { // Determine effective value of each two bytes valueFront = (sampleByteArray[i] & 0xFF) | (sampleByteArray[i + 1] << 8); valueEnd = (sampleByteArray[corrLength - i - 1] & 0xFF) | (sampleByteArray[corrLength - i] << 8); // Adapt value valueFront = (int) (valueFront * ((double) i / (double) samples)); valueEnd = (int) (valueEnd * ((double) i / (double) samples)); // Write it back into array sampleByteArray[i] = (byte) (valueFront & 0xFF); sampleByteArray[i + 1] = (byte) (valueFront >> 8); sampleByteArray[corrLength - i - 1] = (byte) (valueEnd & 0xFF); sampleByteArray[corrLength - i] = (byte) (valueEnd >> 8); } return sampleByteArray; } /* * The code below is taken from * http://www.jsresources.org/examples/AudioConverter.java.html */ @Override public AudioInputStream standardize(AudioInputStream stream) { addDebugMessage("Original format: " + stream.getFormat()); if (stream.getFormat().getChannels() != TARGET_AUDIO_FORMAT.getChannels()) { stream = convertChannels(TARGET_AUDIO_FORMAT.getChannels(), stream); } boolean bDoConvertSampleSize = (stream.getFormat().getSampleSizeInBits() != TARGET_AUDIO_FORMAT.getSampleSizeInBits()); boolean bDoConvertEndianness = (stream.getFormat().isBigEndian() != TARGET_AUDIO_FORMAT.isBigEndian()); if (bDoConvertSampleSize || bDoConvertEndianness) { stream = convertSampleSizeAndEndianness(TARGET_AUDIO_FORMAT.getSampleSizeInBits(), TARGET_AUDIO_FORMAT.isBigEndian(), stream); } if (!equals(stream.getFormat().getSampleRate(), TARGET_AUDIO_FORMAT.getSampleRate())) { stream = convertSampleRate(TARGET_AUDIO_FORMAT.getSampleRate(), stream); } addDebugMessage("Converted format: " + stream.getFormat()); return stream; } private AudioInputStream convertChannels(int nChannels, AudioInputStream sourceStream) { AudioFormat sourceFormat = sourceStream.getFormat(); AudioFormat targetFormat = new AudioFormat(sourceFormat.getEncoding(), sourceFormat.getSampleRate(), sourceFormat.getSampleSizeInBits(), nChannels, calculateFrameSize(nChannels, sourceFormat.getSampleSizeInBits()), sourceFormat.getFrameRate(), sourceFormat.isBigEndian()); return AudioSystem.getAudioInputStream(targetFormat, sourceStream); } private AudioInputStream convertSampleSizeAndEndianness(int nSampleSizeInBits, boolean bBigEndian, AudioInputStream sourceStream) { AudioFormat sourceFormat = sourceStream.getFormat(); AudioFormat targetFormat = new AudioFormat(sourceFormat.getEncoding(), sourceFormat.getSampleRate(), nSampleSizeInBits, sourceFormat.getChannels(), calculateFrameSize(sourceFormat.getChannels(), nSampleSizeInBits), sourceFormat.getFrameRate(), bBigEndian); return AudioSystem.getAudioInputStream(targetFormat, sourceStream); } private AudioInputStream convertSampleRate(float fSampleRate, AudioInputStream sourceStream) { AudioFormat sourceFormat = sourceStream.getFormat(); AudioFormat targetFormat = new AudioFormat(sourceFormat.getEncoding(), fSampleRate, sourceFormat.getSampleSizeInBits(), sourceFormat.getChannels(), sourceFormat.getFrameSize(), fSampleRate, sourceFormat.isBigEndian()); SampleRateConversionProvider sampleRateConversionProvider = new SampleRateConversionProvider(); if (sampleRateConversionProvider.isConversionSupported(targetFormat, sourceFormat)) { return sampleRateConversionProvider.getAudioInputStream(targetFormat, sourceStream); } return sourceStream; } private int calculateFrameSize(int nChannels, int nSampleSizeInBits) { return ((nSampleSizeInBits + 7) / 8) * nChannels; } private boolean equals(float f1, float f2) { return (Math.abs(f1 - f2) < 1E-9F); } }