-
Notifications
You must be signed in to change notification settings - Fork 622
Samples: Add OboeDJ #2359
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Samples: Add OboeDJ #2359
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| # OboeDJ Sample | ||
|
|
||
| A high-performance, low-latency DJ sample application using the Oboe library. It features two independent playback decks, real-time audio scratching, and a Jetpack Compose UI. | ||
|
|
||
| ## Features | ||
|
|
||
| - **Dual Playback Decks**: Play two tracks simultaneously. | ||
| - **Audio Scratching**: Interactive vinyl wheels allow you to scratch audio in real-time. | ||
| - **BPM Synchronization**: Automatically sync the tempo of one deck to the other. | ||
| - **Track Queueing**: Load a third track into either deck dynamically. | ||
| - **Crossfader**: Smoothly mix between the left and right decks. | ||
|
|
||
| ## UI Overview | ||
|
|
||
| - **Vinyl Wheels**: Tap and drag to scratch. Release to resume normal playback. | ||
| - **Play/Pause Buttons**: Control playback for each deck. | ||
| - **Speed Sliders**: Adjust playback speed multiplier (0.5x to 2.0x). | ||
| - **Sync BPM Button**: Automatically calculates and sets the speed multiplier to match the other deck's tempo. | ||
| - **Crossfader**: Located at the bottom to mix between decks. | ||
| - **Queue Console**: Load the next track ("Window Seat") into Deck 1 or Deck 2. | ||
|
|
||
| ## Technical Details | ||
|
|
||
| - **Engine**: C++ `DJEngine` managing Oboe streams and mixing. | ||
| - **Interpolation**: `SoundPlayer` uses linear interpolation for smooth variable speed and reverse playback. | ||
| - **State Management**: Jetpack Compose state hoisting for synchronized UI/Audio engine states. | ||
|
|
||
| Images | ||
| ----------- | ||
|  |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| apply plugin: 'com.android.application' | ||
| apply plugin: 'kotlin-android' | ||
| apply plugin: 'org.jetbrains.kotlin.plugin.compose' | ||
|
|
||
| android { | ||
| compileSdkVersion 35 | ||
| defaultConfig { | ||
| applicationId 'com.google.oboe.samples.oboedj' | ||
| minSdkVersion 23 | ||
| targetSdkVersion 36 | ||
| compileSdkVersion 36 | ||
| versionCode 1 | ||
| versionName '1.0' | ||
| externalNativeBuild { | ||
| cmake { | ||
| cppFlags "-std=c++17" | ||
| arguments '-DANDROID_STL=c++_static' | ||
| abiFilters 'armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64' | ||
| } | ||
| } | ||
| } | ||
| buildTypes { | ||
| release { | ||
| minifyEnabled false | ||
| proguardFiles getDefaultProguardFile('proguard-android.txt'), | ||
| 'proguard-rules.pro' | ||
| } | ||
| } | ||
| compileOptions { | ||
| sourceCompatibility JavaVersion.VERSION_18 | ||
| targetCompatibility JavaVersion.VERSION_18 | ||
| } | ||
| kotlinOptions { | ||
| jvmTarget = "18" | ||
| } | ||
| externalNativeBuild { | ||
| cmake { | ||
| path 'src/main/cpp/CMakeLists.txt' | ||
| } | ||
| } | ||
| buildFeatures { | ||
| compose true | ||
| } | ||
| composeOptions { | ||
| kotlinCompilerExtensionVersion compose_version | ||
| } | ||
| namespace 'com.google.oboe.samples.oboedj' | ||
| } | ||
|
|
||
| dependencies { | ||
| implementation fileTree(include: ['*.jar'], dir: 'libs') | ||
| implementation project(':audio-device') | ||
| implementation project(':parselib') // We need parselib for WAV reading | ||
| implementation 'androidx.appcompat:appcompat:1.7.0' | ||
| implementation 'androidx.constraintlayout:constraintlayout:2.2.1' | ||
|
|
||
| // Compose | ||
| implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycle_version" | ||
| implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version" | ||
| implementation "androidx.compose.ui:ui:$compose_version" | ||
| implementation "androidx.compose.material3:material3:1.3.2" | ||
| implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" | ||
| implementation 'androidx.activity:activity-compose:1.10.1' | ||
| implementation 'androidx.compose.runtime:runtime-livedata:1.10.0' | ||
| } |
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| <?xml version="1.0" encoding="utf-8"?> | ||
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | ||
| <application | ||
| android:label="OboeDJ" | ||
| android:supportsRtl="true" | ||
| android:theme="@style/Theme.AppCompat.NoActionBar" | ||
| android:icon="@mipmap/ic_launcher"> | ||
| <activity | ||
| android:name=".MainActivity" | ||
| android:exported="true"> | ||
| <intent-filter> | ||
| <action android:name="android.intent.action.MAIN" /> | ||
| <category android:name="android.intent.category.LAUNCHER" /> | ||
| </intent-filter> | ||
| </activity> | ||
| </application> | ||
| </manifest> |
Binary file not shown.
Binary file not shown.
Binary file not shown.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| cmake_minimum_required(VERSION 3.22.1) | ||
|
|
||
| project(OboeDJ) | ||
|
|
||
| # Pull in required libs | ||
| set(PARSELIB_DIR ../../../../parselib) | ||
| set(IOLIB_DIR ../../../../iolib) | ||
| set(OBOE_DIR ../../../../../) | ||
|
|
||
| add_subdirectory(${OBOE_DIR} ./oboe-bin) | ||
|
|
||
| # include folders | ||
| include_directories( | ||
| ${OBOE_DIR}/include | ||
| ${OBOE_DIR}/samples/shared | ||
| ${PARSELIB_DIR}/src/main/cpp | ||
| ${IOLIB_DIR}/src/main/cpp | ||
| ${CMAKE_CURRENT_LIST_DIR} | ||
| ) | ||
|
|
||
| # Include parselib & iolib CMake to build the static libraries | ||
| include(${PARSELIB_DIR}/src/main/cpp/CMakeLists.txt) | ||
| include(${IOLIB_DIR}/src/main/cpp/CMakeLists.txt) | ||
|
|
||
| # App specific sources | ||
| set(APP_SOURCES | ||
| native-lib.cpp | ||
| DJEngine.cpp | ||
| ) | ||
|
|
||
| # Build the OboeDJ (native) library | ||
| add_library( | ||
| oboedj SHARED ${APP_SOURCES} | ||
| ) | ||
|
|
||
| # Enable optimization flags | ||
| target_compile_options(oboedj PRIVATE -Wall -Werror "$<$<CONFIG:RELEASE>:-O3 -ffast-math>") | ||
|
|
||
| target_link_libraries( | ||
| oboedj | ||
| -Wl,--whole-archive | ||
| parselib | ||
| iolib | ||
| -Wl,--no-whole-archive | ||
| oboe | ||
| log | ||
| ) | ||
|
|
||
| target_link_options(oboedj PRIVATE "-Wl,-z,max-page-size=16384") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| /* | ||
| * Copyright 2026 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 "DJEngine.h" | ||
| #include <android/log.h> | ||
|
|
||
| static const char* TAG = "DJEngine"; | ||
|
|
||
| namespace oboedj { | ||
|
|
||
| DJEngine::DJEngine() | ||
| : mCrossfader(0.5f) // Center | ||
| , mChannelCount(2) | ||
| , mSampleRate(48000) { // Default, will be updated | ||
|
|
||
| // Initialize two empty decks | ||
| mDecks.push_back(std::make_shared<Deck>(nullptr)); | ||
| mDecks.push_back(std::make_shared<Deck>(nullptr)); | ||
| } | ||
|
|
||
| DJEngine::~DJEngine() { | ||
| stopStream(); | ||
| } | ||
|
|
||
| bool DJEngine::openStream() { | ||
| std::lock_guard<std::mutex> lock(mLock); | ||
| if (mStream) return true; // Already open | ||
|
|
||
| oboe::AudioStreamBuilder builder; | ||
| builder.setDirection(oboe::Direction::Output) | ||
| ->setPerformanceMode(oboe::PerformanceMode::LowLatency) | ||
| ->setSharingMode(oboe::SharingMode::Exclusive) | ||
| ->setFormat(oboe::AudioFormat::Float) | ||
| ->setChannelCount(mChannelCount) | ||
| ->setDataCallback(this); | ||
|
|
||
| oboe::Result result = builder.openStream(mStream); | ||
| if (result != oboe::Result::OK) { | ||
| __android_log_print(ANDROID_LOG_ERROR, TAG, "Failed to open stream: %s", oboe::convertToText(result)); | ||
| return false; | ||
| } | ||
|
|
||
| mSampleRate = mStream->getSampleRate(); | ||
| __android_log_print(ANDROID_LOG_INFO, TAG, "Stream opened with sample rate: %d", mSampleRate); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| bool DJEngine::startStream() { | ||
| std::lock_guard<std::mutex> lock(mLock); | ||
| if (!mStream) return false; | ||
|
|
||
| if (mStream->getState() == oboe::StreamState::Started) return true; | ||
|
|
||
| oboe::Result result = mStream->requestStart(); | ||
| if (result != oboe::Result::OK) { | ||
| __android_log_print(ANDROID_LOG_ERROR, TAG, "Failed to start stream: %s", oboe::convertToText(result)); | ||
| return false; | ||
| } | ||
| return true; | ||
| } | ||
|
|
||
| void DJEngine::stopStream() { | ||
| std::lock_guard<std::mutex> lock(mLock); | ||
| if (mStream) { | ||
| mStream->stop(); | ||
| mStream->close(); | ||
| mStream.reset(); | ||
| } | ||
| } | ||
|
|
||
| void DJEngine::loadTrack(uint8_t* buffer, int32_t length, int32_t deckIndex) { | ||
| if (deckIndex < 0 || deckIndex >= mDecks.size()) return; | ||
|
|
||
| parselib::MemInputStream stream(buffer, length); | ||
| parselib::WavStreamReader reader(&stream); | ||
| reader.parse(); | ||
|
|
||
| auto sampleBuffer = std::make_shared<iolib::SampleBuffer>(); | ||
| sampleBuffer->loadSampleData(&reader); | ||
|
|
||
| // Statically resample to device rate if needed | ||
| sampleBuffer->resampleData(mSampleRate); | ||
|
|
||
| // Update the deck | ||
| mDecks[deckIndex] = std::make_shared<Deck>(sampleBuffer); | ||
| __android_log_print(ANDROID_LOG_INFO, TAG, "Loaded track for deck %d", deckIndex); | ||
| } | ||
|
|
||
| void DJEngine::setDeckSpeed(int32_t deckIndex, float speed) { | ||
| if (deckIndex >= 0 && deckIndex < mDecks.size()) { | ||
| mDecks[deckIndex]->setSpeed(speed); | ||
| } | ||
| } | ||
|
|
||
| void DJEngine::setDeckPlaying(int32_t deckIndex, bool isPlaying) { | ||
| if (deckIndex >= 0 && deckIndex < mDecks.size()) { | ||
| mDecks[deckIndex]->setPlaying(isPlaying); | ||
| } | ||
| } | ||
|
|
||
| void DJEngine::setCrossfader(float position) { | ||
| mCrossfader.store(position); | ||
| } | ||
|
|
||
| oboe::DataCallbackResult DJEngine::onAudioReady(oboe::AudioStream *oboeStream, | ||
| void *audioData, | ||
| int32_t numFrames) { | ||
|
|
||
| float* floatData = static_cast<float*>(audioData); | ||
| int32_t numChannels = oboeStream->getChannelCount(); | ||
|
|
||
| // Clear buffer first | ||
| memset(audioData, 0, numFrames * numChannels * sizeof(float)); | ||
|
|
||
| float crossfade = mCrossfader.load(); | ||
| float leftVolume = 1.0f - crossfade; | ||
| float rightVolume = crossfade; | ||
|
|
||
| // We need a temp buffer for each deck to scale volume | ||
| std::vector<float> tempBuffer(numFrames * numChannels, 0.0f); | ||
|
|
||
| for (size_t i = 0; i < mDecks.size(); ++i) { | ||
| if (mDecks[i]->isPlaying()) { | ||
| std::fill(tempBuffer.begin(), tempBuffer.end(), 0.0f); | ||
| mDecks[i]->renderAudio(tempBuffer.data(), numChannels, numFrames); | ||
|
|
||
| float volume = (i == 0) ? leftVolume : rightVolume; | ||
|
|
||
| for (int32_t j = 0; j < numFrames * numChannels; ++j) { | ||
| floatData[j] += tempBuffer[j] * volume; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return oboe::DataCallbackResult::Continue; | ||
| } | ||
|
|
||
| } // namespace oboedj |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| /* | ||
| * Copyright 2026 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 OBOEDJ_DJENGINE_H | ||
| #define OBOEDJ_DJENGINE_H | ||
|
|
||
| #include <oboe/Oboe.h> | ||
| #include <vector> | ||
| #include <memory> | ||
| #include <atomic> | ||
| #include <mutex> | ||
|
|
||
| // iolib/parselib includes | ||
| #include <player/SampleBuffer.h> | ||
| #include <stream/MemInputStream.h> | ||
| #include <wav/WavStreamReader.h> | ||
|
|
||
| #include "Deck.h" | ||
|
|
||
| namespace oboedj { | ||
|
|
||
| class DJEngine : public oboe::AudioStreamDataCallback { | ||
| public: | ||
| DJEngine(); | ||
| virtual ~DJEngine(); | ||
|
|
||
| bool openStream(); | ||
| bool startStream(); | ||
| void stopStream(); | ||
|
|
||
| void loadTrack(uint8_t* buffer, int32_t length, int32_t deckIndex); | ||
|
|
||
| void setDeckSpeed(int32_t deckIndex, float speed); | ||
| void setDeckPlaying(int32_t deckIndex, bool isPlaying); | ||
| void setCrossfader(float position); // 0.0 (Left) to 1.0 (Right) | ||
|
|
||
| // From AudioStreamDataCallback | ||
| oboe::DataCallbackResult onAudioReady(oboe::AudioStream *oboeStream, | ||
| void *audioData, | ||
| int32_t numFrames) override; | ||
|
|
||
| private: | ||
| std::shared_ptr<oboe::AudioStream> mStream; | ||
| std::vector<std::shared_ptr<Deck>> mDecks; | ||
|
|
||
| std::atomic<float> mCrossfader; // 0.0 to 1.0 | ||
| int32_t mChannelCount; | ||
| int32_t mSampleRate; | ||
|
|
||
| std::mutex mLock; // Protect stream operations | ||
| }; | ||
|
|
||
| } // namespace oboedj | ||
|
|
||
| #endif // OBOEDJ_DJENGINE_H |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since 3 places will use this now, maybe we have a small shared resource for these songs to be referenced from
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, let's do this in a follow-up CL since we are both modifying files