0c7b6b4442
Desktop:
- New dedicated MIDI Controller In port (separate from MIDI In), same CC learn/bind pipeline but never passes notes through; positioned first in the IO column order
- Fixed MIDI In "Not Connected" not persisting across restart (missing save_io_config call)
- Fixed MIDI Out/Transport Out mutual-exclusion incorrectly flagging "Not Connected" as an in-use port
- Renamed "Remote" checkbox to "App" on Fader/Toggle/Transport widgets
- FocusFaderWidget/FocusToggleWidget: new Hardware section (Learn + CC/CH bind) so a physical controller can drive a Channel Focus widget alongside the app/OSC, plus an independent Feedback checkbox that sends translated CC out (for motorized fader / LED sync)
- JL Cooper touch-sense profile ("No Profile" / "MIDI JL Cooper CC Mode") on FaderWidget/FocusFaderWidget: touch channel (value channel - 1, same CC) is filtered out of the value and gates incoming OSC/DAW feedback while touching
- FocusFeedbackWidget: "Custom" checkbox to show a manually-typed static string instead of live OSC feedback, disabling Learn/OSC input and suppressing the iPad broadcast while active
- New daw-config-reaper/ folder with reaper-osc-config-paul-custom.ReaperOSC
iOS:
- New FocusToggleWidget (dedicated Channel Focus toggle, split out of shared ToggleWidget) with colorName-aware darken/border and isMomentary handling — keeps Channel-Focus-only behavior off the regular ToggleWidget
- Arrange mode long-press menu: text alignment (left/center/right) and a 10-color named-palette Text Color picker for TitleWidget/FocusFeedbackWidget, persisted in the saved layout
- FocusFeedbackWidget: Reset Size action, monospaced-digit font fix for timestamp/counter jitter
- New "Cancel Changes" button in Arrange mode — reverts layout/hides/aligns to the state at Arrange-mode entry and restores desktop-side Remote/dest assignments
- Grid line brightness increased for visibility
- Bonjour discovery for desktop auto-connect
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
146 lines
6.6 KiB
Swift
146 lines
6.6 KiB
Swift
import UIKit
|
|
|
|
extension ArrangeObjects {
|
|
|
|
// MARK: - WebSocket
|
|
|
|
func connectWebSocket(url: URL) {
|
|
self.urlSession = URLSession(configuration: .default, delegate: self, delegateQueue: .main)
|
|
self.wsClient = self.urlSession?.webSocketTask(with: url)
|
|
self.wsClient?.resume()
|
|
self.updateUIFromConnectionState(.connected)
|
|
self.wsClientReceiveMessage()
|
|
}
|
|
|
|
func wsClientReceiveMessage() {
|
|
self.wsClient?.receive { [weak self] result in
|
|
guard let self = self else { return }
|
|
switch result {
|
|
case .success(let message):
|
|
switch message {
|
|
case .string(let text): self.wsClientHandleIncomingJSON(text)
|
|
default: break
|
|
}
|
|
self.wsClientReceiveMessage()
|
|
case .failure:
|
|
DispatchQueue.main.async {
|
|
self.updateUIFromConnectionState(.disconnected)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func wsClientHandleIncomingJSON(_ text: String) {
|
|
self.loggingView.log(text, category: .incoming)
|
|
guard let data = text.data(using: .utf8),
|
|
let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
|
let event = json["event"] as? String else { return }
|
|
|
|
switch event {
|
|
case "preset":
|
|
guard self.autoSwitchEnabled else {
|
|
self.loggingView.log("preset event ignored — auto switch is off", category: .local)
|
|
return
|
|
}
|
|
guard let presetData = json["data"] as? [String: Any],
|
|
let presetBytes = try? JSONSerialization.data(withJSONObject: presetData) else {
|
|
let msg = "preset: missing or malformed 'data' field in envelope"
|
|
self.loggingView.log(msg, category: .local)
|
|
DispatchQueue.main.async {
|
|
let alert = UIAlertController(title: "Preset Load Error", message: msg, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
self.present(alert, animated: true)
|
|
}
|
|
return
|
|
}
|
|
do {
|
|
let decoder = JSONDecoder()
|
|
decoder.keyDecodingStrategy = .convertFromSnakeCase
|
|
let preset = try decoder.decode(ModelPresetLoadToRemoteDevice.self, from: presetBytes)
|
|
self.loggingView.log(
|
|
"✓ preset loaded: '\(preset.presetName)' uuid=\(preset.presetUuid) faders=\(preset.faders.count) toggles=\(preset.toggles.count) transports=\(preset.transports.count)",
|
|
category: .local
|
|
)
|
|
if let encoded = try? JSONEncoder().encode(preset),
|
|
let jsonStr = String(data: encoded, encoding: .utf8) {
|
|
self.loggingView.log(jsonStr, category: .local)
|
|
}
|
|
DispatchQueue.main.async {
|
|
self.currentPreset = preset
|
|
guard self.isChannelFocusMode == false else {
|
|
self.loggingView.log("preset event cached but not rendered — channel focus is active", category: .local)
|
|
return
|
|
}
|
|
let isNewPreset = preset.presetUuid != self.presetUuid
|
|
if self.isLocked == false && isNewPreset {
|
|
let alert = UIAlertController(
|
|
title: "Preset Switched",
|
|
message: "Switching to:\n\"\(preset.presetName)\" — \(preset.displayTitle)\n\nSave your current layout before switching?",
|
|
preferredStyle: .alert
|
|
)
|
|
alert.addAction(UIAlertAction(title: "Save & Switch", style: .default) { _ in
|
|
self.saveLayout()
|
|
self.buildUIFromPreset(preset)
|
|
})
|
|
alert.addAction(UIAlertAction(title: "Discard & Switch", style: .destructive) { _ in
|
|
self.buildUIFromPreset(preset)
|
|
})
|
|
self.present(alert, animated: true)
|
|
} else {
|
|
self.buildUIFromPreset(preset)
|
|
}
|
|
}
|
|
} catch {
|
|
let msg = "preset decode failed: \(error.localizedDescription)"
|
|
self.loggingView.log(msg, category: .local)
|
|
DispatchQueue.main.async {
|
|
let alert = UIAlertController(title: "Preset Load Error", message: msg, preferredStyle: .alert)
|
|
alert.addAction(UIAlertAction(title: "OK", style: .default))
|
|
self.present(alert, animated: true)
|
|
}
|
|
}
|
|
case "widget_update":
|
|
if let uid = json["uid"] as? String, let value = json["value"] as? Int {
|
|
DispatchQueue.main.async { self.updateWidget(uid: uid, value: value) }
|
|
}
|
|
case "widget_label":
|
|
if let uid = json["uid"] as? String, let label = json["label"] as? String {
|
|
DispatchQueue.main.async { self.updateWidgetLabel(uid: uid, label: label) }
|
|
}
|
|
case "widget_update_f":
|
|
if let uid = json["uid"] as? String, let value = json["value"] as? Double {
|
|
DispatchQueue.main.async { self.widgets[uid]?.applyFloatValue(value) }
|
|
}
|
|
case "widget_feedback":
|
|
if let uid = json["uid"] as? String, let text = json["text"] as? String {
|
|
DispatchQueue.main.async { self.widgets[uid]?.applyFeedbackText(text) }
|
|
}
|
|
case "daw_state":
|
|
DispatchQueue.main.async { self.updateUIFromDawState(json) }
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
func wsClientSendJSON(_ dict: [String: Any]) {
|
|
guard let data = try? JSONSerialization.data(withJSONObject: dict),
|
|
let text = String(data: data, encoding: .utf8) else { return }
|
|
self.loggingView.log(text, category: .outgoing)
|
|
self.wsClient?.send(.string(text)) { _ in }
|
|
}
|
|
|
|
}
|
|
|
|
// MARK: - WebSocket Delegate
|
|
extension ArrangeObjects: URLSessionWebSocketDelegate {
|
|
|
|
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
|
|
self.updateUIFromConnectionState(.connected)
|
|
}
|
|
|
|
func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
|
|
self.updateUIFromConnectionState(.disconnected)
|
|
}
|
|
|
|
}
|