From 4145456d2f0ec7ba138ecb3d9c154b2fe8788e97 Mon Sep 17 00:00:00 2001 From: verbiiyo Date: Thu, 20 Nov 2025 13:16:04 +0900 Subject: [PATCH 1/6] start audio --- ios/Audio/AudioEngine.swift | 70 ++++++++++++++++++++++++++ ios/Audio/FloatRingBuffer.swift | 51 +++++++++++++++++++ ios/Modules/AudioBridge.m | 7 +++ ios/Modules/AudioBridge.swift | 22 ++++++++ pycontroller/app/(tabs)/audio.tsx | 41 +++++++++++++-- pycontroller/src/audio/sineStreamer.js | 45 +++++++++++++++++ 6 files changed, 231 insertions(+), 5 deletions(-) create mode 100644 ios/Audio/AudioEngine.swift create mode 100644 ios/Audio/FloatRingBuffer.swift create mode 100644 ios/Modules/AudioBridge.m create mode 100644 ios/Modules/AudioBridge.swift create mode 100644 pycontroller/src/audio/sineStreamer.js diff --git a/ios/Audio/AudioEngine.swift b/ios/Audio/AudioEngine.swift new file mode 100644 index 00000000..73620c48 --- /dev/null +++ b/ios/Audio/AudioEngine.swift @@ -0,0 +1,70 @@ +import Foundation +import AVFoundation + +class AudioEngine { + static let shared = AudioEngine() + let ringBuffer = FloatRingBuffer(capacity: 48000 * 2) // ~2 seconds at 48kHz + private let engine = AVAudioEngine() + private let playerNode = AVAudioPlayerNode() + private let sampleRate: Double = 48000 + private let bufferSize: AVAudioFrameCount = 1024 + private var isPlaying = false + + private init() { + setupAudio() + } + + private func setupAudio() { + let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)! + engine.attach(playerNode) + engine.connect(playerNode, to: engine.mainMixerNode, format: format) + try? engine.start() + playerNode.play() + isPlaying = true + scheduleBuffers() + } + + private func scheduleBuffers() { + let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)! + func scheduleNext() { + guard isPlaying else { return } + let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: bufferSize)! + let frames = ringBuffer.read(count: Int(bufferSize)) + if frames.count < Int(bufferSize) { + // underrun: fill with silence + for i in frames.count.. Int { + return ringBuffer.count + } + + func play() { + if !isPlaying { + playerNode.play() + isPlaying = true + scheduleBuffers() + } + } + + func pause() { + if isPlaying { + playerNode.pause() + isPlaying = false + } + } +} diff --git a/ios/Audio/FloatRingBuffer.swift b/ios/Audio/FloatRingBuffer.swift new file mode 100644 index 00000000..5e67538d --- /dev/null +++ b/ios/Audio/FloatRingBuffer.swift @@ -0,0 +1,51 @@ +import Foundation + +class FloatRingBuffer { + private var buffer: [Float] + private let capacity: Int + private var writeIndex: Int = 0 + private var readIndex: Int = 0 + private var count_: Int = 0 + private let lock = DispatchSemaphore(value: 1) + + init(capacity: Int) { + self.capacity = capacity + self.buffer = [Float](repeating: 0, count: capacity) + } + + var count: Int { + return count_ + } + + func write(_ samples: [Float]) { + lock.wait() + defer { lock.signal() } + for sample in samples { + if count_ < capacity { + buffer[writeIndex] = sample + writeIndex = (writeIndex + 1) % capacity + count_ += 1 + } else { + // overflow: drop extra samples + break + } + } + } + + func read(count: Int) -> [Float] { + lock.wait() + defer { lock.signal() } + var out: [Float] = [] + for _ in 0.. 0 { + out.append(buffer[readIndex]) + readIndex = (readIndex + 1) % capacity + count_ -= 1 + } else { + // underrun: output silence + out.append(0.0) + } + } + return out + } +} diff --git a/ios/Modules/AudioBridge.m b/ios/Modules/AudioBridge.m new file mode 100644 index 00000000..00bea4e8 --- /dev/null +++ b/ios/Modules/AudioBridge.m @@ -0,0 +1,7 @@ +#import + +@interface RCT_EXTERN_MODULE(AudioBridge, NSObject) +RCT_EXTERN_METHOD(pushSamples:(NSArray *)samples) +RCT_EXTERN_METHOD(getBufferedSamples:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) ++ (BOOL)requiresMainQueueSetup { return NO; } +@end diff --git a/ios/Modules/AudioBridge.swift b/ios/Modules/AudioBridge.swift new file mode 100644 index 00000000..c5ef7d8d --- /dev/null +++ b/ios/Modules/AudioBridge.swift @@ -0,0 +1,22 @@ +import Foundation +import React + +@objc(AudioBridge) +class AudioBridge: NSObject { + @objc + func pushSamples(_ samples: [NSNumber]) { + let floatSamples = samples.map { $0.floatValue } + AudioEngine.shared.pushSamples(floatSamples) + } + + @objc + func getBufferedSamples(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + let count = AudioEngine.shared.getBufferedSamples() + resolve(count) + } + + @objc + static func requiresMainQueueSetup() -> Bool { + return false + } +} diff --git a/pycontroller/app/(tabs)/audio.tsx b/pycontroller/app/(tabs)/audio.tsx index 26ad0430..11f793c0 100644 --- a/pycontroller/app/(tabs)/audio.tsx +++ b/pycontroller/app/(tabs)/audio.tsx @@ -1,12 +1,43 @@ -import React from 'react'; -import { View, Text } from 'react-native'; +import React, { useState } from 'react'; +import { View, Text, TouchableOpacity } from 'react-native'; +import { startSineStream, stopSineStream } from '../../src/audio/sineStreamer'; + export default function AudioTab() { - // No way to detect audio devices without native code in React Native + const [playing, setPlaying] = useState(false); + + const handlePlay = () => { + startSineStream(); + setPlaying(true); + }; + + const handlePause = () => { + stopSineStream(); + setPlaying(false); + }; + return ( - Audio - Coming soon + Audio + + + Play + + + Pause + + + + {playing ? 'Sine waves streaming...' : 'Press Play to start audio'} + ); } diff --git a/pycontroller/src/audio/sineStreamer.js b/pycontroller/src/audio/sineStreamer.js new file mode 100644 index 00000000..d7803d17 --- /dev/null +++ b/pycontroller/src/audio/sineStreamer.js @@ -0,0 +1,45 @@ +import { NativeModules } from 'react-native'; +const { AudioBridge } = NativeModules; + +const SAMPLE_RATE = 48000; +const CHUNK_SIZE = 2048; +const FREQS = [440, 660]; +let phase = [0, 0]; + +function generateSineChunk() { + const chunk = new Float32Array(CHUNK_SIZE); + for (let i = 0; i < CHUNK_SIZE; i++) { + let sample = 0; + for (let j = 0; j < FREQS.length; j++) { + sample += Math.sin(phase[j]); + phase[j] += 2 * Math.PI * FREQS[j] / SAMPLE_RATE; + if (phase[j] > 2 * Math.PI) phase[j] -= 2 * Math.PI; + } + chunk[i] = sample / FREQS.length; + } + return chunk; +} + +let running = false; + +async function loop() { + if (!running) return; + const buffered = await AudioBridge.getBufferedSamples(); + const seconds = buffered / SAMPLE_RATE; + let timeout = 2; + if (seconds < 0.3) timeout = 0; + else if (seconds > 0.8) timeout = 10; + const chunk = generateSineChunk(); + AudioBridge.pushSamples(Array.from(chunk)); + setTimeout(loop, timeout); +} + +export function startSineStream() { + if (running) return; + running = true; + loop(); +} + +export function stopSineStream() { + running = false; +} From 3917cd1e9857d4501221a059477ad1f1c85102d7 Mon Sep 17 00:00:00 2001 From: verbiiyo Date: Thu, 20 Nov 2025 13:21:44 +0900 Subject: [PATCH 2/6] oops had to move it --- ios/Audio/FloatRingBuffer.swift | 51 -------------- ios/HelloModule.swift | 9 --- ios/HelloModuleBridge.m | 7 -- ios/Modules/AudioBridge.m | 7 -- ios/Modules/AudioBridge.swift | 22 ------ .../sensorlib/ios/AudioModule.swift | 70 ++++++++++++++++++- pycontroller/src/audio/sineStreamer.js | 10 ++- 7 files changed, 78 insertions(+), 98 deletions(-) delete mode 100644 ios/Audio/FloatRingBuffer.swift delete mode 100644 ios/HelloModule.swift delete mode 100644 ios/HelloModuleBridge.m delete mode 100644 ios/Modules/AudioBridge.m delete mode 100644 ios/Modules/AudioBridge.swift rename ios/Audio/AudioEngine.swift => pycontroller/sensorlib/ios/AudioModule.swift (54%) diff --git a/ios/Audio/FloatRingBuffer.swift b/ios/Audio/FloatRingBuffer.swift deleted file mode 100644 index 5e67538d..00000000 --- a/ios/Audio/FloatRingBuffer.swift +++ /dev/null @@ -1,51 +0,0 @@ -import Foundation - -class FloatRingBuffer { - private var buffer: [Float] - private let capacity: Int - private var writeIndex: Int = 0 - private var readIndex: Int = 0 - private var count_: Int = 0 - private let lock = DispatchSemaphore(value: 1) - - init(capacity: Int) { - self.capacity = capacity - self.buffer = [Float](repeating: 0, count: capacity) - } - - var count: Int { - return count_ - } - - func write(_ samples: [Float]) { - lock.wait() - defer { lock.signal() } - for sample in samples { - if count_ < capacity { - buffer[writeIndex] = sample - writeIndex = (writeIndex + 1) % capacity - count_ += 1 - } else { - // overflow: drop extra samples - break - } - } - } - - func read(count: Int) -> [Float] { - lock.wait() - defer { lock.signal() } - var out: [Float] = [] - for _ in 0.. 0 { - out.append(buffer[readIndex]) - readIndex = (readIndex + 1) % capacity - count_ -= 1 - } else { - // underrun: output silence - out.append(0.0) - } - } - return out - } -} diff --git a/ios/HelloModule.swift b/ios/HelloModule.swift deleted file mode 100644 index fb7ca673..00000000 --- a/ios/HelloModule.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Foundation - -@objc(HelloModule) -class HelloModule: NSObject { - @objc - func getHelloWorld(_ callback: @escaping RCTResponseSenderBlock) { - callback(["Hello world from Swift"]) - } -} diff --git a/ios/HelloModuleBridge.m b/ios/HelloModuleBridge.m deleted file mode 100644 index c3158efe..00000000 --- a/ios/HelloModuleBridge.m +++ /dev/null @@ -1,7 +0,0 @@ -#import - -@interface RCT_EXTERN_MODULE(HelloModule, NSObject) - -RCT_EXTERN_METHOD(getHelloWorld:(RCTResponseSenderBlock)callback) - -@end diff --git a/ios/Modules/AudioBridge.m b/ios/Modules/AudioBridge.m deleted file mode 100644 index 00bea4e8..00000000 --- a/ios/Modules/AudioBridge.m +++ /dev/null @@ -1,7 +0,0 @@ -#import - -@interface RCT_EXTERN_MODULE(AudioBridge, NSObject) -RCT_EXTERN_METHOD(pushSamples:(NSArray *)samples) -RCT_EXTERN_METHOD(getBufferedSamples:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) -+ (BOOL)requiresMainQueueSetup { return NO; } -@end diff --git a/ios/Modules/AudioBridge.swift b/ios/Modules/AudioBridge.swift deleted file mode 100644 index c5ef7d8d..00000000 --- a/ios/Modules/AudioBridge.swift +++ /dev/null @@ -1,22 +0,0 @@ -import Foundation -import React - -@objc(AudioBridge) -class AudioBridge: NSObject { - @objc - func pushSamples(_ samples: [NSNumber]) { - let floatSamples = samples.map { $0.floatValue } - AudioEngine.shared.pushSamples(floatSamples) - } - - @objc - func getBufferedSamples(_ resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { - let count = AudioEngine.shared.getBufferedSamples() - resolve(count) - } - - @objc - static func requiresMainQueueSetup() -> Bool { - return false - } -} diff --git a/ios/Audio/AudioEngine.swift b/pycontroller/sensorlib/ios/AudioModule.swift similarity index 54% rename from ios/Audio/AudioEngine.swift rename to pycontroller/sensorlib/ios/AudioModule.swift index 73620c48..e88e192a 100644 --- a/ios/Audio/AudioEngine.swift +++ b/pycontroller/sensorlib/ios/AudioModule.swift @@ -1,4 +1,21 @@ -import Foundation +import ExpoModulesCore + +public class AudioEngineModule: Module { + public func definition() -> ModuleDefinition { + Name("AudioEngine") + + AsyncFunction("pushSamples") { (samples: [Double]) in + let floatSamples = samples.map { Float($0) } + AudioEngine.shared.pushSamples(floatSamples) + } + + AsyncFunction("getBufferedSamples") { () -> Int in + return AudioEngine.shared.getBufferedSamples() + } + } +} + +// MARK: - AudioEngine Singleton import AVFoundation class AudioEngine { @@ -68,3 +85,54 @@ class AudioEngine { } } } + +// MARK: - Lock-free FloatRingBuffer +class FloatRingBuffer { + private var buffer: [Float] + private let capacity: Int + private var writeIndex: Int = 0 + private var readIndex: Int = 0 + private var count_: Int = 0 + private let lock = DispatchSemaphore(value: 1) + + init(capacity: Int) { + self.capacity = capacity + self.buffer = [Float](repeating: 0, count: capacity) + } + + var count: Int { + return count_ + } + + func write(_ samples: [Float]) { + lock.wait() + defer { lock.signal() } + for sample in samples { + if count_ < capacity { + buffer[writeIndex] = sample + writeIndex = (writeIndex + 1) % capacity + count_ += 1 + } else { + // overflow: drop extra samples + break + } + } + } + + func read(count: Int) -> [Float] { + lock.wait() + defer { lock.signal() } + var out: [Float] = [] + for _ in 0.. 0 { + out.append(buffer[readIndex]) + readIndex = (readIndex + 1) % capacity + count_ -= 1 + } else { + // underrun: output silence + out.append(0.0) + } + } + return out + } +} diff --git a/pycontroller/src/audio/sineStreamer.js b/pycontroller/src/audio/sineStreamer.js index d7803d17..ef5fb4a5 100644 --- a/pycontroller/src/audio/sineStreamer.js +++ b/pycontroller/src/audio/sineStreamer.js @@ -1,5 +1,12 @@ import { NativeModules } from 'react-native'; -const { AudioBridge } = NativeModules; + +let AudioBridge = null; +try { + AudioBridge = requireNativeModule('AudioBridge'); +} catch { + AudioBridge = null; +} + const SAMPLE_RATE = 48000; const CHUNK_SIZE = 2048; @@ -24,6 +31,7 @@ let running = false; async function loop() { if (!running) return; + const buffered = await AudioBridge.getBufferedSamples(); const seconds = buffered / SAMPLE_RATE; let timeout = 2; From 4d1a6e5da6c44cc234d1a53f813132149b41b06d Mon Sep 17 00:00:00 2001 From: verbiiyo Date: Thu, 20 Nov 2025 13:23:32 +0900 Subject: [PATCH 3/6] handle AudioEngine rename in ts --- pycontroller/src/audio/sineStreamer.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/pycontroller/src/audio/sineStreamer.js b/pycontroller/src/audio/sineStreamer.js index ef5fb4a5..9a218f88 100644 --- a/pycontroller/src/audio/sineStreamer.js +++ b/pycontroller/src/audio/sineStreamer.js @@ -1,11 +1,5 @@ -import { NativeModules } from 'react-native'; - -let AudioBridge = null; -try { - AudioBridge = requireNativeModule('AudioBridge'); -} catch { - AudioBridge = null; -} +import { requireNativeModule } from 'expo-modules-core'; +const AudioEngine = requireNativeModule('AudioEngine'); const SAMPLE_RATE = 48000; @@ -31,14 +25,13 @@ let running = false; async function loop() { if (!running) return; - - const buffered = await AudioBridge.getBufferedSamples(); + const buffered = await AudioEngine.getBufferedSamples(); const seconds = buffered / SAMPLE_RATE; let timeout = 2; if (seconds < 0.3) timeout = 0; else if (seconds > 0.8) timeout = 10; const chunk = generateSineChunk(); - AudioBridge.pushSamples(Array.from(chunk)); + AudioEngine.pushSamples(Array.from(chunk)); setTimeout(loop, timeout); } From e94f67f4e5bfa7a5619bd4b0ca9631b3293337cb Mon Sep 17 00:00:00 2001 From: verbiiyo Date: Thu, 20 Nov 2025 13:29:24 +0900 Subject: [PATCH 4/6] this may work? --- .../sensorlib/ios/SensorlibModule.swift | 17 +++++++++++------ pycontroller/src/audio/sineStreamer.js | 6 +++--- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/pycontroller/sensorlib/ios/SensorlibModule.swift b/pycontroller/sensorlib/ios/SensorlibModule.swift index 0a50a64d..f79644be 100644 --- a/pycontroller/sensorlib/ios/SensorlibModule.swift +++ b/pycontroller/sensorlib/ios/SensorlibModule.swift @@ -35,13 +35,10 @@ public class SensorlibModule: Module { // Unified haptics play function AsyncFunction("playHaptic") { (input: HapticPatternInput) in let engine = try HapticsEngineManager.shared.getEngine() - var events: [CHHapticEvent] = [] var curves: [CHHapticParameterCurve] = [] - let intensity = input.intensity ?? 1.0 let sharpness = input.sharpness ?? 0.5 - switch input.type { case "transient": let event = CHHapticEvent(eventType: .hapticTransient, @@ -63,7 +60,6 @@ public class SensorlibModule: Module { relativeTime: 0, duration: duration) events.append(event) - if let curvePoints = input.curve { var intensityCurvePoints: [CHHapticParameterCurve.ControlPoint] = [] var sharpnessCurvePoints: [CHHapticParameterCurve.ControlPoint] = [] @@ -83,14 +79,23 @@ public class SensorlibModule: Module { } } default: - throw HapticError.unknownType(input.type) + throw HapticError.unknownType(input.type) } - let pattern = try CHHapticPattern(events: events, parameterCurves: curves) let player = try engine.makePlayer(with: pattern) try player.start(atTime: 0) } + // Expose audio engine methods + AsyncFunction("pushAudioSamples") { (samples: [Double]) in + let floatSamples = samples.map { Float($0) } + AudioEngine.shared.pushSamples(floatSamples) + } + + AsyncFunction("getAudioBufferedSamples") { () -> Int in + return AudioEngine.shared.getBufferedSamples() + } + // Enables the module to be used as a native view. Definition components that are accepted as part of the // view definition: Prop, Events. View(SensorlibView.self) { diff --git a/pycontroller/src/audio/sineStreamer.js b/pycontroller/src/audio/sineStreamer.js index 9a218f88..3f23031a 100644 --- a/pycontroller/src/audio/sineStreamer.js +++ b/pycontroller/src/audio/sineStreamer.js @@ -1,5 +1,5 @@ import { requireNativeModule } from 'expo-modules-core'; -const AudioEngine = requireNativeModule('AudioEngine'); +const Sensorlib = requireNativeModule('Sensorlib'); const SAMPLE_RATE = 48000; @@ -25,13 +25,13 @@ let running = false; async function loop() { if (!running) return; - const buffered = await AudioEngine.getBufferedSamples(); + const buffered = await Sensorlib.getAudioBufferedSamples(); const seconds = buffered / SAMPLE_RATE; let timeout = 2; if (seconds < 0.3) timeout = 0; else if (seconds > 0.8) timeout = 10; const chunk = generateSineChunk(); - AudioEngine.pushSamples(Array.from(chunk)); + Sensorlib.pushAudioSamples(Array.from(chunk)); setTimeout(loop, timeout); } From db6d857d9d9e52f65f394a534a3e3bb6f019c03f Mon Sep 17 00:00:00 2001 From: verbiiyo Date: Thu, 20 Nov 2025 13:38:28 +0900 Subject: [PATCH 5/6] compiles --- .../sensorlib/ios/SensorlibModule.swift | 123 +++++++++++++++++- 1 file changed, 122 insertions(+), 1 deletion(-) diff --git a/pycontroller/sensorlib/ios/SensorlibModule.swift b/pycontroller/sensorlib/ios/SensorlibModule.swift index f79644be..918baeba 100644 --- a/pycontroller/sensorlib/ios/SensorlibModule.swift +++ b/pycontroller/sensorlib/ios/SensorlibModule.swift @@ -2,6 +2,8 @@ import CoreHaptics import CoreHaptics import ExpoModulesCore +import AVFoundation + // Error type for haptics enum HapticError: Error { case missingDuration @@ -105,8 +107,127 @@ public class SensorlibModule: Module { view.webView.load(URLRequest(url: url)) } } - Events("onLoad") } } } + +// MARK: - AudioEngine Singleton +class AudioEngine { + static let shared = AudioEngine() + let ringBuffer = FloatRingBuffer(capacity: 48000 * 2) // ~2 seconds at 48kHz + private let engine = AVAudioEngine() + private let playerNode = AVAudioPlayerNode() + private let sampleRate: Double = 48000 + private let bufferSize: AVAudioFrameCount = 1024 + private var isPlaying = false + + private init() { + setupAudio() + } + + private func setupAudio() { + let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)! + engine.attach(playerNode) + engine.connect(playerNode, to: engine.mainMixerNode, format: format) + try? engine.start() + playerNode.play() + isPlaying = true + scheduleBuffers() + } + + private func scheduleBuffers() { + let format = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1)! + func scheduleNext() { + guard isPlaying else { return } + let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: bufferSize)! + let frames = ringBuffer.read(count: Int(bufferSize)) + if frames.count < Int(bufferSize) { + // underrun: fill with silence + for i in frames.count.. Int { + return ringBuffer.count + } + + func play() { + if !isPlaying { + playerNode.play() + isPlaying = true + scheduleBuffers() + } + } + + func pause() { + if isPlaying { + playerNode.pause() + isPlaying = false + } + } +} + +// MARK: - Lock-free FloatRingBuffer +class FloatRingBuffer { + private var buffer: [Float] + private let capacity: Int + private var writeIndex: Int = 0 + private var readIndex: Int = 0 + private var count_: Int = 0 + private let lock = DispatchSemaphore(value: 1) + + init(capacity: Int) { + self.capacity = capacity + self.buffer = [Float](repeating: 0, count: capacity) + } + + var count: Int { + return count_ + } + + func write(_ samples: [Float]) { + lock.wait() + defer { lock.signal() } + for sample in samples { + if count_ < capacity { + buffer[writeIndex] = sample + writeIndex = (writeIndex + 1) % capacity + count_ += 1 + } else { + // overflow: drop extra samples + break + } + } + } + + func read(count: Int) -> [Float] { + lock.wait() + defer { lock.signal() } + var out: [Float] = [] + for _ in 0.. 0 { + out.append(buffer[readIndex]) + readIndex = (readIndex + 1) % capacity + count_ -= 1 + } else { + // underrun: output silence + out.append(0.0) + } + } + return out + } +} From a61fb62b3edf1ed9c19d79f97430a2d833533828 Mon Sep 17 00:00:00 2001 From: verbiiyo Date: Thu, 20 Nov 2025 13:43:45 +0900 Subject: [PATCH 6/6] just a flat wave --- pycontroller/src/audio/sineStreamer.js | 39 +++++++++++--------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/pycontroller/src/audio/sineStreamer.js b/pycontroller/src/audio/sineStreamer.js index 3f23031a..2e3e71ea 100644 --- a/pycontroller/src/audio/sineStreamer.js +++ b/pycontroller/src/audio/sineStreamer.js @@ -1,44 +1,37 @@ import { requireNativeModule } from 'expo-modules-core'; const Sensorlib = requireNativeModule('Sensorlib'); - const SAMPLE_RATE = 48000; const CHUNK_SIZE = 2048; -const FREQS = [440, 660]; -let phase = [0, 0]; +const FREQ = 440; +let phase = 0; +let running = false; -function generateSineChunk() { +function generateConstantChunk(value = 0.5) { const chunk = new Float32Array(CHUNK_SIZE); for (let i = 0; i < CHUNK_SIZE; i++) { - let sample = 0; - for (let j = 0; j < FREQS.length; j++) { - sample += Math.sin(phase[j]); - phase[j] += 2 * Math.PI * FREQS[j] / SAMPLE_RATE; - if (phase[j] > 2 * Math.PI) phase[j] -= 2 * Math.PI; - } - chunk[i] = sample / FREQS.length; + chunk[i] = value; } return chunk; } -let running = false; - -async function loop() { - if (!running) return; - const buffered = await Sensorlib.getAudioBufferedSamples(); - const seconds = buffered / SAMPLE_RATE; - let timeout = 2; - if (seconds < 0.3) timeout = 0; - else if (seconds > 0.8) timeout = 10; - const chunk = generateSineChunk(); +async function sineLoop() { + while (running) { + const buffered = await Sensorlib.getAudioBufferedSamples(); + const seconds = buffered / SAMPLE_RATE; + let timeout = 20; + if (seconds < 0.3) timeout = 10; + else if (seconds > 0.8) timeout = 40; + const chunk = generateConstantChunk(); Sensorlib.pushAudioSamples(Array.from(chunk)); - setTimeout(loop, timeout); + await new Promise(resolve => setTimeout(resolve, timeout)); + } } export function startSineStream() { if (running) return; running = true; - loop(); + sineLoop(); } export function stopSineStream() {