`
jim443
  • 浏览: 7904 次
  • 性别: Icon_minigender_1
  • 来自: 广州
社区版块
存档分类
最新评论

android 录音机&示波器

 
阅读更多

1、 功能需求

1. 软件运行平台为android平台

2、 性能要求

1. 应用软件可以适应不同android硬件平台

2. 要求提供源码,最好封装为函数库。

3. 支持平台:Android1.6 Android2.0 Android2.1 Android2.2(及以上平台)

4. 支持的手机分辨率:320x240 320x480 480x800  (具有自适应性)

5. 可以根据重力感应切换屏幕方向

6. 要求声音的实时波形显示

7. 录音的时间长度显示

8. 文件名自动按照系统时间保存,如20120601.amr

9. 在可以列出历史录音列表(文件列表)

 

 

 

 

 

 

 

关键代码如下:

 

package com.android.record;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;

import com.android.record.Draw.DrawThread;

import android.app.Activity;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Bundle;
import android.os.SystemClock;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.SurfaceView;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Button;
import android.widget.Chronometer;

/**
 * class name:TestAudioRecord<BR>
 * class description:用AudioRecord来进行录音<BR>
 * PS: <BR>
 * 
 * @version 1.00 2012/07/17
 * @author CODYY)liangqinsheng
 */
public class SoundRecord extends Activity {
    // 音频获取源
    private int audioSource = MediaRecorder.AudioSource.MIC;
    // 设置音频采样率,44100是目前的标准,但是某些设备仍然支持22050,16000,11025
    private static int sampleRateInHz = 8000;
    // 设置音频的录制的声道CHANNEL_IN_STEREO为双声道,CHANNEL_CONFIGURATION_MONO为单声道
    private static int channelConfig = AudioFormat.CHANNEL_IN_STEREO;
    // 音频数据格式:PCM 16位每个样本。保证设备支持。PCM 8位每个样本。不一定能得到设备支持。
    private static int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
    // 缓冲区字节大小
    private int bufferSizeInBytes = 0;
    private Button mRecordButton;
    private Button mStopButton;
    private Button mPauseButton;
    private Button mListButton;
    private AudioRecord audioRecord;
    private boolean isRecord = false;// 设置正在录制的状态
    // AudioName裸音频数据文件
    private static final String AudioName = "/sdcard/record_sound/love.3gpp";
    // NewAudioName可播放的音频文件
    private static String NewAudioName = "/sdcard/record_sound/new.wav";

    // private ArrayList<String> recordFiles;
    private SimpleDateFormat sdf;

    private int mPause = 0;
    private Chronometer chronometer;
    private long passTime;
    Paint mPaint;
    SurfaceView sfv;
    public ArrayList<short[]> inBuf = new ArrayList<short[]>();
    public int baseLine = 0;
    public int rateX = 16;
    public int rateY = 5;

    public void onCreate(Bundle savedInstanceState) {
	super.onCreate(savedInstanceState);
	getWindow().setFormat(PixelFormat.TRANSLUCENT);// 让界面横屏
	requestWindowFeature(Window.FEATURE_NO_TITLE);// 去掉界面标题
	getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
		WindowManager.LayoutParams.FLAG_FULLSCREEN);
	// 重新设置界面大小
	setContentView(R.layout.main);
	sdf = new SimpleDateFormat("yyMMddhhmmss");
	sfv = (SurfaceView) this.findViewById(R.id.surfaceView);
	mPaint = new Paint();
	mPaint.setColor(Color.GREEN);// 画笔为绿色
	mPaint.setStrokeWidth(1);// 设置画笔粗细
	baseLine = sfv.getHeight();
	init();
    }

    private void init() {
	mRecordButton = (Button) this.findViewById(R.id.recordButton);
	mStopButton = (Button) this.findViewById(R.id.stopButton);
	mPauseButton = (Button) this.findViewById(R.id.pauseButton);
	mListButton = (Button) this.findViewById(R.id.listButton);
	mRecordButton.setOnClickListener(new TestAudioListener());
	mStopButton.setOnClickListener(new TestAudioListener());
	mPauseButton.setOnClickListener(new TestAudioListener());
	mListButton.setOnClickListener(new TestAudioListener());
	chronometer = (Chronometer) findViewById(R.id.chronometer);
	creatAudioRecord();
    }

    private void creatAudioRecord() {
	// 获得缓冲区字节大小
	bufferSizeInBytes = AudioRecord.getMinBufferSize(sampleRateInHz,
		channelConfig, audioFormat);
	// 创建AudioRecord对象
	audioRecord = new AudioRecord(audioSource, sampleRateInHz,
		channelConfig, audioFormat, bufferSizeInBytes);
    }

    class TestAudioListener implements OnClickListener {
	@Override
	public void onClick(View v) {
	    if (v == mRecordButton) {
		mPause = 0;
		startRecord();
		// 开始记时
		chronometer.setFormat(null);
		chronometer.setBase(SystemClock.elapsedRealtime());
		chronometer.start();
		new DrawThread(sfv, mPaint).start();
	    }
	    if (v == mStopButton) {
		stopRecord();
		chronometer.stop();
		inBuf.clear();// 清除
	    }
	    if (v == mPauseButton) {
		if (mPause == 1) {
		    mPause = 0;
		    chronometer.setBase(chronometer.getBase()
			    + (SystemClock.elapsedRealtime() - passTime));
		    chronometer.start();
		} else {
		    mPause = 1;
		    chronometer.stop();
		    passTime = SystemClock.elapsedRealtime();
		}
	    }
	    if (v == mListButton) {
		Intent intent = new Intent(SoundRecord.this, FileList.class);
		startActivity(intent);
	    }
	}
    }

    private void startRecord() {
	NewAudioName = "/sdcard/record_sound/" + sdf.format(new Date()) + ".wav";
	if (audioRecord == null)
	    audioRecord = new AudioRecord(audioSource, sampleRateInHz,
		    channelConfig, audioFormat, bufferSizeInBytes);
	audioRecord.startRecording();
	// 让录制状态为true
	isRecord = true;
	// 开启音频文件写入线程
	new Thread(new AudioRecordThread()).start();
    }

    private void stopRecord() {
	close();
    }

    private void close() {
	if (audioRecord != null) {
	    System.out.println("stopRecord");
	    isRecord = false;// 停止文件写入
	    audioRecord.stop();
	    audioRecord.release();// 释放资源
	    audioRecord = null;
	}
    }

    class AudioRecordThread implements Runnable {
	@Override
	public void run() {

	    writeDateTOFile();// 往文件中写入裸数据isRecord
	    copyWaveFile(AudioName, NewAudioName);// 给裸数据加上头文件
	}
    }

    /**
     * 这里将数据写入文件,但是并不能播放,因为AudioRecord获得的音频是原始的裸音频, 如果需要播放就必须加入一些格式或者编码的头信息。
     */
    private void writeDateTOFile() {
	// new一个byte数组用来存一些字节数据,大小为缓冲区大小
	byte[] audiodata = new byte[bufferSizeInBytes];
	FileOutputStream fos = null;
	int readsize = 0;
	try {
	    File file = new File(AudioName);
	    if (file.exists()) {
		file.delete();
	    }
	    fos = new FileOutputStream(file);// 建立一个可存取字节的文件
	} catch (Exception e) {
	    e.printStackTrace();
	}
	while (isRecord == true) {
	    readsize = audioRecord.read(audiodata, 0, bufferSizeInBytes);
    	    if(mPause == 0) {
		short[] tmpBuf = new short[readsize / 4];
		for (int i = 0, ii = 0; i < tmpBuf.length; i++, ii = i * 4) {
		    tmpBuf[i] = audiodata[ii];
		}
		synchronized (inBuf) {//
		    inBuf.add(tmpBuf);// 添加数据
		}
	    }
	    if (AudioRecord.ERROR_INVALID_OPERATION != readsize) {
		try {
		    if (mPause == 0 && fos != null)
			fos.write(audiodata);
		} catch (IOException e) {
		    e.printStackTrace();
		}
	    }
	}
	try {
	    fos.close();// 关闭写入流
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }

    /**
     * 这里得到可播放的音频文件
     * 
     * @param inFilename
     * @param outFilename
     */
    private void copyWaveFile(String inFilename, String outFilename) {
	FileInputStream in = null;
	FileOutputStream out = null;
	long totalAudioLen = 0;
	long totalDataLen = totalAudioLen + 36;
	long longSampleRate = sampleRateInHz;
	int channels = 2;
	long byteRate = 16 * sampleRateInHz * channels / 8;
	byte[] data = new byte[bufferSizeInBytes];
	try {
	    in = new FileInputStream(inFilename);
	    out = new FileOutputStream(outFilename);
	    totalAudioLen = in.getChannel().size();
	    totalDataLen = totalAudioLen + 36;
	    int j = 0;
	    WriteWaveFileHeader(out, totalAudioLen, totalDataLen,
		    longSampleRate, channels, byteRate);
	    while (in.read(data) != -1) {
		out.write(data);
		System.out.println("j:" + j);
		j++;
		// out.flush();
	    }

	    in.close();
	    out.close();
	} catch (FileNotFoundException e) {
	    e.printStackTrace();
	} catch (IOException e) {
	    e.printStackTrace();
	}
    }

    /**
     * 44字节的wav头文件信息
     */
    private void WriteWaveFileHeader(FileOutputStream out, long totalAudioLen,
	    long totalDataLen, long longSampleRate, int channels, long byteRate)
	    throws IOException {
	byte[] header = new byte[44];
	header[0] = 'R'; // RIFF/WAVE header
	header[1] = 'I';
	header[2] = 'F';
	header[3] = 'F';
	header[4] = (byte) (totalDataLen & 0xff);
	header[5] = (byte) ((totalDataLen >> 8) & 0xff);
	header[6] = (byte) ((totalDataLen >> 16) & 0xff);
	header[7] = (byte) ((totalDataLen >> 24) & 0xff);
	header[8] = 'W';
	header[9] = 'A';
	header[10] = 'V';
	header[11] = 'E';
	header[12] = 'f'; // 'fmt ' chunk
	header[13] = 'm';
	header[14] = 't';
	header[15] = ' ';
	header[16] = 16; // 4 bytes: size of 'fmt ' chunk
	header[17] = 0;
	header[18] = 0;
	header[19] = 0;
	header[20] = 1; // format = 1
	header[21] = 0;
	header[22] = (byte) channels;
	header[23] = 0;
	header[24] = (byte) (longSampleRate & 0xff);
	header[25] = (byte) ((longSampleRate >> 8) & 0xff);
	header[26] = (byte) ((longSampleRate >> 16) & 0xff);
	header[27] = (byte) ((longSampleRate >> 24) & 0xff);
	header[28] = (byte) (byteRate & 0xff);
	header[29] = (byte) ((byteRate >> 8) & 0xff);
	header[30] = (byte) ((byteRate >> 16) & 0xff);
	header[31] = (byte) ((byteRate >> 24) & 0xff);
	header[32] = (byte) (2 * 16 / 8); // block align
	header[33] = 0;
	header[34] = 16; // bits per sample
	header[35] = 0;
	header[36] = 'd';
	header[37] = 'a';
	header[38] = 't';
	header[39] = 'a';
	header[40] = (byte) (totalAudioLen & 0xff);
	header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
	header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
	header[43] = (byte) ((totalAudioLen >> 24) & 0xff);
	out.write(header, 0, 44);
    }

    @Override
    protected void onDestroy() {
	close();
	super.onDestroy();
    }

	class DrawThread extends Thread {
		private int oldX = 0;// 上次绘制的X坐标
		private int oldY = 0;// 上次绘制的Y坐标
		private SurfaceView sfv;// 画板
		private int X_index = 0;// 当前画图所在屏幕X轴的坐标
		private Paint mPaint;// 画笔
		public DrawThread(SurfaceView sfv, Paint mPaint) {
			this.sfv = sfv;
			this.mPaint = mPaint;
		}
		public void run() {
			while (isRecord) {
				ArrayList<short[]> buf = new ArrayList<short[]>();
				synchronized (inBuf) {
					if (inBuf.size() == 0)
						continue;
					buf = (ArrayList<short[]>) inBuf.clone();// 保存
					inBuf.clear();// 清除
				}
				for (int i = 0; i < buf.size(); i++) {
					short[] tmpBuf = buf.get(i);
					SimpleDraw(X_index, tmpBuf, rateY, baseLine);// 把缓冲区数据画出来
					X_index = X_index + tmpBuf.length;
					if (X_index > sfv.getWidth()) {
						X_index = 0;
					}
				}
			}
		}
		/**
		 * 绘制指定区域
		 * 
		 * @param start
		 *            X轴开始的位置(全屏)
		 * @param buffer
		 *            缓冲区
		 * @param rate
		 *            Y轴数据缩小的比例
		 * @param baseLine
		 *            Y轴基线
		 */
		void SimpleDraw(int start, short[] buffer, int rate, int baseLine) {
			if (start == 0)
				oldX = 0;
			Canvas canvas = sfv.getHolder().lockCanvas(
					new Rect(start, 0, start + buffer.length, sfv.getHeight()));// 关键:获取画布
			canvas.drawColor(Color.BLACK);// 清除背景
			int y;
			for (int i = 0; i < buffer.length; i++) {// 有多少画多少
				int x = i + start;
				y = buffer[i] / rate + baseLine;// 调节缩小比例,调节基准线
				canvas.drawLine(oldX, oldY, x, y, mPaint);
				oldX = x;
				oldY = y;
			}
			sfv.getHolder().unlockCanvasAndPost(canvas);// 解锁画布,提交画好的图像
		}
	}

}

 画图类:package com.android.record;

import java.util.ArrayList;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.media.AudioRecord;
import android.view.SurfaceView;
public class Draw {
	public ArrayList<short[]> inBuf = new ArrayList<short[]>();
	private boolean isRecording = false;// 线程控制标记
	/**
	 * X轴缩小的比例
	 */
	public int rateX = 4;
	/**
	 * Y轴缩小的比例
	 */
	public int rateY = 4;
	/**
	 * Y轴基线
	 */
	public int baseLine = 0;
	/**
	 * 初始化
	 */
	public void init(int rateX, int rateY, int baseLine) {
		this.rateX = rateX;
		this.rateY = rateY;
		this.baseLine = baseLine;
	}
	/**
	 * 开始
	 * 
	 * @param recBufSize
	 *            AudioRecord的MinBufferSize
	 */
	public void Start(AudioRecord audioRecord, int recBufSize, SurfaceView sfv,
			Paint mPaint) {
		isRecording = true;
//		new RecordThread(audioRecord, recBufSize).start();// 开始绘制线程
		new DrawThread(sfv, mPaint).start();// 开始绘制线程
	}
	/**
	 * 停止
	 */
	public void Stop() {
		isRecording = false;
		inBuf.clear();// 清除
	}
	/**
	 * 负责从MIC保存数据到inBuf
	 * 
	 * @author GV
	 * 
	 */
	class RecordThread extends Thread {
		private int recBufSize;
		private AudioRecord audioRecord;
		public RecordThread(AudioRecord audioRecord, int recBufSize) {
			this.audioRecord = audioRecord;
			this.recBufSize = recBufSize;
		}
		public void run() {
			try {
				short[] buffer = new short[recBufSize];
				while (isRecording) {
					// 从MIC保存数据到缓冲区
					int bufferReadResult = audioRecord.read(buffer, 0,
							recBufSize);
					short[] tmpBuf = new short[bufferReadResult / rateX];
					for (int i = 0, ii = 0; i < tmpBuf.length; i++, ii = i
							* rateX) {
						tmpBuf[i] = buffer[ii];
					}
					synchronized (inBuf) {//
						inBuf.add(tmpBuf);// 添加数据
					}
				}
			} catch (Throwable t) {
			}
		}
	};
	/**
	 * 负责绘制inBuf中的数据
	 * 
	 * @author GV
	 * 
	 */
	class DrawThread extends Thread {
		private int oldX = 0;// 上次绘制的X坐标
		private int oldY = 0;// 上次绘制的Y坐标
		private SurfaceView sfv;// 画板
		private int X_index = 0;// 当前画图所在屏幕X轴的坐标
		private Paint mPaint;// 画笔
		public DrawThread(SurfaceView sfv, Paint mPaint) {
			this.sfv = sfv;
			this.mPaint = mPaint;
		}
		public void run() {
			while (isRecording) {
				ArrayList<short[]> buf = new ArrayList<short[]>();
				synchronized (inBuf) {
					if (inBuf.size() == 0)
						continue;
					buf = (ArrayList<short[]>) inBuf.clone();// 保存
					inBuf.clear();// 清除
				}
				for (int i = 0; i < buf.size(); i++) {
					short[] tmpBuf = buf.get(i);
					SimpleDraw(X_index, tmpBuf, rateY, baseLine);// 把缓冲区数据画出来
					X_index = X_index + tmpBuf.length;
					if (X_index > sfv.getWidth()) {
						X_index = 0;
					}
				}
			}
		}
		/**
		 * 绘制指定区域
		 * 
		 * @param start
		 *            X轴开始的位置(全屏)
		 * @param buffer
		 *            缓冲区
		 * @param rate
		 *            Y轴数据缩小的比例
		 * @param baseLine
		 *            Y轴基线
		 */
		void SimpleDraw(int start, short[] buffer, int rate, int baseLine) {
			if (start == 0)
				oldX = 0;
			Canvas canvas = sfv.getHolder().lockCanvas(
					new Rect(start, 0, start + buffer.length, sfv.getHeight()));// 关键:获取画布
			canvas.drawColor(Color.BLACK);// 清除背景
			int y;
			for (int i = 0; i < buffer.length; i++) {// 有多少画多少
				int x = i + start;
				y = buffer[i] / rate + baseLine;// 调节缩小比例,调节基准线
				canvas.drawLine(oldX, oldY, x, y, mPaint);
				oldX = x;
				oldY = y;
			}
			sfv.getHolder().unlockCanvasAndPost(canvas);// 解锁画布,提交画好的图像
		}
	}
}
 完整的项目见附件.

关键技术点:
1.使用AudioRecord 进行录音;
2.录音写文件与现实示波同时进行;
2个线程同步,主要数据只有一份,所以得拷贝一份给2个线程用,不然的话录音会短截;
3.主要控制好字节流(转化成数字)在画布上的显示,需要多次调试才达到效果;
4.暂停与进行录音需要一个线程控制,如果按了暂停,就设置线程里面有一个开关(例如mPause),暂停了就不写字节流和显示图像,按继续才开始。结束的时候就保持文件和把图像清空;

如有不懂的童鞋欢迎与我联系。

 

分享到:
评论

相关推荐

    android 示波器

    在Android平台上开发一个示波器小程序,可以展示正弦和余弦信号,是将嵌入式计算和可视化技术应用于移动设备的一个典型实例。这个小程序利用Android系统的强大功能,为用户提供实时信号显示,使得用户能够在手机或...

    android录音机带录音波形及播放波形

    总的来说,"android录音机带录音波形及播放波形"项目涵盖了Android音频处理、图形渲染、多线程编程以及Web技术的集成。对于初学者来说,这是一个很好的实践项目,能帮助他们全面理解Android应用开发的多个方面。在...

    android 录音机 源码

    综上所述,通过`AudioRecord`和`AudioTrack`的配合,可以实现一个简单的Android录音机应用。在`AudioRecPlay`这个项目中,我们可以看到具体的代码实现,学习如何将理论知识应用到实践中。理解并掌握这两个类的使用,...

    录音示波器

    录音示波器是一种结合了录音功能与示波器特性的工具,主要面向电子工程爱好者、学生以及初学者,提供了一种简易的实验和学习环境。它不仅能够录制音频,还能实时显示音频信号的波形,帮助用户理解音频信号的变化规律...

    android 对讲机&amp;amp;录音机类安卓源码包源代码合集(10个).zip

    android 对讲机&录音机类安卓源码包源代码合集(10个) Andorid录音+变声+转mp3 Android电子麦克风 AudioRecord 仿微信的录音功能 对讲机参考资料 开心网语音发送模块的录音功能 按住说话,开始录音,停止录音 神聊...

    android 录音机 service 例子

    在这个"android 录音机 service 例子"中,我们将深入探讨如何结合Service和MediaRecorder来创建一个能够后台录制音频的应用。 1. **Service基础** Service组件是Android应用开发中的关键部分,它可以在没有用户...

    android录音机demo

    android录音机

    Android录音的声波动画

    在Android开发中,创建一个能够显示声波动画的录音应用是一项有趣的挑战,它结合了多媒体处理、用户界面设计以及实时数据可视化。以下是对这个"Android录音的声波动画"项目的关键知识点的详细解释: 1. **Android ...

    android音频口示波器和波形发生器

    "android音频口示波器和波形发生器"项目是一个很好的实践,它涵盖了Android音频编程的关键知识点。让我们深入探讨一下这些技术要点。 首先,`示波器`是显示音频信号波形的工具,它可以帮助我们实时查看音频数据的...

    android 调用系统自带录音机

    在Android平台上,调用系统自带的录音机是开发者经常遇到的需求,这可以帮助用户方便地录制音频并集成到应用中。本文将深入探讨如何在Android应用中实现这一功能,并结合"AutoRecoder"这个示例来讲解相关知识点。 ...

    Android通过J2ME的录音功能实现简易示波器

    【Android通过J2ME的录音功能实现简易示波器】是一种利用J2ME的多媒体应用编程接口(MMAPI)在Android设备上构建一个简单的模拟示波器的方法。虽然使用智能机可以实现实时读取麦克风输入流,提供更流畅的体验,但...

    Android 录音机Demo

    这个`Android 录音机Demo`应该是为了帮助开发者快速理解和实现音频录制的基本流程。下面我们将深入探讨Android录音的相关知识点。 1. **MediaRecorder类**:这是Android系统提供用于处理多媒体数据录制的核心类。...

    android录音分贝波浪图展示动画

    在Android开发中,有时我们需要实现一些与音频相关的功能,例如录音和实时显示音频波形,以提供用户友好的界面。本篇文章将详细讲解如何在Android应用中创建一个录音分贝波浪图展示动画,主要涉及的技术包括录音、...

    Android录音机,带录音效果

    在Android平台上,开发一款带有录音效果的录音机应用是一项技术性的工作,涉及到多个技术层面,如音频处理、用户界面设计以及动画实现等。本项目"MiCode-SoundRecorder-15aa813"显然是一个关于Android录音机的源代码...

    Android 录音机的实现

    在Android平台上,开发一款录音机应用是一项常见的任务。本文将深入探讨如何实现一个具有暂停功能的Android录音机,包括其核心原理、关键步骤以及代码实现。这个录音机的特点是,当用户暂停时,会保存当前的录音数据...

    仿三星android录音机界面

    本项目"仿三星android录音机界面"旨在提供一个高度还原三星原生录音机应用的UI设计,使得开发者和爱好者可以参考并应用于自己的项目中。下面我们将详细探讨这个项目的相关知识点。 首先,Android录音机应用的核心...

    Android原生录音机

    在Android平台上,开发一款原生录音机应用是一个常见的任务,涉及到多媒体处理、音频录制和播放等技术。Android SDK提供了一套完整的API,使得开发者能够轻松地实现这些功能。本篇文章将详细探讨Android原生录音机的...

    Android 录音机软件-IT计算机-毕业设计.zip

    【Android 录音机软件开发详解】 Android录音机软件是一种常见的移动应用,它允许用户录制、存储和播放音频。在毕业设计中,这样的项目能够帮助学生深入理解Android应用开发的基本原理,包括音频处理、UI设计、文件...

    android录音机

    本文将深入探讨如何在Android上实现录音机功能,基于提供的"android录音机"项目,我们可以学习到以下几个关键知识点: 1. **录音API的使用**:Android提供了MediaRecorder类来处理音频录制。首先,我们需要实例化...

    基于声卡的虚拟示波器

    然而,除了基本的音乐播放和录音功能,声卡还能够被巧妙地利用来实现一些额外的科学应用,例如将电脑转变为一个虚拟示波器。这个概念是基于声卡的模拟数字转换(ADC)能力,它能够将声音信号转化为可处理的数字数据...

Global site tag (gtag.js) - Google Analytics