前言:本意是想像个录屏的软件,这篇先从录音功能开始。
整体思路:采用java官方API——TargetDataLine,从声卡中采集音频数据达到录音效果,采集的数据为PCM裸流,再将PCM转为wav格式。
如果你对音频文件一点也不了解,建议看一下这篇文章中的内容,主要解析了wav文件的格式,并涉及到一些音频有关的概念:
WAV文件格式详解
然后我将分成两部分代码来讲解。
第一部分代码:从声卡中采集数据并保存为pcm文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | package com; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.util.Scanner; import javax.sound.sampled.AudioFormat; import javax.sound.sampled.AudioSystem; import javax.sound.sampled.TargetDataLine; /** * 思路:采用java官方API——TargetDataLine,从声卡中采集音频数据达到录音效果,采集的数据为PCM裸流需要转为wav格式的话参照——PCM转WAV 。 * @author Administrator * */ public class Sound { boolean isStop=false; //采样率 private static float RATE = 44100f; //编码格式PCM private static AudioFormat.Encoding ENCODING = AudioFormat.Encoding.PCM_SIGNED; //帧大小 16 private static int SAMPLE_SIZE = 16; //是否大端 private static boolean BIG_ENDIAN = false;//true //通道数 private static int CHANNELS = 2; public void save(String path) throws Exception { //创建指定文件 File file = new File(path); if(file.isDirectory()) { if(!file.exists()) { file.mkdirs(); } file.createNewFile(); } //设置格式 AudioFormat audioFormat = new AudioFormat(ENCODING,RATE, SAMPLE_SIZE, CHANNELS, (SAMPLE_SIZE / 8) * CHANNELS, RATE, BIG_ENDIAN); //获取线路 TargetDataLine targetDataLine = AudioSystem.getTargetDataLine(audioFormat); targetDataLine.open(); targetDataLine.start(); /**targetDataLine.read() * 从数据线的输入缓冲区读取音频数据,该方法会阻塞,当数据先关闭之后就不会阻塞了 */ Thread thread=new Thread() { int flag = 0; OutputStream os = new FileOutputStream(file); byte[] b = new byte[256]; public void run() { while((flag = targetDataLine.read(b, 0, b.length))>0) {//从声卡中采集数据 try { os.write(b); } catch (IOException e) { // TODO 自动生成的 catch 块 e.printStackTrace(); } // System.out.println(flag); if(isStop) { isStop=false; break; } } } }; thread.start(); //监听按键 Thread thread2=new Thread() { public void run() { Scanner in=new Scanner(System.in); if(in.next().equals("s")) { isStop=true; } } }; thread2.start(); } } |
第二部分代码:将pcm文件转换为wav格式的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | package com; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class PcmToWave { /** * * @param src * src[0]指定pcm文件位置,src[1]指定输出的wav文件存放位置 * @throws Exception */ public static void convertAudioFiles(String[] src) throws Exception { FileInputStream fis = new FileInputStream(src[0]); //获取PCM文件大小 File file=new File(src[0]); int PCMSize =(int) file.length(); //定义wav文件头 //填入参数,比特率等等。这里用的是16位单声道 8000 hz WaveHeader header = new WaveHeader(PCMSize); //长度字段 = 内容的大小(PCMSize) + 头部字段的大小(不包括前面4字节的标识符RIFF以及fileLength本身的4字节) header.fileLength = PCMSize + (44 - 8); header.FmtHdrLeth = 16; header.BitsPerSample = 16; header.Channels = 1; header.FormatTag = 0x0001; header.SamplesPerSec = 44100;//8000; header.BlockAlign = (short)(header.Channels * header.BitsPerSample / 8); header.AvgBytesPerSec = header.BlockAlign * header.SamplesPerSec; header.DataHdrLeth = PCMSize; //获取wav文件头字节数组 byte[] h = header.getHeader(); assert h.length == 44; //WAV标准,头部应该是44字节 System.out.println((PCMSize+44)); // auline.write(h, 0, h.length); byte[] b = new byte[10]; //将文件头写入文件 FileOutputStream fs = new FileOutputStream(src[1]); fs.write(h); //将pcm文件写到文件头后面 FileInputStream fiss = new FileInputStream(src[0]); byte[] bb = new byte[10]; int len = -1; while((len = fiss.read(bb))>0) { fs.write(bb, 0, len); } } } /** * WavHeader辅助类。用于生成头部信息。 * @author Administrator * */ class WaveHeader { /**wav文件头:RIFF区块 * 名称 偏移地址 字节数 端序 内容 * ID 0x00 4Byte 大端 'RIFF' (0x52494646) Size 0x04 4Byte 小端 fileSize - 8 Type 0x08 4Byte 大端 'WAVE'(0x57415645) 解析: 以'RIFF'为标识 Size是整个文件的长度减去ID和Size的长度 Type是WAVE表示后面需要两个子块:Format区块和Data区块 */ /** * FORMAT区块: * 名称 偏移地址 字节数 端序 内容 ID 0x00 4Byte 大端 'fmt ' (0x666D7420) Size 0x04 4Byte 小端 16 AudioFormat 0x08 2Byte 小端 音频格式 NumChannels 0x0A 2Byte 小端 声道数 SampleRate 0x0C 4Byte 小端 采样率 ByteRate 0x10 4Byte 小端 每秒数据字节数 BlockAlign 0x14 2Byte 小端 数据块对齐 BitsPerSample 0x16 2Byte 小端 采样位数 解析: 以'fmt '为标识 Size表示该区块数据的长度(不包含ID和Size的长度) AudioFormat表示Data区块存储的音频数据的格式,PCM音频数据的值为1 NumChannels表示音频数据的声道数,1:单声道,2:双声道 SampleRate表示音频数据的采样率 ByteRate每秒数据字节数 = SampleRate * NumChannels * BitsPerSample / 8 BlockAlign每个采样所需的字节数 = NumChannels * BitsPerSample / 8 BitsPerSample每个采样存储的bit数,8:8bit,16:16bit,32:32bit */ /** * DATA区块 * * 名称 偏移地址 字节数 端序 内容 ID 0x00 4Byte 大端 'data' (0x64617461) Size 0x04 4Byte 小端 N Data 0x08 NByte 小端 音频数据 解析: 以'data'为标识 Size表示音频数据的长度,N = ByteRate * seconds Data音频数据 */ public final char fileID[] = {'R', 'I', 'F', 'F'}; public int fileLength; public short FormatTag; public short Channels; public int SamplesPerSec; public int AvgBytesPerSec; public short BlockAlign; public short BitsPerSample; public char DataHdrID[] = {'d','a','t','a'}; public int DataHdrLeth; public char wavTag[] = {'W', 'A', 'V', 'E'};; public char FmtHdrID[] = {'f', 'm', 't', ' '}; public int FmtHdrLeth; public WaveHeader() {}//无参构造方法 /** * * @param a */ public WaveHeader(int a) { } public byte[] getHeader() throws IOException { //创建一个输出流,用于将各个字节数组写入缓存中,缓存区会自动增长。然后可以将整个输出流转换为完整的字节数组,关闭该流不会有任何效果。 ByteArrayOutputStream bos = new ByteArrayOutputStream(); WriteChar(bos, fileID); WriteInt(bos, fileLength); WriteChar(bos, wavTag); WriteChar(bos, FmtHdrID); WriteInt(bos,FmtHdrLeth); WriteShort(bos,FormatTag); WriteShort(bos,Channels); WriteInt(bos,SamplesPerSec); WriteInt(bos,AvgBytesPerSec); WriteShort(bos,BlockAlign); WriteShort(bos,BitsPerSample); WriteChar(bos,DataHdrID); WriteInt(bos,DataHdrLeth); bos.flush(); byte[] r = bos.toByteArray(); bos.close(); return r; } private void WriteShort(ByteArrayOutputStream bos, int s) throws IOException { byte[] mybyte = new byte[2]; mybyte[1] =(byte)( (s << 16) >> 24 );//存放高位 mybyte[0] =(byte)( (s << 24) >> 24 );//存放低位 bos.write(mybyte); } private void WriteInt(ByteArrayOutputStream bos, int n) throws IOException { byte[] buf = new byte[4]; buf[3] =(byte)( n >> 24 ); buf[2] =(byte)( (n << 8) >> 24 ); buf[1] =(byte)( (n << 16) >> 24 ); buf[0] =(byte)( (n << 24) >> 24 ); bos.write(buf); } private void WriteChar(ByteArrayOutputStream bos, char[] id) { for (int i=0; i<id.length; i++) { char c = id[i]; bos.write(c); } } } |
这个需要注意的一件事就是,wav格式的文件是以小端形式来存储的,即低位存放在低位内存中,高位存放在高位内存中。所以你在生成pcm文件的时候,一定要以小端形式存储数据,否则播放音频文件的时候完全是混乱的杂音。另外,还需要注意的是,pcm文件与wav文件的采样率要一致,它会根据采样率来计算播放时长。