它是微软公司为Windows系统开发的一种标准的数字音频可,直接存储声音波形,并且它的波形曲线还原的真实感很好,但是也有缺点:存储磁盘空间大,多用于存储简短的声音片段。
是PC机上最为流行的声音文件格式。
格式大小:采样率一般是:44.1K,16bit采样精度,存储:WAV格式大小 = 44.1KHz(采样率) X 16bit(采样位数) X 2(双声道) X 播放时间
WAV格式是没有压缩无损的,MP3格式是按1:12压缩保存的。
由三个块组成:如图:
前44个字节是音频文件的说明,也就是从44个字节后开始就是正式数据了
WAV文件'一般'由3个区块组成:RIFF chunk、Format chunk和Data chunk,当然不包括文件中还有一些附加块。
wav文件中的数据块是小端存储-但是Java以大端存储-所以在进制转换时要转换成大端:
Java大端:
默认网络传输字节为大端,java 全部为大端(与平台无关)
小端存储:字节或半字节的最低位字节(Least Significant Bit,LSB)放置于于内存最低位字节地址上。(通俗:低字节数据存储在低地址)
大端存储:数据低位保存在内存的高地址中,数据的高位保存在内存的低地址中。
(通俗:低字节数据存储在低地址)
区别
1,简单:大端存储或小端存储都是由系统设计的,区别在于低地址存储的数据,因此可以写程序进行判断。
2,详情:计算机中以字节为存储单位,每个地址单元都对应着一个字节,对于大于8位的数据类型,就会产生在寄存器中存放顺序问题。
什么是寄存器(Register):
基本寄存器和移位寄存器两大类。基本寄存器只能并行送入数据,也只能并行输出。移位寄存器中的数据可以在移位脉冲作用下依次逐位右移或左移,数据既可以并行输入、并行输出,也可以串行输入、串行输出,还可以并行输入、串行输出,或串行输入、并行输出,十分灵活,用途也很广。
寄存器作用了解:是中央处理器内的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指令、数据和位址。
寄存器又分为内部寄存器与外部寄存器,所谓内部寄存器,其实也是一些小的存储单元,也能存储数据。但同存储器相比,寄存器又有自己独有的特点:
功能:是存储二进制,它是由具有存储功能的触发器组合起来构成的。
寄存器中:
1,从前往后,先低字节,后高地址,一个存储单元,存放一个字节。
大端:
十进制数字:4328719365
十六进制:0x0102030405
:[0x01(低地址),0x02,0x03,0x04,0x05]
注意:这里很好展示了第一个单元是低地址!
小端:
还是一样十进制:4328719365
:[0x05(低地址),0x04,0x03,0x02,0x01]
注意:无论是小端存储或者大端存储,它读取到的永远是:0x0102030405
我们看个例子:
//转成大端
public static byte[] int_To_Byte_BigEnding(int num) {
byte[] D = new byte[4];
D[3] = (byte) (num & 0xff);
D[2] = (byte) (num >> 8 & 0xff);
D[1] = (byte) (num >> 16 & 0xff);
D[0] = (byte) (num >> 24 & 0xff);
return D;
}
//转成大端
public static byte[] int_To_Byte_SmallEnding(int num) {
byte[] D = new byte[4];
D[0] = (byte) (num & 0xff);
D[1] = (byte) (num >> 8 & 0xff);
D[2] = (byte) (num >> 16 & 0xff);
D[3] = (byte) (num >> 24 & 0xff);
return D;
}
是不是明白道理就简单多了?--有人问oxff是什么意思?
首先了解一下, &表示只有两个位同时为1,才能得到1, 0x代表16进制数,而0xff表示的数二进制1111 1111 占一个字节.和其进行&操作的数,最低8位,不会发生变化.
而这里只是为了取它的高低.
比如:
out.write(num&0xff)
取低八位写入高地址中
out.write((num>>8)&0xff)
取高八位写入地
了解完成后我们就可以开始,wav的讲解了。
什么是RIFF:是一种在标记块中存储数据的元格式,把资料储存在被标记的区块中,
前4个字节为资源交换文件标志。
1.整个WAV都是由RIFF:数据块组成,可以将整个WAV文件看作一个完整的RIFF数据块,在这个RIFF数据块中又包含若干的子块存放不同的信息。
2.首部结构如下:
截取方式:截取方式也很简单,我们以二进制读取后,以ASCII读取字串:
读取:public @NotNull static
ByteArrayOutputStream Io_P_OUT(V PATH) throws IOException { FileInputStream obtainData = new FileInputStream(PATH.toString()); int StorageByte; byte[] StorageDataHome = new byte[(int) new File(PATH.toString()).length()]; ByteArrayOutputStream RETURN_DATA = new ByteArrayOutputStream(); while ((StorageByte = obtainData.read(StorageDataHome)) > 0) { RETURN_DATA.write(StorageDataHome, 0, StorageByte); RETURN_DATA.flush(); } obtainData.close(); return RETURN_DATA; } System.out.println(new String(__response.Io_P_OUT("PATH").toByteArray(),StandardCharsets.US_ASCII));
这样就可以看到如下信息了:
Format-Chunk:
0x0C 4 String 'fmt'标志 0x10 4 UInteger 块长度 0x12 2 UShort PCM格式类别 0x14 2 UShort 声道数目 0x18 4 UInteger 采样率 0x1C 4 UInteger 传输速率 0x1E 2 UShort 数据块对齐 0x20 2 UShort 每样本比特数 0x22 2 UShort 可选
附加块Fact-Chunk:
当使用压缩编码的WAV文件时,必须要有Fact-Chunk,这个块中只有一个数据,就是声道的采样总数.
Fact chunk前4个字节为"fact(ID)",然后是4字节的大小,表示本块包含数据的多少-不包含ID和SIZE.
偏移量 字节数 数据类型 内容 0x26 4 String 'fact'标志 0x2A 4 UInteger 块长度 0x2E 4 UInteger 附加信息
数据块Data-Chunk:
0x32 | 4 | String | 'data'文件标志(0x64617461) |
0x36 |
4 |
UInteger | 数据块总长 |
前一段是数据ID,后一段是:代表Data数据块的长度。
DataChunk块:头部和实际数据组成,数据块标示(4bytes)+数据块长度(4bytes)+实际数据。
编码包括了两方面内容,一是按一定格式存储数据,二是采用一定的算法压缩数据。
WAV格式对音频流的编码没有硬性规定,支持非压缩的PCM。
上代码,提取转换代码:
@NotNull public static boolean _W(byte[] source,byte[] DATA,int Start ,int End){ if (DATA != null) { if (source.length-End >= (0)) { System.arraycopy(DATA, Start, source, 0, End); return true; } else { return false; } } else { return false; } } --小端转大端读取 public static byte[] Io_S_Ending_B(String Path) throws IOException {FileChannel F =(FileChannel) Files.newByteChannel(java.nio.file.Path.of(Path), java.nio.file.StandardOpenOption.READ); ByteBuffer B = ByteBuffer.allocate((int) F.size());B.order(ByteOrder.LITTLE_ENDIAN);F.read(B);B.flip();byte[] Data=new byte[B.remaining()]; B.get(Data);return Data;} byte[]sub=__response.Io_S_Ending_B("路径XX.wav"); byte[] pcm=new byte[sub.length]; __response._W(pcm,sub,44,pcm.length-44); AudioFormat.Encoding A=new AudioFormat.Encoding("PCM_SIGNED"); SourceDataLine auline; AudioFormat af = new AudioFormat(A,48000, 16, 2,(16/8)*2,48000,false); try { auline = AudioSystem.getSourceDataLine(af); } catch (LineUnavailableException e) { throw new RuntimeException(e); } try { auline.open(af); } catch (LineUnavailableException e) { throw new RuntimeException(e); } auline.start(); ByteArrayInputStream D=new ByteArrayInputStream(pcm); byte[] DD = new byte[1024]; while(D.read(DD)!=-1){ auline.write(DD,0,DD.length); }
其实这和刚才的读取wav数据块一样是pcm音频数据
录音我们需要使用到TargetDataLine官方的API数据从声卡中捕获数据。
他的采样率一般是44.1K,16bit采样精度,双声道,速率则为44.1K×16×2,小端端序,wav格式保存,之前说过wav->对音频流的编码没有硬性规定,支持非压缩的PCM。脉冲编码调制格式。
ThreadPoolExecutor core = new ThreadPoolExecutor(1, 2, 100, TimeUnit.MILLISECONDS,
new LinkedBlockingDeque<>(10));
//这里我直接使用线程池了,为了保证录音时,我们可以输入end结束,后续可以自行更改。
//第一次输入
System.out.println("Start开始end关闭");
Scanner aa = new Scanner(System.in);
String a = aa.next();
if (a.equalsIgnoreCase("Start")) {
core.submit(() -> {
float a1 = 44000F;
//采样
int a2 = 16;
//样本中的中位数8,16
int a3 = 2;
//声道
boolean a4 = true;
//签名
boolean a5 = false;
//给定参数
//TargetDataLine是一种可以从中 DataLine 读取音频数据的类型。最常见的示例是从音频捕获设备获取数据的数据线
//DataLine(简单来说就是捕获数据的)
//AudioFormat通道样本
AudioFormat.Encoding A=new AudioFormat.Encoding("PCM_SIGNED");
DataLine.Info a6 = new DataLine.Info(TargetDataLine.class, new AudioFormat(
A
,
a1,
a2,
a3,
(a2 / 8) * a3,
a1, a5));
//音频编码
//每秒样本数
//每个样本中的位数
// 通道数(1 个用于单声道,2 个用于立体声,依此类推)
//每帧中的字节数
//每秒帧数
//指示单个样本的数据是否以大端字节顺序存储(false 表示小端
//通俗来讲就是匹配拿数据,最后一步时将数据以我们限定的格式输出
TargetDataLine b = null;
try {
b = (TargetDataLine) AudioSystem.getLine(a6);
} catch (LineUnavailableException e) {
throw new RuntimeException(e);
}
System.out.println("开始录音");
AudioFileFormat.Type a8 = null;
//指定的文件类型
File a9 = null;
//声明文件地址
a8 = AudioFileFormat.Type.WAVE;
//类型
a9 = new File("保存路径");
//文件地址
try {
b.open();
//音频所用格式
b.start();
AudioSystem.write(new AudioInputStream(b), a8, a9);
//写入文件数据
} catch (Exception ignored) {
}
});
}
//第二次输入
Scanner aaaa = new Scanner(System.in);
String aaaaa = aaaa.next();
if (aaaaa.equalsIgnoreCase("end")) {
System.exit(0);
}
操作:输入不分大小start开始录音,end结束录音。
前面讲了提取音频的方法也可以自己练习一下,播放一下自己录的音频,提取各类信息。
一起和Atomic君学习!。