如何使用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位:
(请注意, 二进制补码表示中的最正值比最负值小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
变量中,我们:
- 按位AND与掩码
0xFF
(即0b1111_1111
)的每个byte
,以避免在自动提升byte
时的符号扩展 。 (当对它们执行算术时,char
,byte
和short
被提升为int
。)另请参阅Java中的value & 0xff
什么作用? - 将每个
byte
移位到位。 - 按位或
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次顺序。
这也可以推广到循环,这可以在本答案底部的完整代码中看到。 (请参阅unpackAnyBit
和packAnyBit
方法。)
现在我们将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)保持不变。 这是算术右移的一个很好的特性。
然后将样本标准化,如某些假设中所述 。
如果您的代码很简单,则可能不需要编写显式符号扩展名
当从一个整数类型转换为更大的类型(例如byte
到int
时,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#intBitsToFloat
和Double#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.ULAW
和Encoding.ALAW
?
这些是压缩压缩编解码器,在电话等中更常见。 它们受javax.sound.sampled
支持我认为是因为它们被Sun的Au格式使用 。 (但是,它不仅限于这种类型的容器。例如,WAV可以包含这些编码。)
您可以将A-law和μ-law概念化,就像它们是浮点格式一样。 这些是PCM格式,但值的范围是非线性的。
解码它们有两种方法。 我将展示使用数学公式的方式。 您也可以通过直接操作二进制文件来解码它们,这在本博客文章中有所描述,但它更加深奥。
对于两者,压缩数据是8位。 标准A-law在解码时为13位,μ-law在解码时为14位; 然而,应用该公式产生±1.0
的范围。
在应用公式之前,有三件事要做:
- 由于涉及数据完整性的原因,一些比特被标准地反转用于存储。
- 它们被存储为符号和幅度(而不是两个补码)。
- 该公式还需要
±1.0
的范围,因此必须缩放8位值。
对于μ律, 所有位都被反转,因此:
temp ^= 0xffL; // 0xff == 0b1111_1111
(注意我们不能使用~
,因为我们不想反转long
的高位。)
对于A律, 每隔一位都是反转的,所以:
temp ^= 0x55L; // 0x55 == 0b0101_0101
(XOR可用于进行反转。请参阅如何设置,清除和切换? )
要将符号和幅度转换为二进制补码,我们:
- 检查符号位是否已设置。
- 如果是,请清除符号位并取消该数字。
// 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(); } }