From dd7a4296e8322a73c5a8c0b3958e6e952c6aa06e Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Fri, 8 Dec 2023 09:18:02 -0500 Subject: [PATCH] Add STK WIP Recipe --- Cookbook/Cookbook.xcodeproj/project.pbxproj | 17 +++ .../Sources/CookbookCommon/ContentView.swift | 1 + .../Recipes/MiniApps/Arpeggiator.swift | 14 +- .../Recipes/MiniApps/Audio3D.swift | 2 - .../Recipes/MiniApps/InstrumentSFZ.swift | 46 +++--- .../Recipes/MiniApps/SpriteKitAudio.swift | 1 - .../Recipes/WIP/DunneSynth.swift | 29 ++-- .../Recipes/WIP/InputDeviceDemo.swift | 1 + .../Recipes/WIP/MIDIPortTest.swift | 1 + .../Recipes/WIP/PolyphonicOscillator.swift | 38 +++-- .../Recipes/WIP/PolyphonicSTK+MIDIKit.swift | 144 ++++++++++++++++++ .../CookbookKeyboard.swift | 36 +++++ 12 files changed, 260 insertions(+), 70 deletions(-) create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift diff --git a/Cookbook/Cookbook.xcodeproj/project.pbxproj b/Cookbook/Cookbook.xcodeproj/project.pbxproj index 3f467fb..a75d527 100644 --- a/Cookbook/Cookbook.xcodeproj/project.pbxproj +++ b/Cookbook/Cookbook.xcodeproj/project.pbxproj @@ -16,6 +16,7 @@ 5A7F40462B21FD06000A28F9 /* Waveform in Frameworks */ = {isa = PBXBuildFile; productRef = 5A7F40452B21FD06000A28F9 /* Waveform */; }; 5A7F40492B21FE34000A28F9 /* PianoRoll in Frameworks */ = {isa = PBXBuildFile; productRef = 5A7F40482B21FE34000A28F9 /* PianoRoll */; }; 5A7F404C2B220667000A28F9 /* STKAudioKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5A7F404B2B220667000A28F9 /* STKAudioKit */; }; + 5A7F40572B22774A000A28F9 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5A7F40562B22774A000A28F9 /* MIDIKit */; }; C446DE542528D8E700138D0A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C446DE522528D8E700138D0A /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ @@ -40,6 +41,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5A7F40572B22774A000A28F9 /* MIDIKit in Frameworks */, 5A7F404C2B220667000A28F9 /* STKAudioKit in Frameworks */, 5A7F40432B21F314000A28F9 /* Flow in Frameworks */, 5A7F40492B21FE34000A28F9 /* PianoRoll in Frameworks */, @@ -133,6 +135,7 @@ 5A7F40452B21FD06000A28F9 /* Waveform */, 5A7F40482B21FE34000A28F9 /* PianoRoll */, 5A7F404B2B220667000A28F9 /* STKAudioKit */, + 5A7F40562B22774A000A28F9 /* MIDIKit */, ); productName = Cookbook; productReference = C446DE442528D8E600138D0A /* Cookbook.app */; @@ -166,6 +169,7 @@ 5A7F40442B21FD06000A28F9 /* XCRemoteSwiftPackageReference "Waveform" */, 5A7F40472B21FE34000A28F9 /* XCRemoteSwiftPackageReference "PianoRoll" */, 5A7F404A2B220667000A28F9 /* XCRemoteSwiftPackageReference "STKAudioKit" */, + 5A7F40552B22774A000A28F9 /* XCRemoteSwiftPackageReference "MIDIKit" */, ); productRefGroup = C446DE452528D8E600138D0A /* Products */; projectDirPath = ""; @@ -431,6 +435,14 @@ minimumVersion = 5.5.4; }; }; + 5A7F40552B22774A000A28F9 /* XCRemoteSwiftPackageReference "MIDIKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/orchetect/MIDIKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.4; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -458,6 +470,11 @@ package = 5A7F404A2B220667000A28F9 /* XCRemoteSwiftPackageReference "STKAudioKit" */; productName = STKAudioKit; }; + 5A7F40562B22774A000A28F9 /* MIDIKit */ = { + isa = XCSwiftPackageProductDependency; + package = 5A7F40552B22774A000A28F9 /* XCRemoteSwiftPackageReference "MIDIKit" */; + productName = MIDIKit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C446DE3C2528D8E600138D0A /* Project object */; diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift index 7611620..fca6073 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift @@ -225,6 +225,7 @@ struct MasterView: View { NavigationLink("Input Device Demo", destination: InputDeviceDemoView()) NavigationLink("MIDI Port Test", destination: MIDIPortTestView()) NavigationLink("Polyphonic Oscillator", destination: PolyphonicOscillatorView()) + NavigationLink("Polyphonic STK + MIDIKit", destination: PolyphonicSTKView()) NavigationLink("Roland Tb303 Filter", destination: RolandTB303FilterView()) } } diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift index 4cdf764..a46c04e 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift @@ -126,15 +126,13 @@ struct ArpeggiatorView: View { @Environment(\.colorScheme) var colorScheme var body: some View { - VStack{ - NodeOutputView(conductor.instrument) - HStack { - CookbookKnob(text: "BPM", parameter: $conductor.tempo, range: 20.0...250.0) - CookbookKnob(text: "Length", parameter: $conductor.noteLength, range: 0.0...1.0) - } - CookbookKeyboard(noteOn: conductor.noteOn, - noteOff: conductor.noteOff) + NodeOutputView(conductor.instrument) + HStack { + CookbookKnob(text: "BPM", parameter: $conductor.tempo, range: 20.0...250.0) + CookbookKnob(text: "Length", parameter: $conductor.noteLength, range: 0.0...1.0) } + CookbookKeyboard(noteOn: conductor.noteOn, + noteOff: conductor.noteOff) .cookbookNavBarTitle("Arpeggiator") .onAppear { conductor.start() diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Audio3D.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Audio3D.swift index b5c3996..d25944c 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Audio3D.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Audio3D.swift @@ -178,8 +178,6 @@ struct AudioKit3DView: View { .onDisappear { viewModel.conductor.stop() } - .background(colorScheme == .dark ? - Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) } } diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift index 83dbf80..b086e14 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift @@ -37,30 +37,28 @@ struct InstrumentSFZView: View { @Environment(\.colorScheme) var colorScheme var body: some View { - VStack{ - HStack { - ForEach(0...7, id: \.self){ - ParameterRow(param: conductor.instrument.parameters[$0]) - } - }.padding(5) - HStack { - ForEach(8...15, id: \.self){ - ParameterRow(param: conductor.instrument.parameters[$0]) - } - }.padding(5) - HStack { - ForEach(16...23, id: \.self){ - ParameterRow(param: conductor.instrument.parameters[$0]) - } - }.padding(5) - HStack { - ForEach(24...30, id: \.self){ - ParameterRow(param: conductor.instrument.parameters[$0]) - } - }.padding(5) - CookbookKeyboard(noteOn: conductor.noteOn, - noteOff: conductor.noteOff) - } + HStack { + ForEach(0...7, id: \.self){ + ParameterRow(param: conductor.instrument.parameters[$0]) + } + }.padding(5) + HStack { + ForEach(8...15, id: \.self){ + ParameterRow(param: conductor.instrument.parameters[$0]) + } + }.padding(5) + HStack { + ForEach(16...23, id: \.self){ + ParameterRow(param: conductor.instrument.parameters[$0]) + } + }.padding(5) + HStack { + ForEach(24...30, id: \.self){ + ParameterRow(param: conductor.instrument.parameters[$0]) + } + }.padding(5) + CookbookKeyboard(noteOn: conductor.noteOn, + noteOff: conductor.noteOff) .cookbookNavBarTitle("Instrument SFZ") .onAppear { conductor.start() diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift index 51d7a0a..3849545 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift @@ -101,7 +101,6 @@ struct SpriteKitAudioView: View { VStack { SpriteView(scene: scene).frame(maxWidth: .infinity, maxHeight: .infinity).ignoresSafeArea() } - .cookbookNavBarTitle("SpriteKit Audio") .onAppear { conductor.start() }.onDisappear { diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift index 3c32bd4..34b9dd3 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift @@ -35,21 +35,20 @@ struct DunneSynthView: View { @Environment(\.colorScheme) var colorScheme var body: some View { - VStack{ - NodeOutputView(conductor.instrument) - HStack { - ForEach(0...6, id: \.self){ - ParameterRow(param: conductor.instrument.parameters[$0]) - } - }.padding(5) - HStack { - ForEach(7...13, id: \.self){ - ParameterRow(param: conductor.instrument.parameters[$0]) - } - }.padding(5) - CookbookKeyboard(noteOn: conductor.noteOn, - noteOff: conductor.noteOff) - } + NodeOutputView(conductor.instrument) + HStack { + ForEach(0...6, id: \.self){ + ParameterRow(param: conductor.instrument.parameters[$0]) + } + }.padding(5) + HStack { + ForEach(7...13, id: \.self){ + ParameterRow(param: conductor.instrument.parameters[$0]) + } + }.padding(5) + CookbookKeyboard(noteOn: conductor.noteOn, + noteOff: conductor.noteOff) + .cookbookNavBarTitle("Dunne Synth") .onAppear { conductor.start() } diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/InputDeviceDemo.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/InputDeviceDemo.swift index 9bccc20..38c8127 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/InputDeviceDemo.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/InputDeviceDemo.swift @@ -71,5 +71,6 @@ struct InputDeviceDemoView: View { }) .keyboardShortcut(.space, modifiers: []) } + .cookbookNavBarTitle("Input Device Demo") } } diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/MIDIPortTest.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/MIDIPortTest.swift index 1d32338..3f05c61 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/MIDIPortTest.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/MIDIPortTest.swift @@ -220,6 +220,7 @@ struct MIDIPortTestView: View { } } } + .cookbookNavBarTitle("MIDI Port Test") .onAppear { conductor.start() } diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift index 4bdfd05..91e8671 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift @@ -12,7 +12,7 @@ class PolyphonicOscillatorConductor: ObservableObject, HasAudioEngine { var notes = Array(repeating: 0, count: 11) var osc = [Oscillator(), Oscillator(), Oscillator(), Oscillator(), Oscillator(), Oscillator(), Oscillator(), Oscillator(), Oscillator(), Oscillator(), Oscillator()] - + func noteOn(pitch: Pitch, point _: CGPoint) { for num in 0 ... 10 { if notes[num] == 0 { @@ -23,7 +23,7 @@ class PolyphonicOscillatorConductor: ObservableObject, HasAudioEngine { } } } - + func noteOff(pitch: Pitch) { for num in 0 ... 10 { if notes[num] == pitch.intValue { @@ -33,7 +33,7 @@ class PolyphonicOscillatorConductor: ObservableObject, HasAudioEngine { } } } - + init() { for num in 0 ... 10 { osc[num].amplitude = 0.0 @@ -47,23 +47,21 @@ class PolyphonicOscillatorConductor: ObservableObject, HasAudioEngine { struct PolyphonicOscillatorView: View { @StateObject var conductor = PolyphonicOscillatorConductor() @Environment(\.colorScheme) var colorScheme - + var body: some View { - VStack { - if conductor.engine.output != nil { - NodeOutputView(conductor.engine.output!) - } - CookbookKeyboard(noteOn: conductor.noteOn, - noteOff: conductor.noteOff) - - }.cookbookNavBarTitle("Polyphonic Oscillator") - .onAppear { - conductor.start() - } - .onDisappear { - conductor.stop() - } - .background(colorScheme == .dark ? - Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) + if conductor.engine.output != nil { + NodeOutputView(conductor.engine.output!) + } + CookbookKeyboard(noteOn: conductor.noteOn, + noteOff: conductor.noteOff) + .cookbookNavBarTitle("Polyphonic Oscillator") + .onAppear { + conductor.start() + } + .onDisappear { + conductor.stop() + } + .background(colorScheme == .dark ? + Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) } } diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift new file mode 100644 index 0000000..2a10ef2 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift @@ -0,0 +1,144 @@ +import AudioKit +import AudioKitEX +import AudioKitUI +import AudioToolbox +import Keyboard +import SoundpipeAudioKit +import STKAudioKit +import SwiftUI +import Tonic +import MIDIKit +import DunneAudioKit + +class PolyphonicSTKConductor: ObservableObject, HasAudioEngine { + let engine = AudioEngine() + let mixer = Mixer() + var notes = Array(repeating: 0, count: 11) + var osc = [RhodesPianoKey(), RhodesPianoKey(), RhodesPianoKey(), RhodesPianoKey(), RhodesPianoKey(), + RhodesPianoKey(), RhodesPianoKey(), RhodesPianoKey(), RhodesPianoKey(), RhodesPianoKey(), RhodesPianoKey()] + + // MIDI Manager (MIDI methods are in SoundFont+MIDI) + let midiManager = MIDIManager( + clientName: "TestAppMIDIManager", + model: "TestApp", + manufacturer: "MyCompany" + ) + + var env: Array + + var numPlaying = 0 + func noteOn(pitch: Pitch, velocity: Int = 127) { + numPlaying += 1 + if numPlaying > 10 { + numPlaying = 0 + } + osc[numPlaying].trigger(note: MIDINoteNumber(pitch.intValue), velocity: MIDIVelocity(velocity)) + notes[numPlaying] = pitch.intValue + env[numPlaying].openGate() + } + + func noteOn(pitch: Pitch, point _: CGPoint) { + noteOn(pitch: pitch, velocity: 120) + + } + + func noteOff(pitch: Pitch) { + for num in 0 ... 10 { + if notes[num] == pitch.intValue { + env[num].closeGate() + notes[num] = 0 + } + } + } + + init() { + env = [AmplitudeEnvelope(osc[0]),AmplitudeEnvelope(osc[1]),AmplitudeEnvelope(osc[2]),AmplitudeEnvelope(osc[3]),AmplitudeEnvelope(osc[4]),AmplitudeEnvelope(osc[5]),AmplitudeEnvelope(osc[6]),AmplitudeEnvelope(osc[7]),AmplitudeEnvelope(osc[8]),AmplitudeEnvelope(osc[9]),AmplitudeEnvelope(osc[10])] + + for envelope in env { + envelope.attackDuration = 0 + envelope.releaseDuration = 0.2 + mixer.addInput(envelope) + } + + engine.output = mixer + + // Set up MIDI + MIDIConnect() + } + + // Connect MIDI on init + func MIDIConnect() { + do { + print("Starting MIDI services.") + try midiManager.start() + } catch { + print("Error starting MIDI services:", error.localizedDescription) + } + + do { + try midiManager.addInputConnection( + to: .allOutputs, // no need to specify if we're using .allEndpoints + tag: "Listener", + filter: .owned(), // don't allow self-created virtual endpoints + receiver: .events { [weak self] events in + // Note: this handler will be called on a background thread + // so call the next line on main if it may result in UI updates + DispatchQueue.main.async { + events.forEach { self?.received(midiEvent: $0) } + } + } + ) + } catch { + print( + "Error setting up managed MIDI all-listener connection:", + error.localizedDescription + ) + } + } + + // MIDI Events + private func received(midiEvent: MIDIKit.MIDIEvent) { + switch midiEvent { + case .noteOn(let payload): + print("Note On:", payload.note, payload.velocity, payload.channel) + noteOn(pitch: Pitch(Int8(payload.note.number.uInt8Value)), velocity: Int(payload.velocity.midi1Value.uInt8Value)) + NotificationCenter.default.post(name: .MIDIKey, object: nil, userInfo: ["info": payload.note.number.uInt8Value, "bool": true]) + case .noteOff(let payload): + print("Note Off:", payload.note, payload.velocity, payload.channel) + noteOff(pitch: Pitch(Int8(payload.note.number.uInt8Value))) + NotificationCenter.default.post(name: .MIDIKey, object: nil, userInfo: ["info": payload.note.number.uInt8Value, "bool": false]) + case .cc(let payload): + print("CC:", payload.controller, payload.value, payload.channel) + case .programChange(let payload): + print("Program Change:", payload.program, payload.channel) + default: + break + } + } +} + +extension NSNotification.Name { + static let MIDIKey = Notification.Name("MIDIKey") +} + +struct PolyphonicSTKView: View { + @StateObject var conductor = PolyphonicSTKConductor() + @Environment(\.colorScheme) var colorScheme + + var body: some View { + if conductor.engine.output != nil { + NodeOutputView(conductor.engine.output!) + } + MIDIKitKeyboard(noteOn: conductor.noteOn, + noteOff: conductor.noteOff) + .cookbookNavBarTitle("Polyphonic STK + MIDIKit") + .onAppear { + conductor.start() + } + .onDisappear { + conductor.stop() + } + .background(colorScheme == .dark ? + Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/CookbookKeyboard.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/CookbookKeyboard.swift index 8b1f69a..f540588 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/CookbookKeyboard.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/CookbookKeyboard.swift @@ -11,3 +11,39 @@ struct CookbookKeyboard: View { noteOn: noteOn, noteOff: noteOff) } } + +struct MIDIKitKeyboard: View { + var noteOn: (Pitch, CGPoint) -> Void = { _, _ in } + var noteOff: (Pitch) -> Void + var body: some View { + Keyboard(layout: .piano(pitchRange: Pitch(48) ... Pitch(64)), + noteOn: noteOn, noteOff: noteOff){ pitch, isActivated in + MIDIKitKeyboardKey(pitch: pitch, + isActivated: isActivated, color: .red) + }.cornerRadius(5) + } +} + +struct MIDIKitKeyboardKey: View { + @State var MIDIKeyPressed = [Bool](repeating: false, count: 128) + var pitch : Pitch + var isActivated : Bool + var color: Color + + var body: some View { + VStack{ + KeyboardKey(pitch: pitch, + isActivated: isActivated, + text: "", + whiteKeyColor: .white, + blackKeyColor: .black, + pressedColor: color, + flatTop: true, + isActivatedExternally: MIDIKeyPressed[pitch.intValue]) + }.onReceive(NotificationCenter.default.publisher(for: .MIDIKey), perform: { obj in + if let userInfo = obj.userInfo, let info = userInfo["info"] as? UInt8, let val = userInfo["bool"] as? Bool { + self.MIDIKeyPressed[Int(info)] = val + } + }) + } +}