/* * MixingFloatAudioInputStream * * This file is part of jsresources.org */ /* * Copyright (c) 2006 by Florian Bomers * Copyright (c) 1999 - 2001 by Matthias Pfisterer * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * - Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. */ /* |<--- this code is formatted to fit into 80 columns --->| */ import java.io.*; import java.util.*; import javax.sound.sampled.*; /* * This is a class of Tritonus, you need to have tritonus_share.jar in the * classpath. Get it from http://www.tritonus.org . */ import org.tritonus.share.sampled.FloatSampleBuffer; /** * Mixing of multiple AudioInputStreams to one AudioInputStream. This class * takes a collection of AudioInputStreams and mixes them together. Being a * subclass of AudioInputStream itself, reading from instances of this class * behaves as if the mixdown result of the input streams is read. * <p> * This class uses the FloatSampleBuffer for easy conversion using normalized * samples in the buffers. * * @author Florian Bomers * @author Matthias Pfisterer */ public class MixingFloatAudioInputStream extends AudioInputStream { private List audioInputStreamList; /** * Attenuate the stream by how many dB per mixed stream. For example, if * attenuationPerStream is 2dB, and 3 streams are mixed together, the mixed * stream will be attenuated by 6dB. Set to 0 to not attenuate the signal, * which will usually give good results if only 2 streams are mixed * together. */ private float attenuationPerStream = 0.1f; /** * The linear factor to apply to all samples (derived from * attenuationPerStream). This is a factor in the range of 0..1 (depending * on attenuationPerStream and the number of streams). */ private float attenuationFactor = 1.0f; private FloatSampleBuffer mixBuffer; private FloatSampleBuffer readBuffer; /** * A buffer for byte to float conversion. */ private byte[] tempBuffer; public MixingFloatAudioInputStream(AudioFormat audioFormat, Collection audioInputStreams) { super(new ByteArrayInputStream(new byte[0]), audioFormat, AudioSystem.NOT_SPECIFIED); audioInputStreamList = new ArrayList(audioInputStreams); // set up the static mix buffer with initially no samples. Note that // using a static mix buffer prevents that this class can be used at // once from different threads, but that wouldn't be useful anyway. But // by re-using this buffer we save a lot of garbage collection. mixBuffer = new FloatSampleBuffer(audioFormat.getChannels(), 0, audioFormat.getSampleRate()); // ditto for the read buffer. It is used for reading samples from the // underlying streams. readBuffer = new FloatSampleBuffer(); // calculate the linear attenuation factor attenuationFactor = decibel2linear(-1.0f * attenuationPerStream * audioInputStreamList.size()); } /** * The maximum of the frame length of the input stream is calculated and * returned. If at least one of the input streams has length * <code>AudioInputStream.NOT_SPECIFIED</code>, this value is returned. */ public long getFrameLength() { long lLengthInFrames = 0; Iterator streamIterator = audioInputStreamList.iterator(); while (streamIterator.hasNext()) { AudioInputStream stream = (AudioInputStream) streamIterator.next(); long lLength = stream.getFrameLength(); if (lLength == AudioSystem.NOT_SPECIFIED) { return AudioSystem.NOT_SPECIFIED; } else { lLengthInFrames = Math.max(lLengthInFrames, lLength); } } return lLengthInFrames; } public int read() throws IOException { byte[] samples = new byte[1]; int ret = read(samples); if (ret != 1) { return -1; } return samples[0]; } public int read(byte[] abData, int nOffset, int nLength) throws IOException { // set up the mix buffer with the requested size mixBuffer.changeSampleCount(nLength / getFormat().getFrameSize(), false); // initialize the mixBuffer with silence mixBuffer.makeSilence(); // remember the maximum number of samples actually mixed int maxMixed = 0; Iterator streamIterator = audioInputStreamList.iterator(); while (streamIterator.hasNext()) { AudioInputStream stream = (AudioInputStream) streamIterator.next(); // calculate how many bytes we need to read from this stream int needRead = mixBuffer.getSampleCount() * stream.getFormat().getFrameSize(); // set up the temporary byte buffer if (tempBuffer == null || tempBuffer.length < needRead) { tempBuffer = new byte[needRead]; } // read from the source stream int bytesRead = stream.read(tempBuffer, 0, needRead); if (bytesRead == -1) { // end of stream: remove it from the list of streams. streamIterator.remove(); continue; } // now convert this buffer to float samples readBuffer.initFromByteArray(tempBuffer, 0, bytesRead, stream.getFormat()); if (maxMixed < readBuffer.getSampleCount()) { maxMixed = readBuffer.getSampleCount(); } // the actual mixing routine: add readBuffer to mixBuffer // can only mix together as many channels as available int maxChannels = Math.min(mixBuffer.getChannelCount(), readBuffer.getChannelCount()); for (int channel = 0; channel < maxChannels; channel++) { // get the arrays of the normalized float samples float[] readSamples = readBuffer.getChannel(channel); float[] mixSamples = mixBuffer.getChannel(channel); // Never use readSamples.length or mixSamples.length: the length // of the array may be longer than the actual buffer ("lazy" // deletion). int maxSamples = Math.min(mixBuffer.getSampleCount(), readBuffer.getSampleCount()); // in a loop, add each "read" sample to the mix buffer // can only mix as many samples as available. Also apply the // attenuation factor. // Note1: the attenuation factor could also be applied only once // in a separate loop after mixing all the streams together, // saving processor time in case of many mixed streams. // Note2: adding everything together here will not cause // clipping, because all samples are in float format. for (int sample = 0; sample < maxSamples; sample++) { mixSamples[sample] += attenuationFactor * readSamples[sample]; } } } // loop over streams if (maxMixed == 0) { // nothing written to the mixBuffer if (audioInputStreamList.size() == 0) { // nothing mixed, no more streams available: end of stream return -1; } // nothing written, but still streams to read from return 0; } // finally convert the mix Buffer to the requested byte array. // This routine will handle clipping, i.e. if there are samples > 1.0f // in the mix buffer, they will be clipped to 1.0f and converted to the // specified audioFormat's sample format. mixBuffer.convertToByteArray(0, maxMixed, abData, nOffset, getFormat()); return maxMixed * getFormat().getFrameSize(); } /** * calls skip() on all input streams. There is no way to assure that the * number of bytes really skipped is the same for all input streams. Due to * that, this method always returns the passed value. In other words: the * return value is useless (better ideas appreciated). */ public long skip(long lLength) throws IOException { Iterator streamIterator = audioInputStreamList.iterator(); while (streamIterator.hasNext()) { AudioInputStream stream = (AudioInputStream) streamIterator.next(); stream.skip(lLength); } return lLength; } /** * The minimum of available() of all input stream is calculated and * returned. */ public int available() throws IOException { int nAvailable = 0; Iterator streamIterator = audioInputStreamList.iterator(); while (streamIterator.hasNext()) { AudioInputStream stream = (AudioInputStream) streamIterator.next(); nAvailable = Math.min(nAvailable, stream.available()); } return nAvailable; } public void close() throws IOException { // TODO: should we close all streams in the list? } /** * Calls mark() on all input streams. */ public void mark(int nReadLimit) { Iterator streamIterator = audioInputStreamList.iterator(); while (streamIterator.hasNext()) { AudioInputStream stream = (AudioInputStream) streamIterator.next(); stream.mark(nReadLimit); } } /** * Calls reset() on all input streams. */ public void reset() throws IOException { Iterator streamIterator = audioInputStreamList.iterator(); while (streamIterator.hasNext()) { AudioInputStream stream = (AudioInputStream) streamIterator.next(); stream.reset(); } } /** * returns true if all input stream return true for markSupported(). */ public boolean markSupported() { Iterator streamIterator = audioInputStreamList.iterator(); while (streamIterator.hasNext()) { AudioInputStream stream = (AudioInputStream) streamIterator.next(); if (!stream.markSupported()) { return false; } } return true; } public static float decibel2linear(float decibels) { return (float) Math.pow(10.0, decibels / 20.0); } } /** * MixingFloatAudioInputStream.java ** */