Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions samples/OboeDJ/README.md
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
-----------
![oboedj_image](oboedj_image.png)
65 changes: 65 additions & 0 deletions samples/OboeDJ/build.gradle
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'
}
Binary file added samples/OboeDJ/oboedj_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions samples/OboeDJ/src/main/AndroidManifest.xml
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 added samples/OboeDJ/src/main/assets/song1.wav

Copy link
Copy Markdown
Collaborator

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

Copy link
Copy Markdown
Collaborator Author

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

Binary file not shown.
Binary file added samples/OboeDJ/src/main/assets/song2.wav
Binary file not shown.
Binary file added samples/OboeDJ/src/main/assets/song3.wav
Binary file not shown.
49 changes: 49 additions & 0 deletions samples/OboeDJ/src/main/cpp/CMakeLists.txt
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")
152 changes: 152 additions & 0 deletions samples/OboeDJ/src/main/cpp/DJEngine.cpp
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
68 changes: 68 additions & 0 deletions samples/OboeDJ/src/main/cpp/DJEngine.h
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
Loading
Loading