Android 纯音生成方法

因项目需要在 APP 中播放纯音,耳机测听的需求,了解在 Android 系统中,纯音播放有两种方式,一种是在程序中模拟出一个波形满足正弦波的音频数据,另一种就是事先准备好多个音频文件然后直接播放。若使用事先准备的音频,效果是可以达到(且可能更好),但需要准备各种分贝、频率的音频文件,算下来,要制作上百个,所以这里讨论通过程序模拟生成纯音。



所有声音都是有正弦波组成,只不过纯音是固定频率的正弦波。主要思路就是,可以使用 sin 函数 实现想要的频率的正弦波,然后再用AudioTrack类来实现声音的播放。

一、简单纯音计算

首先正弦波的高度设置为 127,因为这里使用 8 位的采样率,2 的 8 次方就应该是 256,所以正弦波的波峰就应该是 127 了。


/** 正弦波的高度 **/ public static final int HEIGHT = 127; /** 2PI **/ public static final double TWOPI = 2 * 3.1415; /** * 生成正弦波 * @param wave * @param waveLen 每段正弦波的长度 * @param length 总长度 * @return */ public static byte[] sin(byte[] wave, int waveLen, int length) { for (int i = 0; i < length; i++) { wave[i] = (byte) (HEIGHT * (1 - Math.sin(TWOPI * ((i % waveLen) * 1.00 / waveLen)))); } return wave; }

初始化 AudioTrack 首先传入一个指定频率,这里先将声音总频率设置为 44100,然后计算出单个正弦波(也就是 2PI)的长度,最后将单个正弦波的长度乘以频率取得声波的实际频率,这样的是为了避免声波最后出现一段空的声波。 然后将参数传入上述的 sin 方法中取得正弦波。AudioFormat.ENCODING_PCM_8BIT 就是指定 AudioTrack 使用 8 位采样率。

    /**
     * 设置频率
     * @param rate
     */
    public void start(int rate) {
        if (rate > 0) {
            Hz = rate;
            waveLen = 44100 / Hz;
            length = waveLen * Hz;
            audioTrack = new AudioTrack(AudioManager.STREAM_MUSIC, 44100,
                    AudioFormat.CHANNEL_CONFIGURATION_STEREO, // CHANNEL_CONFIGURATION_MONO,
                    AudioFormat.ENCODING_PCM_8BIT, length, AudioTrack.MODE_STREAM);
            //生成正弦波
            wave = SinWave.sin(wave, waveLen, length);
            if (audioTrack != null) {
                audioTrack.play();
            }
        } else {
            return;
        }
    }

AudioTrack.play() 是还不能播放声音的,因为这个时候 AudioTrack 里面还没有声波数据。下面这段代码喂数据才能真的播放声音:

    audioTrack.write(wave, 0, length);

这种生产的方法,在频率高的时候,就会产生误差,频率越高,误差越大。

二、生成不同分贝和频率的纯音

根据不同分贝,不同频率的生成纯音。

  • 初始化 AudioTrack
  • 播放 audioTrackz.play()
  • 喂数据 AudioTrackZThread
    private int sampleRateInHz = 16000; // 采样率,MAX 44100
    private int mChannel = AudioFormat.CHANNEL_CONFIGURATION_MONO;// 声道 :单声道
    private int mSampBit = AudioFormat.ENCODING_PCM_16BIT;// 采样精度 :16bit
    private AudioTrackZThread audioTrackZThread;
    private boolean isRunning = false;
    private AudioTrack audioTrackz;

    public WaveOutZ() {
        int bufferSize = AudioTrack.getMinBufferSize(sampleRateInHz, mChannel, mSampBit);
        audioTrackz = new AudioTrack(AudioManager.STREAM_MUSIC, sampleRateInHz, mChannel, mSampBit, bufferSize * 2, AudioTrack.MODE_STREAM);
        audioTrackz.setStereoVolume(1.0f, 1.0f);
        audioTrackz.play();
    }

    public void palyWaveZ(float rate, int db) {
        if (audioTrackZThread==null) {
            audioTrackZThread = new AudioTrackZThread();
            audioTrackZThread.start();
        }
        audioTrackZThread.setDb(rate,db);
    }

    public void colseWaveZ() {
        if (audioTrackz != null) {
            if (!AudioTrackZThread.interrupted()) {
                isRunning = false;
                audioTrackZThread=null;
            }
        }
    }

    class AudioTrackZThread extends Thread {
        private short m_iAmp = Short.MAX_VALUE;
        private short m_bitDateZ[] = new short[44100];
        private double x = 2.0 * Math.PI * 8250.0 / 44100.0;
        private int mM_bitDateZSize;

        public void setDb(float rate, int db) {
            x = 2.0 * Math.PI * rate / 44100.0;
            m_iAmp = (short) ((Math.pow(10.0, db / 20.0) * 1.414) * Short.MAX_VALUE);
            for (int i = 0; i < 44100; i++) {
                m_bitDateZ[i] = (short) (m_iAmp * Math.sin(x * i));
            }
            mM_bitDateZSize = m_bitDateZ.length;
        }

        @Override
        public void run() {
            isRunning = true;

            do {
                audioTrackz.write(m_bitDateZ, 0, mM_bitDateZSize); 
            } while (isRunning);
            super.run();
        }
    }