mirror of
https://github.com/Cateners/tiny_computer.git
synced 2026-05-21 00:45:49 +08:00
Microphone support
This commit is contained in:
@@ -107,6 +107,12 @@ android {
|
||||
//checkReleaseBuilds false
|
||||
abortOnError false
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "src/main/cpp/CMakeLists.txt"
|
||||
version "3.22.1"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
pickFirst 'lib/arm64-v8a/libc++_shared.so'
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||
|
||||
<application
|
||||
|
||||
11
android/app/src/main/cpp/CMakeLists.txt
Normal file
11
android/app/src/main/cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
cmake_minimum_required(VERSION 3.18.1)
|
||||
project("native-socket")
|
||||
|
||||
# 添加库
|
||||
add_library(native-socket SHARED native-socket.cpp)
|
||||
|
||||
# 链接日志库
|
||||
find_library(log-lib log)
|
||||
|
||||
# 指定目标属性
|
||||
target_link_libraries(native-socket ${log-lib})
|
||||
78
android/app/src/main/cpp/native-socket.cpp
Normal file
78
android/app/src/main/cpp/native-socket.cpp
Normal file
@@ -0,0 +1,78 @@
|
||||
#include <jni.h>
|
||||
#include <string>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
#include <android/log.h>
|
||||
#include <cerrno>
|
||||
|
||||
#define LOG_TAG "NativeAudio"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||
|
||||
int server_fd = -1;
|
||||
int client_fd = -1;
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_example_tiny_1computer_AudioStream_nativeInit(JNIEnv *env, jobject thiz, jstring path) {
|
||||
const char *socket_path = env->GetStringUTFChars(path, 0);
|
||||
|
||||
server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (server_fd == -1) {
|
||||
LOGE("Socket creation failed");
|
||||
return -1;
|
||||
}
|
||||
|
||||
struct sockaddr_un addr;
|
||||
memset(&addr, 0, sizeof(addr));
|
||||
addr.sun_family = AF_UNIX;
|
||||
strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
|
||||
unlink(socket_path); // Remove existing file if any
|
||||
|
||||
if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) == -1) {
|
||||
LOGE("Bind failed: %s", strerror(errno));
|
||||
close(server_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (listen(server_fd, 1) == -1) {
|
||||
LOGE("Listen failed");
|
||||
close(server_fd);
|
||||
return -1;
|
||||
}
|
||||
|
||||
env->ReleaseStringUTFChars(path, socket_path);
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_example_tiny_1computer_AudioStream_nativeAccept(JNIEnv *env, jobject thiz) {
|
||||
if (server_fd == -1) return -1;
|
||||
// Blocks here until Linux connects
|
||||
client_fd = accept(server_fd, NULL, NULL);
|
||||
if (client_fd == -1) {
|
||||
LOGE("Accept failed: %s", strerror(errno));
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jint JNICALL
|
||||
Java_com_example_tiny_1computer_AudioStream_nativeSend(JNIEnv *env, jobject thiz, jbyteArray data, jint size) {
|
||||
if (client_fd == -1) return -1;
|
||||
|
||||
jbyte *buffer = env->GetByteArrayElements(data, NULL);
|
||||
ssize_t sent = write(client_fd, buffer, size);
|
||||
env->ReleaseByteArrayElements(data, buffer, JNI_ABORT);
|
||||
|
||||
if (sent == -1 && errno != EAGAIN) {
|
||||
LOGE("Write failed (Broken Pipe?)");
|
||||
return -1;
|
||||
}
|
||||
return sent;
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_example_tiny_1computer_AudioStream_nativeClose(JNIEnv *env, jobject thiz) {
|
||||
if (client_fd != -1) { close(client_fd); client_fd = -1; }
|
||||
if (server_fd != -1) { close(server_fd); server_fd = -1; }
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package com.example.tiny_computer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioRecord
|
||||
import android.media.MediaRecorder
|
||||
import android.util.Log
|
||||
|
||||
object AudioStream {
|
||||
init {
|
||||
System.loadLibrary("native-socket")
|
||||
}
|
||||
|
||||
private var isStreaming = false
|
||||
private var recordingThread: Thread? = null
|
||||
|
||||
// Native functions
|
||||
private external fun nativeInit(path: String): Int
|
||||
private external fun nativeAccept(): Int
|
||||
private external fun nativeSend(data: ByteArray, size: Int): Int
|
||||
private external fun nativeClose()
|
||||
|
||||
@SuppressLint("MissingPermission") // Ensure RECORD_AUDIO is granted in Manifest
|
||||
fun startStreaming(path: String) {
|
||||
if (isStreaming) return
|
||||
isStreaming = true
|
||||
|
||||
recordingThread = Thread {
|
||||
// 1. Initialize Socket Server
|
||||
if (nativeInit(path) < 0) {
|
||||
Log.e("AudioStream", "Failed to bind socket")
|
||||
isStreaming = false
|
||||
return@Thread
|
||||
}
|
||||
|
||||
// 2. Wait for Linux client to connect (Blocking)
|
||||
Log.d("AudioStream", "Waiting for connection on $path...")
|
||||
if (nativeAccept() < 0) {
|
||||
Log.e("AudioStream", "Accept failed")
|
||||
isStreaming = false
|
||||
return@Thread
|
||||
}
|
||||
Log.d("AudioStream", "Client connected!")
|
||||
|
||||
// 3. Setup AudioRecord
|
||||
val sampleRate = 44100
|
||||
val bufferSize = AudioRecord.getMinBufferSize(
|
||||
sampleRate,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT
|
||||
)
|
||||
|
||||
val recorder = AudioRecord(
|
||||
MediaRecorder.AudioSource.MIC,
|
||||
sampleRate,
|
||||
AudioFormat.CHANNEL_IN_MONO,
|
||||
AudioFormat.ENCODING_PCM_16BIT,
|
||||
bufferSize
|
||||
)
|
||||
|
||||
val data = ByteArray(bufferSize)
|
||||
recorder.startRecording()
|
||||
|
||||
val discardMillis = 5000 // 丢弃前5秒
|
||||
val discardBytes = (sampleRate * 2 * discardMillis / 1000).toInt() // 16bit = 2字节
|
||||
var bytesRead = 0
|
||||
|
||||
// 先读取并丢弃初始数据
|
||||
while (bytesRead < discardBytes && isStreaming) {
|
||||
val readBytes = recorder.read(data, 0, minOf(bufferSize, discardBytes - bytesRead))
|
||||
if (readBytes > 0) {
|
||||
bytesRead += readBytes
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Streaming Loop
|
||||
while (isStreaming) {
|
||||
val readBytes = recorder.read(data, 0, bufferSize)
|
||||
if (readBytes > 0) {
|
||||
val sent = nativeSend(data, readBytes)
|
||||
if (sent < 0) break // Socket broken
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
recorder.stop()
|
||||
recorder.release()
|
||||
nativeClose()
|
||||
}
|
||||
recordingThread?.start()
|
||||
}
|
||||
|
||||
fun stopStreaming() {
|
||||
isStreaming = false
|
||||
nativeClose() // Unblocks the native Accept/Send if hung
|
||||
recordingThread?.join()
|
||||
recordingThread = null
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,12 @@ class MainActivity: FlutterActivity() {
|
||||
"getNativeLibraryPath" -> {
|
||||
result.success(getApplicationInfo().nativeLibraryDir)
|
||||
}
|
||||
"startStreaming" -> {
|
||||
AudioStream.startStreaming(call.argument("path")!!)
|
||||
}
|
||||
"stopStreaming" -> {
|
||||
AudioStream.stopStreaming()
|
||||
}
|
||||
else -> {
|
||||
// 不支持的方法名
|
||||
result.notImplemented()
|
||||
|
||||
Reference in New Issue
Block a user