如何使用 Java Sound 中的音频样本数据?
Posted
技术标签:
【中文标题】如何使用 Java Sound 中的音频样本数据?【英文标题】:How do I use audio sample data from Java Sound? 【发布时间】:2015-01-05 15:03:09 【问题描述】:这个问题通常作为另一个问题的一部分被问到,但事实证明答案很长。我决定在这里回答它,以便我可以在其他地方链接到它。
虽然我目前不知道 Java 可以为我们生成音频样本的方式,但如果将来发生变化,这可能是它的一个地方。我知道JavaFX
有一些类似的东西,例如AudiospectrumListener
,但仍然不是直接访问示例的方法。
我正在使用javax.sound.sampled
进行播放和/或录制,但我想对音频做点什么。
也许我想直观地显示它或以某种方式对其进行处理。
如何使用 Java Sound 访问音频样本数据?
另见:
Java Sound Tutorials(官方) Java Sound Resources(非官方)【问题讨论】:
【参考方案1】:嗯,最简单的答案是,目前 Java 无法为程序员生成示例数据。
This quote is from the official tutorial:
信号处理有两种应用方式:
通过查询
Control
对象,然后根据用户的需要设置控件,您可以使用混音器或其组件线支持的任何处理。混音器和线路支持的典型控件包括增益、声像和混响控件。如果混音器或其线路未提供您需要的处理类型,您的程序可以直接对音频字节进行操作,并根据需要对其进行操作。
本页更详细地讨论了第一种技术,因为没有针对第二种技术的特殊 API。
使用javax.sound.sampled
播放很大程度上充当文件和音频设备之间的桥梁。从文件中读取字节并发送出去。
不要假设字节是有意义的音频样本!除非您碰巧有一个 8 位 AIFF 文件,否则它们不是。 (另一方面,如果样本 肯定是 8 位有符号的,那么您可以对它们进行算术运算。使用 8 位是避免此处描述的复杂性的一种方法,如果你只是在玩。)
因此,我将列举AudioFormat.Encoding
的类型并描述如何自己解码它们。这个答案不会涵盖如何对其进行编码,但它包含在底部的完整代码示例中。编码大多只是逆向的解码过程。
这是一个很长的答案,但我想给出一个全面的概述。
一点关于数字音频
一般在解释数字音频时,我们指的是Linear Pulse-Code Modulation (LPCM)。
以固定间隔对连续声波进行采样,并将振幅量化为某个尺度的整数。
这里显示的是一个采样并量化为 4 位的正弦波:
(请注意two's complement 表示中的最正值比最负值小 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)
...
换句话说,对于立体声,数组中的样本只是左右交替。
一些假设
所有代码示例都将采用以下声明:
byte[] bytes;
byte
数组,从 AudioInputStream
读取。
float[] samples;
我们要填充的输出样本数组。
float sample;
我们目前正在处理的样本。
long temp;
用于一般操作的中间值。
int i;
byte
数组中当前样本数据开始的位置。
我们会将float[]
数组中的所有样本标准化为-1f <= sample <= 1f
的范围。我见过的所有浮点音频都是这样来的,非常方便。
如果我们的源音频还不是这样(例如整数样本),我们可以使用以下方法自行对其进行归一化:
sample = sample / fullScale(bitsPerSample);
其中fullScale
是 2bitsPerSample - 1,即Math.pow(2, bitsPerSample-1)
。
如何将 byte
数组强制转换为有意义的数据?
byte
数组包含分割的样本帧,并且全部排成一行。这实际上非常简单,除了称为 endianness 的东西,它是每个样本数据包中 byte
s 的顺序。
这是一个图表。此示例(打包到 byte
数组中)包含十进制值 9999:
24 位样本为大端: 字节[i] 字节[i + 1] 字节[i + 2] ┌──────┐ ┌──────┐ ┌──────┐ 00000000 00100111 00001111 24 位样本为 little-endian: 字节[i] 字节[i + 1] 字节[i + 2] ┌──────┐ ┌──────┐ ┌──────┐ 00001111 00100111 00000000
它们拥有相同的二进制值;但是,byte
的顺序是相反的。
byte
s 排在不太重要的 byte
s 之前。
在 little-endian 中,较不重要的 byte
s 排在较重要的 bytes
之前。
WAV 文件以小端顺序存储,AIFF files 以大端顺序存储。字节序可以从AudioFormat.isBigEndian
获取。
要连接 byte
s 并将它们放入我们的 long temp
变量中,我们:
-
每个
byte
与掩码0xFF
(即0b1111_1111
)按位与,以避免sign-extension在byte
被自动提升时。 (char
、byte
和 short
在对其执行算术运算时提升为 int
。)另见 What does value & 0xff
do in Java?
将每个 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)
);
请注意,移位顺序是根据字节顺序颠倒的。
这也可以概括为一个循环,可以在这个答案底部的完整代码中看到。 (参见unpackAnyBit
和packAnyBit
方法。)
现在我们已经将byte
s 连接在一起,我们可以采取更多步骤将它们变成样本。接下来的步骤取决于实际的编码。
如何解码Encoding.PCM_SIGNED
?
二进制补码必须扩展。这意味着如果最高有效位 (MSB) 设置为 1,我们将用 1 填充其上方的所有位。如果设置了符号位,算术右移(>>
)会自动为我们完成填充,所以我通常这样做:
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 1111 1111
正值(MSB 为 0)保持不变。这是算术右移的一个很好的属性。
然后对样本进行归一化,如一些假设中所述。
如果你的代码很简单,你可能不需要写显式的符号扩展
当从一种整数类型转换为更大的类型时,Java 会自动进行符号扩展,例如 byte
到 int
。如果您知道您的输入和输出格式始终是有符号的,那么您可以在前面的步骤中连接字节时使用自动符号扩展。
回想一下上面的部分(如何将字节数组强制转换为有意义的数据?),我们使用b & 0xFF
来防止发生符号扩展。如果您只是从最高的byte
中删除& 0xFF
,则会自动进行符号扩展。
例如,以下解码有符号、大端、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 对应于最大负符号值。 无符号值 2bitsPerSample - 1 对应于有符号值 0。 2bitsPerSample 的无符号值对应于最正的有符号值。所以这很简单。只需减去偏移量:
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
?
这些是companding 压缩编解码器,在电话等中更常见。我假设它们由javax.sound.sampled
支持,因为它们被Sun's Au format 使用。 (但是,它不仅限于这种类型的容器。例如,WAV 可以包含这些编码。)
您可以将A-law 和μ-law 概念化,就像它们是浮点格式一样。这些是 PCM 格式,但值的范围是非线性的。
有两种解码方法。我将展示使用数学公式的方式。您也可以通过直接操作 described in this blog post 的二进制文件来解码它们,但它看起来更深奥。
对于两者,压缩数据都是 8 位的。标准 A 律在解码时为 13 位,μ 律在解码时为 14 位;但是,应用该公式会产生±1.0
的范围。
在应用公式之前,需要做三件事:
-
由于涉及数据完整性的原因,为了存储,某些位在标准上被反转。
它们存储为符号和幅度(而不是二进制补码)。
公式还需要
±1.0
的范围,因此必须对 8 位值进行缩放。
对于μ-law所有位都是反转的,所以:
temp ^= 0xffL; // 0xff == 0b1111_1111
(注意我们不能使用~
,因为我们不想反转long
的高位。)
对于 A-law,每隔一个位都是倒置的,所以:
temp ^= 0x55L; // 0x55 == 0b0101_0101
(异或可以用来做反转。见How do you set, clear and toggle a bit?)
要将符号和大小转换为二进制补码,我们:
-
检查是否设置了符号位。
如果是这样,清除符号位并将数字取反。
// 0x80 == 0b1000_0000
if ((temp & 0x80L) != 0)
temp ^= 0x80L;
temp = -temp;
然后对编码后的数字进行缩放,方法与一些假设中描述的方式相同:
sample = temp / fullScale(8);
现在我们可以应用扩展了。
那么,翻译成 Java 的 μ-law 公式为:
sample = (float) (
signum(sample)
*
(1.0 / 255.0)
*
(pow(256.0, abs(sample)) - 1.0)
);
那么翻译成 Java 的 A-law 公式是:
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.*;
/**
* <p>Performs simple audio format conversion.</p>
*
* <p>Example usage:</p>
*
* <pre>@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);
* </pre>
*
* @author Radiodef
* @see <a href="http://***.com/a/26824664/2891664">Overview on Stack Overflow</a>
*/
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).
* <p>
* 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.
* <p>
* 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;
【讨论】:
通过包含主题的每一个细节来传播知识的伟大意愿和热情。 +1。 我遇到了这个算法和 24 位深度的问题;它适用于 16 位和 32 位。当我收听 24 位音频(在您的算法读取之后)时,听起来好像有大量剪辑正在进行 - 但我仍然可以隐约看出它是同一个样本。我怀疑这些值可能在某些地方溢出。 16位和24位波形对比(蓝色波形为错误的24位输出):prnt.sc/sicu84 我在尝试在这个 24 位示例上实例化SourceDataLine
时也遇到错误(我还不知道这个类的用途,我正在遵循 javadoc 中的示例代码)。 Exception in thread "main" java.lang.IllegalArgumentException: No line matching interface SourceDataLine supporting format PCM_SIGNED 44100.0 Hz, 24 bit, stereo, 6 bytes/frame, little-endian is supported.
【参考方案2】:
这是您从当前播放的声音中获取实际样本数据的方式。 other excellent answer 将告诉您数据的含义。除了我的 Windows 10 机器 YMMV 之外,还没有在其他操作系统上尝试过它。对我来说,它会提取当前系统默认的录音设备。在 Windows 上将其设置为“立体声混音”而不是“麦克风”以获得播放声音。您可能需要切换“显示禁用的设备”才能看到“立体声混音”。
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();
【讨论】:
以上是关于如何使用 Java Sound 中的音频样本数据?的主要内容,如果未能解决你的问题,请参考以下文章
Java getAudioInputStream 试图读取音频文件,得到 javax.sound.sampled.UnsupportedAudioFileException,