Mic input gain (was 3x) and per-user receive gain (was 2x) had asymmetric boost ceilings. Raise both, plus the desktop aux input gain (was 3x), to a uniform 4x (400%) on macOS, iOS, and Windows. The master Output volume slider is unchanged (still 1x). No core changes needed: the C ABI only clamps negatives, so the ceilings live entirely in the client UI sliders. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
124 lines
4.8 KiB
Swift
124 lines
4.8 KiB
Swift
import AppKit
|
|
import VoiceCatCore
|
|
|
|
final class PerUserTuningSheet: NSViewController {
|
|
|
|
private let client: VoiceCatClient
|
|
private let userId: UInt32
|
|
private let nickname: String
|
|
|
|
private var streams: [StreamSummary] = []
|
|
private var rows: [StreamRow] = []
|
|
|
|
private struct StreamRow {
|
|
let streamId: UInt32
|
|
let label: String
|
|
let gainSlider: NSSlider
|
|
let muteCheckbox: NSButton
|
|
let nrCheckbox: NSButton
|
|
}
|
|
|
|
init(client: VoiceCatClient, userId: UInt32, nickname: String) {
|
|
self.client = client
|
|
self.userId = userId
|
|
self.nickname = nickname
|
|
super.init(nibName: nil, bundle: nil)
|
|
}
|
|
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
override func loadView() {
|
|
view = NSView(frame: NSRect(x: 0, y: 0, width: 400, height: 200))
|
|
}
|
|
|
|
override func viewDidLoad() {
|
|
super.viewDidLoad()
|
|
streams = client.listUserStreams(userId)
|
|
buildUI()
|
|
}
|
|
|
|
private func buildUI() {
|
|
let titleLabel = NSTextField(labelWithString: "Volume & Noise Settings — \(nickname)")
|
|
titleLabel.font = .boldSystemFont(ofSize: 13)
|
|
|
|
var rowViews: [NSView] = [titleLabel]
|
|
|
|
for stream in streams {
|
|
let (_, state) = client.getRemoteStream(userId: userId, streamId: stream.id)
|
|
let gain = state?.gain ?? 1.0
|
|
let muted = state?.muted ?? false
|
|
let nr = state?.noiseReduction ?? false
|
|
|
|
let gainSlider = NSSlider(value: Double(gain * 100), minValue: 0, maxValue: 400, target: self, action: #selector(sliderChanged))
|
|
gainSlider.tag = Int(stream.id)
|
|
gainSlider.numberOfTickMarks = 0
|
|
gainSlider.setAccessibilityLabel("Volume for \(stream.label): \(Int(gain * 100)) percent")
|
|
|
|
let muteCheckbox = NSButton(checkboxWithTitle: "Mute", target: self, action: #selector(muteChanged))
|
|
muteCheckbox.state = muted ? .on : .off
|
|
muteCheckbox.tag = Int(stream.id)
|
|
muteCheckbox.setAccessibilityLabel("Mute \(stream.label)")
|
|
|
|
let nrCheckbox = NSButton(checkboxWithTitle: "Noise reduction", target: self, action: #selector(nrChanged))
|
|
nrCheckbox.state = nr ? .on : .off
|
|
nrCheckbox.tag = Int(stream.id)
|
|
nrCheckbox.setAccessibilityLabel("Noise reduction for \(stream.label)")
|
|
|
|
let streamLabel = NSTextField(labelWithString: "\(stream.label):")
|
|
let gainLabel = NSTextField(labelWithString: "Volume:")
|
|
let row = NSStackView(views: [streamLabel, gainLabel, gainSlider, muteCheckbox, nrCheckbox])
|
|
row.orientation = .horizontal; row.spacing = 8
|
|
rowViews.append(row)
|
|
|
|
rows.append(StreamRow(streamId: stream.id, label: stream.label,
|
|
gainSlider: gainSlider, muteCheckbox: muteCheckbox,
|
|
nrCheckbox: nrCheckbox))
|
|
}
|
|
|
|
if streams.isEmpty {
|
|
rowViews.append(NSTextField(labelWithString: "This user has no active streams."))
|
|
}
|
|
|
|
let doneButton = NSButton(title: "Done", target: self, action: #selector(doneClicked))
|
|
doneButton.bezelStyle = .rounded; doneButton.keyEquivalent = "\r"
|
|
doneButton.setAccessibilityLabel("Close settings")
|
|
rowViews.append(NSStackView(views: [NSView(), doneButton]))
|
|
|
|
let stack = NSStackView(views: rowViews)
|
|
stack.orientation = .vertical
|
|
stack.spacing = 10
|
|
stack.edgeInsets = NSEdgeInsets(top: 16, left: 16, bottom: 16, right: 16)
|
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
|
view.addSubview(stack)
|
|
NSLayoutConstraint.activate([
|
|
stack.topAnchor.constraint(equalTo: view.topAnchor),
|
|
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
])
|
|
}
|
|
|
|
@objc private func sliderChanged(_ sender: NSSlider) {
|
|
apply(streamId: UInt32(sender.tag))
|
|
}
|
|
|
|
@objc private func muteChanged(_ sender: NSButton) {
|
|
apply(streamId: UInt32(sender.tag))
|
|
}
|
|
|
|
@objc private func nrChanged(_ sender: NSButton) {
|
|
apply(streamId: UInt32(sender.tag))
|
|
}
|
|
|
|
private func apply(streamId: UInt32) {
|
|
guard let row = rows.first(where: { $0.streamId == streamId }) else { return }
|
|
let gain = Float(row.gainSlider.doubleValue) / 100.0
|
|
let muted = row.muteCheckbox.state == .on
|
|
let nr = row.nrCheckbox.state == .on
|
|
client.setRemoteStream(userId: userId, streamId: streamId,
|
|
gain: gain, muted: muted, noiseReduction: nr)
|
|
row.gainSlider.setAccessibilityLabel("Volume for \(row.label): \(Int(row.gainSlider.doubleValue)) percent")
|
|
}
|
|
|
|
@objc private func doneClicked() { dismiss(nil) }
|
|
}
|