java实现录音并保存为wav格式的音频文件

前言:本意是想像个录屏的软件,这篇先从录音功能开始。
整体思路:采用java官方API——TargetDataLine,从声卡中采集音频数据达到录音效果,采集的数据为PCM裸流,再将PCM转为wav格式。

如果你对音频文件一点也不了解,建议看一下这篇文章中的内容,主要解析了wav文件的格式,并涉及到一些音频有关的概念:
WAV文件格式详解

然后我将分成两部分代码来讲解。

第一部分代码:从声卡中采集数据并保存为pcm文件。

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格式的文件。

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文件的采样率要一致,它会根据采样率来计算播放时长。

你可能感兴趣的:(录音,java)