文档章节

Android音频编解码和混音实现

叶大侠
 叶大侠
发布于 2016/03/11 20:10
字数 2353
阅读 2982
收藏 13
点赞 4
评论 14

相关源码:https://github.com/YeDaxia/MusicPlus



认识数字音频:

在实现之前,我们先来了解一下数字音频的有关属性。

采样频率(Sample Rate):每秒采集声音的数量,它用赫兹(Hz)来表示。(采样率越高越靠近原声音的波形)
采样精度(Bit Depth):指记录声音的动态范围,它以位(Bit)为单位。(声音的幅度差)
声音通道(Channel):声道数。比如左声道右声道。

采样量化后的音频最终是一串数字,声音的大小(幅度)会体现在这个每个数字数值大小上;而声音的高低(频率)和声音的音色(Timbre)都和时间维度有关,会体现在数字之间的差异上。

    在编码解码之前,我们先来感受一下原始的音频数据究竟是什么样的。我们知道wav文件里面放的就是原始的PCM数据,下面我们通过AudioTrack来直接把这些数据write进去播放出来。下面是某个wav文件的格式,关于wav的格式内容可以看:http://soundfile.sapp.org/doc/WaveFormat/ ,可以通过Binary Viewer等工具去查看一下wav文件的二进制内容。

播放wav文件:

int sampleRateInHz = 44100;
int channelConfig = AudioFormat.CHANNEL_OUT_STEREO;
int audioFormat = AudioFormat.ENCODING_PCM_16BIT;

int bufferSizeInBytes = AudioTrack.getMinBufferSize(sampleRateInHz, channelConfig, audioFormat);
AudioTrack audioTrack = new  AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, AudioTrack.MODE_STREAM);
audioTrack.play();
			
FileInputStream audioInput = null;
try {
	audioInput = new FileInputStream(audioFile);//put your wav file in

	audioInput.read(new byte[44]);//skid 44 wav header
	
	byte[] audioData = new byte[512];
	
	while(audioInput.read(audioData)!= -1){
		audioTrack.write(audioData, 0, audioData.length); //play raw audio bytes
	}
	
} catch (FileNotFoundException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}finally{
	audioTrack.stop();
	audioTrack.release();
	if(audioInput != null)
		try {
			audioInput.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
}

如果你有试过一下上面的例子,那你应该对音频的源数据有了一个概念了。

 音频的解码:

通过上面的介绍,我们不难知道,解码的目的就是让编码后的数据恢复成wav中的源数据。

利用MediaExtractor和MediaCodec来提取编码后的音频数据并解压成音频源数据:

final String encodeFile = "your encode audio file path";
MediaExtractor extractor = new MediaExtractor();
extractor.setDataSource(encodeFile);

MediaFormat mediaFormat = null;
for (int i = 0; i < extractor.getTrackCount(); i++) {
	MediaFormat format = extractor.getTrackFormat(i);
	String mime = format.getString(MediaFormat.KEY_MIME);
	if (mime.startsWith("audio/")) {
		extractor.selectTrack(i);
		mediaFormat = format;
		break;
	}
}

if(mediaFormat == null){
	DLog.e("not a valid file with audio track..");
	extractor.release();
	return null;
}

FileOutputStream fosDecoder = new FileOutputStream(outDecodeFile);//your out file path

String mediaMime = mediaFormat.getString(MediaFormat.KEY_MIME);
MediaCodec codec = MediaCodec.createDecoderByType(mediaMime);
codec.configure(mediaFormat, null, null, 0);
codec.start();

ByteBuffer[] codecInputBuffers = codec.getInputBuffers();
ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers();

final long kTimeOutUs = 5000;
MediaCodec.BufferInfo info = new MediaCodec.BufferInfo();
boolean sawInputEOS = false;
boolean sawOutputEOS = false;
int totalRawSize = 0;
try{
	while (!sawOutputEOS) {
		if (!sawInputEOS) {
			int inputBufIndex = codec.dequeueInputBuffer(kTimeOutUs);
			if (inputBufIndex >= 0) {
				ByteBuffer dstBuf = codecInputBuffers[inputBufIndex];
				int sampleSize = extractor.readSampleData(dstBuf, 0);
				if (sampleSize < 0) {
					DLog.i(TAG, "saw input EOS.");
					sawInputEOS = true;
					codec.queueInputBuffer(inputBufIndex,0,0,0,MediaCodec.BUFFER_FLAG_END_OF_STREAM );
				} else {
					long presentationTimeUs = extractor.getSampleTime();
					codec.queueInputBuffer(inputBufIndex,0,sampleSize,presentationTimeUs,0);
					extractor.advance();
				}
			}
		}
		int res = codec.dequeueOutputBuffer(info, kTimeOutUs);
		if (res >= 0) {

			 int outputBufIndex = res;
			// Simply ignore codec config buffers.
			if ((info.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG)!= 0) {
				 DLog.i(TAG, "audio encoder: codec config buffer");
				 codec.releaseOutputBuffer(outputBufIndex, false);
				 continue;
			 }
			 
			if(info.size != 0){
				
				ByteBuffer outBuf = codecOutputBuffers[outputBufIndex];
				
				outBuf.position(info.offset);
				outBuf.limit(info.offset + info.size);
				byte[] data = new byte[info.size];
				outBuf.get(data);
				totalRawSize += data.length;
				fosDecoder.write(data);
				
			}
			
			codec.releaseOutputBuffer(outputBufIndex, false);
			
			if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
				DLog.i(TAG, "saw output EOS.");
				sawOutputEOS = true;
			}
			
		} else if (res == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
			codecOutputBuffers = codec.getOutputBuffers();
			DLog.i(TAG, "output buffers have changed.");
		} else if (res == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
			MediaFormat oformat = codec.getOutputFormat();
			DLog.i(TAG, "output format has changed to " + oformat);
		}
	}
}finally{
	fosDecoder.close();
	codec.stop();
	codec.release();
	extractor.release();
}

解压之后,可以用AudioTrack来播放验证一下这些数据是否正确。

音频的混音:

音频混音的原理: 量化的语音信号的叠加等价于空气中声波的叠加。

反应到音频数据上,也就是把同一个声道的数值进行简单的相加,但是这样同时会产生一个问题,那就是相加的结果可能会溢出,当然为了解决这个问题已经有很多方案了,在这里我们采用简单的平均算法(average audio mixing algorithm, 简称V算法)。在下面的演示程序中,我们假设音频文件是的采样率,通道和采样精度都是一样的,这样会便于处理。另外要注意的是,在源音频数据中是按照little-endian的顺序来排放的,PCM值为0表示没声音(振幅为0)。

public void mixAudios(File[] rawAudioFiles){
	
	final int fileSize = rawAudioFiles.length;

	FileInputStream[] audioFileStreams = new FileInputStream[fileSize];
	File audioFile = null;
	
	FileInputStream inputStream;
	byte[][] allAudioBytes = new byte[fileSize][];
	boolean[] streamDoneArray = new boolean[fileSize];
	byte[] buffer = new byte[512];
	int offset;
	
	try {
		
		for (int fileIndex = 0; fileIndex < fileSize; ++fileIndex) {
			audioFile = rawAudioFiles[fileIndex];
			audioFileStreams[fileIndex] = new FileInputStream(audioFile);
		}

		while(true){
			
			for(int streamIndex = 0 ; streamIndex < fileSize ; ++streamIndex){
				
				inputStream = audioFileStreams[streamIndex];
				if(!streamDoneArray[streamIndex] && (offset = inputStream.read(buffer)) != -1){
					allAudioBytes[streamIndex] = Arrays.copyOf(buffer,buffer.length);
				}else{
					streamDoneArray[streamIndex] = true;
					allAudioBytes[streamIndex] = new byte[512];
				}
			}
			
			byte[] mixBytes = mixRawAudioBytes(allAudioBytes);
			
			//mixBytes 就是混合后的数据
			
			boolean done = true;
			for(boolean streamEnd : streamDoneArray){
				if(!streamEnd){
					done = false;
				}
			}
			
			if(done){
				break;
			}
		}
		
	} catch (IOException e) {
		e.printStackTrace();
		if(mOnAudioMixListener != null)
			mOnAudioMixListener.onMixError(1);
	}finally{
		try {
			for(FileInputStream in : audioFileStreams){
				if(in != null)
					in.close();
			}
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}

/**
 * 每一行是一个音频的数据
 */
byte[] averageMix(byte[][] bMulRoadAudioes) {
		
		if (bMulRoadAudioes == null || bMulRoadAudioes.length == 0)
			return null;

		byte[] realMixAudio = bMulRoadAudioes[0];
		
		if(bMulRoadAudioes.length == 1)
			return realMixAudio;
		
		for(int rw = 0 ; rw < bMulRoadAudioes.length ; ++rw){
			if(bMulRoadAudioes[rw].length != realMixAudio.length){
				Log.e("app", "column of the road of audio + " + rw +" is diffrent.");
				return null;
			}
		}
		
		int row = bMulRoadAudioes.length;
		int coloum = realMixAudio.length / 2;
		short[][] sMulRoadAudioes = new short[row][coloum];

		for (int r = 0; r < row; ++r) {
			for (int c = 0; c < coloum; ++c) {
				sMulRoadAudioes[r][c] = (short) ((bMulRoadAudioes[r][c * 2] & 0xff) | (bMulRoadAudioes[r][c * 2 + 1] & 0xff) << 8);
			}
		}

		short[] sMixAudio = new short[coloum];
		int mixVal;
		int sr = 0;
		for (int sc = 0; sc < coloum; ++sc) {
			mixVal = 0;
			sr = 0;
			for (; sr < row; ++sr) {
				mixVal += sMulRoadAudioes[sr][sc];
			}
			sMixAudio[sc] = (short) (mixVal / row);
		}

		for (sr = 0; sr < coloum; ++sr) {
			realMixAudio[sr * 2] = (byte) (sMixAudio[sr] & 0x00FF);
			realMixAudio[sr * 2 + 1] = (byte) ((sMixAudio[sr] & 0xFF00) >> 8);
		}

		return realMixAudio;
}

同样,你可以把混音后的数据用AudioTrack播放出来,验证一下混音的效果。

音频的编码:

对音频进行编码的目的用更少的空间来存储和传输,有有损编码和无损编码,其中我们常见的Mp3和ACC格式就是有损编码。在下面的例子中,我们通过MediaCodec来对混音后的数据进行编码,在这里,我们将采用ACC格式来进行。

ACC音频有ADIF和ADTS两种,第一种适用于磁盘,第二种则可以用于流的传输,它是一种帧序列。我们这里用ADTS这种来进行编码,首先要了解一下它的帧序列的构成:

ADTS的帧结构:

header
body

ADTS帧的Header组成:

Length (bits) Description
12 syncword 0xFFF, all bits must be 1
1 MPEG Version: 0 for MPEG-4, 1 for MPEG-2
2 Layer: always 0
1 protection absent, Warning, set to 1 if there is no CRC and 0 if there is CRC
2 profile, the MPEG-4 Audio Object Type minus 1
4 MPEG-4 Sampling Frequency Index (15 is forbidden)
1 private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding
3 MPEG-4 Channel Configuration (in the case of 0, the channel configuration is sent via an inband PCE)
1 originality, set to 0 when encoding, ignore when decoding
1 home, set to 0 when encoding, ignore when decoding
1 copyrighted id bit, the next bit of a centrally registered copyright identifier, set to 0 when encoding, ignore when decoding
1 copyright id start, signals that this frame's copyright id bit is the first bit of the copyright id, set to 0 when encoding, ignore when decoding
13 frame length, this value must include 7 or 9 bytes of header length: FrameLength = (ProtectionAbsent == 1 ? 7 : 9) + size(AACFrame)
11 Buffer fullness
2 Number of AAC frames (RDBs) in ADTS frame minus 1, for maximum compatibility always use 1 AAC frame per ADTS frame
16 CRC if protection absent is 0

我们的思路就很明确了,把编码后的每一帧数据加上header写到文件中,保存后的.acc文件应该是可以被播放器识别播放的。为了简单,我们还是假设之前生成的混音数据源的采样率是44100Hz,通道数是2,采样精度是16Bit。

把音频源数据编码成ACC格式完成源代码:

class AACAudioEncoder{
	private final static String TAG = "AACAudioEncoder";
	private final static String AUDIO_MIME = "audio/mp4a-latm";
	private final static long audioBytesPerSample = 44100*16/8;
	private String rawAudioFile;
	AACAudioEncoder(String rawAudioFile) {
		this.rawAudioFile = rawAudioFile;
	}
	@Override
	public void encodeToFile(String outEncodeFile) {
		FileInputStream fisRawAudio = null;
		FileOutputStream fosAccAudio = null;
		try {
			fisRawAudio = new FileInputStream(rawAudioFile);
			fosAccAudio = new FileOutputStream(outEncodeFile);
			final MediaCodec audioEncoder = createACCAudioDecoder();
			audioEncoder.start();
			ByteBuffer[] audioInputBuffers = audioEncoder.getInputBuffers();
			ByteBuffer[] audioOutputBuffers = audioEncoder.getOutputBuffers();
			boolean sawInputEOS = false;
	        boolean sawOutputEOS = false;
	        long audioTimeUs = 0 ;
			BufferInfo outBufferInfo = new BufferInfo();
			boolean readRawAudioEOS = false;
			byte[] rawInputBytes = new byte[4096];
			int readRawAudioCount = 0;
			int rawAudioSize = 0;
			long lastAudioPresentationTimeUs = 0;
			int inputBufIndex, outputBufIndex;
	        while(!sawOutputEOS){
	        	if (!sawInputEOS) {
	        		 inputBufIndex = audioEncoder.dequeueInputBuffer(10000);
				     if (inputBufIndex >= 0) {
				           ByteBuffer inputBuffer = audioInputBuffers[inputBufIndex];
				           inputBuffer.clear();
				           int bufferSize = inputBuffer.remaining();
				           if(bufferSize != rawInputBytes.length){
				        	   rawInputBytes = new byte[bufferSize];
				           }
				           if(!readRawAudioEOS){
				        	   readRawAudioCount = fisRawAudio.read(rawInputBytes);
				        	   if(readRawAudioCount == -1){
				        		   readRawAudioEOS = true;
				        	   }
				           }
				           if(readRawAudioEOS){
			        		   audioEncoder.queueInputBuffer(inputBufIndex,0 , 0 , 0 ,MediaCodec.BUFFER_FLAG_END_OF_STREAM);
				        	   sawInputEOS = true;
				           }else{
				        	   inputBuffer.put(rawInputBytes, 0, readRawAudioCount);
					           rawAudioSize += readRawAudioCount;
					           audioEncoder.queueInputBuffer(inputBufIndex, 0, readRawAudioCount, audioTimeUs, 0);
					           audioTimeUs = (long) (1000000 * (rawAudioSize / 2.0) / audioBytesPerSample);
				           }
				     }
	        	}
	        	outputBufIndex = audioEncoder.dequeueOutputBuffer(outBufferInfo, 10000);
	        	if(outputBufIndex >= 0){
	        		// Simply ignore codec config buffers.
	        		if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG)!= 0) {
	                     DLog.i(TAG, "audio encoder: codec config buffer");
	                     audioEncoder.releaseOutputBuffer(outputBufIndex, false);
	                     continue;
	                 }
	        		if(outBufferInfo.size != 0){
		        		 ByteBuffer outBuffer = audioOutputBuffers[outputBufIndex];
		        		 outBuffer.position(outBufferInfo.offset);
		        		 outBuffer.limit(outBufferInfo.offset + outBufferInfo.size);
		        		 DLog.i(TAG, String.format(" writing audio sample : size=%s , presentationTimeUs=%s", outBufferInfo.size, outBufferInfo.presentationTimeUs));
		        		 if(lastAudioPresentationTimeUs < outBufferInfo.presentationTimeUs){
			        		 lastAudioPresentationTimeUs = outBufferInfo.presentationTimeUs;
			        		 int outBufSize   = outBufferInfo.size;
			        		 int outPacketSize = outBufSize + 7;
			        		 outBuffer.position(outBufferInfo.offset);
			        		 outBuffer.limit(outBufferInfo.offset + outBufSize);
			        		 byte[] outData = new byte[outBufSize + 7];
			        		 addADTStoPacket(outData, outPacketSize);
			        		 outBuffer.get(outData, 7, outBufSize);
			        		 fosAccAudio.write(outData, 0, outData.length);
			                 DLog.i(TAG, outData.length + " bytes written.");
		        		 }else{
		        			 DLog.e(TAG, "error sample! its presentationTimeUs should not lower than before.");
		        		 }
	        		}
	        		audioEncoder.releaseOutputBuffer(outputBufIndex, false);
	                 if ((outBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
				           sawOutputEOS = true;
				     }
	        	}else if (outputBufIndex == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) {
	        		audioOutputBuffers = audioEncoder.getOutputBuffers();
			    } else if (outputBufIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) {
			    	MediaFormat audioFormat = audioEncoder.getOutputFormat();
			    	DLog.i(TAG, "format change : "+ audioFormat);
			    }
	        }
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		} finally {
			try {
				if (fisRawAudio != null)
					fisRawAudio.close();
				if(fosAccAudio != null)
					fosAccAudio.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	private MediaCodec createACCAudioDecoder() throws IOException {
		MediaCodec	codec = MediaCodec.createEncoderByType(AUDIO_MIME);
		MediaFormat format = new MediaFormat();
		format.setString(MediaFormat.KEY_MIME, AUDIO_MIME);
		format.setInteger(MediaFormat.KEY_BIT_RATE, 128000);
		format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 2);
		format.setInteger(MediaFormat.KEY_SAMPLE_RATE, 44100);
		format.setInteger(MediaFormat.KEY_AAC_PROFILE,MediaCodecInfo.CodecProfileLevel.AACObjectLC);
		codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
		return codec;
	}
	/**
     *  Add ADTS header at the beginning of each and every AAC packet.
     *  This is needed as MediaCodec encoder generates a packet of raw
     *  AAC data.
     *
     *  Note the packetLen must count in the ADTS header itself.
     **/
    private void addADTStoPacket(byte[] packet, int packetLen) {
        int profile = 2;  //AAC LC
        //39=MediaCodecInfo.CodecProfileLevel.AACObjectELD;
        int freqIdx = 4;  //44.1KHz
        int chanCfg = 2;  //CPE
        // fill in ADTS data
        packet[0] = (byte)0xFF;
        packet[1] = (byte)0xF9;
        packet[2] = (byte)(((profile-1)<<6) + (freqIdx<<2) +(chanCfg>>2));
        packet[3] = (byte)(((chanCfg&3)<<6) + (packetLen>>11));
        packet[4] = (byte)((packetLen&0x7FF) >> 3);
        packet[5] = (byte)(((packetLen&7)<<5) + 0x1F);
        packet[6] = (byte)0xFC;
    }
}

参考资料:

数字音频: http://en.flossmanuals.net/pure-data/ch003_what-is-digital-audio/

WAV文件格式: http://soundfile.sapp.org/doc/WaveFormat/

ACC文件格式: http://www.cnblogs.com/caosiyang/archive/2012/07/16/2594029.html

有关Android Media编程的一些CTS:https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts

WAV转ACC相关问题: http://stackoverflow.com/questions/18862715/how-to-generate-the-aac-adts-elementary-stream-with-android-mediacodec

© 著作权归作者所有

共有 人打赏支持
叶大侠

叶大侠

粉丝 56
博文 44
码字总数 67312
作品 5
广州
程序员
加载中

评论(14)

叶大侠
叶大侠

引用来自“maomaohang”的评论

叶大侠,我想请教一个问题,假设我有两个音轨(录音和背景音乐)要混音,可否分别设置音量大小?
可以啊,降低音轨幅度再混音。
maomaohang
maomaohang
叶大侠,我想请教一个问题,假设我有两个音轨(录音和背景音乐)要混音,可否分别设置音量大小?
淡蛋蛋
淡蛋蛋

引用来自“淡蛋蛋”的评论

DEMO在实现视频混音的时候有闪退哦

引用来自“叶大侠”的评论

Android版本?
是的啊~
叶大侠
叶大侠

引用来自“淡蛋蛋”的评论

DEMO在实现视频混音的时候有闪退哦
Android版本?
淡蛋蛋
淡蛋蛋
DEMO在实现视频混音的时候有闪退哦
这不科学
这不科学
图片音频视频等做细了也是很难的。佩服
叶大侠
叶大侠

引用来自“daixiong”的评论

我勒个去,为什么必须要等一个小时才能发表评论啊?哎,饭都没吃,班也没下,就想发个评论,有这么难么9

下班吃饭去喽,拜拜13

引用来自“DarcyYe”的评论

哈哈~ 谢谢!!能帮到你我很开心~

引用来自“daixiong”的评论

我尝试加入录音功能,实现唱吧那样的k歌效果,在解码的时候录音,解码得到byte【】数据与录音得到的byte【】数据混合,打印日志里面的显示每次都有得到混合数据,数组长度第一次是188,之后每次都是4068(?好像),但是输出数据的文件只有几KB,求解?
一步一步来: 录音是否解码成功->背景音是否解码成功->两个音轨的采样频率,通道,位数是否一致(不一致要对齐处理)->执行混合算法->检验混合后的效果
d
daixiong

引用来自“daixiong”的评论

我勒个去,为什么必须要等一个小时才能发表评论啊?哎,饭都没吃,班也没下,就想发个评论,有这么难么9

下班吃饭去喽,拜拜13

引用来自“DarcyYe”的评论

哈哈~ 谢谢!!能帮到你我很开心~
我尝试加入录音功能,实现唱吧那样的k歌效果,在解码的时候录音,解码得到byte【】数据与录音得到的byte【】数据混合,打印日志里面的显示每次都有得到混合数据,数组长度第一次是188,之后每次都是4068(?好像),但是输出数据的文件只有几KB,求解?
叶大侠
叶大侠

引用来自“daixiong”的评论

我勒个去,为什么必须要等一个小时才能发表评论啊?哎,饭都没吃,班也没下,就想发个评论,有这么难么9

下班吃饭去喽,拜拜13
哈哈~ 谢谢!!能帮到你我很开心~
d
daixiong
再次感谢楼主的无私奉献
WebRTC音频引擎实现分析

WebRTC的音频引擎作为两大基础多媒体引擎之一,实现了音频数据的采集、前处理、编码、发送、接收、解码、混音、后处理、播放等一系列处理流程。本文在深入分析WebRTC源代码的基础上,学习并总...

weizhenwei ⋅ 2017/12/10 ⋅ 0

直播,音视频编码器和解码器(EasyDarwin)-Android

使用摄像头采集视频数据,并通过MediaCodec进行H264编码,之后打包成RTSP格式并上传的。 TextuewView也提供了一个setTransform方法,该方法接收一个matrix参数,使用该参数对当前的渲染内容进...

shareus ⋅ 05/18 ⋅ 0

Android NDK开发之旅30--FFmpeg视频播放

1.播放多媒体文件步骤 通常情况下,我们下载的视频文件如MP4,MKV、FLV等都属于封装格式,就是把音视频数据按照相应的规范,打包成一个文本文件。我们可以使用MediaInfo这个工具查看媒体文件...

香沙小熊 ⋅ 2017/12/08 ⋅ 0

WebRTC源码架构浅析

WebRTC源码架构浅析 Google 在2010年花了6千8百万美元收购了大名鼎鼎的 Global IP Sound/Solutions (GIPS) 公司, 得到了它的 VoIP 相关技术的专利和软件. 第二年, Google就把这些软件开源了,...

ideawu ⋅ 2013/08/14 ⋅ 2

播放器/短视频 SDK 架构设计

短视频 SDK 架构设计实践- http://blog.csdn.net/dev_csdn/article/details/78683826 短视频开发需要的预备知识及难点:贴纸,特效/美颜/混音/内置滤镜等SO,不使用 ffmpeg 的软解软编,而是...

shareus ⋅ 2017/12/01 ⋅ 0

SylixOS音频驱动移植

1. 适用范围 本文档为实现Nuc970平台音频驱动的方法总结,以此提供一些SylixOS音频驱动移植方法的参考。 2. 原理概述 2.1 Codec编解码芯片 声音信号分为模拟信号和数字信号,Codec编解码芯片...

zhywxyy ⋅ 2017/04/21 ⋅ 0

一套代码,快速实现一个语音聊天室

前言:本文将简要分享几个语音聊天室的应用场景,并讲述基于声网SDK,实现语音聊天室的步骤。 语音聊天在泛娱乐社交行业中有着重要的地位,行业中很多佼佼者也都为用户提供了语音聊天室,甚至...

Agora ⋅ 前天 ⋅ 0

技术解析:如何实现K歌App中的实时合唱

之前我们解析过很多社交直播App中不同场景的开发,比如在线K歌、小程序直播、多人视频聊天、AR等。 我们最近在知乎看到了一个问题「为什么k歌软件始终没有开发出实时合唱功能?」,我们只在知...

Agora ⋅ 05/31 ⋅ 0

Android平台上裁剪m4a

Android手机上设置铃声的操作是比较灵活的,一般读者听到一首喜欢的歌曲,马上就可以对这首歌曲进行裁剪,裁剪到片段后,再通过系统的接口设置为铃声(电话铃声、闹钟铃声等)。 前提是,播放...

奇哥3 ⋅ 04/28 ⋅ 0

android音频编辑之音频裁剪的示例代码

... /** 裁剪音频 */private void cutAudio() { String path1 = tvAudioPath1.getText().toString(); if(TextUtils.isEmpty(path1)){ToastUtil.showToast("音频路径为空");return;} float s......

qq_39539367 ⋅ 05/23 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

Springboot2 之 Spring Data Redis 实现消息队列——发布/订阅模式

一般来说,消息队列有两种场景,一种是发布者订阅者模式,一种是生产者消费者模式,这里利用redis消息“发布/订阅”来简单实现订阅者模式。 实现之前先过过 redis 发布订阅的一些基础概念和操...

Simonton ⋅ 24分钟前 ⋅ 0

error:Could not find gradle

一.更新Android Studio后打开Project,报如下错误: Error: Could not find com.android.tools.build:gradle:2.2.1. Searched in the following locations: file:/D:/software/android/andro......

Yao--靠自己 ⋅ 昨天 ⋅ 0

Spring boot 项目打包及引入本地jar包

Spring Boot 项目打包以及引入本地Jar包 [TOC] 上篇文章提到 Maven 项目添加本地jar包的三种方式 ,本篇文章记录下在实际项目中的应用。 spring boot 打包方式 我们知道,传统应用可以将程序...

Os_yxguang ⋅ 昨天 ⋅ 0

常见数据结构(二)-树(二叉树,红黑树,B树)

本文介绍数据结构中几种常见的树:二分查找树,2-3树,红黑树,B树 写在前面 本文所有图片均截图自coursera上普林斯顿的课程《Algorithms, Part I》中的Slides 相关命题的证明可参考《算法(第...

浮躁的码农 ⋅ 昨天 ⋅ 0

android -------- 混淆打包报错 (warning - InnerClass ...)

最近做Android混淆打包遇到一些问题,Android Sdutio 3.1 版本打包的 错误如下: Android studio warning - InnerClass annotations are missing corresponding EnclosingMember annotation......

切切歆语 ⋅ 昨天 ⋅ 0

eclipse酷炫大法之设置主题、皮肤

eclipse酷炫大法 目前两款不错的eclipse 1.系统设置 Window->Preferences->General->Appearance 2.Eclipse Marketplace下载【推荐】 Help->Eclipse Marketplace->搜索‘theme’进行安装 比如......

anlve ⋅ 昨天 ⋅ 0

vim编辑模式、vim命令模式、vim实践

vim编辑模式 编辑模式用来输入或修改文本内容,编辑模式除了Esc外其他键几乎都是输入 如何进入编辑模式 一般模式输入以下按键,均可进入编辑模式,左下角提示 insert(中文为插入) 字样 i ...

蛋黄Yolks ⋅ 昨天 ⋅ 0

大数据入门基础:SSH介绍

什么是ssh 简单说,SSH是一种网络协议,用于计算机之间的加密登录。 如果一个用户从本地计算机,使用SSH协议登录另一台远程计算机,我们就可以认为,这种登录是安全的,即使被中途截获,密码...

董黎明 ⋅ 昨天 ⋅ 0

web3j教程

web3j是一个轻量级、高度模块化、响应式、类型安全的Java和Android类库提供丰富API,用于处理以太坊智能合约及与以太坊网络上的客户端(节点)进行集成。 汇智网最新发布的web3j教程,详细讲解...

汇智网教程 ⋅ 昨天 ⋅ 0

谷歌:安全问题机制并不如你想象中安全

腾讯科技讯 5月25日,如今的你或许已经对许多网站所使用的“安全问题机制”习以为常了,但你真的认为包括“你第一个宠物的名字是什么?”这些问题能够保障你的帐户安全吗? 根据谷歌(微博)安...

问题终结者 ⋅ 昨天 ⋅ 0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部