Files
袁智鸿 6314188835 prepare v1.1.4 release with native bridge and stability cleanups
- align Android/iOS native bridge implementations and audio handling paths
- improve Bluetooth provisioning/verification flow and related error handling
- refactor WebSocket, music, and device utility logic for more stable behavior
- clean up noisy debug logs and normalize comments across Flutter and native code
- update AR view, dance/agent/device pages, and platform integration details
2026-04-28 10:57:01 +08:00

187 lines
5.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
SPDX-FileCopyrightText: 2026 M5Stack Technology CO LTD
SPDX-License-Identifier: MIT
*/
import Foundation
import Flutter
import AVFoundation
class NativeBridge {
static let shared = NativeBridge()
private var channel: FlutterMethodChannel?
private var audioPlayChannel: FlutterBasicMessageChannel?
private weak var flutterViewController: FlutterViewController?
private let channelName = "com.m5stack.stackchan/native"
private let audioPlayChannelName = "com.m5stack.stackchan/audio_play"
private var audioEngine: AVAudioEngine?
private var audioPlayerNode: AVAudioPlayerNode?
private let sampleRate: Double = 16000.0
private let channels: AVAudioChannelCount = 1
private var isAudioInitialized = false
private let audioQueue = DispatchQueue(label: "com.stackchan.audio", qos: .userInitiated)
private let audioFormat: AVAudioFormat? = AVAudioFormat(
commonFormat: .pcmFormatFloat32,
sampleRate: 16_000,
channels: 1,
interleaved: true
)
private init() {}
func setup(with viewController: FlutterViewController) {
self.flutterViewController = viewController
let binaryMessenger = viewController.binaryMessenger
channel = FlutterMethodChannel(name: channelName, binaryMessenger: binaryMessenger)
audioPlayChannel = FlutterBasicMessageChannel(
name: audioPlayChannelName,
binaryMessenger: binaryMessenger,
codec: FlutterBinaryCodec()
)
audioPlayChannel?.setMessageHandler { [weak self] message, reply in
guard let self = self, let data = message as? Data else {
reply(nil)
return
}
self.audioQueue.async { [weak self] in
self?.playAudio(pcmData: data)
}
reply(nil)
}
}
private func playAudio(pcmData: Data) {
guard let audioFormat = audioFormat else {
return
}
if !isAudioInitialized {
guard setupAudioSession() else {
return
}
guard setupAudioEngine() else {
return
}
isAudioInitialized = true
}
guard let engine = audioEngine, let playerNode = audioPlayerNode else {
resetAudio()
return
}
if !engine.isRunning {
do {
try engine.start()
} catch {
resetAudio()
return
}
}
var floatBuffer = pcmData.withUnsafeBytes { (bytes: UnsafeRawBufferPointer) -> [Float] in
let int16Buffer = bytes.bindMemory(to: Int16.self)
var floats = [Float](repeating: 0, count: int16Buffer.count)
//3
for i in 0..<int16Buffer.count {
floats[i] = min(max(Float(int16Buffer[i]) / Float(Int16.max) * 3.0, -1.0), 1.0)
}
return floats
}
guard let buffer = AVAudioPCMBuffer(pcmFormat: audioFormat,
frameCapacity: AVAudioFrameCount(floatBuffer.count)) else { return }
buffer.frameLength = buffer.frameCapacity
memcpy(buffer.floatChannelData![0], &floatBuffer, floatBuffer.count * MemoryLayout<Float>.size)
playerNode.scheduleBuffer(buffer)
if !playerNode.isPlaying {
playerNode.play()
}
}
// MARK: - -50
private func setupAudioSession() -> Bool {
do {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playback, mode: .default)
try session.setActive(true)
return true
} catch {
let nsError = error as NSError
return false
}
}
// MARK: - -10868
private func setupAudioEngine() -> Bool {
guard let audioFormat = audioFormat else {
return false
}
let engine = AVAudioEngine()
let playerNode = AVAudioPlayerNode()
engine.attach(playerNode)
// translated comment
engine.connect(playerNode, to: engine.mainMixerNode, format: audioFormat)
do {
try engine.start()
} catch {
return false
}
self.audioEngine = engine
self.audioPlayerNode = playerNode
return true
}
private func resetAudio() {
audioPlayerNode?.stop()
audioEngine?.stop()
audioEngine = nil
audioPlayerNode = nil
isAudioInitialized = false
}
func stopPlayPCM() {
audioQueue.async { [weak self] in
self?.resetAudio()
}
}
func sendMessage(method: Method,_ arguments: Any? = nil,_ completion: ((Any?) -> Void)? = nil) {
guard method != .unknown else {
completion?(nil)
return
}
channel?.invokeMethod(method.rawValue, arguments: arguments) { result in
if let error = result as? FlutterError {
}
completion?(result)
}
}
func setMethodCallHandler(handler: @escaping FlutterMethodCallHandler) {
channel?.setMethodCallHandler(handler)
}
}
enum Method: String, CaseIterable {
case wifiName
case unknown
case stopPlayPCM
static func fromString(_ name: String) -> Method {
return Method(rawValue: name) ?? .unknown
}
}