AudioRecord creates spaces of zeros on Android 5.01

Using AudioRecord, I tried to write a test application to record a couple of seconds of audio to be displayed on the screen. However, I seem to be getting a repeating pattern of zero value areas as shown below. I'm not sure if this is normal behavior or a bug in my code.

zeros

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);
    }
}

      

+3


source to share


3 answers


The navigation bar icons look like you are probably running Android 5, and there is a bug in the Android 5.0 release that could cause exactly the problem you are seeing.

Writing to shorts gave an erroneous return value when previewing L and with significant code rework during the commit process that they mistakenly doubled the offset argument in 5.0. Your code increments the index by the (correct) amount it read in each call, but the pointer math error in the internal internal sounds doubles the offset you go through, which means that each recording period ends with an equal unwritten period, for the buffer you see like those spaces of zeros.

The issue has been posted at http://code.google.com/p/android/issues/detail?id=80866



The patch submitted at that time last fall was rejected because they said they had already dealt with it domestically. Looking at the git history for AOSP 5.1, which appears to have been an internal commit 283a9d9e1 from Nov 13, which was not open yet when I ran into this later this month. Although I haven't tried it on 5.1 yet, it looks like it should fix it, so it is most likely broken with 5.0-5.02 (and differently on L preview), but works correctly with 4.4 and earlier, as well as with 5.1 and later.

The simplest workaround for consistent behavior in broken and non-breaking release versions is to avoid passing a non-zero offset when writing shortcuts - as I fixed the program I ran into the problem with. The harder idea is to try and figure out if you have a broken version, and if so, halve the passed argument. One method is to detect the version of the device, but it is possible that some manufacturers or builds of ROM 5.0 could be patched, so you can go one step further and do a short write with a test offset for the zeroed buffer and then scan it to see below. where the non-null data actually starts.

+2


source


I can't test your code right now, but I can provide you with some sample code you can test:

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();
    }
}

      



and just start and just:

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;
    }
}

      

0


source


Don't skip half the bias of the read function as suggested in the accepted answer. The offset is an integer and may not be uniform. This will degrade the sound quality and will be incompatible with Android versions other than 5.0.1. and 5.0.2. I've used the following work which works for all android versions. I changed:

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

      

in

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);

      

In words, instead of letting the read function copy the data to the offset position of the large buffer, I let the read function copy the data into the smaller buffer. Then I insert this data manually at the offset position of the large buffer.

0


source







All Articles