AudioRecord 在 Android 5.01 上产生零间隙

AudioRecord producing gaps of zeroes on Android 5.01

我尝试使用 AudioRecord 编写一个测试应用程序来录制几秒钟的音频以显示在屏幕上。但是,我似乎得到了如下所示的零值区域的重复模式。我不确定这是正常行为还是我的代码中的错误。

MainActivity.java

public class MainActivity extends Activity implements OnClickListener 
{
    private static final int SAMPLE_RATE = 44100;
    private Button recordButton, playButton;
    private String filePath;
    private boolean recording;
    private AudioRecord record;
    private short[] data;
    private TestView testView;

    @Override
    protected void onCreate(Bundle savedInstanceState) 
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Button recordButton = (Button) this.findViewById(R.id.recordButton);
        recordButton.setOnClickListener(this);

        Button playButton = (Button)findViewById(R.id.playButton);
        playButton.setOnClickListener(this);

        FrameLayout frame = (FrameLayout)findViewById(R.id.myFrame);
        frame.addView(testView = new TestView(this));
    }

    @Override
    public void onClick(View v) 
    {
        if(v.getId() == R.id.recordButton)
        {
            if(!recording)
            {
                int bufferSize = AudioRecord.getMinBufferSize(  SAMPLE_RATE,
                                                                AudioFormat.CHANNEL_IN_MONO,
                                                                AudioFormat.ENCODING_PCM_16BIT);

                record = new AudioRecord(   MediaRecorder.AudioSource.MIC,
                                            SAMPLE_RATE,
                                            AudioFormat.CHANNEL_IN_MONO,
                                            AudioFormat.ENCODING_PCM_16BIT,
                                            bufferSize * 2);

                data = new short[10 * SAMPLE_RATE]; // Records up to 10 seconds

                new Thread()
                {
                    @Override
                    public void run() 
                    {
                        recordAudio();
                    }

                }.start();

                recording = true;

                Toast.makeText(this, "recording...", Toast.LENGTH_SHORT).show();
            }
            else
            {
                recording = false;
                Toast.makeText(this, "finished", Toast.LENGTH_SHORT).show();
            }
        }
        else if(v.getId() == R.id.playButton)
        {   
            testView.invalidate();
            Toast.makeText(this, "play/pause", Toast.LENGTH_SHORT).show();
        }
    }

    void recordAudio()
    {
        record.startRecording();
        int index = 0;
        while(recording)
        {
            try {
                Thread.sleep(50);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            int result = record.read(data, index, SAMPLE_RATE); // read 1 second at a time
            if(result == AudioRecord.ERROR_INVALID_OPERATION || result == AudioRecord.ERROR_BAD_VALUE)
            {
                App.d("SOME SORT OF RECORDING ERROR MATE");
                return;
            }
            else
            {
                index += result; // increment by number of bytes read
                App.d("read: "+result);
            }
        }
        record.stop();
        data = Arrays.copyOf(data, index);

        testView.setData(data);
    }

    @Override
    protected void onPause() 
    {

        super.onPause();
    }
}

TestView.java

public class TestView extends View 
{
    private short[] data;
    Paint paint = new Paint();
    Path path = new Path();
    float min, max;

    public TestView(Context context) 
    {
        super(context);

        paint.setColor(Color.BLACK);
        paint.setStrokeWidth(1);
        paint.setStyle(Style.FILL_AND_STROKE);
    }

    void setData(short[] data)
    {
        min = Short.MAX_VALUE;
        max = Short.MIN_VALUE;
        this.data = data;
        for(int i = 0; i < data.length; i++)
        {
            if(data[i] < min)
                min = data[i];

            if(data[i] > max)
                max = data[i];
        }
    }

    @Override
    protected void onDraw(Canvas canvas)
    {
        canvas.drawRGB(255, 255, 255);
        if(data != null)
        {
            float interval = (float)this.getWidth()/data.length;
            for(int i = 0; i < data.length; i+=10)
                canvas.drawCircle(i*interval,(data[i]-min)/(max - min)*this.getHeight(),5 ,paint);

        }
        super.onDraw(canvas);
    }
}

我现在无法检查您的代码,但我可以为您提供一些您可以测试的示例代码:

private static int channel_config = AudioFormat.CHANNEL_IN_MONO;
private static int format = AudioFormat.ENCODING_PCM_16BIT;
private static int Fs = 16000;
private static int minBufferSize; 
private boolean isRecording;
private boolean isProcessing;
private boolean isNewAudioFragment;

private final static int bytesPerSample = 2; // As it is 16bit PCM
private final double amplification = 1.0; // choose a number as you like
private static int frameLength = 512; // number of samples per frame => 32[ms] @Fs = 16[KHz]
private static int windowLength = 16; // number of frames per window => 512[ms] @Fs = 16[KHz]
private static int maxBufferedWindows = 8; // number of buffered windows => 4096 [ms] @Fs = 16[KHz]

private static int bufferSize = frameLength*bytesPerSample;
private static double[] hannWindow = new double[frameLength*bytesPerSample];

private Queue<byte[]> queue = new LinkedList<byte[]>();
private Semaphore semaphoreProcess = new Semaphore(0, true);

private RecordSignal recordSignalThread;
private ProcessSignal processSignalThread;

public static class RecorderSingleton {
    public static RecorderSingleton instance = new RecorderSingleton();
    private AudioRecord recordInstance = null;

    private RecorderSingleton() {
        minBufferSize = AudioRecord.getMinBufferSize(Fs, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);
        while(minBufferSize>bufferSize) {
            bufferSize = bufferSize*2;
        }
    }

    public boolean init() {
        recordInstance = new AudioRecord(MediaRecorder.AudioSource.MIC, Fs, channel_config, format, bufferSize);
        if (recordInstance.getState() != AudioRecord.STATE_INITIALIZED) {
            Log.d("audiotestActivity", "Fail to initialize AudioRecord object");
            Log.d("audiotestActivity", "AudioRecord.getState()=" + recordInstance.getState());
        }
        if (recordInstance.getState() == AudioRecord.STATE_UNINITIALIZED) {
            return false;
        }
        return true;
    }

    public int getBufferSize() {return bufferSize;}

    public boolean start() {
        if (recordInstance != null && recordInstance.getState() != AudioRecord.STATE_UNINITIALIZED) {
            if (recordInstance.getRecordingState() != AudioRecord.RECORDSTATE_STOPPED) {
                recordInstance.stop();
            }
            recordInstance.release();
        }
        if (!init()) {
            return false;
        }
        recordInstance.startRecording();
        return true;
    }
    public int read(byte[] audioBuffer) {
        if (recordInstance == null) {
            return AudioRecord.ERROR_INVALID_OPERATION;
        }
        int ret = recordInstance.read(audioBuffer, 0, bufferSize);
        return ret;
    }
    public void stop() {
        if (recordInstance == null) {
            return;
        }
        if(recordInstance.getState()==AudioRecord.STATE_UNINITIALIZED) {
            Log.d("AudioTest", "instance uninitialized");
            return;
        }
        if(recordInstance.getState()==AudioRecord.STATE_INITIALIZED) {
            recordInstance.stop();
            recordInstance.release();
        }
    }
}

public class RecordSignal implements Runnable {
    private boolean cancelled = false;
    public void run() {
        Looper.prepare();
        // We're important...android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
        int bufferRead = 0;
        byte[] inAudioBuffer;
        if (!RecorderSingleton.instance.start()) {
            return;
        }
        try {
            Log.d("audiotestActivity", "Recorder Started");
            while(isRecording) {
                inAudioBuffer = null;
                inAudioBuffer = new byte[bufferSize];
                bufferRead = RecorderSingleton.instance.read(inAudioBuffer);
                if (bufferRead == AudioRecord.ERROR_INVALID_OPERATION) {
                    throw new IllegalStateException("read() returned AudioRecord.ERROR_INVALID_OPERATION");
                } else if (bufferRead == AudioRecord.ERROR_BAD_VALUE) {
                    throw new IllegalStateException("read() returned AudioRecord.ERROR_BAD_VALUE");
                }
                queue.add(inAudioBuffer);
                semaphoreProcess.release();
            }
        } 
        finally {
            // Close resources...
            stop();
        }
        Looper.loop();
    }
    public void stop() {
        RecorderSingleton.instance.stop();
    }
    public void cancel() {
        setCancelled(true);
    }
    public boolean isCancelled() {
        return cancelled;
    }
    public void setCancelled(boolean cancelled) {
        this.cancelled = cancelled;
    }
}

public class ProcessSignal implements Runnable {
    public void run() {
        Looper.prepare();
//android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_DEFAULT);
        while(isProcessing) {
            try {
                semaphoreProcess.acquire();
                byte[] outAudioBuffer = new byte[frameLength*bytesPerSample*(bufferSize/(frameLength*bytesPerSample))];
                outAudioBuffer = queue.element();
                if(queue.size()>0) {
                    // do something, process your samples
                }
                queue.poll();
            } 
            catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Looper.loop();
    }
}

并简单地启动和停止:

public void startAudioTest() {
    if(recordSignalThread!=null) {
        recordSignalThread.stop();
        recordSignalThread.cancel();
        recordSignalThread = null;
    }
    if(processSignalThread!=null) {
        processSignalThread = null;
    }
    recordSignalThread = new RecordSignal();
    processSignalThread = new ProcessSignal();
    new Thread(recordSignalThread).start();
    new Thread(processSignalThread).start();
    isRecording = true;
    isProcessing = true;
}

public void stopAudioTest() {
    isRecording = false;
    isProcessing = false;
    if(processSignalThread!=null) {
        processSignalThread = null;
    }
    if(recordSignalThread!=null) {
        recordSignalThread.cancel();
        recordSignalThread = null;
    }
}

您的导航栏图标看起来像是您可能在 Android 5 上 运行,并且 Android 5.0 版本中存在一个错误,它可能会导致您遇到的问题正在看

短片记录在 L 预览中给出了错误的 return 值,并且在修复过程中大量修改代码时,他们在 5.0 版本中错误地将偏移量参数加倍。您的代码将索引增加它在每次调用中读取的(正确的)数量,但是音频内部的指针数学错误将使您传递的偏移量加倍,这意味着每个记录周期结束后跟一个相等的未写入周期 -缓冲,您将其视为那些零间隙。

问题已在 http://code.google.com/p/android/issues/detail?id=80866

报告

去年秋天那个时候提交的补丁被拒绝了,因为他们说他们已经在内部处理了。查看 AOSP 5.1 的 git 历史记录,这似乎是 11 月 13 日的内部提交 283a9d9e1,当我当月晚些时候遇到它时还没有 public。虽然我还没有在 5.1 上尝试过这个,但似乎应该修复它,所以它很可能在 5.0-5.02 中被破坏(并且在 L 预览中以不同的方式)但在 4.4 和更早版本中也能正常工作与 5.1 及更高版本一样。

在损坏和未损坏的发行版本之间保持一致行为的最简单解决方法是避免在记录短裤时传递非零偏移量 - 这就是我在遇到问题时修复程序的方式。一个更复杂的想法是尝试弄清楚您是否使用损坏的版本,如果是,则将传递的参数减半。一种方法是检测设备版本,但可以想象某些供应商或自定义 ROM 5.0 版本可能已打补丁,因此您可以更进一步,使用测试偏移量对零缓冲区进行简短记录,然后将其扫描到查看非零数据实际开始的位置。

不要按照接受的答案中的建议将一半的偏移量传递给读取函数。偏移量是一个整数,可能是一个奇数。这将导致音频质量不佳,并且与 android 5.0.1 以外的版本不兼容。和 5.0.2。我使用了以下解决方法,适用于所有 android 版本。我改变了:

short[] buffer = new short[frame_size*(frame_rate)]; 
num = record.read(buffer, offset, frame_size); 

进入

short[] buffer = new short[frame_size*(frame_rate)];
short[] buffer_bugfix = new short[frame_size]; 
num = record.read(buffer_bugfix, 0, frame_size);
System.arraycopy(buffer_bugfix, 0, buffer, offset, frame_size);

换句话说,不是让读取函数将数据复制到大缓冲区的偏移位置,而是让读取函数将数据复制到较小的缓冲区。然后我手动将这些数据插入到大缓冲区的偏移位置。