|
|
|
@ -5,12 +5,12 @@ import SwiftUI
|
|
|
|
/// Channel strips of the selected output device; drop an NDI source or a microphone onto a strip.
|
|
|
|
/// Channel strips of the selected output device; drop an NDI source or a microphone onto a strip.
|
|
|
|
struct SoundView: View {
|
|
|
|
struct SoundView: View {
|
|
|
|
let audio: AudioController
|
|
|
|
let audio: AudioController
|
|
|
|
|
|
|
|
let sourceList: SourceListModel
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
var body: some View {
|
|
|
|
ScrollView {
|
|
|
|
ScrollView {
|
|
|
|
VStack(alignment: .leading, spacing: Theme.Space.card) {
|
|
|
|
VStack(alignment: .leading, spacing: Theme.Space.card) {
|
|
|
|
header
|
|
|
|
header
|
|
|
|
MicrophoneChips(audio: audio)
|
|
|
|
|
|
|
|
if let device = audio.selectedDevice {
|
|
|
|
if let device = audio.selectedDevice {
|
|
|
|
if let error = audio.matrix.errors[device.uid] {
|
|
|
|
if let error = audio.matrix.errors[device.uid] {
|
|
|
|
Text(error).font(Theme.Font.body).foregroundStyle(Theme.Color.kohaku)
|
|
|
|
Text(error).font(Theme.Font.body).foregroundStyle(Theme.Color.kohaku)
|
|
|
|
@ -22,7 +22,7 @@ struct SoundView: View {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 300), spacing: Theme.Space.card)], spacing: Theme.Space.card) {
|
|
|
|
LazyVGrid(columns: [GridItem(.adaptive(minimum: 300), spacing: Theme.Space.card)], spacing: Theme.Space.card) {
|
|
|
|
ForEach(0..<device.outputChannels, id: \.self) { channel in
|
|
|
|
ForEach(0..<device.outputChannels, id: \.self) { channel in
|
|
|
|
ChannelStrip(audio: audio, device: device, channel: channel)
|
|
|
|
ChannelStrip(audio: audio, sourceList: sourceList, device: device, channel: channel)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
} else {
|
|
|
|
@ -57,34 +57,9 @@ struct SoundView: View {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private struct MicrophoneChips: View {
|
|
|
|
|
|
|
|
let audio: AudioController
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
|
|
|
|
VStack(alignment: .leading, spacing: Theme.Space.unit) {
|
|
|
|
|
|
|
|
Text("Mikrofone – über die Sprechtaste")
|
|
|
|
|
|
|
|
.font(Theme.Font.caption)
|
|
|
|
|
|
|
|
.foregroundStyle(Theme.Color.nezumi)
|
|
|
|
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
|
|
|
|
|
|
HStack(spacing: Theme.Space.unit) {
|
|
|
|
|
|
|
|
ForEach(audio.matrix.inputDevices) { device in
|
|
|
|
|
|
|
|
Text(verbatim: device.name)
|
|
|
|
|
|
|
|
.font(Theme.Font.caption)
|
|
|
|
|
|
|
|
.foregroundStyle(Theme.Color.washi)
|
|
|
|
|
|
|
|
.padding(.horizontal, Theme.Space.unit)
|
|
|
|
|
|
|
|
.padding(.vertical, Theme.Space.unit / 2)
|
|
|
|
|
|
|
|
.background(Theme.Color.kasumi, in: RoundedRectangle(cornerRadius: Theme.Radius.chip))
|
|
|
|
|
|
|
|
.overlay(RoundedRectangle(cornerRadius: Theme.Radius.chip).strokeBorder(Theme.Color.usuzumi))
|
|
|
|
|
|
|
|
.draggable("input:\(device.uid)")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private struct ChannelStrip: View {
|
|
|
|
private struct ChannelStrip: View {
|
|
|
|
let audio: AudioController
|
|
|
|
let audio: AudioController
|
|
|
|
|
|
|
|
let sourceList: SourceListModel
|
|
|
|
let device: AudioDevice
|
|
|
|
let device: AudioDevice
|
|
|
|
let channel: Int
|
|
|
|
let channel: Int
|
|
|
|
@State private var targeted = false
|
|
|
|
@State private var targeted = false
|
|
|
|
@ -93,19 +68,41 @@ private struct ChannelStrip: View {
|
|
|
|
audio.matrix.routes.filter { $0.deviceUID == device.uid && $0.channel == channel }
|
|
|
|
audio.matrix.routes.filter { $0.deviceUID == device.uid && $0.channel == channel }
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private var addMenu: some View {
|
|
|
|
|
|
|
|
Menu {
|
|
|
|
|
|
|
|
Section("NDI") {
|
|
|
|
|
|
|
|
ForEach(sourceList.sources, id: \.ndiName) { source in
|
|
|
|
|
|
|
|
Button(source.label) { audio.add(.ndi(name: source.ndiName), channel: channel) }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
Section("Mikrofone") {
|
|
|
|
|
|
|
|
ForEach(audio.matrix.inputDevices) { input in
|
|
|
|
|
|
|
|
Button(input.name) { audio.add(.input(deviceUID: input.uid), channel: channel) }
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
} label: {
|
|
|
|
|
|
|
|
Image(systemName: "plus")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
.menuStyle(.borderlessButton)
|
|
|
|
|
|
|
|
.menuIndicator(.hidden)
|
|
|
|
|
|
|
|
.fixedSize()
|
|
|
|
|
|
|
|
.help("Quelle hinzufügen")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
var body: some View {
|
|
|
|
var body: some View {
|
|
|
|
VStack(alignment: .leading, spacing: Theme.Space.unit) {
|
|
|
|
VStack(alignment: .leading, spacing: Theme.Space.unit) {
|
|
|
|
HStack {
|
|
|
|
HStack {
|
|
|
|
Text(verbatim: "Out \(channel + 1)").font(Theme.Font.title).foregroundStyle(Theme.Color.washi)
|
|
|
|
Text(verbatim: "Out \(channel + 1)").font(Theme.Font.title).foregroundStyle(Theme.Color.washi)
|
|
|
|
Spacer()
|
|
|
|
Spacer()
|
|
|
|
LevelText(db: audio.channelMeters["\(device.uid)#\(channel)"] ?? Level.floorDB)
|
|
|
|
LevelText(db: audio.channelMeters["\(device.uid)#\(channel)"] ?? Level.floorDB)
|
|
|
|
|
|
|
|
addMenu
|
|
|
|
}
|
|
|
|
}
|
|
|
|
LevelMeter(db: audio.channelMeters["\(device.uid)#\(channel)"] ?? Level.floorDB)
|
|
|
|
LevelMeter(db: audio.channelMeters["\(device.uid)#\(channel)"] ?? Level.floorDB)
|
|
|
|
|
|
|
|
|
|
|
|
Divider().overlay(Theme.Color.usuzumi)
|
|
|
|
Divider().overlay(Theme.Color.usuzumi)
|
|
|
|
|
|
|
|
|
|
|
|
if routes.isEmpty {
|
|
|
|
if routes.isEmpty {
|
|
|
|
Text("Quelle hierher ziehen")
|
|
|
|
Text("Quelle oder Mikrofon hierher ziehen")
|
|
|
|
.font(Theme.Font.body)
|
|
|
|
.font(Theme.Font.body)
|
|
|
|
.foregroundStyle(Theme.Color.nezumi)
|
|
|
|
.foregroundStyle(Theme.Color.nezumi)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
@ -178,8 +175,11 @@ private struct RouteRow: View {
|
|
|
|
.controlSize(.small)
|
|
|
|
.controlSize(.small)
|
|
|
|
Text(verbatim: route.gainDB <= Level.floorDB ? "−∞ dB" : String(format: "%+.1f dB", route.gainDB))
|
|
|
|
Text(verbatim: route.gainDB <= Level.floorDB ? "−∞ dB" : String(format: "%+.1f dB", route.gainDB))
|
|
|
|
.font(Theme.Font.number)
|
|
|
|
.font(Theme.Font.number)
|
|
|
|
.foregroundStyle(Theme.Color.nezumi)
|
|
|
|
.foregroundStyle(route.gainDB == 0 ? Theme.Color.nezumi : Theme.Color.washi)
|
|
|
|
.frame(width: 70, alignment: .trailing)
|
|
|
|
.frame(width: 70, alignment: .trailing)
|
|
|
|
|
|
|
|
.contentShape(Rectangle())
|
|
|
|
|
|
|
|
.onTapGesture(count: 2) { audio.matrix.setGain(route.id, db: 0) }
|
|
|
|
|
|
|
|
.help("Doppelklick: Pegel auf 0 dB")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
LevelMeter(db: audio.meters[route.id] ?? Level.floorDB)
|
|
|
|
LevelMeter(db: audio.meters[route.id] ?? Level.floorDB)
|
|
|
|
if let count = audio.dropouts[route.id], count > 0 {
|
|
|
|
if let count = audio.dropouts[route.id], count > 0 {
|
|
|
|
@ -187,6 +187,12 @@ private struct RouteRow: View {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.padding(.vertical, Theme.Space.unit / 2)
|
|
|
|
.padding(.vertical, Theme.Space.unit / 2)
|
|
|
|
|
|
|
|
.contextMenu {
|
|
|
|
|
|
|
|
Button("Pegel auf 0 dB") { audio.matrix.setGain(route.id, db: 0) }
|
|
|
|
|
|
|
|
Button(route.muted ? "Stummschaltung aufheben" : "Stummschalten") { audio.matrix.setMuted(route.id, !route.muted) }
|
|
|
|
|
|
|
|
Divider()
|
|
|
|
|
|
|
|
Button("Route entfernen", role: .destructive) { audio.remove(route.id) }
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|