From e2d814fb1c86687b5fd47341c88ed46d1446d5cc Mon Sep 17 00:00:00 2001 From: Aurelius Prochazka Date: Sun, 11 Sep 2022 00:44:22 -0700 Subject: [PATCH] Views for demos --- Package.swift | 9 +- Sources/AudioKitUI/Controls/ADSRWidget.swift | 18 ++ .../MultitouchGestureRecognizer.swift | 261 ------------------ .../Controls/ParameterEditor2.swift | 76 +++++ .../AudioKitUI/Controls/ParameterSlider.swift | 32 +++ .../Helpers/AudioKitUIHelpers.swift | 4 +- .../Visualizations/AmplitudeView.swift | 2 +- .../Visualizations/DryWetMixView.swift | 31 +++ 8 files changed, 166 insertions(+), 267 deletions(-) create mode 100644 Sources/AudioKitUI/Controls/ADSRWidget.swift delete mode 100644 Sources/AudioKitUI/Controls/MultitouchGestureRecognizer.swift create mode 100644 Sources/AudioKitUI/Controls/ParameterEditor2.swift create mode 100644 Sources/AudioKitUI/Controls/ParameterSlider.swift create mode 100644 Sources/AudioKitUI/Visualizations/DryWetMixView.swift diff --git a/Package.swift b/Package.swift index f9011e8..707929a 100644 --- a/Package.swift +++ b/Package.swift @@ -5,11 +5,14 @@ import PackageDescription let package = Package( name: "AudioKitUI", - platforms: [.macOS(.v12), .iOS(.v14)], + platforms: [.macOS(.v12), .iOS(.v15)], products: [.library(name: "AudioKitUI", targets: ["AudioKitUI"])], - dependencies: [.package(url: "https://github.com/AudioKit/AudioKit.git", from: "5.3.0")], + dependencies: [ + .package(url: "https://github.com/AudioKit/AudioKit.git", from: "5.3.0"), + .package(url: "https://github.com/AudioKit/Controls.git", from: "1.1.0"), + ], targets: [ - .target(name: "AudioKitUI", dependencies: ["AudioKit"], resources: [.process("Resources")]), + .target(name: "AudioKitUI", dependencies: ["AudioKit", "Controls"], resources: [.process("Resources")]), .testTarget(name: "AudioKitUITests", dependencies: ["AudioKitUI"]), ] ) diff --git a/Sources/AudioKitUI/Controls/ADSRWidget.swift b/Sources/AudioKitUI/Controls/ADSRWidget.swift new file mode 100644 index 0000000..8afdb3a --- /dev/null +++ b/Sources/AudioKitUI/Controls/ADSRWidget.swift @@ -0,0 +1,18 @@ +import AudioKit +import AVFoundation +import SwiftUI + +struct ADSRWidget: UIViewRepresentable { + typealias UIViewType = ADSRView + var callback: (AUValue, AUValue, AUValue, AUValue) -> Void + + func makeUIView(context _: Context) -> ADSRView { + let view = ADSRView(callback: callback) + view.bgColor = .systemBackground + return view + } + + func updateUIView(_: ADSRView, context _: Context) { + // + } +} diff --git a/Sources/AudioKitUI/Controls/MultitouchGestureRecognizer.swift b/Sources/AudioKitUI/Controls/MultitouchGestureRecognizer.swift deleted file mode 100644 index 5937b98..0000000 --- a/Sources/AudioKitUI/Controls/MultitouchGestureRecognizer.swift +++ /dev/null @@ -1,261 +0,0 @@ -// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKitUI/ - -import AudioKit -#if !os(macOS) || targetEnvironment(macCatalyst) -import UIKit.UIGestureRecognizerSubclass - -/// Extension of `UIGestureRecognizerDelegate` which allows the delegate to receive messages relating to -/// individual touches. The `delegate` property can be set to a class -/// implementing `MultitouchGestureRecognizerDelegate` and it will receive these messages. -@objc public protocol MultitouchGestureRecognizerDelegate: UIGestureRecognizerDelegate { - /// Called when a touch is started. - @objc optional func multitouchGestureRecognizer(_ gestureRecognizer: MultitouchGestureRecognizer, - touchDidBegin touch: UITouch) - - /// Called when a touch is updates. - @objc optional func multitouchGestureRecognizer(_ gestureRecognizer: MultitouchGestureRecognizer, - touchDidMove touch: UITouch) - - /// Called when a touch is cancelled. - @objc optional func multitouchGestureRecognizer(_ gestureRecognizer: MultitouchGestureRecognizer, - touchDidCancel touch: UITouch) - - /// Called when a touch is ended. - @objc optional func multitouchGestureRecognizer(_ gestureRecognizer: MultitouchGestureRecognizer, - touchDidEnd touch: UITouch) -} - -/// `UIGestureRecognizer` subclass which tracks the state of individual touches. -public class MultitouchGestureRecognizer: UIGestureRecognizer { - /// Denotes the way the list of touches is managed. - public enum Mode { - /// The first touch in is the first touch out. - case stack - - /// The first touch in is the last touch out. - case queue - } - - /// The touch management mode. - public var mode: Mode = .stack - - /// The maximum number of touches allowed in the stack/queue. Defaults to `0`, signifying unlimited touches. - /// If `count` is decreased past the current number of touches, any excess touches will be ended immediately. - public var count: Int = 0 { - didSet { - // swiftlint:disable empty_count - if count != 0 { - while count < touches.count { - switch mode { - case .stack: - if let lastTouch = touches.last { - end(lastTouch) - } - case .queue: - if let firstTouch = touches.first { - end(firstTouch) - } - } - } - } - } - } - - /// If `sustain` is set to `true`, when touches end they will be retained in `touches` until such time as all - /// touches have ended and a new touch begins. - /// If `sustain` is switched from `true` to `false`, any currently sustained touches will be ended immediately. - public var sustain: Bool = true { - didSet { - if oldValue == true, sustain == false { - end() - } - } - } - - /// The currently tracked collection of touches. May contain touches after they have ended, - /// if `sustain` is set to `true`. - public private(set) lazy var touches = [UITouch]() - - /// The current gesture recognizer state, as it pertains to the `sustain` setting. - public enum MultitouchState { - /// All touches are ended, and none are being sustained. - case ready - - /// One more more touches are currently in progress. - case live - - /// All touches have ended, but one or more is being retained in the `touches` collection - /// thanks to the `sustain` setting. - case sustained - } - - /// The current multitouch gesture recognizer state. - public var multitouchState: MultitouchState { - if touches.isEmpty { - return .ready - } else if touches.filter({ $0.phase != .ended }).isNotEmpty { - return .live - } else { - return .sustained - } - } - - // MARK: - Delegate - - internal var multitouchDelegate: MultitouchGestureRecognizerDelegate? { - return delegate as? MultitouchGestureRecognizerDelegate - } - - // MARK: - Overrides - - /// Handle new touches - public override func touchesBegan(_ touches: Set, with event: UIEvent) { - super.touchesBegan(touches, with: event) - - if sustain { - end() - } - update(touches) - } - - /// Handle moved touches - public override func touchesMoved(_ touches: Set, with event: UIEvent) { - super.touchesMoved(touches, with: event) - - update(touches) - } - - /// Handle cancelled touches - public override func touchesCancelled(_ touches: Set, with event: UIEvent) { - super.touchesCancelled(touches, with: event) - - update(touches) - } - - /// Handle ended touches - public override func touchesEnded(_ touches: Set, with event: UIEvent) { - super.touchesEnded(touches, with: event) - - update(touches) - } - - // MARK: - Touch updating - - private func update(_ touches: Set) { - for touch in touches { - switch touch.phase { - case .began: - start(touch) - case .moved: - move(touch) - case .stationary: - move(touch) - case .cancelled: - cancel(touch) - case .ended where sustain: - move(touch) - case .ended: - end(touch) - case .regionEntered: - break - case .regionMoved: - break - case .regionExited: - break - @unknown default: - fatalError("Unknown touch phase!") - } - } - } - - private func end() { - for touch in touches where touch.phase == .ended { - end(touch) - } - } - - // MARK: - Single touches - - private func start(_ touch: UITouch) { - guard count == 0 || count > touches.count else { - if let firstTouch = touches.first, mode == .queue { - end(firstTouch) - start(touch) - } - return - } - - touches.append(touch) - multitouchDelegate?.multitouchGestureRecognizer?(self, touchDidBegin: touch) - } - - private func move(_ touch: UITouch) { - if touches.contains(touch) { - multitouchDelegate?.multitouchGestureRecognizer?(self, touchDidMove: touch) - } - } - - private func cancel(_ touch: UITouch) { - if touches.contains(touch) { - touches.remove(touch) - multitouchDelegate?.multitouchGestureRecognizer?(self, touchDidCancel: touch) - } - } - - private func end(_ touch: UITouch) { - if touches.contains(touch) { - touches.remove(touch) - multitouchDelegate?.multitouchGestureRecognizer?(self, touchDidEnd: touch) - } - } -} - -// MARK: - Centroid helpers - -extension MultitouchGestureRecognizer { - /// The average of all touch locations in the current view. - public var centroid: CGPoint? { - guard let view = view, touches.isNotEmpty else { - return nil - } - - let location = touches.reduce(.zero) { (location, touch) -> CGPoint in - let touchLocation = touch.location(in: view) - - return CGPoint( - x: location.x + touchLocation.x / CGFloat(touches.count), - y: location.y + touchLocation.y / CGFloat(touches.count) - ) - } - - return location - } - - /// The average of all previous touch locations in the current view. - public var previousCentroid: CGPoint? { - guard let view = view, touches.isNotEmpty else { - return nil - } - - let location = touches.reduce(.zero) { (location, touch) -> CGPoint in - let touchLocation = touch.previousLocation(in: view) - - return CGPoint( - x: location.x + touchLocation.x / CGFloat(touches.count), - y: location.y + touchLocation.y / CGFloat(touches.count) - ) - } - - return location - } -} - -// MARK: - Private extensions - -extension Array where Element: Equatable { - mutating func remove(_ element: Element) { - self = filter { $0 != element } - } -} - -#endif diff --git a/Sources/AudioKitUI/Controls/ParameterEditor2.swift b/Sources/AudioKitUI/Controls/ParameterEditor2.swift new file mode 100644 index 0000000..ba36a2c --- /dev/null +++ b/Sources/AudioKitUI/Controls/ParameterEditor2.swift @@ -0,0 +1,76 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKitUI/ + +import AudioKit +import Controls +import SwiftUI +import CoreMIDI + +/// Hack to get SwiftUI to poll and refresh our UI. +class Refresher: ObservableObject { + @Published var version = 0 +} + +public struct ParameterEditor2: View { + var param: NodeParameter + @StateObject var refresher = Refresher() + + public init(param: NodeParameter) { + self.param = param + } + + func floatToDoubleRange(_ floatRange: ClosedRange) -> ClosedRange { + Double(floatRange.lowerBound) ... Double(floatRange.upperBound) + } + + func getBinding() -> Binding { + Binding( + get: { param.value }, + set: { param.value = $0; refresher.version += 1} + ) + } + + func getIntBinding() -> Binding { + Binding(get: { Int(param.value) }, set: { param.value = AUValue($0); refresher.version += 1 }) + } + + func intValues() -> [Int] { + Array(Int(param.range.lowerBound) ... Int(param.range.upperBound)) + } + var format: String { + if (param.range.upperBound - param.range.lowerBound) > 20 { + return "%0.0f" + } else { + return "%0.2f" + } + } + + public var body: some View { + VStack(alignment: .center) { + Text(param.def.name).font(.title2) + Text("\(String(format: format, param.value))") + + switch param.def.unit { + case .boolean: + Toggle(isOn: Binding(get: { param.value == 1.0 }, set: { + param.value = $0 ? 1.0 : 0.0; refresher.version += 1 + }), label: { Text(param.def.name) }) + case .indexed: + if param.range.upperBound - param.range.lowerBound < 5 { + Picker(param.def.name, selection: getIntBinding()) { + ForEach(intValues(), id: \.self) { value in + Text("\(value)").tag(value) + } + } + .pickerStyle(.segmented) + } else { + SmallKnob(value: getBinding(), range: param.range) + .frame(maxHeight: 200) + + } + default: + SmallKnob(value: getBinding(), range: param.range) + .frame(maxHeight: 200) + } + } + } +} diff --git a/Sources/AudioKitUI/Controls/ParameterSlider.swift b/Sources/AudioKitUI/Controls/ParameterSlider.swift new file mode 100644 index 0000000..e428b6a --- /dev/null +++ b/Sources/AudioKitUI/Controls/ParameterSlider.swift @@ -0,0 +1,32 @@ +import AVFoundation +import Controls +import SwiftUI + +struct ParameterSlider: View { + var text: String + @Binding var parameter: AUValue + var range: ClosedRange + var format: String = "%0.2f" + var units: String = "" + + var body: some View { + VStack { + Text(text) + if units == "" || units == "Generic" { + Text("\(parameter, specifier: format)") + } else if units == "%" || units == "Percent" { + Text("\(parameter * 100, specifier: "%0.f")%") + } else if units == "Percent-0-100" { // for audio units that use 0-100 instead of 0-1 + Text("\(parameter, specifier: "%0.f")%") + } else if units == "Hertz" { + Text("\(parameter, specifier: "%0.2f") Hz") + } else { + Text("\(parameter, specifier: format) \(units)") + } + SmallKnob(value: $parameter, range: range) + .frame(maxHeight: 200) + } + } +} + + diff --git a/Sources/AudioKitUI/Helpers/AudioKitUIHelpers.swift b/Sources/AudioKitUI/Helpers/AudioKitUIHelpers.swift index e9d1c29..123820d 100644 --- a/Sources/AudioKitUI/Helpers/AudioKitUIHelpers.swift +++ b/Sources/AudioKitUI/Helpers/AudioKitUIHelpers.swift @@ -28,8 +28,8 @@ extension View { extension Shape { @ViewBuilder - func flexableFill(fillType: FillType) -> some View { - switch fillType { + func flexibleFill(type: FillType) -> some View { + switch type { case let .solid(color): fill(color) case let .gradient(gradient): diff --git a/Sources/AudioKitUI/Visualizations/AmplitudeView.swift b/Sources/AudioKitUI/Visualizations/AmplitudeView.swift index 0e4e1c1..e47a795 100644 --- a/Sources/AudioKitUI/Visualizations/AmplitudeView.swift +++ b/Sources/AudioKitUI/Visualizations/AmplitudeView.swift @@ -67,7 +67,7 @@ public struct AmplitudeView: View { // colored rectangle in the back if !isClipping { Rectangle() - .flexableFill(fillType: fillType) + .flexibleFill(type: fillType) } else { Rectangle() .fill(Color.red) diff --git a/Sources/AudioKitUI/Visualizations/DryWetMixView.swift b/Sources/AudioKitUI/Visualizations/DryWetMixView.swift new file mode 100644 index 0000000..2f040d5 --- /dev/null +++ b/Sources/AudioKitUI/Visualizations/DryWetMixView.swift @@ -0,0 +1,31 @@ +import AudioKit +import AVFoundation +import SwiftUI + +struct DryWetMixView: View { + var dry: Node + var wet: Node + var mix: Node + + var height: CGFloat = 100 + + func plot(_ node: Node, label: String, color: Color) -> some View { + VStack { + HStack { Text(label); Spacer() } + ZStack { + RoundedRectangle(cornerRadius: 10) + .foregroundColor(Color(hue: 0, saturation: 0, brightness: 0.5, opacity: 0.2)) + .frame(height: height) + NodeOutputView(node, color: color).frame(height: height).clipped() + } + } + } + + var body: some View { + VStack(spacing: 30) { + plot(dry, label: "Input", color: .red) + plot(wet, label: "Processed Signal", color: .blue) + plot(mix, label: "Mixed Output", color: .purple) + } + } +}