Skip to content

Commit cac62c8

Browse files
committed
Add Swift bindings
2 parents 869c94f + e855b51 commit cac62c8

18 files changed

+1471
-15
lines changed

CMakeLists.txt

+70
Original file line numberDiff line numberDiff line change
@@ -143,3 +143,73 @@ foreach(SAMPLE ${SAMPLES})
143143
install(FILES ${SAMPLE} DESTINATION bin/assets/samples)
144144
endif()
145145
endforeach()
146+
147+
if (APPLE)
148+
set(SWIFT_SOURCE_FILES
149+
"labsound-c.h"
150+
"labsound-c.cpp"
151+
"tinycthread.h"
152+
"tinycthread.c"
153+
"flecs.h"
154+
"flecs.c"
155+
"Swift/LabSoundWrapper.h"
156+
"Swift/LabSoundWrapper.mm"
157+
"Swift/LabSoundDemoApp.swift"
158+
"Swift/LabSoundDemo (iOS)-Bridging-Header.h"
159+
"Swift/LabSoundDemo (macOS)-Bridging-Header.h"
160+
"Swift/Controls/Control.swift"
161+
"Swift/Controls/ControlGeometry.swift"
162+
"Swift/Controls/Helpers.swift"
163+
"Swift/Controls/Implementations/Ribbon.swift"
164+
"Swift/Controls/Implementations/SmallKnob.swift"
165+
"Swift/Controls/PlanarGeometry.swift"
166+
"Swift/Controls/SingleTouchView.swift"
167+
)
168+
set(COPYRIGHT "2023 Nick Porcino\nBSD License")
169+
set(ICON_NAME "LabSoundDemo.icns")
170+
set(ICON_PATH "assets/LabSoundDemo.icns")
171+
set_source_files_properties(${ICON_PATH} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")
172+
173+
enable_language("Swift")
174+
175+
add_executable(LabSoundSwiftDemo MACOSX_BUNDLE ${SWIFT_SOURCE_FILES} ${ICON_PATH})
176+
177+
set_property(TARGET LabSoundSwiftDemo PROPERTY C_STANDARD 11)
178+
set_property(TARGET LabSoundSwiftDemo PROPERTY CXX_STANDARD 17)
179+
180+
target_link_libraries(LabSoundSwiftDemo
181+
LabSound::LabSound
182+
LabSoundRtAudio::LabSoundRtAudio
183+
${PLATFORM_LIBS})
184+
target_include_directories(LabSoundSwiftDemo PRIVATE "${LABSOUNDDEMO_ROOT}")
185+
186+
set_target_properties(LabSoundSwiftDemo PROPERTIES
187+
Swift_LANGUAGE_VERSION 5.0
188+
XCODE_ATTRIBUTE_SWIFT_OBJC_INTERFACE_HEADER_NAME "LabSoundWrapper.h"
189+
XCODE_ATTRIBUTE_DERIVED_FILE_DIR "${PROJECT_BINARY_DIR}"
190+
XCODE_ATTRIBUTE_SWIFT_OBJC_BRIDGING_HEADER "${PROJECT_SOURCE_DIR}/Swift/LabSoundDemo (macOS)-Bridging-Header.h"
191+
MACOSX_BUNDLE_ICON_FILE ${ICON_NAME}
192+
MACOSX_BUNDLE_COPYRIGHT ${COPYRIGHT}
193+
)
194+
195+
install(TARGETS LabSoundSwiftDemo
196+
CONFIGURATIONS Debug Release
197+
BUNDLE DESTINATION Debug/ COMPONENT Runtime
198+
RUNTIME DESTINATION Debug/ COMPONENT Runtime
199+
BUNDLE DESTINATION Release/ COMPONENT Runtime
200+
RUNTIME DESTINATION Release/ COMPONENT Runtime
201+
)
202+
203+
# Install dynamic libraries to the bundle
204+
# https://github.com/ionyshch/cmake-bundle-macos/blob/master/CMakeLists.txt
205+
set(APPS "${CMAKE_BINARY_DIR}/Debug/${PROJECT_NAME}.app")
206+
install(CODE "
207+
include(BundleUtilities)
208+
fixup_bundle(\"${APPS}\" \"\" \"${CMAKE_BINARY_DIR}/Debug\")"
209+
)
210+
211+
set(CPACK_GENERATOR "DRAGNDROP")
212+
include(CPack)
213+
214+
endif(APPLE)
215+

LabSoundCDemo.c

+28-15
Original file line numberDiff line numberDiff line change
@@ -163,17 +163,17 @@ int main(int argc, char** argcv)
163163
}
164164
//-------------------------------------------------------------------------
165165

166-
if (true)
166+
if (false)
167167
{
168168
printf("test: play a file, wait for end\n");
169169
char buff[1024];
170170
snprintf(buff, sizeof(buff), "%ssamples/mono-music-clip.wav", asset_base);
171171
ls_BusData musicClip = ls->bus_create_from_file(ls, buff, false);
172172
if (musicClip.id != ls_BusData_empty.id)
173173
{
174-
ls_Node sampledAudio;
175-
ls_InputPin sampledAudio_srcBus;
176-
ls_OutputPin sa_out;
174+
ls_Node sampledAudio;
175+
ls_InputPin sampledAudio_srcBus;
176+
ls_OutputPin sa_out;
177177
ls_Connection connection3;
178178

179179
sampledAudio = ls->node_create(ls, san_s, SampledAudio_s);
@@ -209,19 +209,32 @@ int main(int argc, char** argcv)
209209
ls_BusData trainClip = ls->bus_create_from_file(ls, buff, false);
210210
if (trainClip.id != ls_BusData_empty.id)
211211
{
212-
ls_Node sampledAudio = ls->node_create(ls, san_s, SampledAudio_s);
213-
ls_InputPin src = ls->node_setting(ls, sampledAudio, sourceBus_s);
214-
ls_OutputPin sa_out = ls->node_indexed_output(ls, sampledAudio, 0);
212+
ls_Node sampledAudio;
213+
ls_InputPin src;
214+
ls_OutputPin sa_out;
215+
ls_Node stPanner;
216+
ls_OutputPin stPanner_out;
217+
ls_InputPin stPanner_in;
218+
ls_Connection connection3;
219+
ls_Connection connection4;
220+
ls_InputPin pan_param;
221+
222+
sampledAudio = ls->node_create(ls, san_s, SampledAudio_s);
223+
stPanner = ls->node_create(ls, stpan_s, StereoPanner_s);
224+
225+
src = ls->node_setting(ls, sampledAudio, sourceBus_s);
226+
sa_out = ls->node_indexed_output(ls, sampledAudio, 0);
227+
stPanner_out = ls->node_indexed_output(ls, stPanner, 0);
228+
stPanner_in = ls->node_indexed_input(ls, stPanner, 0);
229+
pan_param = ls->node_parameter(ls, stPanner, pan_s);
230+
231+
connection4 = ls->connect(ls, stPanner_in, sa_out);
232+
connection3 = ls->connect(ls, dest_in, stPanner_out);
233+
215234
ls->set_bus(ls, src, trainClip);
216-
217-
ls_Node stPanner = ls->node_create(ls, stpan_s, StereoPanner_s);
218-
ls_OutputPin stPanner_out = ls->node_indexed_output(ls, stPanner, 0);
219-
ls_InputPin stPanner_in = ls->node_indexed_input(ls, stPanner, 0);
220-
ls_Connection connection3 = ls->connect(ls, dest_in, stPanner_out);
221-
ls_Connection connection4 = ls->connect(ls, stPanner_in, sa_out);
222235
ls->node_schedule(ls, sampledAudio, (ls_Seconds) { 0.f }, -1); // -1 to loop forever
223-
ls_InputPin pan_param = ls->node_parameter(ls, stPanner, pan_s);
224-
236+
ls->node_diagnose(ls, sampledAudio);
237+
225238
float seconds = 4.f;
226239
float half = seconds * 0.5f;
227240
for (float i = 0; i < seconds; i += 0.01f) {

Swift/Controls/Control.swift

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import SwiftUI
2+
3+
/// A view in which dragging on it will change bound variables and perform closures
4+
public struct Control<Content: View>: View {
5+
let content: (GeometryProxy) -> Content
6+
var geometry: ControlGeometry
7+
var onStarted: () -> Void
8+
var onEnded: () -> Void
9+
@Binding var value: Float
10+
var range: ClosedRange<Float>
11+
var padding: CGSize
12+
13+
@State var hasStarted = false
14+
@State var rect: CGRect = .zero
15+
@State var touchLocation: CGPoint = .zero {
16+
didSet {
17+
value = geometry.calculateValue(value: value,
18+
in: range,
19+
from: oldValue,
20+
to: touchLocation,
21+
inRect: rect,
22+
padding: padding)
23+
}
24+
}
25+
26+
/// Initialize the draggable
27+
/// - Parameters:
28+
/// - value: Value that is controlled
29+
/// - in range: The limits of the value (defaults to 0-1)
30+
/// - geometry: Gesture movement geometry specification
31+
/// - onStarted: Closure to perform when the drag starts
32+
/// - onEnded: Closure to perform when the drag finishes
33+
/// - content: View to render
34+
public init(value: Binding<Float>,
35+
in range: ClosedRange<Float> = 0 ... 1,
36+
geometry: ControlGeometry = .twoDimensionalDrag(),
37+
padding: CGSize = .zero,
38+
onStarted: @escaping () -> Void = {},
39+
onEnded: @escaping () -> Void = {},
40+
@ViewBuilder content: @escaping (GeometryProxy) -> Content)
41+
{
42+
self.geometry = geometry
43+
_value = value
44+
self.range = range
45+
self.onStarted = onStarted
46+
self.onEnded = onEnded
47+
self.content = content
48+
self.padding = padding
49+
}
50+
51+
public var body: some View {
52+
GeometryReader { proxy in
53+
ZStack {
54+
content(proxy)
55+
SingleTouchView { touch in
56+
if let touch = touch {
57+
if !hasStarted {
58+
onStarted()
59+
hasStarted = true
60+
}
61+
touchLocation = touch
62+
} else {
63+
touchLocation = .zero
64+
onEnded()
65+
hasStarted = false
66+
}
67+
}
68+
}
69+
.onAppear {
70+
rect = proxy.frame(in: .local)
71+
}
72+
.onChange(of: proxy.size) { newValue in
73+
rect = proxy.frame(in: .local)
74+
}
75+
}
76+
}
77+
}

Swift/Controls/ControlGeometry.swift

+98
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import SwiftUI
2+
3+
/// Geometry defines how the touch point's location affect the control values
4+
public enum ControlGeometry {
5+
6+
/// Horizontal slider controls in which you want the position to be exactly at the touch
7+
case horizontalPoint
8+
9+
/// Vertical slider controls in which you want the position to be exactly at the touch
10+
case verticalPoint
11+
12+
/// Knobs or controls that can start at any value and be changed with horizotal motion
13+
case horizontalDrag(xSensitivity: Double = 1.0)
14+
15+
/// Knobs or controls that can start at any value and be changed with vertical motion
16+
case verticalDrag(ySensitivity: Double = 1.0)
17+
18+
/// Controls that can change with either vertical or horizontal movement
19+
/// This is the type of knob used on AudioKit SynthOne
20+
case twoDimensionalDrag(xSensitivity: Double = 1.0, ySensitivity: Double = 1.0)
21+
22+
/// Larger knobs that you want to rotate immediately to the touch point
23+
case angle(angularRange: ClosedRange<Angle> = Angle.zero ... Angle(degrees: 360))
24+
25+
/// This allows the user to drag around the center
26+
case angularDrag(angularRange: ClosedRange<Angle> = Angle.zero ... Angle(degrees: 360))
27+
28+
func calculateValue(value: Float,
29+
in range: ClosedRange<Float> = 0 ... 1,
30+
from oldValue: CGPoint,
31+
to touchLocation: CGPoint,
32+
inRect rect: CGRect,
33+
padding: CGSize) -> Float
34+
{
35+
guard touchLocation != .zero else { return value }
36+
37+
var temp = (value - range.lowerBound) / (range.upperBound - range.lowerBound)
38+
39+
let x = touchLocation.x - padding.width
40+
let y = touchLocation.y - padding.height
41+
42+
switch self {
43+
case .horizontalPoint:
44+
temp = Float(x / (rect.size.width - 2 * padding.width))
45+
46+
case .verticalPoint:
47+
temp = Float(1.0 - y / (rect.size.height - 2 * padding.height))
48+
49+
case let .horizontalDrag(xSensitivity: xSensitivity):
50+
if oldValue != .zero {
51+
temp += Float((touchLocation.x - oldValue.x) * xSensitivity / (rect.size.width - 2 * padding.width))
52+
}
53+
54+
case let .verticalDrag(ySensitivity: ySensitivity):
55+
if oldValue != .zero {
56+
temp -= Float((touchLocation.y - oldValue.y) * ySensitivity / (rect.size.height - 2 * padding.height))
57+
}
58+
59+
case let .twoDimensionalDrag(xSensitivity: xSensitivity, ySensitivity: ySensitivity):
60+
if oldValue != .zero {
61+
temp += Float((touchLocation.x - oldValue.x) * xSensitivity / (rect.size.width - 2 * padding.width))
62+
temp -= Float((touchLocation.y - oldValue.y) * ySensitivity / (rect.size.height - 2 * padding.height))
63+
}
64+
case let .angle(angularRange: angularRange):
65+
let polar = polarCoordinate(point: touchLocation, rect: rect)
66+
let width = angularRange.upperBound.degrees - angularRange.lowerBound.degrees
67+
68+
temp = Float((polar.angle.degrees - angularRange.lowerBound.degrees) / width)
69+
70+
case .angularDrag(angularRange: _):
71+
if oldValue != .zero {
72+
let oldPolar = polarCoordinate(point: oldValue, rect: rect)
73+
let newPolar = polarCoordinate(point: touchLocation, rect: rect)
74+
temp += Float((newPolar.angle.radians - oldPolar.angle.radians) / (2.0 * .pi))
75+
}
76+
}
77+
78+
// Bound and convert to range
79+
let newValue = max(0.0, min(1.0, temp)) * (range.upperBound - range.lowerBound) + range.lowerBound
80+
81+
return newValue
82+
}
83+
84+
func polarCoordinate(point: CGPoint, rect: CGRect) -> PolarCoordinate {
85+
// Calculate the x and y distances from the center
86+
let deltaX = (point.x - rect.midX) / (rect.width / 2.0)
87+
let deltaY = (point.y - rect.midY) / (rect.height / 2.0)
88+
89+
// Convert to polar
90+
let radius = max(0.0, min(1.0, sqrt(pow(deltaX, 2) + pow(deltaY, 2))))
91+
var theta = atan((point.y - rect.midY) / (point.x - rect.midX))
92+
93+
// Rotate to clockwise polar from -y axis (most like a knob)
94+
theta += .pi * (deltaX > 0 ? 1.5 : 0.5)
95+
96+
return PolarCoordinate(radius: Float(radius), angle: Angle(radians: theta))
97+
}
98+
}

Swift/Controls/Helpers.swift

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import SwiftUI
2+
3+
public extension View {
4+
func squareFrame(_ squareSide: CGFloat) -> some View {
5+
frame(width: squareSide, height: squareSide)
6+
}
7+
}
8+
9+
extension CGRect {
10+
func offset(by off: CGSize) -> CGRect {
11+
offsetBy(dx: off.width, dy: off.height)
12+
}
13+
}

0 commit comments

Comments
 (0)