Mehrere sound byte arrays mixen um sie über eine SourceDataLine auszugeben

Hi ich schon wieder,

Jetzt versuche ich mehrere byte arrays zu mischen um sie über eine SourceDataLine abspielen zu können!
Es funktioniert auch einigermaßen mit folgendem Code:

	public static void playByteArrays(AudioFormat format, byte[]... byteArrays) throws Exception {
		AudioInputStream[] streams = new AudioInputStream[byteArrays.length];
		for(int i=0;i<byteArrays.length;i++)
			streams**  = new AudioInputStream(new ByteArrayInputStream(byteArrays**),format,byteArrays**.length/format.getFrameSize());
		playStreams(format, streams);
	}
	public static void playStreams(AudioFormat format, AudioInputStream... streams) throws Exception {
		SourceDataLine sourceDataLine = (SourceDataLine)AudioSystem.getLine(new DataLine.Info(SourceDataLine.class,format));
		sourceDataLine.open(format);
		sourceDataLine.start();
		
		boolean allStreamsFinished = false;
		int bufferSize = 4444;
		byte[][] buffers = new byte[streams.length][bufferSize];
		short[][] sampleBuffers = new short[streams.length][bufferSize/2];

		byte[] outBuffer = new byte[bufferSize];
		short[] sampleOutBuffer = new short[bufferSize/2];
		while(!allStreamsFinished) {
			//fill buffers and check if there are streams left running
			allStreamsFinished=true;
			for (int i=0;i<buffers.length;i++)
				if(streams**.read(buffers**,0,buffers**.length) > 0)
					allStreamsFinished = false;
			if (allStreamsFinished) break;

			//fill sample buffers
			for (int i=0;i<buffers.length;i++)
				ByteBuffer.wrap(buffers**).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(sampleBuffers**);

			//mix samples
			sampleOutBuffer = getMixedSamples(bufferSize/2, sampleBuffers);

			//fill outBuffer
            ByteBuffer.wrap(outBuffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(sampleOutBuffer);

            sourceDataLine.write(outBuffer, 0, bufferSize);
		}

		sourceDataLine.drain();
	    sourceDataLine.stop();
	    sourceDataLine.close();
	}

Sowie der folgenden Funktion für das Mixen:

	public static short[] getMixedSamples(int sampleSize, short[]... sampleBuffers) {
		short[] outSamples = new short[sampleSize];

        for (int s = 0; s < outSamples.length; s++) {
        	short computedVal = 0;
        	for (short[] sampleBuffer:sampleBuffers)
        		if (sampleBuffer.length>0)
        			computedVal+=sampleBuffer[s];
        	//reduziert die Lautstärke um die Anzahl der Channels ändert aber nichts an der Welle
//            outSamples[s] = (short) (computedVal/sampleBuffers.length);
            outSamples[s] = computedVal;
        }

		return outSamples;
	}```

Das Problem sind gelegentliche Klick, Klack und andere Störgeräusche!
Ich hab sehr viel rumgegoogelt und folgende sehr spannende Website gefunden:
[Mixing and normalizing digital audio](http://www.voegler.eu/pub/audio/digital-audio-mixing-and-normalization.html)

Dort steht etwas von einem Logarithmus und auch genau wie der aussieht, aber ich habe keine Ahnung wie ich den jetzt in Java Code konvertieren kann!

Wäre nett wenn mir jemand dabei oder mit einem Anstoß helfen könnte =)

Danke


EDIT:
Ausprobieren könnt ihr die Funktion einfach so:
public static void main(final String[] args)  {

File f1 = …
File f2 = …
AudioInputStream sourceAIS_1 = AudioSystem.getAudioInputStream(f1 );
AudioFormat sourceFormat_1 = sourceAIS_1.getFormat();
AudioInputStream convert1AIS_1 = AudioSystem.getAudioInputStream(SOUND_UTIL.getConvertFormat(sourceFormat_1), sourceAIS_1);
AudioInputStream audioInputStream_1 = new AudioInputStream(convert1AIS_1, convert1AIS_1.getFormat(), convert1AIS_1.getFrameLength());
byte[] sound1 = UTIL.toByteArray(audioInputStream_1);

		AudioInputStream sourceAIS_2 = AudioSystem.getAudioInputStream(f2);
	    AudioFormat sourceFormat_2 = sourceAIS_2.getFormat();
	    AudioInputStream convert1AIS_2 = AudioSystem.getAudioInputStream(SOUND_UTIL.getConvertFormat(sourceFormat_2), sourceAIS_2);
	    AudioInputStream audioInputStream_2 = new AudioInputStream(convert1AIS_2, convert1AIS_2.getFormat(), convert1AIS_2.getFrameLength());
		byte[] sound2 = UTIL.toByteArray(audioInputStream_2);

		playByteArrays(convert1AIS_1.getFormat(), sound1, sound2);

}

public static AudioFormat getConvertFormat(AudioFormat sourceFormat) {
	return new AudioFormat(AudioFormat.Encoding.PCM_SIGNED, sourceFormat.getSampleRate(), 16, sourceFormat.getChannels(), sourceFormat.getChannels()*2, sourceFormat.getSampleRate(), false);
}

Hat jemand im neuen Jahr vielleicht noch eine Idee für mein Problem?

Frohes Neues Nebenbei! :slight_smile:

Nichts konkretes, sorry (wieder was wo @Dow_Jones was sagen könnte…). Die Seite ist schon hübsch aufgemacht, aber die komplett durchzuarbeiten und alles richtig einzuordnen könnte aufwändig sein. Da kommen einige Formeln mit “Log” vor, kann man konkreter sagen, um welche es geht (“6.2 Logarithmic Dynamic Range Compression”, http://www.voegler.eu/pub/audio/images/math/audio_math_fl.png ?) und wie der “Stub” assieht, mit dem sie implementiert werden soll?

void magicMix(ShortBuffer s0, ShortBuffer s1, ShortBuffer target)  { ... }

?

Was auf der verlinkten Seite steht hat schon Hand und Fuß, aber leider schweigt sich der Autor darüber aus inwiefern seine Lösung (für mehrere Kanäle) vernünftig klingt. -_-

Die Umsetzung seiner Formel in Java kann eigentlich ersteinmal ohne Umschweife geschehen:

    /**
     * 
     * @param sum
     *  Summe der PCM-Samples der einzelnen Kanäle, 
     *  normiert auf -1*channelCount bis +1*channelCount
     * 
     * @param channelCount
     *  Anzahl der Kanäle
     * 
     * @param alpha
     * 
     * @param threshold
     * 
     * @return 
     *  resultierendes, normiertes PCM-Sample (-1 bis +1)
     */
    private static double normalizeSum(
            double sum, 
            int channelCount, 
            double alpha, 
            double threshold)
    {
        double sign = sum >= 0 ? 1 : -1;
        double absolutValue = sum * sign;
        double result;
        
        if( absolutValue > threshold ) {
            double l1 = Math.log( 1d + alpha * (absolutValue-threshold) / (channelCount-threshold) );
            double l2 = Math.log( 1d + alpha );
            result = l1 / l2;
            result *= 1d - threshold;
            result += threshold;
            result *= sign;
        } else {
            result = sum;
        }
        
        return result;
    }

Zu beachten:

  • es wird davon ausgegegangen das die einzelnen Kanäle PCM-Werte zwischen -1.0 und +1.0 liefern; die Summe (sum) der PCM-Samples muss, bei channelCount vielen Kanälen, dementsprechend zwischen -channelCount und +channelCount liegen
  • den threshold kann man frei wählen, der Autor verwendet gerne den Wert 0.6
  • alpha wird gemäß dem threshold (und der Anzahl der Kanäle) errechnet. Auf der Webseite kann man sich leider nur die alphas bei 2 Kanälen berechnen lassen, wenn man mehr braucht muss man wohl seinen eigenen Kürbis verschleißen.

Bei konstantem threshold = 0.6 und alpha = 7.48 schaut das Resultat für 1-3 Kanäle so aus (türkis: Summe der PCM-Samples, blau: mit der obigen Methode kompandiertes Signal):

Man sieht glaube ich deutlich das die Parameter für 2 Kanäle hin optimiert wurden. Mit zunehmender Kanalzahl wird der Knick beim Thresholdwert 0,6 immer deutlicher. Da empfiehlt es sich wohl dringend bei mehr Kanälen zumindest den threshold herunterzusetzen, am besten aber auch das Alpha für die Anzahl der Kanäle entsprechend zu berechnen.

Das Ganze als Hörprobe (1.6 mb MP3 bei Share Online): Rock’n Roll Overdose/The Hawaiians
Sekunden 0-10: Original Musikschnipsel
Sekunden 10-20: als 1 Kanal gemixt
Sekunden 20-30: als 2 Kanäle gemixt
Sekunden 30-40: als 3 Kanäle gemixt
Sekunden 40-50: als 4 Kanäle gemixt
Sekunden 50-60: als 5 Kanäle gemixt
Sekunden 60-70: nochmal das Original
Für die Hörprobe wurde ein 10 Sekunden langer Musikschnipsel verwendet (PCM, Mono, 16 Bit, 44.100 Hz) und mit der obigen Methode in Java „gemixt“. Threshold war dabei konstant 0,6, alpha war konstant 7,48. Das Original Musikstück wurde dabei auf allen n Kanälen eingespeist - was sicherlich einen Extremfall darstellt. Die Güte der Ausgabe mag jeder selber bewerten. Ich persönlich denke aber das man mal eine Weile herumprobieren sollte um geeignete thresholds und alphas zu bestimmen.

PS:

Das ist nicht überraschend. Wenn die Summen den Wertebereich verlassen werden in deiner Implementierung die oberen Bits einfach abgeschnitten (Überlaufarithmethik), wodurch das Signal schon ziemlich gestört werden kann. Daher arbeitet man so einer Stelle gerne mit Sättigungsarithmetik.
Überlaufarithmetik: f(x) = x & 0xffff
Sättigungsarithmetik: f(x) = ( x>0xffff ? 0xffff : x ) bzw. ( x < 0 ? 0 : x )

OK

Vielen Dank für deine Mühe schonmal!

Nur noch eine Frage:

Wie konvertiere ich die double Zahlen nun in short Zahlen?

Was ich kriege ist:


normalizeSum: -2.1580779726031696
normalizeSum: -1.9523101272610623
normalizeSum: 1.9523101272610623
normalizeSum: 1.9523101272610623
normalizeSum: 1.9523101272610623
normalizeSum: 0.0
normalizeSum: 2.0821589958589715
normalizeSum: 2.0821589958589715
normalizeSum: 2.1580779726031696
normalizeSum: 2.1580779726031696

Du musst die Werte vor dem Aufruf der Funktion entsprechend ihrem Wertebereich auf das Intervall ±channelCount herunterskalieren und das Ergebnis anschließend wieder in den gewünschten Wertebereich heraufskalieren. Bei 16 Bit signed PCM-Werten:

	double sum = ...

	// Normalisierung auf das Intervall 
	// -channelCount bis +channelCount
	// per Division durch 32768
	sum /= 0x8000;

	// Ergebnis liegt im Bereich -1 bis +1
	double result = normalize(sum, channelCount, 7.48d, 0.6d);

	// Multiplikation mit 32768 macht daraus 
	// wieder 16 Bit signed Werte
	result *= 0x8000;

	short newPcmSample = (short) result;

Funktioniert :slight_smile:

Danke für deine Hilfe!

Hier für andere nochmal die komplete Funktion:

	public static void playByteArrays(AudioFormat format, byte[]... byteArrays) throws Exception {
		AudioInputStream[] streams = new AudioInputStream[byteArrays.length];
		for(int i=0;i<byteArrays.length;i++)
			streams**  = new AudioInputStream(new ByteArrayInputStream(byteArrays**),format,byteArrays**.length/format.getFrameSize());
		playStreams(format, streams);
	}
	public static void playStreams(AudioFormat format, AudioInputStream... streams) throws Exception {
		SourceDataLine sourceDataLine = (SourceDataLine)AudioSystem.getLine(new DataLine.Info(SourceDataLine.class,format));
		sourceDataLine.open(format);
		sourceDataLine.start();

		boolean allStreamsFinished = false;
		int bufferSize = 4444;
		byte[][] buffers = new byte[streams.length][bufferSize];
		short[][] sampleBuffers = new short[streams.length][bufferSize/2];

		byte[] outBuffer = new byte[bufferSize];
		short[] sampleOutBuffer = new short[bufferSize/2];
		while(!allStreamsFinished) {
			//fill buffers and check if there are streams left running
			allStreamsFinished=true;
			for (int i=0;i<buffers.length;i++)
				if(streams**.read(buffers**,0,buffers**.length) > 0)
					allStreamsFinished = false;
			if (allStreamsFinished) break;

			//fill sample buffers
			for (int i=0;i<buffers.length;i++)
				ByteBuffer.wrap(buffers**).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().get(sampleBuffers**);

			//mix samples
			sampleOutBuffer = getMixedSamples(bufferSize/2, sampleBuffers);

			//fill outBuffer
            ByteBuffer.wrap(outBuffer).order(ByteOrder.LITTLE_ENDIAN).asShortBuffer().put(sampleOutBuffer);

            sourceDataLine.write(outBuffer, 0, bufferSize);
		}

		sourceDataLine.drain();
	    sourceDataLine.stop();
	    sourceDataLine.close();
	}
	public static short[] getMixedSamples(int sampleSize, short[]... sampleBuffers) {
		short[] outSamples = new short[sampleSize];

        for (int s = 0; s < outSamples.length; s++) {
        	double computedVal = 0;
        	for (short[] sampleBuffer:sampleBuffers)
        		if (sampleBuffer.length>0)
        			computedVal+=sampleBuffer[s];
        	computedVal /= 0x8000;
        	double normalized = normalizeSum(computedVal, sampleBuffers.length, 7.48d, 0.6d);
        	computedVal = normalized*0x8000;
            outSamples[s] = (short) computedVal;
        }

		return outSamples;
	}
	private static double normalizeSum(double sum,int channelCount,double alpha,double threshold) {
		double sign = sum >= 0 ? 1 : -1;
        double absolutValue = sum * sign;
        double result;

        if( absolutValue > threshold ) {
            double l1 = Math.log( 1d + alpha * (absolutValue-threshold) / (channelCount-threshold) );
            double l2 = Math.log( 1d + alpha );
            result = l1 / l2;
            result *= 1d - threshold;
            result += threshold;
            result *= sign;
        } else {
            result = sum;
        }

        return result;
    }```