Merge branch 'master' into dturner-patch-1
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 4f65c2a..533ada1 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -18,6 +18,8 @@
src/opensles/EngineOpenSLES.cpp
src/opensles/OpenSLESUtilities.cpp
src/opensles/OutputMixerOpenSLES.cpp
+ src/common/StabilizedCallback.cpp
+ src/common/Trace.cpp
)
add_library(oboe STATIC ${oboe_sources})
diff --git a/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp b/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp
index ebb1d9d..3d9d012 100644
--- a/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp
+++ b/apps/OboeTester/app/src/main/cpp/AudioProcessorBase.cpp
@@ -32,7 +32,7 @@
AudioFloatPort::AudioFloatPort(AudioProcessorBase &parent, int samplesPerFrame)
: AudioPort(parent, samplesPerFrame), mFloatBuffer(NULL) {
int numFloats = MAX_BLOCK_SIZE * mSamplesPerFrame;
- mFloatBuffer = new float[numFloats];
+ mFloatBuffer = new float[numFloats]{};
}
AudioFloatPort::~AudioFloatPort() {
diff --git a/apps/OboeTester/app/src/main/cpp/FifoProcessor.h b/apps/OboeTester/app/src/main/cpp/FifoProcessor.h
index 37de256..a562b79 100644
--- a/apps/OboeTester/app/src/main/cpp/FifoProcessor.h
+++ b/apps/OboeTester/app/src/main/cpp/FifoProcessor.h
@@ -44,7 +44,7 @@
AudioResult onProcess(
uint64_t framePosition,
- int numFrames) {
+ int numFrames) override {
float *buffer = output.getFloatBuffer(numFrames);
return mFifoBuffer.readNow(buffer, numFrames);
}
diff --git a/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.cpp b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.cpp
new file mode 100644
index 0000000..ba5ad9f
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.cpp
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#include <unistd.h>
+#include "AudioProcessorBase.h"
+#include "ManyToMultiConverter.h"
+
+ManyToMultiConverter::ManyToMultiConverter(int32_t channelCount)
+ : inputs(channelCount)
+ , output(*this, channelCount) {
+ for (int i = 0; i < channelCount; i++) {
+ inputs[i] = std::make_unique<AudioInputPort>(*this, 1);
+ }
+}
+
+AudioResult ManyToMultiConverter::onProcess(
+ uint64_t framePosition,
+ int numFrames) {
+ int32_t channelCount = output.getSamplesPerFrame();
+
+ for (int i = 0; i < channelCount; i++) {
+ inputs[i]->pullData(framePosition, numFrames);
+ }
+
+
+ for (int ch = 0; ch < channelCount; ch++) {
+ const float *inputBuffer = inputs[ch]->getFloatBuffer(numFrames);
+ float *outputBuffer = output.getFloatBuffer(numFrames) + ch;
+
+ for (int i = 0; i < numFrames; i++) {
+ // read one, write into the proper interleaved output channel
+ float sample = *inputBuffer++;
+ *outputBuffer = sample;
+ outputBuffer += channelCount; // advance to next multichannel frame
+ }
+ }
+ return AUDIO_RESULT_SUCCESS;
+}
+
diff --git a/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.h b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.h
new file mode 100644
index 0000000..3cdf376
--- /dev/null
+++ b/apps/OboeTester/app/src/main/cpp/ManyToMultiConverter.h
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#ifndef OBOETESTER_MANY_TO_MULTI_CONVERTER_H
+#define OBOETESTER_MANY_TO_MULTI_CONVERTER_H
+
+#include <unistd.h>
+#include <sys/types.h>
+#include <vector>
+
+#include "AudioProcessorBase.h"
+
+/**
+ * Combine multiple mono inputs into one multi-channel output.
+ */
+class ManyToMultiConverter : public AudioProcessorBase {
+public:
+ explicit ManyToMultiConverter(int32_t channelCount);
+
+ virtual ~ManyToMultiConverter() = default;
+
+ AudioResult onProcess(
+ uint64_t framePosition,
+ int numFrames) override;
+
+ void setEnabled(bool enabled) {};
+
+ std::vector<std::unique_ptr<AudioInputPort> > inputs;
+ AudioOutputPort output;
+
+private:
+};
+
+#endif //OBOETESTER_MANY_TO_MULTI_CONVERTER_H
diff --git a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp
index 0f761dc..53746b4 100644
--- a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp
+++ b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.cpp
@@ -23,8 +23,6 @@
, output(*this, channelCount) {
}
-MonoToMultiConverter::~MonoToMultiConverter() { }
-
AudioResult MonoToMultiConverter::onProcess(
uint64_t framePosition,
int numFrames) {
diff --git a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h
index 6bae467..9132fa1 100644
--- a/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h
+++ b/apps/OboeTester/app/src/main/cpp/MonoToMultiConverter.h
@@ -14,23 +14,26 @@
* limitations under the License.
*/
-#ifndef NATIVEOBOE_MONO_TO_MULTI_CONVERTER_H
-#define NATIVEOBOE_MONO_TO_MULTI_CONVERTER_H
+#ifndef OBOETESTER_MONO_TO_MULTI_CONVERTER_H
+#define OBOETESTER_MONO_TO_MULTI_CONVERTER_H
#include <unistd.h>
#include <sys/types.h>
#include "AudioProcessorBase.h"
+/**
+ * Duplicate a single mono input across multiple channels of output.
+ */
class MonoToMultiConverter : AudioProcessorBase {
public:
explicit MonoToMultiConverter(int32_t channelCount);
- virtual ~MonoToMultiConverter();
+ virtual ~MonoToMultiConverter() = default;
AudioResult onProcess(
uint64_t framePosition,
- int numFrames);
+ int numFrames) override;
void setEnabled(bool enabled) {};
@@ -40,5 +43,4 @@
private:
};
-
-#endif //NATIVEOBOE_MONO_TO_MULTI_CONVERTER_H
+#endif //OBOETESTER_MONO_TO_MULTI_CONVERTER_H
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
index 93d573e..207c636 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.cpp
@@ -18,6 +18,11 @@
#define SECONDS_TO_RECORD 10
+
+NativeAudioContext::NativeAudioContext()
+ : sineGenerators(MAX_SINE_OSCILLATORS) {
+}
+
void NativeAudioContext::close() {
stopBlockingIOThread();
@@ -26,8 +31,9 @@
}
delete oboeStream;
oboeStream = nullptr;
- delete monoToMulti;
- monoToMulti = nullptr;
+ manyToMulti.reset(nullptr);
+ monoToMulti.reset(nullptr);
+ audioStreamGateway.reset(nullptr);
}
bool NativeAudioContext::isMMapUsed() {
@@ -58,18 +64,34 @@
LOGI("%s() mToneType = %d", __func__, mToneType);
switch (mToneType) {
case ToneType::SawPing:
- sawPingGenerator.output.connect(&monoToMulti->input);
+ sawPingGenerator.output.connect(&(monoToMulti->input));
+ monoToMulti->output.connect(&(audioStreamGateway.get()->input));
break;
case ToneType::Sine:
- sineGenerator.output.connect(&monoToMulti->input);
+ for (int i = 0; i < mChannelCount; i++) {
+ sineGenerators[i].output.connect(manyToMulti->inputs[i].get());
+ }
+ manyToMulti->output.connect(&(audioStreamGateway.get()->input));
break;
case ToneType::Impulse:
- impulseGenerator.output.connect(&monoToMulti->input);
+ impulseGenerator.output.connect(&(monoToMulti->input));
+ monoToMulti->output.connect(&(audioStreamGateway.get()->input));
break;
}
}
}
+void NativeAudioContext::setChannelEnabled(int channelIndex, bool enabled) {
+ if (manyToMulti == nullptr) {
+ return;
+ }
+ if (enabled) {
+ sineGenerators[channelIndex].output.connect(manyToMulti->inputs[channelIndex].get());
+ } else {
+ manyToMulti->inputs[channelIndex]->disconnect();
+ }
+}
+
int NativeAudioContext::open(jint sampleRate,
jint channelCount,
jint format,
@@ -128,10 +150,13 @@
SECONDS_TO_RECORD * mSampleRate);
mInputAnalyzer.setRecording(mRecording.get());
} else {
-
- sineGenerator.setSampleRate(oboeStream->getSampleRate());
- sineGenerator.frequency.setValue(440.0);
- sineGenerator.amplitude.setValue(AMPLITUDE_SINE);
+ double frequency = 440.0;
+ for (int i = 0; i < mChannelCount; i++) {
+ sineGenerators[i].setSampleRate(oboeStream->getSampleRate());
+ sineGenerators[i].frequency.setValue(frequency);
+ frequency *= 4.0 / 3.0; // each sine is a higher frequency
+ sineGenerators[i].amplitude.setValue(AMPLITUDE_SINE);
+ }
impulseGenerator.setSampleRate(oboeStream->getSampleRate());
impulseGenerator.frequency.setValue(440.0);
@@ -141,20 +166,19 @@
sawPingGenerator.frequency.setValue(FREQUENCY_SAW_PING);
sawPingGenerator.amplitude.setValue(AMPLITUDE_SAW_PING);
- monoToMulti = new MonoToMultiConverter(mChannelCount);
- connectTone();
+ manyToMulti = std::make_unique<ManyToMultiConverter>(mChannelCount);
+ monoToMulti = std::make_unique<MonoToMultiConverter>(mChannelCount);
// We needed the proxy because we did not know the channelCount
// when we setup the Builder.
audioStreamGateway = std::make_unique<AudioStreamGateway>(mChannelCount);
+ connectTone();
+
if (useCallback) {
oboeCallbackProxy.setCallback(audioStreamGateway.get());
}
- // Input will get connected by setToneType()
- monoToMulti->output.connect(&(audioStreamGateway.get()->input));
-
// Set starting size of buffer.
constexpr int kDefaultNumBursts = 2; // "double buffer"
int32_t numBursts = kDefaultNumBursts;
diff --git a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
index ccc9f46..e1324ca 100644
--- a/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
+++ b/apps/OboeTester/app/src/main/cpp/NativeAudioContext.h
@@ -19,6 +19,7 @@
#include <dlfcn.h>
#include <thread>
+#include <vector>
#include "common/OboeDebug.h"
#include "oboe/Oboe.h"
@@ -26,6 +27,7 @@
#include "AudioStreamGateway.h"
#include "ImpulseGenerator.h"
#include "InputStreamCallbackAnalyzer.h"
+#include "ManyToMultiConverter.h"
#include "MonoToMultiConverter.h"
#include "MultiChannelRecording.h"
#include "OboeStreamCallbackProxy.h"
@@ -33,8 +35,9 @@
#include "SawPingGenerator.h"
#include "SineGenerator.h"
+#define MAX_SINE_OSCILLATORS 8
#define AMPLITUDE_SINE 1.0
-#define FREQUENCY_SAW_PING 1200.0
+#define FREQUENCY_SAW_PING 800.0
#define AMPLITUDE_SAW_PING 1.0
#define AMPLITUDE_IMPULSE 0.7
@@ -53,6 +56,8 @@
class NativeAudioContext {
public:
+ NativeAudioContext();
+
void close();
bool isMMapUsed();
@@ -121,7 +126,9 @@
oboe::Result result1 = stopPlayback();
oboe::Result result2 = stopAudio();
- sineGenerator.stop();
+ for (int i = 0; i < mChannelCount; i++) {
+ sineGenerators[i].stop();
+ }
impulseGenerator.stop();
sawPingGenerator.stop();
if (audioStreamGateway != nullptr) {
@@ -137,8 +144,8 @@
builder.setChannelCount(mChannelCount)
->setSampleRate(mSampleRate)
->setFormat(oboe::AudioFormat::Float)
- ->setCallback(&mPlayRecordingCallback);
- // ->setAudioApi(oboe::AudioApi::OpenSLES);
+ ->setCallback(&mPlayRecordingCallback)
+ ->setAudioApi(oboe::AudioApi::OpenSLES);
oboe::Result result = builder.openStream(&playbackStream);
LOGD("NativeAudioContext::startPlayback() openStream() returned %d", result);
if (result != oboe::Result::OK) {
@@ -163,7 +170,9 @@
oboe::Result start() {
stop();
- sineGenerator.start();
+ for (int i = 0; i < mChannelCount; i++) {
+ sineGenerators[i].start();
+ }
impulseGenerator.start();
sawPingGenerator.start();
if (audioStreamGateway != nullptr) {
@@ -199,11 +208,15 @@
void setAmplitude(double amplitude) {
LOGD("%s(%f)", __func__, amplitude);
- sineGenerator.amplitude.setValue(amplitude);
+ for (int i = 0; i < mChannelCount; i++) {
+ sineGenerators[i].amplitude.setValue(amplitude);
+ }
sawPingGenerator.amplitude.setValue(amplitude);
impulseGenerator.amplitude.setValue(amplitude);
}
+ void setChannelEnabled(int channelIndex, bool enabled);
+
oboe::AudioStream *oboeStream = nullptr;
InputStreamCallbackAnalyzer mInputAnalyzer;
bool useCallback = true;
@@ -244,14 +257,16 @@
std::atomic<bool> threadEnabled{false};
std::thread *dataThread = nullptr;
- OboeStreamCallbackProxy oboeCallbackProxy;
- SineGenerator sineGenerator;
- ImpulseGenerator impulseGenerator;
- SawPingGenerator sawPingGenerator;
- MonoToMultiConverter *monoToMulti = nullptr;
- oboe::AudioStream *playbackStream = nullptr;
- std::unique_ptr<float> dataBuffer{};
+ OboeStreamCallbackProxy oboeCallbackProxy;
+ std::vector<SineGenerator> sineGenerators;
+ ImpulseGenerator impulseGenerator;
+ SawPingGenerator sawPingGenerator;
+ oboe::AudioStream *playbackStream = nullptr;
+
+ std::unique_ptr<float> dataBuffer{};
+ std::unique_ptr<ManyToMultiConverter> manyToMulti;
+ std::unique_ptr<MonoToMultiConverter> monoToMulti;
std::unique_ptr<AudioStreamGateway> audioStreamGateway{};
std::unique_ptr<MultiChannelRecording> mRecording{};
diff --git a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
index a47f31c..7e32478 100644
--- a/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
+++ b/apps/OboeTester/app/src/main/cpp/jni-bridge.cpp
@@ -392,4 +392,10 @@
}
+JNIEXPORT void JNICALL
+Java_com_google_sample_oboe_manualtest_OboeAudioOutputStream_setChannelEnabled(
+ JNIEnv *env, jobject, jint channelIndex, jboolean enabled) {
+ engine.setChannelEnabled(channelIndex, enabled);
+}
+
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java
index 4ac3cde..2d49ae8 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/AudioOutputTester.java
@@ -61,4 +61,7 @@
mCurrentAudioStream.setAmplitude(amplitude);
}
+ public void setChannelEnabled(int channelIndex, boolean enabled) {
+ mOboeAudioOutputStream.setChannelEnabled(channelIndex, enabled);
+ }
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java
index e63fff6..763e4c3 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/OboeAudioOutputStream.java
@@ -31,11 +31,12 @@
return false;
}
- public native void setToneEnabled(boolean b);
+ public native void setToneEnabled(boolean enabled);
public native void setToneType(int index);
@Override
public native void setAmplitude(double amplitude);
+ public native void setChannelEnabled(int channelIndex, boolean enabled);
}
diff --git a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
index 47d61e5..f0e5f64 100644
--- a/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
+++ b/apps/OboeTester/app/src/main/java/com/google/sample/oboe/manualtest/TestOutputActivity.java
@@ -17,13 +17,18 @@
package com.google.sample.oboe.manualtest;
import android.os.Bundle;
+import android.view.View;
+import android.widget.CheckBox;
import com.google.sample.oboe.manualtest.R;
/**
* Base class for output test activities
*/
-public class TestOutputActivity extends TestOutputActivityBase {
+public final class TestOutputActivity extends TestOutputActivityBase {
+
+ public static final int MAX_CHANNEL_BOXES = 8;
+ private CheckBox[] mChannelBoxes;
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -34,6 +39,31 @@
updateEnabledWidgets();
mAudioStreamTester = mAudioOutTester = AudioOutputTester.getInstance();
+
+ mChannelBoxes = new CheckBox[MAX_CHANNEL_BOXES];
+ int ic = 0;
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox0);
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox1);
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox2);
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox3);
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox4);
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox5);
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox6);
+ mChannelBoxes[ic++] = (CheckBox) findViewById(R.id.channelBox7);
+ configureChannelBoxes(0);
+ }
+
+ public void openAudio() {
+ super.openAudio();
+ int channelCount = mAudioOutTester.getCurrentAudioStream().getChannelCount();
+ configureChannelBoxes(channelCount);
+ }
+
+ private void configureChannelBoxes(int channelCount) {
+ for (int i = 0; i < mChannelBoxes.length; i++) {
+ mChannelBoxes[i].setChecked(i < channelCount);
+ mChannelBoxes[i].setEnabled(i < channelCount);
+ }
}
public void startAudio() {
@@ -47,4 +77,15 @@
super.stopAudio();
}
+ public void closeAudio() {
+ configureChannelBoxes(0);
+ super.closeAudio();
+ }
+
+ public void onChannelBoxClicked(View view) {
+ CheckBox checkBox = (CheckBox) view;
+ String text = (String) checkBox.getText();
+ int channelIndex = Integer.parseInt(text);
+ mAudioOutTester.setChannelEnabled(channelIndex, checkBox.isChecked());
+ }
}
diff --git a/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml b/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
index ff9f58b..63d0d48 100644
--- a/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
+++ b/apps/OboeTester/app/src/main/res/layout/activity_test_output.xml
@@ -12,4 +12,72 @@
<include layout="@layout/merge_audio_common"/>
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal">
+
+ <TextView
+ android:id="@+id/channelText"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Channels:" />
+
+ <CheckBox
+ android:id="@+id/channelBox0"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="0" />
+
+ <CheckBox
+ android:id="@+id/channelBox1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="1" />
+
+ <CheckBox
+ android:id="@+id/channelBox2"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="2" />
+
+ <CheckBox
+ android:id="@+id/channelBox3"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="3" />
+
+ <CheckBox
+ android:id="@+id/channelBox4"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="4" />
+
+ <CheckBox
+ android:id="@+id/channelBox5"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="5" />
+
+ <CheckBox
+ android:id="@+id/channelBox6"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="6" />
+
+ <CheckBox
+ android:id="@+id/channelBox7"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:onClick="onChannelBoxClicked"
+ android:text="7" />
+
+ </LinearLayout>
</LinearLayout>
diff --git a/docs/AppsUsingOboe.md b/docs/AppsUsingOboe.md
index ebfac25..af56804 100644
--- a/docs/AppsUsingOboe.md
+++ b/docs/AppsUsingOboe.md
@@ -67,3 +67,5 @@
FluidSynth - [App on Play Store](https://play.google.com/store/apps/details?id=net.volcanomobile.fluidsynthmidi)
OPL3 MIDI Synth FM - [App on Play Store](https://play.google.com/store/apps/details?id=net.volcanomobile.opl3midisynth)
+
+MIDI Sequencer app is very handy for testing. [App on Play Store](https://play.google.com/store/apps/details?id=net.volcanomobile.midisequencer)
diff --git a/docs/ChangeLog.md b/docs/ChangeLog.md
index cc2bf64..d206afa 100644
--- a/docs/ChangeLog.md
+++ b/docs/ChangeLog.md
@@ -1,6 +1,8 @@
# Changelog
-## 1.0
+**This changelog is deprecated**. See the [Oboe releases page](https://github.com/google/oboe/releases) for the contents of each release.
+
+## [1.0.0](https://github.com/google/oboe/releases/tag/1.0.0)
#### 2nd October 2018
First production ready version
diff --git a/docs/FAQ.md b/docs/FAQ.md
index 710dea9..5ae2fe3 100644
--- a/docs/FAQ.md
+++ b/docs/FAQ.md
@@ -12,3 +12,17 @@
Oboe requires a callback to get a low latency stream and that does not work well with Java.
Note that [`AudioTrack.PERFORMANCE_MODE_LOW_LATENCY`](https://developer.android.com/reference/android/media/AudioTrack#PERFORMANCE_MODE_LOW_LATENCY) was added in API 26, For API 24 or 25 use [`AudioAttributes.FLAG_LOW_LATENCY`](https://developer.android.com/reference/kotlin/android/media/AudioAttributes#flag_low_latency). That was deprecated but will still work with later APIs.
+
+## Can I use Oboe to play compressed audio files, such as MP3 or AAC?
+Oboe only works with PCM data. It does not include any extraction or decoding classes. For this you can use:
+
+1) [FFmpeg](https://www.ffmpeg.org/) - very fast decoding speeds, but can be difficult to configure and compile. [There's a good article on compiling FFmpeg 4.0 here](https://medium.com/@karthikcodes1999/cross-compiling-ffmpeg-4-0-for-android-b988326f16f2).
+2) [The NDK media classes](https://developer.android.com/ndk/reference/group/media), specifically `NdkMediaExtractor` and `NdkMediaCodec` - they're approximately 10X slower than FFmpeg but ship with Android. [Code sample here](https://github.com/googlesamples/android-ndk/tree/master/native-codec).
+
+If you don't need the lowest possible audio latency you may want to investigate using the following Java/Kotlin APIs which support playback of compressed audio files:
+
+- [MediaPlayer](https://developer.android.com/reference/android/media/MediaPlayer)
+- [SoundPool](https://developer.android.com/reference/android/media/SoundPool)
+
+## My question isn't listed, where can I ask it?
+Please ask questions on [Stack Overflow](https://stackoverflow.com/questions/ask) with the [Oboe tag](https://stackoverflow.com/tags/oboe).
\ No newline at end of file
diff --git a/docs/GettingStarted.md b/docs/GettingStarted.md
index 857bb32..2674e24 100644
--- a/docs/GettingStarted.md
+++ b/docs/GettingStarted.md
@@ -4,9 +4,9 @@
## Adding Oboe to your project
### 1. Clone the github repository
-Start by cloning the Oboe repository:
+Start by cloning the [latest stable release](https://github.com/google/oboe/releases/) of the Oboe repository, for example:
- git clone https://github.com/google/oboe
+ git clone -b 1.1-stable https://github.com/google/oboe
**Make a note of the path which you cloned oboe into - you will need it shortly**
diff --git a/include/oboe/Oboe.h b/include/oboe/Oboe.h
index e59fa89..c3a0bca 100644
--- a/include/oboe/Oboe.h
+++ b/include/oboe/Oboe.h
@@ -32,5 +32,6 @@
#include "oboe/AudioStreamBuilder.h"
#include "oboe/Utilities.h"
#include "oboe/Version.h"
+#include "oboe/StabilizedCallback.h"
#endif //OBOE_OBOE_H
diff --git a/include/oboe/StabilizedCallback.h b/include/oboe/StabilizedCallback.h
new file mode 100644
index 0000000..3f1a689
--- /dev/null
+++ b/include/oboe/StabilizedCallback.h
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#ifndef OBOE_STABILIZEDCALLBACK_H
+#define OBOE_STABILIZEDCALLBACK_H
+
+#include <cstdint>
+#include "oboe/AudioStream.h"
+
+namespace oboe {
+
+class StabilizedCallback : public AudioStreamCallback {
+
+public:
+ explicit StabilizedCallback(AudioStreamCallback *callback);
+
+ DataCallbackResult
+ onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override;
+
+ void onErrorBeforeClose(AudioStream *oboeStream, Result error) override {
+ return mCallback->onErrorBeforeClose(oboeStream, error);
+ }
+
+ void onErrorAfterClose(AudioStream *oboeStream, Result error) override {
+
+ // Reset all fields now that the stream has been closed
+ mFrameCount = 0;
+ mEpochTimeNanos = 0;
+ mOpsPerNano = 1;
+ return mCallback->onErrorAfterClose(oboeStream, error);
+ }
+
+private:
+
+ AudioStreamCallback *mCallback = nullptr;
+ int64_t mFrameCount = 0;
+ int64_t mEpochTimeNanos = 0;
+ double mOpsPerNano = 1;
+
+ void generateLoad(int64_t durationNanos);
+};
+
+/**
+ * cpu_relax is an architecture specific method of telling the CPU that you don't want it to
+ * do much work. asm volatile keeps the compiler from optimising these instructions out.
+ */
+#if defined(__i386__) || defined(__x86_64__)
+#define cpu_relax() asm volatile("rep; nop" ::: "memory");
+
+#elif defined(__arm__) || defined(__mips__)
+ #define cpu_relax() asm volatile("":::"memory")
+
+#elif defined(__aarch64__)
+#define cpu_relax() asm volatile("yield" ::: "memory")
+
+#else
+#error "cpu_relax is not defined for this architecture"
+#endif
+
+}
+
+#endif //OBOE_STABILIZEDCALLBACK_H
diff --git a/samples/MegaDrone/README.md b/samples/MegaDrone/README.md
index ae72768..5865fdb 100644
--- a/samples/MegaDrone/README.md
+++ b/samples/MegaDrone/README.md
@@ -12,6 +12,8 @@
4) Setting the buffer size to 2 bursts
5) Using the `-Ofast` compiler optimization flag, even when building the `Debug` variant
6) Using [`getExclusiveCores`](https://developer.android.com/reference/android/os/Process#getExclusiveCores()) (API 24+) and thread affinity to bind the audio thread to the best available CPU core(s)
+7) Using a `StabilizedCallback` which aims to spend a fixed percentage of the callback time to avoid CPU frequency scaling ([video explanation](https://www.youtube.com/watch?v=C0BPXZIvG-Q&feature=youtu.be&t=1158))
+
This code was presented at [AES Milan](http://www.aes.org/events/144/) and [Droidcon Berlin](https://www.de.droidcon.com/) as part of a talk on Oboe.
diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp b/samples/MegaDrone/src/main/cpp/AudioEngine.cpp
index 0471a12..2a19fde 100644
--- a/samples/MegaDrone/src/main/cpp/AudioEngine.cpp
+++ b/samples/MegaDrone/src/main/cpp/AudioEngine.cpp
@@ -25,7 +25,9 @@
mCpuIds = cpuIds;
AudioStreamBuilder builder;
- builder.setCallback(this);
+
+ mStabilizedCallback = new StabilizedCallback(this);
+ builder.setCallback(mStabilizedCallback);
builder.setPerformanceMode(PerformanceMode::LowLatency);
builder.setSharingMode(SharingMode::Exclusive);
diff --git a/samples/MegaDrone/src/main/cpp/AudioEngine.h b/samples/MegaDrone/src/main/cpp/AudioEngine.h
index e6e2cb8..98aa0c9 100644
--- a/samples/MegaDrone/src/main/cpp/AudioEngine.h
+++ b/samples/MegaDrone/src/main/cpp/AudioEngine.h
@@ -38,6 +38,7 @@
private:
+ StabilizedCallback *mStabilizedCallback = nullptr;
AudioStream *mStream = nullptr;
std::unique_ptr<ISynth> mSynth;
std::vector<int> mCpuIds; // IDs of CPU cores which the audio callback should be bound to
diff --git a/samples/RhythmGame/CMakeLists.txt b/samples/RhythmGame/CMakeLists.txt
index f8bc1fb..c5ae34e 100644
--- a/samples/RhythmGame/CMakeLists.txt
+++ b/samples/RhythmGame/CMakeLists.txt
@@ -12,7 +12,8 @@
src/main/cpp/Game.cpp
# audio engine
- src/main/cpp/audio/SoundRecording.cpp
+ src/main/cpp/audio/AAssetDataSource.cpp
+ src/main/cpp/audio/Player.cpp
src/main/cpp/audio/Mixer.cpp
# UI engine
diff --git a/samples/RhythmGame/src/main/cpp/Game.cpp b/samples/RhythmGame/src/main/cpp/Game.cpp
index 7d1798a..b5ee621 100644
--- a/samples/RhythmGame/src/main/cpp/Game.cpp
+++ b/samples/RhythmGame/src/main/cpp/Game.cpp
@@ -19,14 +19,29 @@
#include "Game.h"
-Game::Game(AAssetManager *assetManager): mAssetManager(assetManager) {
+Game::Game(AAssetManager &assetManager): mAssetManager(assetManager) {
}
void Game::start() {
// Load the RAW PCM data files for both the clap sound and backing track into memory.
- mClap = SoundRecording::loadFromAssets(mAssetManager, "CLAP.raw");
- mBackingTrack = SoundRecording::loadFromAssets(mAssetManager, "FUNKY_HOUSE.raw" );
+ std::shared_ptr<AAssetDataSource> mClapSource(AAssetDataSource::newFromAssetManager(mAssetManager,
+ "CLAP.raw",
+ oboe::ChannelCount::Stereo));
+ if (mClapSource == nullptr){
+ LOGE("Could not load source data for clap sound");
+ return;
+ }
+ mClap = std::make_shared<Player>(mClapSource);
+
+ std::shared_ptr<AAssetDataSource> mBackingTrackSource(AAssetDataSource::newFromAssetManager(mAssetManager,
+ "FUNKY_HOUSE.raw",
+ oboe::ChannelCount::Stereo));
+ if (mBackingTrackSource == nullptr){
+ LOGE("Could not load source data for backing track");
+ return;
+ }
+ mBackingTrack = std::make_shared<Player>(mBackingTrackSource);
mBackingTrack->setPlaying(true);
mBackingTrack->setLooping(true);
@@ -76,6 +91,15 @@
}
}
+void Game::stop(){
+
+ if (mAudioStream != nullptr){
+ mAudioStream->close();
+ delete mAudioStream;
+ mAudioStream = nullptr;
+ }
+}
+
void Game::tap(int64_t eventTimeAsUptime) {
mClap->setPlaying(true);
diff --git a/samples/RhythmGame/src/main/cpp/Game.h b/samples/RhythmGame/src/main/cpp/Game.h
index d707124..3ec955d 100644
--- a/samples/RhythmGame/src/main/cpp/Game.h
+++ b/samples/RhythmGame/src/main/cpp/Game.h
@@ -21,7 +21,8 @@
#include <oboe/Oboe.h>
#include "audio/Mixer.h"
-#include "audio/SoundRecording.h"
+#include "audio/Player.h"
+#include "audio/AAssetDataSource.h"
#include "ui/OpenGLFunctions.h"
#include "utils/LockFreeQueue.h"
#include "utils/UtilityFunctions.h"
@@ -31,9 +32,10 @@
class Game : public AudioStreamCallback {
public:
- explicit Game(AAssetManager *assetManager);
+ explicit Game(AAssetManager&);
void start();
+ void stop();
void onSurfaceCreated();
void onSurfaceDestroyed();
void onSurfaceChanged(int widthInPixels, int heightInPixels);
@@ -45,10 +47,10 @@
onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override;
private:
- AAssetManager *mAssetManager{nullptr};
+ AAssetManager& mAssetManager;
AudioStream *mAudioStream{nullptr};
- SoundRecording *mClap{nullptr};
- SoundRecording *mBackingTrack{nullptr};
+ std::shared_ptr<Player> mClap;
+ std::shared_ptr<Player> mBackingTrack;
Mixer mMixer;
LockFreeQueue<int64_t, kMaxQueueItems> mClapEvents;
@@ -56,6 +58,7 @@
LockFreeQueue<int64_t, kMaxQueueItems> mClapWindows;
LockFreeQueue<TapResult, kMaxQueueItems> mUiEvents;
std::atomic<int64_t> mLastUpdateTime { 0 };
+
};
diff --git a/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp
new file mode 100644
index 0000000..0d6dbc4
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.cpp
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+
+#include <utils/logging.h>
+#include "AAssetDataSource.h"
+
+
+AAssetDataSource* AAssetDataSource::newFromAssetManager(AAssetManager &assetManager,
+ const char *filename,
+ const int32_t channelCount) {
+
+ // Load the backing track
+ AAsset* asset = AAssetManager_open(&assetManager, filename, AASSET_MODE_BUFFER);
+
+ if (asset == nullptr){
+ LOGE("Failed to open track, filename %s", filename);
+ return nullptr;
+ }
+
+ // Get the length of the track (we assume it is stereo 48kHz)
+ off_t trackSizeInBytes = AAsset_getLength(asset);
+
+ // Load it into memory
+ auto *audioBuffer = static_cast<const int16_t*>(AAsset_getBuffer(asset));
+
+ if (audioBuffer == nullptr){
+ LOGE("Could not get buffer for track");
+ return nullptr;
+ }
+
+ auto numFrames = static_cast<int32_t>(trackSizeInBytes / (sizeof(int16_t) * channelCount));
+ LOGD("Opened audio data source, bytes: %ld frames: %d", trackSizeInBytes, numFrames);
+
+ return new AAssetDataSource(asset, audioBuffer, numFrames, channelCount);
+}
diff --git a/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h
new file mode 100644
index 0000000..dd64650
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/AAssetDataSource.h
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef RHYTHMGAME_AASSETDATASOURCE_H
+#define RHYTHMGAME_AASSETDATASOURCE_H
+
+#include <android/asset_manager.h>
+#include "DataSource.h"
+
+class AAssetDataSource : public DataSource {
+
+public:
+
+ ~AAssetDataSource(){
+
+ // Note that this will also delete the data at mBuffer
+ AAsset_close(mAsset);
+ }
+
+ int32_t getTotalFrames() const override { return mTotalFrames; } ;
+ int32_t getChannelCount() const override { return mChannelCount; } ;
+ const int16_t* getData() const override { return mBuffer; };
+
+ static AAssetDataSource* newFromAssetManager(AAssetManager&, const char *, const int32_t);
+
+private:
+
+ AAssetDataSource(AAsset *asset, const int16_t *data, int32_t frames,
+ int32_t channelCount)
+ : mAsset(asset)
+ , mBuffer(data)
+ , mTotalFrames(frames)
+ , mChannelCount(channelCount) {
+ };
+
+ AAsset *mAsset = nullptr;
+ const int16_t* mBuffer;
+ const int32_t mTotalFrames;
+ const int32_t mChannelCount;
+
+};
+#endif //RHYTHMGAME_AASSETDATASOURCE_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/DataSource.h b/samples/RhythmGame/src/main/cpp/audio/DataSource.h
new file mode 100644
index 0000000..6694399
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/DataSource.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#ifndef RHYTHMGAME_AUDIOSOURCE_H
+#define RHYTHMGAME_AUDIOSOURCE_H
+
+#include <cstdint>
+
+class DataSource {
+public:
+ virtual ~DataSource(){};
+ virtual int32_t getTotalFrames() const = 0;
+ virtual int32_t getChannelCount() const = 0;
+ virtual const int16_t* getData() const = 0;
+};
+
+
+#endif //RHYTHMGAME_AUDIOSOURCE_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp b/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp
index 0279617..b6f0028 100644
--- a/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp
+++ b/samples/RhythmGame/src/main/cpp/audio/Mixer.cpp
@@ -24,7 +24,7 @@
}
for (int i = 0; i < mNextFreeTrackIndex; ++i) {
- mTracks[i]->renderAudio(mixingBuffer, numFrames);
+ mTracks[i]->renderAudio(mixingBuffer.data(), numFrames);
for (int j = 0; j < numFrames * kChannelCount; ++j) {
audioData[j] += mixingBuffer[j];
@@ -32,6 +32,9 @@
}
}
-void Mixer::addTrack(RenderableAudio *renderer){
+void Mixer::addTrack(std::shared_ptr<RenderableAudio> renderer){
mTracks[mNextFreeTrackIndex++] = renderer;
+ // If we've reached our track limit then overwrite the first track
+ if (mNextFreeTrackIndex >= kMaxTracks)
+ mNextFreeTrackIndex = 0;
};
\ No newline at end of file
diff --git a/samples/RhythmGame/src/main/cpp/audio/Mixer.h b/samples/RhythmGame/src/main/cpp/audio/Mixer.h
index a101f62..b558390 100644
--- a/samples/RhythmGame/src/main/cpp/audio/Mixer.h
+++ b/samples/RhythmGame/src/main/cpp/audio/Mixer.h
@@ -18,7 +18,7 @@
#define RHYTHMGAME_MIXER_H
-#include "SoundRecording.h"
+#include "Player.h"
#include "RenderableAudio.h"
constexpr int32_t kBufferSize = 192*10; // Temporary buffer is used for mixing
@@ -28,15 +28,13 @@
class Mixer : public RenderableAudio {
public:
- void addTrack(RenderableAudio *renderer);
+ void addTrack(std::shared_ptr<RenderableAudio> renderer);
void renderAudio(int16_t *audioData, int32_t numFrames);
private:
-
- int16_t *mixingBuffer = new int16_t[kBufferSize]; // TODO: smart pointer
- RenderableAudio *mTracks[kMaxTracks]; // TODO: this might be better as a linked list for easy track removal
+ std::array<int16_t, kBufferSize> mixingBuffer;
+ std::shared_ptr<RenderableAudio> mTracks[kMaxTracks]; // TODO: this might be better as a linked list for easy track removal
uint8_t mNextFreeTrackIndex = 0;
};
-
#endif //RHYTHMGAME_MIXER_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/Player.cpp b/samples/RhythmGame/src/main/cpp/audio/Player.cpp
new file mode 100644
index 0000000..2ecd616
--- /dev/null
+++ b/samples/RhythmGame/src/main/cpp/audio/Player.cpp
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#include "Player.h"
+#include "utils/logging.h"
+
+void Player::renderAudio(int16_t *targetData, int32_t numFrames){
+
+ const int32_t channelCount = mSource->getChannelCount();
+
+ if (mIsPlaying){
+
+ int32_t framesToRenderFromData = numFrames;
+ int32_t totalSourceFrames = mSource->getTotalFrames();
+ const int16_t *data = mSource->getData();
+
+ // Check whether we're about to reach the end of the recording
+ if (!mIsLooping && mReadFrameIndex + numFrames >= totalSourceFrames){
+ framesToRenderFromData = totalSourceFrames - mReadFrameIndex;
+ mIsPlaying = false;
+ }
+
+ for (int i = 0; i < framesToRenderFromData; ++i) {
+ for (int j = 0; j < channelCount; ++j) {
+ targetData[(i*channelCount)+j] = data[(mReadFrameIndex*channelCount)+j];
+ }
+
+ // Increment and handle wraparound
+ if (++mReadFrameIndex >= totalSourceFrames) mReadFrameIndex = 0;
+ }
+
+ if (framesToRenderFromData < numFrames){
+ // fill the rest of the buffer with silence
+ renderSilence(&targetData[framesToRenderFromData], numFrames * channelCount);
+ }
+
+ } else {
+ renderSilence(targetData, numFrames * channelCount);
+ }
+}
+
+void Player::renderSilence(int16_t *start, int32_t numSamples){
+ for (int i = 0; i < numSamples; ++i) {
+ start[i] = 0;
+ }
+}
\ No newline at end of file
diff --git a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.h b/samples/RhythmGame/src/main/cpp/audio/Player.h
similarity index 72%
rename from samples/RhythmGame/src/main/cpp/audio/SoundRecording.h
rename to samples/RhythmGame/src/main/cpp/audio/Player.h
index f80f972..5ccdcd7 100644
--- a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.h
+++ b/samples/RhythmGame/src/main/cpp/audio/Player.h
@@ -27,29 +27,34 @@
#include <android/asset_manager.h>
#include "RenderableAudio.h"
+#include "DataSource.h"
-class SoundRecording : public RenderableAudio{
+class Player : public RenderableAudio{
public:
- SoundRecording(const int16_t *sourceData, int32_t numFrames)
- : mData(sourceData)
- , mTotalFrames(numFrames)
+ /**
+ * Construct a new Player from the given DataSource. Players can share the same data source.
+ * For example, you could play two identical sounds concurrently by creating 2 Players with the
+ * same data source.
+ *
+ * @param source
+ */
+ Player(std::shared_ptr<DataSource> source)
+ : mSource(source)
{};
+
void renderAudio(int16_t *targetData, int32_t numFrames);
void resetPlayHead() { mReadFrameIndex = 0; };
void setPlaying(bool isPlaying) { mIsPlaying = isPlaying; resetPlayHead(); };
void setLooping(bool isLooping) { mIsLooping = isLooping; };
- static SoundRecording * loadFromAssets(AAssetManager *assetManager, const char * filename);
-
private:
- int32_t mChannelCount = 2; // TODO: move this into a konstant and maybe add as parameter to ctor
int32_t mReadFrameIndex = 0;
- const int16_t* mData = nullptr;
- int32_t mTotalFrames = 0;
std::atomic<bool> mIsPlaying { false };
std::atomic<bool> mIsLooping { false };
+ std::shared_ptr<DataSource> mSource;
+ void renderSilence(int16_t*, int32_t);
};
#endif //RHYTHMGAME_SOUNDRECORDING_H
diff --git a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.cpp b/samples/RhythmGame/src/main/cpp/audio/SoundRecording.cpp
deleted file mode 100644
index 62e3af5..0000000
--- a/samples/RhythmGame/src/main/cpp/audio/SoundRecording.cpp
+++ /dev/null
@@ -1,74 +0,0 @@
-/*
- * Copyright 2018 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.
- */
-
-#include "SoundRecording.h"
-#include "utils/logging.h"
-
-void SoundRecording::renderAudio(int16_t *targetData, int32_t numFrames){
-
- if (mIsPlaying){
-
- // Check whether we're about to reach the end of the recording
- if (!mIsLooping && mReadFrameIndex + numFrames >= mTotalFrames){
- numFrames = mTotalFrames - mReadFrameIndex;
- mIsPlaying = false;
- }
-
- for (int i = 0; i < numFrames; ++i) {
- for (int j = 0; j < mChannelCount; ++j) {
- targetData[(i*mChannelCount)+j] = mData[(mReadFrameIndex*mChannelCount)+j];
- }
-
- // Increment and handle wraparound
- if (++mReadFrameIndex >= mTotalFrames) mReadFrameIndex = 0;
- }
-
- } else {
- // fill with zeros to output silence
- for (int i = 0; i < numFrames * mChannelCount; ++i) {
- targetData[i] = 0;
- }
- }
-}
-
-SoundRecording * SoundRecording::loadFromAssets(AAssetManager *assetManager, const char *filename) {
-
- // Load the backing track
- AAsset* asset = AAssetManager_open(assetManager, filename, AASSET_MODE_BUFFER);
-
- if (asset == nullptr){
- LOGE("Failed to open track, filename %s", filename);
- return nullptr;
- }
-
- // Get the length of the track (we assume it is stereo 48kHz)
- off_t trackLength = AAsset_getLength(asset);
-
- // Load it into memory
- const int16_t *audioBuffer = static_cast<const int16_t*>(AAsset_getBuffer(asset));
-
- if (audioBuffer == nullptr){
- LOGE("Could not get buffer for track");
- return nullptr;
- }
-
- // There are 4 bytes per frame because
- // each sample is 2 bytes and
- // it's a stereo recording which has 2 samples per frame.
- int32_t numFrames = static_cast<int32_t>(trackLength / 4);
- LOGD("Opened backing track, bytes: %ld frames: %d", trackLength, numFrames);
- return new SoundRecording(audioBuffer, numFrames);
-}
diff --git a/samples/RhythmGame/src/main/cpp/native-lib.cpp b/samples/RhythmGame/src/main/cpp/native-lib.cpp
index b7859ce..ecd941f 100644
--- a/samples/RhythmGame/src/main/cpp/native-lib.cpp
+++ b/samples/RhythmGame/src/main/cpp/native-lib.cpp
@@ -27,41 +27,58 @@
std::unique_ptr<Game> game;
JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_MainActivity_native_1onCreate(JNIEnv *env, jobject instance,
- jobject jAssetManager) {
+Java_com_google_oboe_sample_rhythmgame_MainActivity_native_1onStart(JNIEnv *env, jobject instance,
+ jobject jAssetManager) {
AAssetManager *assetManager = AAssetManager_fromJava(env, jAssetManager);
- game = std::make_unique<Game>(assetManager);
+ if (assetManager == nullptr) {
+ LOGE("Could not obtain the AAssetManager");
+ return;
+ }
+
+ game = std::make_unique<Game>(*assetManager);
game->start();
}
JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceCreated(JNIEnv *env, jobject instance) {
+Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceCreated(JNIEnv *env,
+ jobject instance) {
game->onSurfaceCreated();
}
JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceChanged(JNIEnv *env, jclass type,
- jint width, jint height) {
+Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onSurfaceChanged(JNIEnv *env,
+ jclass type,
+ jint width,
+ jint height) {
game->onSurfaceChanged(width, height);
}
JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onDrawFrame(JNIEnv *env, jclass type) {
+Java_com_google_oboe_sample_rhythmgame_RendererWrapper_native_1onDrawFrame(JNIEnv *env,
+ jclass type) {
game->tick();
}
JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1onTouchInput(JNIEnv *env, jclass type,
- jint event_type,
- jlong time_since_boot_ms,
- jint pixel_x, jint pixel_y) {
+Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1onTouchInput(JNIEnv *env,
+ jclass type,
+ jint event_type,
+ jlong time_since_boot_ms,
+ jint pixel_x,
+ jint pixel_y) {
game->tap(time_since_boot_ms);
}
JNIEXPORT void JNICALL
-Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1surfaceDestroyed__(JNIEnv *env, jclass type) {
+Java_com_google_oboe_sample_rhythmgame_GameSurfaceView_native_1surfaceDestroyed__(JNIEnv *env,
+ jclass type) {
game->onSurfaceDestroyed();
}
+JNIEXPORT void JNICALL
+Java_com_google_oboe_sample_rhythmgame_MainActivity_native_1onStop(JNIEnv *env, jobject instance) {
+
+ game->stop();
+}
}
\ No newline at end of file
diff --git a/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java b/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java
index 57d9a4a..266ec48 100644
--- a/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java
+++ b/samples/RhythmGame/src/main/java/com/google/oboe/sample/rhythmgame/MainActivity.java
@@ -33,8 +33,18 @@
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
- native_onCreate(getAssets());
}
- private native void native_onCreate(AssetManager assetManager);
+ protected void onStart(){
+ super.onStart();
+ native_onStart(getAssets());
+ }
+
+ protected void onStop(){
+ super.onStop();
+ native_onStop();
+ }
+
+ private native void native_onStart(AssetManager assetManager);
+ private native void native_onStop();
}
diff --git a/src/common/StabilizedCallback.cpp b/src/common/StabilizedCallback.cpp
new file mode 100644
index 0000000..7a0adba
--- /dev/null
+++ b/src/common/StabilizedCallback.cpp
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2018 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.
+ */
+
+#include "oboe/StabilizedCallback.h"
+#include "common/AudioClock.h"
+#include "common/Trace.h"
+
+constexpr int32_t kLoadGenerationStepSizeNanos = 20000;
+constexpr float kPercentageOfCallbackToUse = 0.8;
+
+using namespace oboe;
+
+StabilizedCallback::StabilizedCallback(AudioStreamCallback *callback) : mCallback(callback){
+ Trace::initialize();
+}
+
+/**
+ * An audio callback which attempts to do work for a fixed amount of time.
+ *
+ * @param oboeStream
+ * @param audioData
+ * @param numFrames
+ * @return
+ */
+DataCallbackResult
+StabilizedCallback::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) {
+
+ int64_t startTimeNanos = AudioClock::getNanoseconds();
+
+ if (mFrameCount == 0){
+ mEpochTimeNanos = startTimeNanos;
+ }
+
+ int64_t durationSinceEpochNanos = startTimeNanos - mEpochTimeNanos;
+
+ // In an ideal world the callback start time will be exactly the same as the duration of the
+ // frames already read/written into the stream. In reality the callback can start early
+ // or late. By finding the delta we can calculate the target duration for our stabilized
+ // callback.
+ int64_t idealStartTimeNanos = (mFrameCount * kNanosPerSecond) / oboeStream->getSampleRate();
+ int64_t lateStartNanos = durationSinceEpochNanos - idealStartTimeNanos;
+
+ if (lateStartNanos < 0){
+ // This was an early start which indicates that our previous epoch was a late callback.
+ // Update our epoch to this more accurate time.
+ mEpochTimeNanos = startTimeNanos;
+ mFrameCount = 0;
+ }
+
+ int64_t numFramesAsNanos = (numFrames * kNanosPerSecond) / oboeStream->getSampleRate();
+ int64_t targetDurationNanos = (int64_t)
+ (numFramesAsNanos * kPercentageOfCallbackToUse) - lateStartNanos;
+
+ Trace::beginSection("Actual load");
+ DataCallbackResult result = mCallback->onAudioReady(oboeStream, audioData, numFrames);
+ Trace::endSection();
+
+ int64_t executionDurationNanos = AudioClock::getNanoseconds() - startTimeNanos;
+ int64_t stabilizingLoadDurationNanos = targetDurationNanos - executionDurationNanos;
+
+ Trace::beginSection("Stabilized load for %lldns", stabilizingLoadDurationNanos);
+ generateLoad(stabilizingLoadDurationNanos);
+ Trace::endSection();
+
+ // Wraparound: At 48000 frames per second mFrameCount wraparound will occur after 6m years,
+ // significantly longer than the average lifetime of an Android phone.
+ mFrameCount += numFrames;
+ return result;
+}
+
+void StabilizedCallback::generateLoad(int64_t durationNanos) {
+
+ int64_t currentTimeNanos = AudioClock::getNanoseconds();
+ int64_t deadlineTimeNanos = currentTimeNanos + durationNanos;
+
+ // opsPerStep gives us an estimated number of operations which need to be run to fully utilize
+ // the CPU for a fixed amount of time (specified by kLoadGenerationStepSizeNanos).
+ // After each step the opsPerStep value is re-calculated based on the actual time taken to
+ // execute those operations.
+ auto opsPerStep = (int)(mOpsPerNano * kLoadGenerationStepSizeNanos);
+ int64_t stepDurationNanos = 0;
+ int64_t previousTimeNanos = 0;
+
+ while (currentTimeNanos <= deadlineTimeNanos){
+
+ for (int i = 0; i < opsPerStep; i++) cpu_relax();
+
+ previousTimeNanos = currentTimeNanos;
+ currentTimeNanos = AudioClock::getNanoseconds();
+ stepDurationNanos = currentTimeNanos - previousTimeNanos;
+
+ // Calculate exponential moving average to smooth out values, this acts as a low pass filter.
+ // @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average
+ static const float kFilterCoefficient = 0.1;
+ auto measuredOpsPerNano = (double) opsPerStep / stepDurationNanos;
+ mOpsPerNano = kFilterCoefficient * measuredOpsPerNano + (1.0 - kFilterCoefficient) * mOpsPerNano;
+ opsPerStep = (int) (mOpsPerNano * kLoadGenerationStepSizeNanos);
+ }
+}
\ No newline at end of file
diff --git a/src/common/Trace.cpp b/src/common/Trace.cpp
new file mode 100644
index 0000000..5ed445b
--- /dev/null
+++ b/src/common/Trace.cpp
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#include <dlfcn.h>
+#include <cstdio>
+#include "Trace.h"
+#include "OboeDebug.h"
+
+static char buffer[256];
+
+// Tracing functions
+static void *(*ATrace_beginSection)(const char *sectionName);
+
+static void *(*ATrace_endSection)();
+
+typedef void *(*fp_ATrace_beginSection)(const char *sectionName);
+
+typedef void *(*fp_ATrace_endSection)();
+
+bool Trace::mIsTracingSupported = false;
+
+void Trace::beginSection(const char *format, ...){
+
+ if (mIsTracingSupported) {
+ va_list va;
+ va_start(va, format);
+ vsprintf(buffer, format, va);
+ ATrace_beginSection(buffer);
+ va_end(va);
+ } else {
+ LOGE("Tracing is either not initialized (call Trace::initialize()) "
+ "or not supported on this device");
+ }
+}
+
+void Trace::endSection() {
+
+ if (mIsTracingSupported) {
+ ATrace_endSection();
+ }
+}
+
+void Trace::initialize() {
+
+ // Using dlsym allows us to use tracing on API 21+ without needing android/trace.h which wasn't
+ // published until API 23
+ void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL);
+ if (lib == nullptr) {
+ LOGE("Could not open libandroid.so to dynamically load tracing symbols");
+ } else {
+ ATrace_beginSection =
+ reinterpret_cast<fp_ATrace_beginSection >(
+ dlsym(lib, "ATrace_beginSection"));
+ ATrace_endSection =
+ reinterpret_cast<fp_ATrace_endSection >(
+ dlsym(lib, "ATrace_endSection"));
+
+ if (ATrace_beginSection != nullptr && ATrace_endSection != nullptr){
+ mIsTracingSupported = true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/common/Trace.h b/src/common/Trace.h
new file mode 100644
index 0000000..c7965f9
--- /dev/null
+++ b/src/common/Trace.h
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2018 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.
+ */
+
+#ifndef OBOE_TRACE_H
+#define OBOE_TRACE_H
+
+class Trace {
+
+public:
+ static void beginSection(const char *format, ...);
+ static void endSection();
+ static void initialize();
+
+private:
+ static bool mIsTracingSupported;
+};
+
+#endif //OBOE_TRACE_H
\ No newline at end of file