Merge branch 'micDemo' into Integration

This commit is contained in:
TrueZhuanjia
2025-09-08 16:52:45 +08:00
24 changed files with 795 additions and 15 deletions
+6
View File
@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
1829CE3F2E2154DF0074A99A /* SceneDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */; };
18341A042DEB662C0094E5F2 /* FixedTintImageView.m in Sources */ = {isa = PBXBuildFile; fileRef = 18341A032DEB662C0094E5F2 /* FixedTintImageView.m */; };
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1850C8292E667E4000845308 /* MicHandler.swift */; };
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */ = {isa = PBXBuildFile; fileRef = 187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */; };
1888C1922E24C33100108730 /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1912E24C33100108730 /* AboutViewController.swift */; };
1888C1932E24C41000108730 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1888C1902E24C2C800108730 /* AboutView.swift */; };
@@ -155,6 +156,7 @@
1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = "<group>"; };
18341A032DEB662C0094E5F2 /* FixedTintImageView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FixedTintImageView.m; sourceTree = "<group>"; };
1850C8292E667E4000845308 /* MicHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MicHandler.swift; sourceTree = "<group>"; };
187F4B232E04633100EECC59 /* WidgetPanelStackView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WidgetPanelStackView.h; sourceTree = "<group>"; };
187F4B272E04658A00EECC59 /* WidgetPanelStackView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = WidgetPanelStackView.m; sourceTree = "<group>"; };
1888C1902E24C2C800108730 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
@@ -687,6 +689,7 @@
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
1850C8292E667E4000845308 /* MicHandler.swift */,
84E1CDEB2C4F67D300547904 /* CommandManager.swift */,
84203F222C5F24C00032EEED /* OnScreenWidgetView.swift */,
);
@@ -1014,6 +1017,7 @@
FB89462919F646E200339C8A /* mkcert.c in Sources */,
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */,
FB89463019F646E200339C8A /* StreamConfiguration.m in Sources */,
1850C82A2E667E4000845308 /* MicHandler.swift in Sources */,
FBD3495319FF36FB002D2A60 /* SWRevealViewController.m in Sources */,
84203F232C5F24C00032EEED /* OnScreenWidgetView.swift in Sources */,
97E21BB52E2186A3000D5DCA /* imgui_impl_metal.mm in Sources */,
@@ -1271,6 +1275,7 @@
"$(inherited)",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
"$(PROJECT_DIR)/libs/opus/lib",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
@@ -1336,6 +1341,7 @@
"$(inherited)",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-32",
"$(PROJECT_DIR)/third_party/imgui/examples/libs/glfw/lib-vc2010-64",
"$(PROJECT_DIR)/libs/opus/lib",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
+24 -4
View File
@@ -1012,6 +1012,24 @@
<constraint firstAttribute="width" constant="380" id="7VX-PK-miO"/>
</constraints>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="20" id="hcl-DT-peO" userLabel="redirectMicStack">
<rect key="frame" x="432" y="2215" width="280" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Redirect Mic (Experimental)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="jaM-yS-ldb" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="130" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9Gg-3U-Oa7" userLabel="redirectMicSwitch">
<rect key="frame" x="150" y="0.0" width="132" height="31"/>
<constraints>
<constraint firstAttribute="width" constant="130" id="lOz-Mp-9tm"/>
</constraints>
</switch>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
@@ -1074,6 +1092,8 @@
<outlet property="pointerVelocityDividerStack" destination="b1U-nD-gYE" id="ep1-K3-rKj"/>
<outlet property="pointerVelocityFactorStack" destination="vdk-mK-ztu" id="Cik-Rh-pGS"/>
<outlet property="pointerVelocityModeDividerSlider" destination="AcZ-2i-GI4" id="dGf-ZU-w3S"/>
<outlet property="redirectMicStack" destination="hcl-DT-peO" id="g8Y-dq-CiW"/>
<outlet property="redirectMicSwitch" destination="9Gg-3U-Oa7" id="39M-xy-hl8"/>
<outlet property="renderingBackendSelector" destination="53H-ST-OtY" id="jhu-Pw-Phg"/>
<outlet property="renderingBackendStack" destination="6hG-dw-YSr" id="qqz-lA-EOE"/>
<outlet property="resolutionSelector" destination="ckc-Dm-8ex" id="rl6-rx-wd3"/>
@@ -1790,7 +1810,7 @@
<image name="tray.full.fill" catalog="system" width="128" height="88"/>
<image name="video.and.waveform" catalog="system" width="128" height="94"/>
<systemColor name="linkColor">
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="secondarySystemGroupedBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -1799,13 +1819,13 @@
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBrownColor">
<color red="0.63529411764705879" green="0.51764705882352946" blue="0.36862745098039218" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.63529411759999999" green="0.51764705879999995" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemGray3Color">
<color red="0.7803921568627451" green="0.7803921568627451" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.78039215689999997" green="0.78039215689999997" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemTealColor">
<color red="0.18823529411764706" green="0.69019607843137254" blue="0.7803921568627451" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
+20
View File
@@ -1708,6 +1708,24 @@
<constraint firstAttribute="width" constant="380" id="AUi-t1-5ej"/>
</constraints>
</stackView>
<stackView opaque="NO" contentMode="scaleToFill" spacing="20" id="CKa-yk-GSi" userLabel="redirectMicStack">
<rect key="frame" x="20" y="1895" width="280" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Redirect Mic (Experimental)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="nz2-5g-YSQ" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="130" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="96j-Vl-kBM" userLabel="redirectMicSwitch">
<rect key="frame" x="150" y="0.0" width="132" height="31"/>
<constraints>
<constraint firstAttribute="width" constant="130" id="v0k-P3-FoU"/>
</constraints>
</switch>
</subviews>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
@@ -1769,6 +1787,8 @@
<outlet property="pointerVelocityDividerStack" destination="Jp2-5v-Zr0" id="0mB-RI-df8"/>
<outlet property="pointerVelocityFactorStack" destination="PAv-Ep-ZJo" id="fxb-uz-e1a"/>
<outlet property="pointerVelocityModeDividerSlider" destination="OMw-Ew-fBX" id="Yki-z6-7bh"/>
<outlet property="redirectMicStack" destination="CKa-yk-GSi" id="fL6-aV-3Im"/>
<outlet property="redirectMicSwitch" destination="96j-Vl-kBM" id="bHh-Qm-gsv"/>
<outlet property="renderingBackendSelector" destination="53H-ST-OtY" id="jhu-Pw-Ph2"/>
<outlet property="renderingBackendStack" destination="6hG-dw-YSr" id="qqz-lA-EO2"/>
<outlet property="resolutionSelector" destination="7Sl-WI-zyo" id="YMv-Xy-dF9"/>
+1
View File
@@ -79,6 +79,7 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
multiController:(BOOL)multiController
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC
redirectMic:(BOOL)redirectMic
preferredCodec:(uint32_t)preferredCodec
enableYUV444:(BOOL)enableYUV444
enablePIP:(BOOL)enablePIP
+2
View File
@@ -80,6 +80,7 @@
multiController:(BOOL)multiController
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC
redirectMic:(BOOL)redirectMic
preferredCodec:(uint32_t)preferredCodec
enableYUV444:(BOOL)enableYUV444
enablePIP:(BOOL)enablePIP
@@ -127,6 +128,7 @@
settingsToSave.multiController = multiController;
settingsToSave.swapABXYButtons = swapABXYButtons;
settingsToSave.playAudioOnPC = audioOnPC;
settingsToSave.redirectMic = redirectMic;
settingsToSave.preferredCodec = preferredCodec;
settingsToSave.enableYUV444 = enableYUV444;
settingsToSave.enablePIP = enablePIP;
+1
View File
@@ -54,6 +54,7 @@
@property (nonatomic) BOOL multiController;
@property (nonatomic) BOOL swapABXYButtons;
@property (nonatomic) BOOL playAudioOnPC;
@property (nonatomic) BOOL redirectMic;
@property (nonatomic) BOOL optimizeGames;
@property (nonatomic) BOOL enableHdr;
@property (nonatomic) BOOL btMouseSupport;
+1
View File
@@ -90,6 +90,7 @@
self.enablePIP = settings.enablePIP;
self.frameQueueSize = settings.frameQueueSize;
self.playAudioOnPC = settings.playAudioOnPC;
self.redirectMic = settings.redirectMic;
self.enableHdr = settings.enableHdr;
self.optimizeGames = settings.optimizeGames;
self.multiController = settings.multiController;
+460
View File
@@ -0,0 +1,460 @@
//
// MicHandler.swift
// VoidLink
//
// Created by True砖家 on 2025/9/2.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
import AVFoundation
@objc public protocol MicHandlerDelegate: AnyObject {
@objc optional func micHandlerDidFinishPlayback(_ handler: MicHandler)
@objc optional func micHandler(_ handler: MicHandler, didFailWithError error: NSError)
}
@objcMembers
public class MicHandler: NSObject {
private let engine = AVAudioEngine()
private let playerNode = AVAudioPlayerNode()
private var audioSink: Any?
private var micInputFormat: AVAudioFormat!
private var isRecording = false
private var pcm16Buffer: [Int16] = []
private let bufferQueue = DispatchQueue(label: "pcm.buffer.queue")
private var timer: DispatchSourceTimer?
/*
private var recordedBuffers: [AVAudioPCMBuffer] = []
private var recordedPCM16: [Data] = []
private var recordedOpusPackets: [Data] = []
*/
private var opusEncoder: OpaquePointer?
private var opusDecoder: OpaquePointer?
private var sequenceNumber: UInt16 = 0
private let ssrc: UInt32 = 0x12345678
private var globalTimestamp: TimeInterval = 0
public weak var delegate: MicHandlerDelegate?
public override init() {
super.init()
do {
try configureSession()
try configureEngine()
} catch {
notify(error)
}
}
/* ----------- Mic permission -------------*/
/// 请求麦克风权限
/// - Parameter completion: 可选 block,如果为 nil 且未授权,会弹窗提示跳转系统设置
@objc static func requestPermission(_ completion: ((Bool) -> Void)? = nil) {
let permission = AVAudioSession.sharedInstance().recordPermission
switch permission {
case .granted:
completion?(true)
case .denied:
if let callback = completion {
callback(false)
} else {
showSettingsAlert()
}
case .undetermined:
AVAudioSession.sharedInstance().requestRecordPermission { granted in
DispatchQueue.main.async {
if granted {
completion?(true)
} else {
if let callback = completion {
callback(false)
} else {
showSettingsAlert()
}
}
}
}
@unknown default:
if let callback = completion {
callback(false)
} else {
showSettingsAlert()
}
}
}
/// 配置音频会话
@objc static func configureAudioSession() throws {
let audioSession = AVAudioSession.sharedInstance()
try audioSession.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker])
try audioSession.setActive(true)
}
/// 弹窗提示用户跳转系统设置(英文版)
private static func showSettingsAlert() {
guard let topVC = topViewController() else { return }
let alert = UIAlertController(
title: SwiftLocalizationHelper.localizedString(forKey: "Microphone Permission") ,
message: SwiftLocalizationHelper.localizedString(forKey: "micPermissionTip"),
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Cancel"), style: .cancel, handler: nil))
alert.addAction(UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Go to Settings") , style: .default, handler: { _ in
openSettings()
}))
topVC.present(alert, animated: true, completion: nil)
}
/// 打开系统设置
@objc static func openSettings() {
guard let url = URL(string: UIApplication.openSettingsURLString),
UIApplication.shared.canOpenURL(url) else { return }
UIApplication.shared.open(url)
}
/// 检查麦克风权限状态(返回 Int,OC 可用)
@objc static func permissionGranted() -> Bool {
return AVAudioSession.sharedInstance().recordPermission == AVAudioSession.RecordPermission.granted
}
/// 获取最顶层 UIViewController
private static func topViewController(base: UIViewController? = UIApplication.shared.keyWindow?.rootViewController) -> UIViewController? {
if let nav = base as? UINavigationController {
return topViewController(base: nav.visibleViewController)
}
if let tab = base as? UITabBarController, let selected = tab.selectedViewController {
return topViewController(base: selected)
}
if let presented = base?.presentedViewController {
return topViewController(base: presented)
}
return base
}
/* ----------------------------------------*/
/* ----------- Audio Session -------------*/
private func configureOpus(sampleRate: Int32, channels: Int) throws {
var err: Int32 = 0
guard let enc = opus_encoder_create(sampleRate, Int32(channels), OPUS_APPLICATION_VOIP, &err), err == OPUS_OK else {
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
}
opusEncoder = enc
guard let dec = opus_decoder_create(sampleRate, Int32(channels), &err), err == OPUS_OK else {
throw NSError(domain: "Opus", code: Int(err), userInfo: nil)
}
opusDecoder = dec
// Optional: But defaults are fine. Only change when needed:
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_BITRATE_REQUEST), opus_int32(64000)) // Set bitrate
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_COMPLEXITY_REQUEST), opus_int32(5)) // Set complexity
opus_encoder_ctl_wrapper(enc, Int32(OPUS_SET_SIGNAL_REQUEST), OPUS_SIGNAL_MUSIC) // Set signal type
}
@objc public func startTapping() {
// recordedBuffers.removeAll()
isRecording = true
/*
DispatchQueue.main.asyncAfter(deadline: .now() + seconds) { [weak self] in
guard let self = self else { return }
self.isRecording = false
self.stop()
self.playbackRecorded()
self.playbackOpus()
}*/
}
@objc public func stopTapping(stopEngine:Bool) {
isRecording = false
// engine.inputNode.removeTap(onBus: 0)
if(stopEngine){
playerNode.stop()
engine.stop()
}
}
private func configureSession() throws {
let session = AVAudioSession.sharedInstance()
try session.setCategory(.playAndRecord, mode: .videoRecording, options: [.mixWithOthers, .defaultToSpeaker, .allowBluetooth])
try session.setActive(true)
}
private func sendOpusFrame() {
bufferQueue.sync {
if pcm16Buffer.count >= 960 {
let chunk = Array(pcm16Buffer.prefix(960))
var packet = [UInt8](repeating: 0, count: 4000)
guard let enc = self.opusEncoder else {return}
let outBytes = opus_encode(enc, chunk, 960, &packet, Int32(packet.count))
sendMicrophoneData(packet, outBytes)
pcm16Buffer.removeFirst(960)
}
}
}
private func configureEngine() throws {
let input = engine.inputNode
micInputFormat = input.inputFormat(forBus: 0)
try self.configureOpus(sampleRate: Int32(micInputFormat.sampleRate), channels: Int(micInputFormat.channelCount))
let pcm16Format = AVAudioFormat(commonFormat: .pcmFormatInt16, sampleRate: micInputFormat.sampleRate, channels: micInputFormat.channelCount, interleaved: true)
let converter = AVAudioConverter(from: micInputFormat, to: pcm16Format!)
if #available(iOS 13.0, tvOS 13.0, *) {
try AVAudioSession.sharedInstance().setPreferredIOBufferDuration(0.02)
let sinkNode = AVAudioSinkNode { timestamp, frameCount, audioBufferList -> OSStatus in
// 访问 AudioBufferList 数据
// print("sink interval: \((CACurrentMediaTime()-self.globalTimestamp)*1000),frameCount: \(frameCount)")
// self.globalTimestamp = CACurrentMediaTime()
guard self.isRecording else { return noErr}
let abl = audioBufferList.pointee.mBuffers
guard let data = abl.mData?.assumingMemoryBound(to: Float.self) else { return noErr }
let samples = UnsafeBufferPointer(start: data, count: Int(frameCount))
// 转 float32 -> int16
var chunk = [Int16](repeating: 0, count: samples.count)
for i in 0..<samples.count {
let clamped = max(min(samples[i], 1.0), -1.0)
chunk[i] = Int16(clamped * Float(Int16.max))
}
// 追加到缓冲区
self.pcm16Buffer.append(contentsOf: chunk)
return noErr
}
audioSink = sinkNode
engine.attach(audioSink! as! AVAudioNode)
engine.connect(engine.inputNode, to: audioSink as! AVAudioNode, format: micInputFormat)
// 开定时器,每 20ms 触发一次
self.timer = DispatchSource.makeTimerSource(queue: .global())
self.timer?.schedule(deadline: .now()+0.1, repeating: 0.02)
self.timer?.setEventHandler { [weak self] in
self?.sendOpusFrame()
}
self.timer?.resume()
}
else{
engine.attach(playerNode)
engine.connect(playerNode, to: engine.mainMixerNode, format: micInputFormat)
input.installTap(onBus: 0, bufferSize: 5760, format: micInputFormat) { [weak self] buffer, _ in
guard let self = self, self.isRecording else { return }
DispatchQueue.global().async {
let pcm16Buffer = AVAudioPCMBuffer(pcmFormat: pcm16Format!, frameCapacity: buffer.frameCapacity)
try? converter?.convert(to: pcm16Buffer!, from: buffer)
// 处理转换后的 outputBuffer
// ===============================
// 🔹 改动 1:把 buffer 转成 PCM16 并保存
let frameLength = Int(buffer.frameLength)
let channels = Int(buffer.format.channelCount)
var pcm16InterleavedBuffer = [Int16](repeating: 0, count: frameLength * channels)
for ch in 0..<channels {
if let pcm16Data = pcm16Buffer?.int16ChannelData?[ch] {
for i in 0..<frameLength {
pcm16InterleavedBuffer[i * channels + ch] = pcm16Data[i]
}
}
}
// 🔹 新增:Opus 编码并存入缓存
if let enc = self.opusEncoder {
var packet = [UInt8](repeating: 0, count: 8000)
let outBytes = opus_encode(enc, pcm16InterleavedBuffer, Int32(frameLength), &packet, Int32(packet.count))
sendMicrophoneData(packet, outBytes)
}
}
}
}
engine.prepare()
try engine.start()
}
private func notify(_ error: Error) {
delegate?.micHandler?(self, didFailWithError: error as NSError)
}
// ===============================
// 🔹 新增播放 Opus 数据方法
/*
private func playbackOpus() {
guard let dec = opusDecoder else { return }
let channels = Int(micInputFormat.channelCount)
for (index, packet) in recordedOpusPackets.enumerated() {
let maxFrames = 5760 // 最大 120ms
let pcmBuf = UnsafeMutablePointer<Int16>.allocate(capacity: maxFrames * channels)
defer { pcmBuf.deallocate() }
let frameCount = opus_decode(dec,
[UInt8](packet),
Int32(packet.count),
pcmBuf,
Int32(maxFrames),
0)
if frameCount < 0 {
print("Opus decode error: \(frameCount)")
continue
}
// 构造源格式 AVAudioPCMBuffer (PCM16)
let sourceFormat = AVAudioFormat(commonFormat: .pcmFormatInt16,
sampleRate: micInputFormat.sampleRate,
channels: micInputFormat.channelCount,
interleaved: true)!
guard let sourceBuffer = AVAudioPCMBuffer(pcmFormat: sourceFormat,
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
sourceBuffer.frameLength = AVAudioFrameCount(frameCount)
// 填充 PCM16 数据到 sourceBuffer
let srcPointer = sourceBuffer.int16ChannelData![0]
for i in 0..<Int(frameCount * Int32(channels)) {
srcPointer[i] = pcmBuf[i]
}
// 准备目标 buffer (Float32)
guard let floatBuffer = AVAudioPCMBuffer(pcmFormat: micInputFormat,
frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
// 🔹 使用 AVAudioConverter 转换
let converter = AVAudioConverter(from: sourceFormat, to: micInputFormat)!
var error: NSError? = nil
let inputBlock: AVAudioConverterInputBlock = { inNumPackets, outStatus in
outStatus.pointee = .haveData
return sourceBuffer
}
converter.convert(to: floatBuffer, error: &error, withInputFrom: inputBlock)
if let error = error {
print("AVAudioConverter error: \(error)")
continue
}
// 播放
if index == recordedOpusPackets.count - 1 {
playerNode.scheduleBuffer(floatBuffer, at: nil, options: []) { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self.playerNode.stop()
self.engine.stop()
}
}
} else {
playerNode.scheduleBuffer(floatBuffer, at: nil, options: [], completionHandler: nil)
}
}
// 启动 engine
if !engine.isRunning {
do { try engine.start() } catch { print(error) }
}
if !playerNode.isPlaying {
playerNode.play()
}
}
// ===============================
// 🔹 改动 2:播放 PCM16 数据
private func playbackRecorded() {
do {
try configureEngine()
} catch {
notify(error)
}
let channels = Int(micInputFormat.channelCount)
for (index, pcmData) in recordedPCM16.enumerated() {
let frameCount = pcmData.count / (MemoryLayout<Int16>.size * channels)
guard let buf = AVAudioPCMBuffer(pcmFormat: micInputFormat, frameCapacity: AVAudioFrameCount(frameCount)) else { continue }
buf.frameLength = buf.frameCapacity
// PCM16 -> Float32
pcmData.withUnsafeBytes { rawBuf in
let pcmPtr = rawBuf.bindMemory(to: Int16.self).baseAddress!
for i in 0..<frameCount {
for ch in 0..<channels {
buf.floatChannelData?[ch][i] = Float(pcmPtr[i * channels + ch]) / Float(Int16.max)
}
}
}
// 只在最后一个 buffer 设置 completionHandler
if index == recordedPCM16.count - 1 {
playerNode.scheduleBuffer(buf, at: nil, options: []) { [weak self] in
guard let self = self else { return }
// 🔹 回到主线程安全停止
DispatchQueue.main.async {
self.playerNode.stop()
self.engine.stop()
}
}
} else {
playerNode.scheduleBuffer(buf, at: nil, options: [], completionHandler: nil)
}
}
playerNode.play()
}
private func deepCopy(_ buffer: AVAudioPCMBuffer, format: AVAudioFormat) -> AVAudioPCMBuffer? {
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: buffer.frameLength) else { return nil }
out.frameLength = buffer.frameLength
let channels = Int(format.channelCount)
for ch in 0..<channels {
if let src = buffer.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
dst.update(from: src, count: Int(buffer.frameLength))
}
}
return out
}
private func merge(buffers: [AVAudioPCMBuffer], format: AVAudioFormat) -> AVAudioPCMBuffer? {
let total = buffers.reduce(0) { $0 + $1.frameLength }
guard let out = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: total) else { return nil }
out.frameLength = total
var writePos: AVAudioFrameCount = 0
let channels = Int(format.channelCount)
for b in buffers {
let frames = Int(b.frameLength)
for ch in 0..<channels {
if let src = b.floatChannelData?[ch], let dst = out.floatChannelData?[ch] {
dst.advanced(by: Int(writePos)).update(from: src, count: frames)
}
}
writePos += b.frameLength
}
return out
}
*/
}
/* -------------------------------------------------------*/
+4 -2
View File
@@ -58,14 +58,16 @@
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string>
<key>NSMicrophoneUsageDescription</key>
<string>VoidLink uses microphone to stream your voice to gaming PC.</string>
<key>NSBonjourServices</key>
<array>
<string>_nvstream._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<string>VoidLink uses the local network to connect to your gaming PC for streaming.</string>
<key>NSMotionUsageDescription</key>
<string>Moonlight uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
<string>VoidLink uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UISceneConfigurations</key>
@@ -51,6 +51,7 @@
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="pointerVelocityModeDivider" optional="YES" attributeType="Float" defaultValueString="0.5" usesScalarValueType="NO" syncable="YES"/>
<attribute name="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="redirectMic" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="renderingBackend" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="resolutionSelected" optional="YES" attributeType="Integer 16" defaultValueString="4" usesScalarValueType="NO" syncable="YES"/>
<attribute name="reverseMouseWheelDirection" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
+104
View File
@@ -2177,6 +2177,29 @@
}
}
},
"Go to Settings" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "前往系统设置"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "前往系統設定"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "前往系統設定"
}
}
}
},
"Got it!" : {
"extractionState" : "manual",
"localizations" : {
@@ -2788,6 +2811,58 @@
}
}
},
"micPermissionTip" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "- Please grant microphone permission in your system settings, then re-enable “Redirect Mic.”\n\n- Currently, mic redirection is only supported with Sunshine-Foundation (a Sunshine fork).\nThis feature is experimental, and the microphone audio stream is NOT ENCRYPTED at this stage.\n\n- iOS 12 mic redirection has known audio quality issue that can’t be fixed due to technical reasons."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 请前往系统设置授予 App 麦克风权限,再回到此菜单启用麦克风重定向。\n\n- 目前麦克风重定向仅支持 Sunshine 基地版,且为实验性功能。 须注意,现阶段的 Mic 音频流 【未经加密】。\n\n- iOS 12 的麦克风重定向存在因技术原因无法解决的音质问题。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 請前往系統設定授予 App 麥克風權限,再回到此選單啟用麥克風轉送。\n\n- 目前麥克風轉送僅支援 Sunshine 基地版,且為實驗性功能。須注意,現階段的 Mic 音訊流【未經加密】。\n\n- iOS 12 的麥克風轉送因技術原因存在無法解決的音質問題。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 請前往系統設定授予 App 麥克風權限,再回到此選單啟用麥克風轉送。\n\n- 目前麥克風轉送僅支援 Sunshine 基地版,且為實驗性功能。須注意,現階段的 Mic 音訊流【未經加密】。\n\n- iOS 12 的麥克風轉送因技術原因存在無法解決的音質問題。"
}
}
}
},
"Microphone Permission" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "麦克风权限"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "麥克風權限"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "麥克風權限"
}
}
}
},
"Minimum stick offset (0~32766)" : {
"extractionState" : "manual",
"localizations" : {
@@ -4154,6 +4229,35 @@
}
}
},
"redirectMicStackTip" : {
"extractionState" : "manual",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "translated",
"value" : "- Currently, mic redirection is only supported with Sunshine-Foundation (a Sunshine fork).\nThis feature is experimental, and the microphone audio stream is NOT ENCRYPTED at this stage.\n\n- iOS 12 mic redirection has known audio quality issue that can’t be fixed due to technical reasons."
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 目前麦克风重定向仅支持 Sunshine 基地版,且为实验性功能。 须注意,现阶段的 Mic 音频流 【未经加密】。\n\n- iOS 12 的麦克风重定向存在因技术原因无法解决的音质问题。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 目前麥克風轉送僅支援 Sunshine 基地版,且為實驗性功能。須注意,現階段的 Mic 音訊流【未經加密】。\n\n- iOS 12 的麥克風轉送因技術原因存在無法解決的音質問題。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "- 目前麥克風轉送僅支援 Sunshine 基地版,且為實驗性功能。須注意,現階段的 Mic 音訊流【未經加密】。\n\n- iOS 12 的麥克風轉送因技術原因存在無法解決的音質問題。"
}
}
}
},
"Remove" : {
"extractionState" : "manual",
"localizations" : {
+2
View File
@@ -18,6 +18,7 @@
#include "Limelight.h"
#include "opus_multistream.h"
#include "VoidLink-Swift.h"
@implementation Connection {
SERVER_INFORMATION _serverInfo;
@@ -468,6 +469,7 @@ void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t
_streamConfig.bitrate = config.bitRate;
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
_streamConfig.audioConfiguration = config.audioConfiguration;
_streamConfig.redirectMic = config.redirectMic && [MicHandler permissionGranted];
// Since we require iOS 12 or above, we're guaranteed to be running
// on a 64-bit device with ARMv8 crypto instructions, so we don't
+1
View File
@@ -31,6 +31,7 @@
@property int gamepadMask;
@property BOOL optimizeGameSettings;
@property BOOL playAudioOnPC;
@property BOOL redirectMic;
@property BOOL swapABXYButtons;
@property BOOL asyncNativeTouchPriority;
@property int gyroMode;
@@ -820,6 +820,7 @@ static NSMutableSet* hostList;
_streamConfig.bitRate = [streamSettings.bitrate intValue];
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
_streamConfig.redirectMic = streamSettings.redirectMic;
_streamConfig.swapABXYButtons = streamSettings.swapABXYButtons;
_streamConfig.asyncNativeTouchPriority = streamSettings.asyncNativeTouchPriority; // new streamConfig segment
_streamConfig.gyroMode = [streamSettings.gyroMode intValue];
@@ -1193,6 +1194,9 @@ static NSMutableSet* hostList;
// Enable frame pacing mode selector only if not in stream view AND not in performance mode
BOOL shouldEnableFramePacing = !_settingsExpandedInStreamView && (settingsViewController.renderingBackendSelector.selectedSegmentIndex != RENDER_METAL);
[settingsViewController.framePacingModeSelector setEnabled:shouldEnableFramePacing];
// Disable mic switch if sunshine does not support mic redirection
[settingsViewController.redirectMicSwitch setEnabled:!_settingsExpandedInStreamView||streamFrameViewController.micStreamInitialized];
if(_settingsExpandedInStreamView && !streamFrameViewController.micStreamInitialized) [settingsViewController.redirectMicSwitch setOn:false];
}
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position {
@@ -16,7 +16,8 @@
#import "CustomEdgeSlideGestureRecognizer.h"
#import "MenuSectionView.h"
@interface SettingsViewController : UIViewController <RearNavigationBarMenuDelegate, MenuSectionDelegate>
@interface SettingsViewController : UIViewController <RearNavigationBarMenuDelegate, MenuSectionDelegate, MicHandlerDelegate>
@property (strong, nonatomic) IBOutlet UINavigationBar *navigationBar;
@property (strong, nonatomic) UIStackView *parentStack;
@property (strong, nonatomic) IBOutlet UIStackView *resolutionStack;
@@ -125,6 +126,12 @@
@property (strong, nonatomic) IBOutlet UIStackView *emulatedControllerTypeStack;
@property (strong, nonatomic) IBOutlet UISegmentedControl *emulatedControllerTypeSelector;
@property (strong, nonatomic) IBOutlet UIStackView *redirectMicStack;
@property (strong, nonatomic) IBOutlet UISwitch *redirectMicSwitch;
@property (nonatomic, strong) MicHandler *micHandler;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wunguarded-availability"
@@ -303,6 +303,21 @@ BOOL isCustomResolution(int resolutionSelected) {
[self updateResolutionDisplayLabel];
}
- (void)checkAndRequestMicPermission{
if(![MicHandler permissionGranted]) [MicHandler requestPermission:nil];
/*
AVAudioSessionRecordPermission permission = [MicHandler permissionGranted];
switch (permission) {
case AVAudioSessionRecordPermissionGranted:
NSLog(@"AVAudioSessionRecordPermissionGranted");
break;
case AVAudioSessionRecordPermissionDenied:
default:
[MicHandler requestPermission:nil];
NSLog(@"AVAudioSessionRecordPermissionDenied");
}*/
}
// this will also be called back when device orientation changes
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
@@ -315,8 +330,25 @@ BOOL isCustomResolution(int resolutionSelected) {
});
}
- (void)micHandlerDidFinishPlayback:(MicHandler *)handler {
NSLog(@"Playback finished");
}
- (void)micHandler:(MicHandler *)handler didFailWithError:(NSError *)error {
NSLog(@"Mic error: %@", error);
}
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:NO];
/*
[self checkAndRequestMicPermission];
self.micHandler = [MicHandler new];
[self.micHandler startTapping];
*/
[self updateParentStackHorizontalConstraints];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(deviceOrientationDidChange:) // handle orientation change since i made portrait mode available
@@ -634,6 +666,7 @@ BOOL isCustomResolution(int resolutionSelected) {
}
[self addSetting:self.audioOnPcStack ofId:@"audioOnPcStack" withInfoTag:NO withDynamicLabel:NO to:audioSection];
[self addSetting:self.redirectMicStack ofId:@"redirectMicStack" withInfoTag:YES withDynamicLabel:NO to:audioSection];
[self addSetting:self.audioConfigStack ofId:@"audioConfigStack" withInfoTag:NO withDynamicLabel:NO to:audioSection];
[audioSection addToParentStack:_parentStack];
[audioSection setExpanded:YES];
@@ -1098,8 +1131,10 @@ BOOL isCustomResolution(int resolutionSelected) {
tipText = [LocalizationHelper localizedStringForKey:@"framePacingStackTip"];
showOnlineDocAction = false;
}
if([sender.superview.accessibilityIdentifier isEqualToString: @"redirectMicStack"]){
tipText = [LocalizationHelper localizedStringForKey:@"redirectMicStackTip"];
showOnlineDocAction = false;
}
UIAlertController *tipsAlertController = [UIAlertController alertControllerWithTitle: [LocalizationHelper localizedStringForKey:@"Tips"] message:tipText preferredStyle:UIAlertControllerStyleAlert];
@@ -1486,6 +1521,10 @@ BOOL isCustomResolution(int resolutionSelected) {
[self.audioOnPcSwitch setOn:currentSettings.playAudioOnPC];
[self.redirectMicSwitch setOn:currentSettings.redirectMic];
[self.redirectMicSwitch addTarget:self action:@selector(redirectMicSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
[self redirectMicSwitchFlipped:self.redirectMicSwitch];
_lastSelectedResolutionIndex = resolution;
[self.resolutionSelector setSelectedSegmentIndex:resolution];
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
@@ -1807,7 +1846,7 @@ BOOL isCustomResolution(int resolutionSelected) {
oswDynamicLabel.hidden = !customOscEnabled;
NSLog(@"oswDynamicLabel.hidden %d", oswDynamicLabel.hidden);
[self handleOswGestureChange];
if(customOscEnabled && !justEnteredSettingsView) {
if(customOscEnabled && !justEnteredSettingsView && !self.mainFrameViewController.settingsExpandedInStreamView) {
// [self.keyboardToggleFingerNumSlider setValue:3.0];
// [self keyboardToggleFingerNumSliderMoved];
[self showCustomOswTip];
@@ -1959,6 +1998,10 @@ BOOL isCustomResolution(int resolutionSelected) {
}
}
- (void)redirectMicSwitchFlipped:(UISwitch* )sender{
if(sender.isOn) [self checkAndRequestMicPermission];
}
- (void)enableOswForNativeTouchSwitchFlipped:(UISwitch *)sender{
//self.onScreenWidgetStack.hidden = !sender.isOn;
[self setHidden:!sender.isOn forStack:_onScreenWidgetStack];
@@ -2309,11 +2352,16 @@ BOOL isCustomResolution(int resolutionSelected) {
[self findDynamicLabelFromStack:_graphOpacityStack].text = [LocalizationHelper localizedStringForKey:@" %d%% opacity ",(int)sender.value];
}
- (void) saveSettings {
- (void)preSavingActions{
if(self.mainFrameViewController.settingsExpandedInStreamView){
[self.mainFrameViewController requestForBitrate:(uint32_t)_bitrate];
}
if(![MicHandler permissionGranted]) [self.redirectMicSwitch setOn:false];
}
- (void) saveSettings {
[self preSavingActions];
DataManager* dataMan = [[DataManager alloc] init];
Settings* currentSettings = [dataMan retrieveSettings];
@@ -2350,6 +2398,7 @@ BOOL isCustomResolution(int resolutionSelected) {
NSInteger gyroMode = self.gyroModeSelector.selectedSegmentIndex;
NSInteger emulatedControllerType = [self segmentIndexToControllerType:self.emulatedControllerTypeSelector.selectedSegmentIndex]; //self.emulatedControllerTypeSelector.selectedSegmentIndex;
BOOL audioOnPC = self.audioOnPcSwitch.isOn;
BOOL redirectMic = self.redirectMicSwitch.isOn;
uint32_t preferredCodec = [self getChosenCodecPreference];
BOOL enableYUV444 = self.yuv444Switch.isOn;
BOOL enablePIP = self.pipSwitch.isOn;
@@ -2395,6 +2444,7 @@ BOOL isCustomResolution(int resolutionSelected) {
multiController:multiController
swapABXYButtons:swapABXYButtons
audioOnPC:audioOnPC
redirectMic:redirectMic
preferredCodec:preferredCodec
enableYUV444:enableYUV444
enablePIP:enablePIP
@@ -35,6 +35,7 @@
@property (nonatomic, strong) AVPictureInPictureController *pipController API_AVAILABLE(ios(9.0));
@property (nonatomic, strong) AVPictureInPictureControllerContentSource *pipContentSource API_AVAILABLE(ios(15.0)); // Needed for iOS 15+ layer-based PiP
@property (nonatomic, assign) MainFrameViewController *mainFrameViewcontroller;
@property (nonatomic, assign) bool micStreamInitialized;
@property (nonatomic, strong) MetalViewController *metalViewController;
@property (nonatomic, strong) ImGuiRenderer *imguiView;
@@ -88,6 +88,7 @@
CustomTapGestureRecognizer *_oscLayoutTapRecoginizer;
LayoutOnScreenControlsViewController *_layoutOnScreenControlsVC;
ToolboxViewController* toolBoxViewController;
MicHandler* micHandler;
#else
UITapGestureRecognizer *_menuTapGestureRecognizer;
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
@@ -356,7 +357,12 @@
[self->_streamView disableOnScreenControls]; //don't know why but this must be called outside the streamview class, just put it here. execute in streamview class cause hang
[self.mainFrameViewcontroller reloadStreamConfig]; // reload streamconfig
NSLog(@"viewJustloaded: %d", viewJustLoaded);
if([MicHandler permissionGranted] && _settings.redirectMic){
[micHandler startTapping];
}
else [micHandler stopTappingWithStopEngine:false];
NSLog(@"viewJustloaded: %d, redirectMic:%d", viewJustLoaded, _settings.redirectMic);
if(!viewJustLoaded) [_controllerSupport updateControllerSupport:self.streamConfig delegate:self];
else viewJustLoaded = false;
// reload controllerSupport obj, this is mandatory for OSC reload,especially when the stream view is launched without OSC
@@ -919,6 +925,8 @@
_extWindow = nil;
if(_streamConfig.redirectMic) [micHandler stopTappingWithStopEngine:true];
self.mainFrameViewcontroller.settingsExpandedInStreamView = false; // reset this flag to false
}
@@ -1111,7 +1119,6 @@
- (void)disconnectRemoteSession {
Log(LOG_I, @"Settings view disconnect the session in stream view");
[self returnToMainFrame];
}
- (void)disconnectAndQuitApp{
@@ -1250,6 +1257,22 @@
}
- (void) stageComplete:(const char*)stageName {
_micStreamInitialized = false;
if(strcmp(stageName, "mic stream establishment")==0){
if(_streamConfig.redirectMic){
dispatch_after((int64_t)(5 * NSEC_PER_SEC), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
dispatch_async(dispatch_get_main_queue(), ^{
self->_micStreamInitialized = true;
self->micHandler = [MicHandler new];
[self->micHandler startTapping];
});
});
}
}
if(strcmp(stageName, "mic stream unsupported or unintialized")==0){
NSLog(@"blablablala");
_micStreamInitialized = false;
}
}
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags {
+2
View File
@@ -5,4 +5,6 @@
#include <Limelight.h>
#import "OnScreenControls.h"
#import "OnScreenButtonState.h"
#import "opus.h"
#import "opus_defines.h"
// #import "RelativeTouchHandler.h"
+30
View File
@@ -1987,6 +1987,36 @@
}
}
},
"jaM-yS-ldb.text" : {
"comment" : "Class = \"UILabel\"; text = \"Redirect Mic (Experimental)\"; ObjectID = \"jaM-yS-ldb\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Redirect Mic (Experimental)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "needs_review",
"value" : "麦克风重定向(实验性)"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "麥克風轉送(實驗性)"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "麥克風轉送(實驗性)"
}
}
}
},
"JaY-XA-1V6.segmentTitles[0]" : {
"comment" : "Class = \"UISegmentedControl\"; JaY-XA-1V6.segmentTitles[0] = \"Xbox 360\"; ObjectID = \"JaY-XA-1V6\";",
"extractionState" : "extracted_with_value",
+30
View File
@@ -2107,6 +2107,36 @@
}
}
},
"nz2-5g-YSQ.text" : {
"comment" : "Class = \"UILabel\"; text = \"Redirect Mic (Experimental)\"; ObjectID = \"nz2-5g-YSQ\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Redirect Mic (Experimental)"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "麦克风重定向(实验性)"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "麥克風轉送(實驗性)"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "麥克風轉送(實驗性)"
}
}
}
},
"o9I-Gt-YRd.text" : {
"comment" : "Class = \"UILabel\"; text = \"Slide Activated\"; ObjectID = \"o9I-Gt-YRd\";",
"extractionState" : "extracted_with_value",
Binary file not shown.
@@ -7,6 +7,10 @@
objects = {
/* Begin PBXBuildFile section */
180B87D62E65DF27007D9E22 /* MicrophoneStream.c in Sources */ = {isa = PBXBuildFile; fileRef = 180B87D42E65DF27007D9E22 /* MicrophoneStream.c */; };
180B87D72E65DF27007D9E22 /* RecorderCallbacks.c in Sources */ = {isa = PBXBuildFile; fileRef = 180B87D52E65DF27007D9E22 /* RecorderCallbacks.c */; };
180B87D82E65DF27007D9E22 /* MicrophoneStream.c in Sources */ = {isa = PBXBuildFile; fileRef = 180B87D42E65DF27007D9E22 /* MicrophoneStream.c */; };
180B87D92E65DF27007D9E22 /* RecorderCallbacks.c in Sources */ = {isa = PBXBuildFile; fileRef = 180B87D52E65DF27007D9E22 /* RecorderCallbacks.c */; };
983091DE24CEB69B00A0835D /* ConnectionTester.c in Sources */ = {isa = PBXBuildFile; fileRef = 983091DD24CEB69B00A0835D /* ConnectionTester.c */; };
983091DF24CEB69B00A0835D /* ConnectionTester.c in Sources */ = {isa = PBXBuildFile; fileRef = 983091DD24CEB69B00A0835D /* ConnectionTester.c */; };
9857AEF01EBE85E10084F99E /* rs.c in Sources */ = {isa = PBXBuildFile; fileRef = 9857AEEE1EBE85E10084F99E /* rs.c */; };
@@ -84,6 +88,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
180B87D42E65DF27007D9E22 /* MicrophoneStream.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = MicrophoneStream.c; sourceTree = "<group>"; };
180B87D52E65DF27007D9E22 /* RecorderCallbacks.c */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.c; path = RecorderCallbacks.c; sourceTree = "<group>"; };
983091DD24CEB69B00A0835D /* ConnectionTester.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = ConnectionTester.c; sourceTree = "<group>"; };
9857AEEE1EBE85E10084F99E /* rs.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; name = rs.c; path = "moonlight-common-c/reedsolomon/rs.c"; sourceTree = "<group>"; };
9857AEEF1EBE85E10084F99E /* rs.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = rs.h; path = "moonlight-common-c/reedsolomon/rs.h"; sourceTree = "<group>"; };
@@ -269,6 +275,8 @@
98AB2E191CAD423B0089BB98 /* src */ = {
isa = PBXGroup;
children = (
180B87D42E65DF27007D9E22 /* MicrophoneStream.c */,
180B87D52E65DF27007D9E22 /* RecorderCallbacks.c */,
98F1453A2667D852004018E3 /* RtpAudioQueue.h */,
98F145392667D852004018E3 /* RtpVideoQueue.h */,
98F145332667D847004018E3 /* RtpAudioQueue.c */,
@@ -415,6 +423,8 @@
98F145362667D847004018E3 /* RtpAudioQueue.c in Sources */,
FB1A67FA21324F4900507771 /* AudioStream.c in Sources */,
FB1A67FB21324F4900507771 /* ByteBuffer.c in Sources */,
180B87D62E65DF27007D9E22 /* MicrophoneStream.c in Sources */,
180B87D72E65DF27007D9E22 /* RecorderCallbacks.c in Sources */,
FB1A67FC21324F4900507771 /* Connection.c in Sources */,
FB1A67FD21324F4900507771 /* ControlStream.c in Sources */,
FB1A67FE21324F4900507771 /* FakeCallbacks.c in Sources */,
@@ -452,6 +462,8 @@
98F145352667D847004018E3 /* RtpAudioQueue.c in Sources */,
98AB2E401CAD425A0089BB98 /* AudioStream.c in Sources */,
98AB2E411CAD425A0089BB98 /* ByteBuffer.c in Sources */,
180B87D82E65DF27007D9E22 /* MicrophoneStream.c in Sources */,
180B87D92E65DF27007D9E22 /* RecorderCallbacks.c in Sources */,
98AB2E421CAD425A0089BB98 /* Connection.c in Sources */,
98AB2E431CAD425A0089BB98 /* ControlStream.c in Sources */,
98AB2E441CAD425A0089BB98 /* FakeCallbacks.c in Sources */,