Skip to content

Commit 0c7368f

Browse files
exzosexzos
authored andcommitted
feat: integrate miniaudio for android
1 parent c37ca71 commit 0c7368f

File tree

12 files changed

+12899
-31
lines changed

12 files changed

+12899
-31
lines changed

.java-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
17

android/CMakeLists.txt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,26 @@ cmake_minimum_required(VERSION 3.9.0)
44
set(PACKAGE_NAME test)
55
set(CMAKE_VERBOSE_MAKEFILE ON)
66
set(CMAKE_CXX_STANDARD 20)
7+
set(CACHE_DIR ${CMAKE_SOURCE_DIR}/build)
78

8-
# Define C++ library and add all sources
9-
add_library(${PACKAGE_NAME} SHARED src/main/cpp/cpp-adapter.cpp)
9+
file(GLOB_RECURSE CORE_SRC RELATIVE ${CMAKE_SOURCE_DIR} "./src/main/cpp/*.cpp")
10+
file(GLOB_RECURSE SOURCES RELATIVE ${CMAKE_SOURCE_DIR} "../cpp/*.cpp")
11+
12+
add_library(
13+
${PACKAGE_NAME}
14+
SHARED
15+
${CORE_SRC}
16+
${SOURCES}
17+
)
1018

1119
# Add Nitrogen specs :)
1220
include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/test+autolinking.cmake)
1321

1422
# Set up local includes
1523
include_directories("src/main/cpp" "../cpp")
1624

25+
target_include_directories(${PACKAGE_NAME} PRIVATE "${CACHE_DIR}/download-cache")
26+
1727
find_library(LOG_LIB log)
1828

1929
# Link all libraries together

android/build.gradle

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,23 @@ android {
6363
}
6464
}
6565

66+
def miniaudioHeader = file("${layout.buildDirectory.asFile.get()}/download-cache/miniaudio.h")
67+
def miniaudioUrl = "https://raw.githubusercontent.com/mackron/miniaudio/master/miniaudio.h"
68+
69+
task downloadMiniaudioHeader {
70+
outputs.file miniaudioHeader
71+
doLast {
72+
if (!miniaudioHeader.exists()) {
73+
miniaudioHeader.parentFile.mkdirs()
74+
new URL(miniaudioUrl).withInputStream { input ->
75+
miniaudioHeader.withOutputStream { output -> output << input }
76+
}
77+
}
78+
}
79+
}
80+
81+
preBuild.dependsOn downloadMiniaudioHeader
82+
6683
packagingOptions {
6784
excludes = [
6885
"META-INF",
@@ -126,4 +143,3 @@ dependencies {
126143
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
127144
implementation project(":react-native-nitro-modules")
128145
}
129-

android/src/main/java/com/margelo/nitro/test/Test.kt

Lines changed: 0 additions & 10 deletions
This file was deleted.

android/src/main/java/com/test/TestPackage.kt renamed to android/src/main/java/com/margelo/nitro/test/TestPackage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.test
1+
package com.margelo.nitro.test
22

33
import com.facebook.react.TurboReactPackage
44
import com.facebook.react.bridge.NativeModule

cpp/HybridTest.cpp

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#include "HybridTest.hpp"
2+
#define MINIAUDIO_IMPLEMENTATION
3+
#include "miniaudio.h"
4+
5+
#include <cmath>
6+
#include <iostream>
7+
#include <algorithm>
8+
#include <android/log.h>
9+
10+
#define LOG_TAG "HybridTest"
11+
12+
using namespace margelo::nitro::test;
13+
14+
bool HybridTest::decodeAudioFile(const std::string& filePath, std::vector<float>& pcmData, unsigned int& sampleRate) {
15+
ma_decoder decoder;
16+
ma_decoder_config config = ma_decoder_config_init(ma_format_f32, 0, 0);
17+
ma_result file = ma_decoder_init_file(filePath.c_str(), &config, &decoder);
18+
if (file != MA_SUCCESS) {
19+
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "Failed to init decoder for %s (error %d)", filePath.c_str(), file);
20+
std::cerr << "Failed to init decoder for " << filePath << std::endl;
21+
return false;
22+
}
23+
24+
sampleRate = decoder.outputSampleRate;
25+
26+
ma_uint64 totalFrames = 0;
27+
if (ma_decoder_get_length_in_pcm_frames(&decoder, &totalFrames) != MA_SUCCESS) {
28+
std::cerr << "Failed to get length in pcm frames" << std::endl;
29+
ma_decoder_uninit(&decoder);
30+
return false;
31+
}
32+
33+
size_t totalSamples = static_cast<size_t>(totalFrames * decoder.outputChannels);
34+
pcmData.resize(totalSamples);
35+
36+
ma_uint64 framesRead = 0;
37+
ma_result result = ma_decoder_read_pcm_frames(&decoder, pcmData.data(), totalFrames, &framesRead);
38+
ma_decoder_uninit(&decoder);
39+
40+
if (result != MA_SUCCESS) {
41+
std::cerr << "Failed to read PCM frames" << std::endl;
42+
return false;
43+
}
44+
45+
if (framesRead != totalFrames) {
46+
std::cerr << "Warning: Frames read (" << framesRead << ") != total frames (" << totalFrames << ")" << std::endl;
47+
}
48+
49+
return framesRead > 0;
50+
}
51+
52+
53+
std::vector<double> HybridTest::computeAmplitude(const std::string& filePath, double outputSampleCount = 1000) {
54+
size_t outputSampleCountInt = static_cast<size_t>(outputSampleCount);
55+
std::vector<float> pcm;
56+
unsigned int sampleRate = 0;
57+
58+
// Decode audio file into PCM data
59+
if (!decodeAudioFile(filePath, pcm, sampleRate)) {
60+
__android_log_print(ANDROID_LOG_ERROR, LOG_TAG, "Failed to decode audio file for amplitude computation: %s", filePath.c_str());
61+
return {};
62+
}
63+
64+
std::vector<double> result;
65+
66+
size_t totalSamples = pcm.size();
67+
if (totalSamples == 0 || outputSampleCount == 0) {
68+
// Return empty result if no samples or zero output requested
69+
return result;
70+
}
71+
72+
// Calculate base block size and remainder to evenly split PCM into outputSampleCount blocks
73+
size_t baseBlockSize = totalSamples / outputSampleCount;
74+
size_t remainder = totalSamples % outputSampleCountInt;
75+
76+
size_t offset = 0;
77+
for (size_t i = 0; i < outputSampleCount; ++i) {
78+
// Distribute remainder samples to the first 'remainder' blocks for even split
79+
size_t currentBlockSize = baseBlockSize + (i < remainder ? 1 : 0);
80+
81+
if (currentBlockSize == 0) {
82+
// If block size is zero (e.g., outputSampleCount > totalSamples), push zero amplitude
83+
result.push_back(0.0);
84+
continue;
85+
}
86+
87+
double sum = 0.0;
88+
// Sum absolute values of samples in the current block
89+
for (size_t j = 0; j < currentBlockSize; ++j) {
90+
sum += std::abs(pcm[offset + j]);
91+
}
92+
// Compute average amplitude for the block
93+
double avg = sum / currentBlockSize;
94+
result.push_back(avg);
95+
96+
// Move offset forward by current block size
97+
offset += currentBlockSize;
98+
}
99+
100+
return result;
101+
}

cpp/HybridTest.hpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#include "HybridTestSpec.hpp"
2+
#include <string>
3+
#include <vector>
4+
5+
namespace margelo::nitro::test {
6+
struct HybridTest: public HybridTestSpec {
7+
HybridTest() : HybridObject(TAG) {}
8+
std::vector<double> computeAmplitude(const std::string& filePath, double samplesPerBlock) override;
9+
10+
bool decodeAudioFile(const std::string& filePath, std::vector<float>& pcmData, unsigned int& sampleRate);
11+
};
12+
}
13+

example/src/App.tsx

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,49 @@
1-
import { Text, View, StyleSheet } from 'react-native';
2-
import { multiply } from 'react-native-test';
3-
4-
const result = multiply(3, 7);
1+
import { useCallback, useEffect, useState } from 'react';
2+
import { View, StyleSheet, ScrollView } from 'react-native';
3+
import { computeAmplitude } from 'react-native-test';
54

65
export default function App() {
6+
const [data, setData] = useState<number[]>([]);
7+
const run = useCallback(async () => {
8+
try {
9+
const result = computeAmplitude(
10+
'/data/data/test.example/files/sample-15s.mp3',
11+
1000
12+
);
13+
setData(result);
14+
} catch (raw) {
15+
console.log(raw);
16+
}
17+
}, []);
18+
19+
useEffect(() => {
20+
// noinspection JSIgnoredPromiseFromCall
21+
run();
22+
}, [run]);
23+
724
return (
8-
<View style={styles.container}>
9-
<Text>Result: {result}</Text>
10-
</View>
25+
<ScrollView horizontal contentContainerStyle={styles.root}>
26+
<View style={styles.container}>
27+
{data.map((item, index) => (
28+
<View key={index} style={[styles.item, { height: 500 * item }]} />
29+
))}
30+
</View>
31+
</ScrollView>
1132
);
1233
}
1334

1435
const styles = StyleSheet.create({
36+
root: {
37+
flexGrow: 1,
38+
},
1539
container: {
1640
flex: 1,
41+
columnGap: 1,
42+
flexDirection: 'row',
1743
alignItems: 'center',
18-
justifyContent: 'center',
44+
},
45+
item: {
46+
width: 3,
47+
backgroundColor: 'red',
1948
},
2049
});

nitro.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"$schema": "https://nitro.margelo.com/nitro.schema.json",
23
"cxxNamespace": ["test"],
34
"ios": {
45
"iosModuleName": "Test"
@@ -9,8 +10,7 @@
910
},
1011
"autolinking": {
1112
"Test": {
12-
"swift": "Test",
13-
"kotlin": "Test"
13+
"cpp": "HybridTest"
1414
}
1515
},
1616
"ignorePaths": ["node_modules"]

src/Test.nitro.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { HybridObject } from 'react-native-nitro-modules';
22

3-
export interface Test
4-
extends HybridObject<{ ios: 'swift'; android: 'kotlin' }> {
5-
multiply(a: number, b: number): number;
3+
export interface Test extends HybridObject<{ ios: 'c++'; android: 'c++' }> {
4+
computeAmplitude(filePath: string, outputSampleCount: number): number[];
65
}

0 commit comments

Comments
 (0)