OboeTester: share WAVE from glitch test
Toggle buttons.
MultiChannelRecording wraps and save last recorded audio.
diff --git a/apps/OboeTester/app/src/main/AndroidManifest.xml b/apps/OboeTester/app/src/main/AndroidManifest.xml
index a629a40..89a825d 100644
--- a/apps/OboeTester/app/src/main/AndroidManifest.xml
+++ b/apps/OboeTester/app/src/main/AndroidManifest.xml
@@ -1,8 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.google.sample.oboe.manualtest"
- android:versionCode="10"
- android:versionName="1.4.01">
+ android:versionCode="10=1"
+ android:versionName="1.4.02">
<!-- versionCode and versionName also have to be updated in build.gradle -->
<uses-feature android:name="android.hardware.microphone" android:required="true" />
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
index 428ce5a..130479f 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.cpp
@@ -27,6 +27,10 @@
int numInputFrames,
void *outputData,
int numOutputFrames) {
+
+ int32_t inputStride = getInputStream()->getChannelCount();
+ int32_t outputStride = getOutputStream()->getChannelCount();
+
// TODO Pull up into superclass
// reset analyzer if we miss some input data
if (numInputFrames < numOutputFrames) {
@@ -36,9 +40,8 @@
} else {
float *inputFloat = (float *) inputData;
float *outputFloat = (float *) outputData;
- int32_t outputStride = getOutputStream()->getChannelCount();
- (void) getLoopbackProcessor()->process(inputFloat, getInputStream()->getChannelCount(),
+ (void) getLoopbackProcessor()->process(inputFloat, inputStride,
outputFloat, outputStride,
numOutputFrames);
@@ -50,5 +53,23 @@
memset(outputFloat, 0, framesLeft * getOutputStream()->getBytesPerFrame());
}
}
+
+ // write the first channel of output and input to the recorder
+ if (mRecording != nullptr) {
+ float buffer[2];
+ float *inputFloat = (float *) inputData;
+ float *outputFloat = (float *) outputData;
+ for (int i = 0; i < numOutputFrames; i++) {
+ buffer[0] = *outputFloat;
+ outputFloat += outputStride;
+ if (i < numInputFrames) {
+ buffer[1] = *inputFloat;
+ inputFloat += inputStride;
+ } else {
+ buffer[1] = 0.0f;
+ }
+ mRecording->write(buffer, 1);
+ }
+ }
return oboe::DataCallbackResult::Continue;
};
diff --git a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
index 0b7ce9f..b323ffb 100644
--- a/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
+++ b/apps/OboeTester/app/src/main/cpp/FullDuplexAnalyzer.h
@@ -23,6 +23,7 @@
#include "oboe/Oboe.h"
#include "FullDuplexStream.h"
#include "LatencyAnalyzer.h"
+#include "MultiChannelRecording.h"
class FullDuplexAnalyzer : public FullDuplexStream {
public:
@@ -47,6 +48,13 @@
virtual LoopbackProcessor *getLoopbackProcessor() = 0;
+ void setRecording(MultiChannelRecording *recording) {
+ mRecording = recording;
+ }
+
+private:
+ MultiChannelRecording *mRecording = nullptr;
+
};
diff --git a/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h b/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h
index 09d2acb..c2acd4a 100644
--- a/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h
+++ b/apps/OboeTester/app/src/main/cpp/MultiChannelRecording.h
@@ -21,6 +21,13 @@
#include <unistd.h>
#include <sys/types.h>
+/**
+ * Store multi-channel audio data in float format.
+ * The most recent data will be saved.
+ * Old data may be overwritten.
+ *
+ * Note that this is not thread safe. Do not read and write from separate threads.
+ */
class MultiChannelRecording {
public:
MultiChannelRecording(int32_t channelCount, int32_t maxFrames)
@@ -34,7 +41,12 @@
}
void rewind() {
- mCursorSample = 0;
+ mReadCursorFrames = mWriteCursorFrames - getSizeInFrames();
+ }
+
+ void clear() {
+ mReadCursorFrames = 0;
+ mWriteCursorFrames = 0;
}
int32_t getChannelCount() {
@@ -42,14 +54,14 @@
}
int32_t getSizeInFrames() {
- return mValidFrames;
+ return (int32_t) std::min(mWriteCursorFrames, static_cast<int64_t>(mMaxFrames));
}
- /**
- * @return pointer to data owned by this object
- */
- float *getData() {
- return mData;
+ int32_t getReadIndex() {
+ return mReadCursorFrames % mMaxFrames;
+ }
+ int32_t getWriteIndex() {
+ return mWriteCursorFrames % mMaxFrames;
}
/**
@@ -61,46 +73,52 @@
* @return number of frames actually written.
*/
int32_t write(int16_t *buffer, int32_t numFrames) {
- int32_t framesEmpty = mMaxFrames - mValidFrames;
- if (numFrames > framesEmpty) {
- numFrames = framesEmpty;
- }
- if (numFrames > 0) {
- int32_t numSamples = numFrames * mChannelCount;
- int32_t index = mValidFrames * mChannelCount;
+ int32_t framesLeft = numFrames;
+ while (framesLeft > 0) {
+ int32_t indexFrame = getWriteIndex();
+ // contiguous writes
+ int32_t framesToEnd = mMaxFrames - indexFrame;
+ int32_t framesNow = std::min(framesLeft, framesToEnd);
+ int32_t numSamples = framesNow * mChannelCount;
+ int32_t sampleIndex = indexFrame * mChannelCount;
+
for (int i = 0; i < numSamples; i++) {
- mData[index++] = buffer[i * mChannelCount] * (1.0f / 32768);
+ mData[sampleIndex++] = buffer[i * mChannelCount] * (1.0f / 32768);
}
- mValidFrames += numFrames;
+
+ mWriteCursorFrames += framesNow;
+ framesLeft -= framesNow;
}
return numFrames;
}
/**
- * Write numFrames from the float buffer into the recording, if there is room.
+ * Write all numFrames from the float buffer into the recording.
+ * Overwrite old data if full.
* @param buffer
* @param numFrames
* @return number of frames actually written.
*/
int32_t write(float *buffer, int32_t numFrames) {
- int32_t framesEmpty = mMaxFrames - mValidFrames;
- if (numFrames > framesEmpty) {
- numFrames = framesEmpty;
- }
- if (numFrames > 0) {
- int32_t numSamples = numFrames * mChannelCount;
- memcpy(mData + (mValidFrames * mChannelCount),
+ int32_t framesLeft = numFrames;
+ while (framesLeft > 0) {
+ int32_t indexFrame = getWriteIndex();
+ // contiguous writes
+ int32_t framesToEnd = mMaxFrames - indexFrame;
+ int32_t framesNow = std::min(framesLeft, framesToEnd);
+ int32_t numSamples = framesNow * mChannelCount;
+ int32_t sampleIndex = indexFrame * mChannelCount;
+
+ memcpy(&mData[sampleIndex],
buffer,
(numSamples * sizeof(float)));
- mValidFrames += numFrames;
+
+ mWriteCursorFrames += framesNow;
+ framesLeft -= framesNow;
}
return numFrames;
}
- float read() {
- return mData[mCursorSample++];
- }
-
/**
* Read numFrames from the recording into the buffer, if there is enough data.
* Start at the cursor position, aligned up to the next frame.
@@ -109,26 +127,32 @@
* @return number of frames actually read.
*/
int32_t read(float *buffer, int32_t numFrames) {
- // round up to nearest frame
- mCursorSample += mChannelCount - (mCursorSample % mChannelCount);
- int32_t framesLeft = mValidFrames - mCursorSample;
- if (numFrames > framesLeft) {
- numFrames = framesLeft;
- }
- if (numFrames > 0) {
- int32_t numSamples = numFrames * mChannelCount;
+ int32_t framesRead = 0;
+ int32_t framesLeft = std::min(numFrames,
+ std::min(mMaxFrames, (int32_t)(mWriteCursorFrames - mReadCursorFrames)));
+ while (framesLeft > 0) {
+ int32_t indexFrame = getReadIndex();
+ // contiguous reads
+ int32_t framesToEnd = mMaxFrames - indexFrame;
+ int32_t framesNow = std::min(framesLeft, framesToEnd);
+ int32_t numSamples = framesNow * mChannelCount;
+ int32_t sampleIndex = indexFrame * mChannelCount;
+
memcpy(buffer,
- &mData[mCursorSample],
+ &mData[sampleIndex],
(numSamples * sizeof(float)));
- mCursorSample += numSamples;
+
+ mReadCursorFrames += framesNow;
+ framesLeft -= framesNow;
+ framesRead += framesNow;
}
- return numFrames;
+ return framesRead;
}
private:
float *mData = nullptr;
- int32_t mValidFrames = 0;
- int32_t mCursorSample = 0;
+ int64_t mReadCursorFrames = 0;
+ int64_t mWriteCursorFrames = 0; // monotonically increasing
const int32_t mChannelCount;
const int32_t mMaxFrames;
};
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 3a64eb5..baf46d8 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -22,8 +22,6 @@
#include "NativeAudioContext.h"
-#define SECONDS_TO_RECORD 10
-
static oboe::AudioApi convertNativeApiToAudioApi(int nativeApi) {
switch (nativeApi) {
default:
@@ -229,7 +227,10 @@
mFramesPerBurst = oboeStream->getFramesPerBurst();
mSampleRate = oboeStream->getSampleRate();
+ createRecording();
+
finishOpen(isInput, oboeStream);
+
}
if (!useCallback) {
@@ -237,8 +238,6 @@
dataBuffer = std::make_unique<float[]>(numSamples);
}
- mRecording = std::make_unique<MultiChannelRecording>(mChannelCount,
- SECONDS_TO_RECORD * mSampleRate);
return ((int)result < 0) ? (int)result : streamIndex;
}
@@ -279,16 +278,19 @@
writer.setFrameRate(mSampleRate);
writer.setSamplesPerFrame(mRecording->getChannelCount());
writer.setBitsPerSample(24);
- int32_t numSamples = mRecording->getSizeInFrames() * mRecording->getChannelCount();
+ float buffer[mRecording->getChannelCount()];
// Read samples from start to finish.
mRecording->rewind();
- for (int32_t i = 0; i < numSamples; i++) {
- writer.write(mRecording->read());
+ for (int32_t frameIndex = 0; frameIndex < mRecording->getSizeInFrames(); frameIndex++) {
+ mRecording->read(buffer, 1 /* numFrames */);
+ for (int32_t i = 0; i < mRecording->getChannelCount(); i++) {
+ writer.write(buffer[i]);
+ }
}
writer.close();
auto myfile = std::ofstream(filename, std::ios::out | std::ios::binary);
- myfile.write((char *)outStream.getData(), outStream.length());
+ myfile.write((char *) outStream.getData(), outStream.length());
myfile.close();
return outStream.length();
@@ -575,6 +577,7 @@
void ActivityGlitches::finishOpen(bool isInput, oboe::AudioStream *oboeStream) {
if (isInput) {
mFullDuplexGlitches->setInputStream(oboeStream);
+ mFullDuplexGlitches->setRecording(mRecording.get());
} else {
mFullDuplexGlitches->setOutputStream(oboeStream);
}
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index 5c895ee..9aadbb0 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -64,6 +64,8 @@
#define LIB_AAUDIO_NAME "libaaudio.so"
#define FUNCTION_IS_MMAP "AAudioStream_isMMapUsed"
+#define SECONDS_TO_RECORD 10
+
typedef struct AAudioStreamStruct AAudioStream;
/**
@@ -178,6 +180,11 @@
int32_t allocateStreamIndex();
void freeStreamIndex(int32_t streamIndex);
+ virtual void createRecording() {
+ mRecording = std::make_unique<MultiChannelRecording>(mChannelCount,
+ SECONDS_TO_RECORD * mSampleRate);
+ }
+
virtual void finishOpen(bool isInput, oboe::AudioStream *oboeStream) {}
virtual oboe::Result startStreams() = 0;
@@ -356,6 +363,12 @@
int32_t getResetCount() {
return getFullDuplexAnalyzer()->getLoopbackProcessor()->getResetCount();
}
+
+protected:
+ void createRecording() override {
+ mRecording = std::make_unique<MultiChannelRecording>(2, // output and input
+ SECONDS_TO_RECORD * mSampleRate);
+ }
};
/**
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
index b1d8a57..7a4a848 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AutoGlitchActivity.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.google.sample.oboe.manualtest;
import android.content.Intent;
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
index d9d8e39..293bfd7 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/BufferSizeView.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.google.sample.oboe.manualtest;
import android.content.Context;
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
index 718c44f..01e3f30 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/GlitchActivity.java
@@ -27,9 +27,10 @@
* Activity to measure the number of glitches.
*/
public class GlitchActivity extends AnalyzerActivity {
- TextView mAnalyzerTextView;
- Button mStartButton;
- Button mStopButton;
+ private TextView mAnalyzerTextView;
+ private Button mStartButton;
+ private Button mStopButton;
+ private Button mShareButton;
// These must match the values in LatencyAnalyzer.h
final static int STATE_IDLE = 0;
@@ -186,6 +187,8 @@
mStartButton = (Button) findViewById(R.id.button_start);
mStopButton = (Button) findViewById(R.id.button_stop);
mStopButton.setEnabled(false);
+ mShareButton = (Button) findViewById(R.id.button_share);
+ mShareButton.setEnabled(false);
mAnalyzerTextView = (TextView) findViewById(R.id.text_analyzer_result);
updateEnabledWidgets();
hideSettingsViews();
@@ -197,6 +200,7 @@
setActivityType(ACTIVITY_GLITCHES);
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
+ mShareButton.setEnabled(false);
}
@Override
@@ -210,6 +214,7 @@
startAudioTest();
mStartButton.setEnabled(false);
mStopButton.setEnabled(true);
+ mShareButton.setEnabled(false);
}
public void startAudioTest() {
@@ -233,6 +238,7 @@
public void onTestFinished() {
mStartButton.setEnabled(true);
mStopButton.setEnabled(false);
+ mShareButton.setEnabled(true);
}
public void stopAudioTest() {
@@ -257,4 +263,9 @@
public int getLastGlitchCount() {
return mGlitchSniffer.geMLastGlitchCount();
}
+
+ @Override
+ String getWaveTag() {
+ return "glitches";
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java
index 0b59181..a988b38 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/ManualGlitchActivity.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.google.sample.oboe.manualtest;
public class ManualGlitchActivity extends GlitchActivity {
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
index f050c70..a6336d7 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/RecorderActivity.java
@@ -91,7 +91,9 @@
}
- public void onShareFile(View view) {
- shareWaveFile();
+ @Override
+ String getWaveTag() {
+ return "recording";
}
+
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
index 1f4e506..077a4ab 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestInputActivity.java
@@ -27,6 +27,7 @@
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.FileProvider;
+import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
@@ -156,11 +157,15 @@
return result;
}
+ String getWaveTag() {
+ return "input";
+ }
+
@NonNull
private File createFileName() {
// Get directory and filename
File dir = getExternalFilesDir(Environment.DIRECTORY_MUSIC);
- return new File(dir, "oboe_recording.wav");
+ return new File(dir, "oboe_" + getWaveTag() + ".wav");
}
public void shareWaveFile() {
@@ -170,7 +175,7 @@
if (result > 0) {
Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
sharingIntent.setType("audio/wav");
- String subjectText = "OboeTester recording at " + getTimestampString();
+ String subjectText = "OboeTester " + getWaveTag() + " at " + getTimestampString();
sharingIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subjectText);
Uri uri = FileProvider.getUriForFile(this,
BuildConfig.APPLICATION_ID + ".provider",
@@ -181,4 +186,7 @@
}
}
+ public void onShareFile(View view) {
+ shareWaveFile();
+ }
}