如何使用Java Sound中的音频样本数据?

这个问题通常被问到是另一个问题的一部分,但事实certificate答案很长。 我决定在这里回答它,所以我可以在其他地方链接到它。

虽然我不知道Java此时可以为我们生成音频样本的方式,但如果将来发生变化,这可能是一个适合它的地方。 我知道JavaFX有这样的东西,例如AudioSpectrumListener ,但仍然不能直接访问样本。


我正在使用javax.sound.sampled进行播放和/或录制,但我想对音频做些什么。

也许我想在视觉上展示它或以某种方式处理它。

如何使用Java Sound访问音频样本数据?

也可以看看:

  • Java声音教程 (官方)
  • Java声音资源 (非官方)

嗯,最简单的答案是,目前Java无法为程序员生成样本数据。

这句话来自官方教程 :

有两种方法可以应用信号处理:

  • 您可以使用混音器或其组件行支持的任何处理,方法是查询Control对象,然后根据用户需要设置控件。 混音器和线路支持的典型控制包括增益,声像和混响控制。

  • 如果混音器或其线路不提供您需要的处理类型,您的程序可以直接在音频字节上操作,根据需要进行操作。

本页更详细地讨论了第一种技术,因为第二种技术没有特殊的API

使用javax.sound.sampled回放主要充当文件和音频设备之间的桥梁。 从文件中读取字节并发送。

不要假设字节是有意义的音频样本! 除非您碰巧有8位AIFF文件,否则它们不是。 (另一方面,如果样本肯定是8位符号,你可以用它们算术。使用8位是避免这里描述的复杂性的一种方法,如果你只是玩游戏。)

因此,我将枚举AudioFormat.Encoding的类型并描述如何自己解码它们。 这个答案将包括如何对它们进行编码,但它包含在底部的完整代码示例中。 编码主要是反向解码过程。

这是一个很长的答案,但我想给出一个全面的概述。


关于数字音频的一点点

通常,当解释数字音频时,我们指的是线性脉冲编码调制 (LPCM)。

以规则的间隔对连续声波进行采样,并将幅度量化为某种比例的整数。

这里显示的是一个正弦波采样并量化为4位:

lpcm_graph

(请注意, 二进制补码表示中的最正值比最负值小1。这是一个需要注意的小细节。例如,如果你剪切音频并忘记这一点,正片段就会溢出。)

当我们在计算机上有音频时,我们有一系列这些样本。 我们想要将byte数组转换为样本数组。

为了解码PCM样本,我们并不关心采样率或通道数,所以我不会在这里说太多。 通道通常是交错的,因此如果我们有一个数组,它们将被存储如下:

 Index 0: Sample 0 (Left Channel) Index 1: Sample 0 (Right Channel) Index 2: Sample 1 (Left Channel) Index 3: Sample 1 (Right Channel) Index 4: Sample 2 (Left Channel) Index 5: Sample 2 (Right Channel) ... 

换句话说,对于立体声,arrays中的样本只是在左右之间交替。


一些假设

所有代码示例都将采用以下声明:

  • byte[] bytes; byte数组,从AudioInputStream读取。
  • float[] samples; 我们要填充的输出样本数组。
  • float sample; 我们目前正在研究的样本。
  • long temp; 用于一般操作的临时值。
  • int i; byte数组中当前样本数据开始的位置。

我们将float[]数组中的所有样本标准化为-1f <= sample <= 1f 。 我见过的所有浮点音频都是这样的,非常方便。

如果我们的源音频不是那样(例如整数样本),我们可以使用以下方法自我标准化:

 sample = sample / fullScale(bitsPerSample); 

其中fullScale是2 bitsPerSample - 1 ,即Math.pow(2, bitsPerSample-1)


如何将byte数组强制转换为有意义的数据?

byte数组包含分割的样本帧,并且全部在一行中。 这实际上非常简单,除了名为endianness的东西,它是每个样本包中byte s的排序。

这是一张图。 此示例(打包到byte数组中)保存十进制值9999:

 作为big-endian的24位样本:

  bytes [i] bytes [i + 1] bytes [i + 2]
  ┌─────────────────────
  00000000 00100111 00001111

  24位样本为little-endian:

  bytes [i] bytes [i + 1] bytes [i + 2]
  ┌─────────────────────
  00001111 00100111 00000000 

它们具有相同的二进制值; 但是, byte顺序是相反的。

  • 在big-endian中,更重要的byte s出现在不太重要的byte s之前。
  • 在little-endian中,较不重要的byte在更重要的bytes之前出现。

WAV文件以little-endian顺序存储, AIFF文件以big-endian顺序存储。 可以从AudioFormat.isBigEndian获得字节AudioFormat.isBigEndian

要连接byte并将它们放入我们的long temp变量中,我们:

  1. 按位AND与掩码0xFF (即0b1111_1111 )的每个byte ,以避免在自动提升byte时的符号扩展 。 (当对它们执行算术时, charbyteshort被提升为int 。)另请参阅Java中的value & 0xff什么作用?
  2. 将每个byte移位到位。
  3. 按位或byte s在一起。

这是一个24位的例子:

 long temp; if (isBigEndian) { temp = ( ((bytes[i ] & 0xffL) << 16) | ((bytes[i + 1] & 0xffL) << 8) | (bytes[i + 2] & 0xffL) ); } else { temp = ( (bytes[i ] & 0xffL) | ((bytes[i + 1] & 0xffL) << 8) | ((bytes[i + 2] & 0xffL) << 16) ); } 

请注意,基于字节顺序颠倒了class次顺序。

这也可以推广到循环,这可以在本答案底部的完整代码中看到。 (请参阅unpackAnyBitpackAnyBit方法。)

现在我们将byte连接在一起,我们可以采取更多步骤将它们转换为样本。 接下来的步骤取决于实际编码。

如何解码Encoding.PCM_SIGNED

必须扩展两个补码。 这意味着如果最高有效位(MSB)设置为1,我们用1s填充它上面的所有位。 如果设置了符号位,算术右移( >> )将自动为我们填充,所以我通常这样做:

 int bitsToExtend = Long.SIZE - bitsPerSample; float sample = (temp << bitsToExtend) >> bitsToExtend. 

(其中Long.SIZE是64.如果我们的temp变量不long ,我们会使用其他东西。如果我们使用例如int temp ,我们将使用32.)

要了解其工作原理,下面是符号扩展8位到16位的示意图:

  11111111是字节值-1,但是short的高位是0。
 将字节的MSB移入短路的MSB位置。

  0000 0000 1111 1111
  << 8
  ───────────────────
  1111 1111 0000 0000

 将其向后移动,右移用1s填充所有高位。
 我们现在的短值为-1。

  1111 1111 0000 0000
  >> 8
  ───────────────────
  1111 1111 1111 1111 

正值(MSB中为0)保持不变。 这是算术右移的一个很好的特性。

然后将样本标准化,如某些假设中所述

如果您的代码很简单,则可能不需要编写显式符号扩展名

当从一个整数类型转换为更大的类型(例如byteint时,Java会自动进行符号扩展。 如果您知道输入和输出格式始终是已签名的,则可以在前一步骤中连接字节时使用自动符号扩展。

回想一下上面的部分( 我如何将字节数组强制转换为有意义的数据? ),我们使用b & 0xFF来防止符号扩展的发生。 如果您只从最高byte删除& 0xFF ,则会自动进行符号扩展。

例如,以下解码签名的big-endian 16位样本:

 for (int i = 0; i < bytes.length; i++) { int sample = (bytes[i] << 8) // high byte is sign-extended | (bytes[i + 1] & 0xFF); // low byte is not // ... } 

如何解码Encoding.PCM_UNSIGNED

我们将其转为签名号码。 无符号样本只是偏移,例如:

  • 无符号值0对应于最负的有符号值。
  • 无符号值2 bitsPerSample - 1对应于有符号值0。
  • 无符号值2 bitsPerSample对应于最正的有符号值。

事实certificate这很简单。 只需减去偏移量:

 float sample = temp - fullScale(bitsPerSample); 

然后将样本标准化,如某些假设中所述

如何解码Encoding.PCM_FLOAT

这是Java 7以来的新特性。

实际上,浮点PCM通常是IEEE 32位或IEEE 64位,并且已经标准化到±1.0的范围。 可以使用实用程序方法Float#intBitsToFloatDouble#longBitsToDouble

 // IEEE 32-bit float sample = Float.intBitsToFloat((int) temp); 
 // IEEE 64-bit double sampleAsDouble = Double.longBitsToDouble(temp); float sample = (float) sampleAsDouble; // or just use double for arithmetic 

如何解码Encoding.ULAWEncoding.ALAW

这些是压缩压缩编解码器,在电话等中更常见。 它们受javax.sound.sampled支持我认为是因为它们被Sun的Au格式使用 。 (但是,它不仅限于这种类型的容器。例如,WAV可以包含这些编码。)

您可以将A-law和μ-law概念化,就像它们是浮点格式一样。 这些是PCM格式,但值的范围是非线性的。

解码它们有两种方法。 我将展示使用数学公式的方式。 您也可以通过直接操作二进制文件来解码它们,这在本博客文章中有所描述,但它更加深奥。

对于两者,压缩数据是8位。 标准A-law在解码时为13位,μ-law在解码时为14位; 然而,应用该公式产生±1.0的范围。

在应用公式之前,有三件事要做:

  1. 由于涉及数据完整性的原因,一些比特被标准地反转用于存储。
  2. 它们被存储为符号和幅度(而不是两个补码)。
  3. 该公式还需要±1.0的范围,因此必须缩放8位值。

对于μ律, 所有位都被反转,因此:

 temp ^= 0xffL; // 0xff == 0b1111_1111 

(注意我们不能使用~ ,因为我们不想反转long的高位。)

对于A律, 每隔一位都是反转的,所以:

 temp ^= 0x55L; // 0x55 == 0b0101_0101 

(XOR可用于进行反转。请参阅如何设置,清除和切换?

要将符号和幅度转换为二进制补码,我们:

  1. 检查符号位是否已设置。
  2. 如果是,请清除符号位并取消该数字。
 // 0x80 == 0b1000_0000 if ((temp & 0x80L) != 0) { temp ^= 0x80L; temp = -temp; } 

然后按照某些假设中描述的相同方式缩放编码数字:

 sample = temp / fullScale(8); 

现在我们可以应用扩展。

然后,转换为Java的μ律公式为:

 sample = (float) ( signum(sample) * (1.0 / 255.0) * (pow(256.0, abs(sample)) - 1.0) ); 

那么翻译成Java的A律公式就是:

 float signum = signum(sample); sample = abs(sample); if (sample < (1.0 / (1.0 + log(87.7)))) { sample = (float) ( sample * ((1.0 + log(87.7)) / 87.7) ); } else { sample = (float) ( exp((sample * (1.0 + log(87.7))) - 1.0) / 87.7 ); } sample = signum * sample; 

这是SimpleAudioConversion类的完整示例代码。

 package mcve.audio; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioFormat.Encoding; import static java.lang.Math.*; /** * 

Performs simple audio format conversion.

* *

Example usage:

* *
{@code AudioInputStream ais = ... ; * SourceDataLine line = ... ; * AudioFormat fmt = ... ; * * // do setup * * for (int blen = 0; (blen = ais.read(bytes)) > -1;) { * int slen; * slen = SimpleAudioConversion.decode(bytes, samples, blen, fmt); * * // do something with samples * * blen = SimpleAudioConversion.encode(samples, bytes, slen, fmt); * line.write(bytes, 0, blen); * }}

* * @author Radiodef * @see Overview on Stack Overflow */ public final class SimpleAudioConversion { private SimpleAudioConversion() {} /** * Converts from a byte array to an audio sample float array. * * @param bytes the byte array, filled by the AudioInputStream * @param samples an array to fill up with audio samples * @param blen the return value of AudioInputStream.read * @param fmt the source AudioFormat * * @return the number of valid audio samples converted * * @throws NullPointerException if bytes, samples or fmt is null * @throws ArrayIndexOutOfBoundsException * if bytes.length is less than blen or * if samples.length is less than blen / bytesPerSample(fmt.getSampleSizeInBits()) */ public static int decode(byte[] bytes, float[] samples, int blen, AudioFormat fmt) { int bitsPerSample = fmt.getSampleSizeInBits(); int bytesPerSample = bytesPerSample(bitsPerSample); boolean isBigEndian = fmt.isBigEndian(); Encoding encoding = fmt.getEncoding(); double fullScale = fullScale(bitsPerSample); int i = 0; int s = 0; while (i < blen) { long temp = unpackBits(bytes, i, isBigEndian, bytesPerSample); float sample = 0f; if (encoding == Encoding.PCM_SIGNED) { temp = extendSign(temp, bitsPerSample); sample = (float) (temp / fullScale); } else if (encoding == Encoding.PCM_UNSIGNED) { temp = unsignedToSigned(temp, bitsPerSample); sample = (float) (temp / fullScale); } else if (encoding == Encoding.PCM_FLOAT) { if (bitsPerSample == 32) { sample = Float.intBitsToFloat((int) temp); } else if (bitsPerSample == 64) { sample = (float) Double.longBitsToDouble(temp); } } else if (encoding == Encoding.ULAW) { sample = bitsToMuLaw(temp); } else if (encoding == Encoding.ALAW) { sample = bitsToALaw(temp); } samples[s] = sample; i += bytesPerSample; s++; } return s; } /** * Converts from an audio sample float array to a byte array. * * @param samples an array of audio samples to encode * @param bytes an array to fill up with bytes * @param slen the return value of the decode method * @param fmt the destination AudioFormat * * @return the number of valid bytes converted * * @throws NullPointerException if samples, bytes or fmt is null * @throws ArrayIndexOutOfBoundsException * if samples.length is less than slen or * if bytes.length is less than slen * bytesPerSample(fmt.getSampleSizeInBits()) */ public static int encode(float[] samples, byte[] bytes, int slen, AudioFormat fmt) { int bitsPerSample = fmt.getSampleSizeInBits(); int bytesPerSample = bytesPerSample(bitsPerSample); boolean isBigEndian = fmt.isBigEndian(); Encoding encoding = fmt.getEncoding(); double fullScale = fullScale(bitsPerSample); int i = 0; int s = 0; while (s < slen) { float sample = samples[s]; long temp = 0L; if (encoding == Encoding.PCM_SIGNED) { temp = (long) (sample * fullScale); } else if (encoding == Encoding.PCM_UNSIGNED) { temp = (long) (sample * fullScale); temp = signedToUnsigned(temp, bitsPerSample); } else if (encoding == Encoding.PCM_FLOAT) { if (bitsPerSample == 32) { temp = Float.floatToRawIntBits(sample); } else if (bitsPerSample == 64) { temp = Double.doubleToRawLongBits(sample); } } else if (encoding == Encoding.ULAW) { temp = muLawToBits(sample); } else if (encoding == Encoding.ALAW) { temp = aLawToBits(sample); } packBits(bytes, i, temp, isBigEndian, bytesPerSample); i += bytesPerSample; s++; } return i; } /** * Computes the block-aligned bytes per sample of the audio format, * using Math.ceil(bitsPerSample / 8.0). *

* Round towards the ceiling because formats that allow bit depths * in non-integral multiples of 8 typically pad up to the nearest * integral multiple of 8. So for example, a 31-bit AIFF file will * actually store 32-bit blocks. * * @param bitsPerSample the return value of AudioFormat.getSampleSizeInBits * @return The block-aligned bytes per sample of the audio format. */ public static int bytesPerSample(int bitsPerSample) { return (int) ceil(bitsPerSample / 8.0); // optimization: ((bitsPerSample + 7) >>> 3) } /** * Computes the largest magnitude representable by the audio format, * using Math.pow(2.0, bitsPerSample - 1). Note that for two's complement * audio, the largest positive value is one less than the return value of * this method. *

* The result is returned as a double because in the case that * bitsPerSample is 64, a long would overflow. * * @param bitsPerSample the return value of AudioFormat.getBitsPerSample * @return the largest magnitude representable by the audio format */ public static double fullScale(int bitsPerSample) { return pow(2.0, bitsPerSample - 1); // optimization: (1L << (bitsPerSample - 1)) } private static long unpackBits(byte[] bytes, int i, boolean isBigEndian, int bytesPerSample) { switch (bytesPerSample) { case 1: return unpack8Bit(bytes, i); case 2: return unpack16Bit(bytes, i, isBigEndian); case 3: return unpack24Bit(bytes, i, isBigEndian); default: return unpackAnyBit(bytes, i, isBigEndian, bytesPerSample); } } private static long unpack8Bit(byte[] bytes, int i) { return bytes[i] & 0xffL; } private static long unpack16Bit(byte[] bytes, int i, boolean isBigEndian) { if (isBigEndian) { return ( ((bytes[i ] & 0xffL) << 8) | (bytes[i + 1] & 0xffL) ); } else { return ( (bytes[i ] & 0xffL) | ((bytes[i + 1] & 0xffL) << 8) ); } } private static long unpack24Bit(byte[] bytes, int i, boolean isBigEndian) { if (isBigEndian) { return ( ((bytes[i ] & 0xffL) << 16) | ((bytes[i + 1] & 0xffL) << 8) | (bytes[i + 2] & 0xffL) ); } else { return ( (bytes[i ] & 0xffL) | ((bytes[i + 1] & 0xffL) << 8) | ((bytes[i + 2] & 0xffL) << 16) ); } } private static long unpackAnyBit(byte[] bytes, int i, boolean isBigEndian, int bytesPerSample) { long temp = 0; if (isBigEndian) { for (int b = 0; b < bytesPerSample; b++) { temp |= (bytes[i + b] & 0xffL) << ( 8 * (bytesPerSample - b - 1) ); } } else { for (int b = 0; b < bytesPerSample; b++) { temp |= (bytes[i + b] & 0xffL) << (8 * b); } } return temp; } private static void packBits(byte[] bytes, int i, long temp, boolean isBigEndian, int bytesPerSample) { switch (bytesPerSample) { case 1: pack8Bit(bytes, i, temp); break; case 2: pack16Bit(bytes, i, temp, isBigEndian); break; case 3: pack24Bit(bytes, i, temp, isBigEndian); break; default: packAnyBit(bytes, i, temp, isBigEndian, bytesPerSample); break; } } private static void pack8Bit(byte[] bytes, int i, long temp) { bytes[i] = (byte) (temp & 0xffL); } private static void pack16Bit(byte[] bytes, int i, long temp, boolean isBigEndian) { if (isBigEndian) { bytes[i ] = (byte) ((temp >>> 8) & 0xffL); bytes[i + 1] = (byte) ( temp & 0xffL); } else { bytes[i ] = (byte) ( temp & 0xffL); bytes[i + 1] = (byte) ((temp >>> 8) & 0xffL); } } private static void pack24Bit(byte[] bytes, int i, long temp, boolean isBigEndian) { if (isBigEndian) { bytes[i ] = (byte) ((temp >>> 16) & 0xffL); bytes[i + 1] = (byte) ((temp >>> 8) & 0xffL); bytes[i + 2] = (byte) ( temp & 0xffL); } else { bytes[i ] = (byte) ( temp & 0xffL); bytes[i + 1] = (byte) ((temp >>> 8) & 0xffL); bytes[i + 2] = (byte) ((temp >>> 16) & 0xffL); } } private static void packAnyBit(byte[] bytes, int i, long temp, boolean isBigEndian, int bytesPerSample) { if (isBigEndian) { for (int b = 0; b < bytesPerSample; b++) { bytes[i + b] = (byte) ( (temp >>> (8 * (bytesPerSample - b - 1))) & 0xffL ); } } else { for (int b = 0; b < bytesPerSample; b++) { bytes[i + b] = (byte) ((temp >>> (8 * b)) & 0xffL); } } } private static long extendSign(long temp, int bitsPerSample) { int bitsToExtend = Long.SIZE - bitsPerSample; return (temp << bitsToExtend) >> bitsToExtend; } private static long unsignedToSigned(long temp, int bitsPerSample) { return temp - (long) fullScale(bitsPerSample); } private static long signedToUnsigned(long temp, int bitsPerSample) { return temp + (long) fullScale(bitsPerSample); } // mu-law constant private static final double MU = 255.0; // A-law constant private static final double A = 87.7; // natural logarithm of A private static final double LN_A = log(A); private static float bitsToMuLaw(long temp) { temp ^= 0xffL; if ((temp & 0x80L) != 0) { temp = -(temp ^ 0x80L); } float sample = (float) (temp / fullScale(8)); return (float) ( signum(sample) * (1.0 / MU) * (pow(1.0 + MU, abs(sample)) - 1.0) ); } private static long muLawToBits(float sample) { double sign = signum(sample); sample = abs(sample); sample = (float) ( sign * (log(1.0 + (MU * sample)) / log(1.0 + MU)) ); long temp = (long) (sample * fullScale(8)); if (temp < 0) { temp = -temp ^ 0x80L; } return temp ^ 0xffL; } private static float bitsToALaw(long temp) { temp ^= 0x55L; if ((temp & 0x80L) != 0) { temp = -(temp ^ 0x80L); } float sample = (float) (temp / fullScale(8)); float sign = signum(sample); sample = abs(sample); if (sample < (1.0 / (1.0 + LN_A))) { sample = (float) (sample * ((1.0 + LN_A) / A)); } else { sample = (float) (exp((sample * (1.0 + LN_A)) - 1.0) / A); } return sign * sample; } private static long aLawToBits(float sample) { double sign = signum(sample); sample = abs(sample); if (sample < (1.0 / A)) { sample = (float) ((A * sample) / (1.0 + LN_A)); } else { sample = (float) ((1.0 + log(A * sample)) / (1.0 + LN_A)); } sample *= sign; long temp = (long) (sample * fullScale(8)); if (temp < 0) { temp = -temp ^ 0x80L; } return temp ^ 0x55L; } }

这是从当前播放声音中获取实际样本数据的方法。 另一个优秀的答案将告诉您数据的含义。 没有在我的Windows 10机器YMMV上的其他操作系统上试过它。 对我来说,它拉动了当前的系统默认录制设备。 在Windows上将其设置为“Stereo Mix”而不是“Microphone”以获得播放声音。 您可能必须切换“显示已禁用的设备”以查看“立体声混音”。

 import javax.sound.sampled.*; public class SampleAudio { private static long extendSign(long temp, int bitsPerSample) { int extensionBits = 64 - bitsPerSample; return (temp << extensionBits) >> extensionBits; } public static void main(String[] args) throws LineUnavailableException { float sampleRate = 8000; int sampleSizeBits = 16; int numChannels = 1; // Mono AudioFormat format = new AudioFormat(sampleRate, sampleSizeBits, numChannels, true, true); TargetDataLine tdl = AudioSystem.getTargetDataLine(format); tdl.open(format); tdl.start(); if (!tdl.isOpen()) { System.exit(1); } byte[] data = new byte[(int)sampleRate*10]; int read = tdl.read(data, 0, (int)sampleRate*10); if (read > 0) { for (int i = 0; i < read-1; i = i + 2) { long val = ((data[i] & 0xffL) << 8L) | (data[i + 1] & 0xffL); long valf = extendSign(val, 16); System.out.println(i + "\t" + valf); } } tdl.close(); } }