From 5839f33009f3ec928a4698ca971546ea3bccd2d2 Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Thu, 7 Dec 2023 11:12:42 -0500 Subject: [PATCH 1/4] Added New Recipes Added Additional Packages category with their respective demos: Controls, Flow, Keyboard, Piano Roll, Synthesis Toolkit, & Waveform Added Arpeggiator recipe (using Sequencer) Added SpriteKit Audio recipe Added DunneAudioKit Synth recipe (in WIP) Added Input Device Demo recipe (already in project but unlinked) Added parameters to Instrument SFZ Added info button Reordered categories --- Cookbook/Cookbook.xcodeproj/project.pbxproj | 73 +++++++- .../Sources/CookbookCommon/ContentView.swift | 139 +++++++++----- .../AdditionalPackages/ControlsView.swift | 116 ++++++++++++ .../Recipes/AdditionalPackages/FlowView.swift | 50 +++++ .../AdditionalPackages/KeyboardView.swift | 177 ++++++++++++++++++ .../AdditionalPackages/PianoRollView.swift | 20 ++ .../Recipes/AdditionalPackages/STKView.swift | 160 ++++++++++++++++ .../AdditionalPackages/WaveformView.swift | 97 ++++++++++ .../Recipes/MiniApps/Arpeggiator.swift | 150 +++++++++++++++ .../{Effects => MiniApps}/Audio3D.swift | 0 .../Recipes/MiniApps/InstrumentEXS.swift | 28 +-- .../Recipes/MiniApps/InstrumentSFZ.swift | 51 +++-- .../Recipes/MiniApps/SpriteKitAudio.swift | 111 +++++++++++ .../AudioFileView.swift | 0 .../CallbackInstrument.swift | 0 .../{ => UncategorizedDemos}/Table.swift | 0 .../Recipes/WIP/DunneSynth.swift | 64 +++++++ .../Recipes/{ => WIP}/InputDeviceDemo.swift | 0 18 files changed, 1155 insertions(+), 81 deletions(-) create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/ControlsView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/FlowView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/KeyboardView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/PianoRollView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/STKView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/WaveformView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift rename Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/{Effects => MiniApps}/Audio3D.swift (100%) create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift rename Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/{ => UncategorizedDemos}/AudioFileView.swift (100%) rename Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/{ => UncategorizedDemos}/CallbackInstrument.swift (100%) rename Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/{ => UncategorizedDemos}/Table.swift (100%) create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift rename Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/{ => WIP}/InputDeviceDemo.swift (100%) diff --git a/Cookbook/Cookbook.xcodeproj/project.pbxproj b/Cookbook/Cookbook.xcodeproj/project.pbxproj index b06e66e..3f467fb 100644 --- a/Cookbook/Cookbook.xcodeproj/project.pbxproj +++ b/Cookbook/Cookbook.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 52; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -12,6 +12,10 @@ 29FC959927CC154B006D8CDF /* CookbookApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29FC959827CC154B006D8CDF /* CookbookApp.swift */; }; 31BA90FC29C371AB00FCD505 /* audio3D.scnassets in Resources */ = {isa = PBXBuildFile; fileRef = 31BA90FB29C371AB00FCD505 /* audio3D.scnassets */; }; 5A0C234827D7CA4E003E281C /* Sounds in Resources */ = {isa = PBXBuildFile; fileRef = 5A0C234727D7CA4E003E281C /* Sounds */; }; + 5A7F40432B21F314000A28F9 /* Flow in Frameworks */ = {isa = PBXBuildFile; productRef = 5A7F40422B21F314000A28F9 /* Flow */; }; + 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 */; }; C446DE542528D8E700138D0A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C446DE522528D8E700138D0A /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ @@ -36,6 +40,10 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5A7F404C2B220667000A28F9 /* STKAudioKit in Frameworks */, + 5A7F40432B21F314000A28F9 /* Flow in Frameworks */, + 5A7F40492B21FE34000A28F9 /* PianoRoll in Frameworks */, + 5A7F40462B21FD06000A28F9 /* Waveform in Frameworks */, 29215CE827CC30CF005B706C /* CookbookCommon in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -121,6 +129,10 @@ name = Cookbook; packageProductDependencies = ( 29215CE727CC30CF005B706C /* CookbookCommon */, + 5A7F40422B21F314000A28F9 /* Flow */, + 5A7F40452B21FD06000A28F9 /* Waveform */, + 5A7F40482B21FE34000A28F9 /* PianoRoll */, + 5A7F404B2B220667000A28F9 /* STKAudioKit */, ); productName = Cookbook; productReference = C446DE442528D8E600138D0A /* Cookbook.app */; @@ -150,6 +162,10 @@ ); mainGroup = C446DE3B2528D8E600138D0A; packageReferences = ( + 5A7F40412B21F314000A28F9 /* XCRemoteSwiftPackageReference "Flow" */, + 5A7F40442B21FD06000A28F9 /* XCRemoteSwiftPackageReference "Waveform" */, + 5A7F40472B21FE34000A28F9 /* XCRemoteSwiftPackageReference "PianoRoll" */, + 5A7F404A2B220667000A28F9 /* XCRemoteSwiftPackageReference "STKAudioKit" */, ); productRefGroup = C446DE452528D8E600138D0A /* Products */; projectDirPath = ""; @@ -382,11 +398,66 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + 5A7F40412B21F314000A28F9 /* XCRemoteSwiftPackageReference "Flow" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AudioKit/Flow"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.3; + }; + }; + 5A7F40442B21FD06000A28F9 /* XCRemoteSwiftPackageReference "Waveform" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AudioKit/Waveform"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.2; + }; + }; + 5A7F40472B21FE34000A28F9 /* XCRemoteSwiftPackageReference "PianoRoll" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AudioKit/PianoRoll"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.7; + }; + }; + 5A7F404A2B220667000A28F9 /* XCRemoteSwiftPackageReference "STKAudioKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/AudioKit/STKAudioKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 5.5.4; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + /* Begin XCSwiftPackageProductDependency section */ 29215CE727CC30CF005B706C /* CookbookCommon */ = { isa = XCSwiftPackageProductDependency; productName = CookbookCommon; }; + 5A7F40422B21F314000A28F9 /* Flow */ = { + isa = XCSwiftPackageProductDependency; + package = 5A7F40412B21F314000A28F9 /* XCRemoteSwiftPackageReference "Flow" */; + productName = Flow; + }; + 5A7F40452B21FD06000A28F9 /* Waveform */ = { + isa = XCSwiftPackageProductDependency; + package = 5A7F40442B21FD06000A28F9 /* XCRemoteSwiftPackageReference "Waveform" */; + productName = Waveform; + }; + 5A7F40482B21FE34000A28F9 /* PianoRoll */ = { + isa = XCSwiftPackageProductDependency; + package = 5A7F40472B21FE34000A28F9 /* XCRemoteSwiftPackageReference "PianoRoll" */; + productName = PianoRoll; + }; + 5A7F404B2B220667000A28F9 /* STKAudioKit */ = { + isa = XCSwiftPackageProductDependency; + package = 5A7F404A2B220667000A28F9 /* XCRemoteSwiftPackageReference "STKAudioKit" */; + productName = STKAudioKit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C446DE3C2528D8E600138D0A /* Project object */; diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift index deee93e..7611620 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift @@ -9,12 +9,15 @@ struct ContentView: View { } struct MasterView: View { + @State private var showingInfo = false var body: some View { Form { Section(header: Text("Categories")) { Group { DisclosureGroup("Mini Apps") { Group { + NavigationLink("Arpeggiator", destination: ArpeggiatorView()) + NavigationLink("Audio 3D", destination: AudioKit3DView()) NavigationLink("Drum Pads", destination: DrumsView()) NavigationLink("Drum Sequencer", destination: DrumSequencerView()) NavigationLink("Drum Synthesizers", destination: DrumSynthesizersView()) @@ -28,21 +31,14 @@ struct MasterView: View { NavigationLink("Music Toy", destination: MusicToyView()) NavigationLink("Noise Generators", destination: NoiseGeneratorsView()) NavigationLink("Recorder", destination: RecorderView()) + NavigationLink("SpriteKit Audio", destination: SpriteKitAudioView()) NavigationLink("Telephone", destination: Telephone()) NavigationLink("Tuner", destination: TunerView()) NavigationLink("Vocal Tract", destination: VocalTractView()) } } } - Group { - DisclosureGroup("Uncategorized Demos") { - Group { - NavigationLink("Audio Files View", destination: AudioFileRecipeView()) - NavigationLink("Callback Instrument", destination: CallbackInstrumentView()) - NavigationLink("Tables", destination: TableRecipeView()) - } - } - } + Group { DisclosureGroup("Operations") { Group { @@ -63,6 +59,7 @@ struct MasterView: View { } } } + Group { DisclosureGroup("Physical Models") { Group { @@ -78,10 +75,10 @@ struct MasterView: View { } } } + Group { DisclosureGroup("Effects") { Group { - NavigationLink("Audio 3D", destination: AudioKit3DView()) NavigationLink("Auto Panner", destination: AutoPannerView()) NavigationLink("Auto Wah", destination: AutoWahView()) NavigationLink("Balancer", destination: BalancerView()) @@ -93,7 +90,7 @@ struct MasterView: View { NavigationLink("Expander", destination: ExpanderView()) } Group { - NavigationLink("Flanger", destination: FlangerView()) + NavigationLink("Flanger", destination: FlangerView()) NavigationLink("MultiTap Delay", destination: MultiTapDelayView()) NavigationLink("Panner", destination: PannerView()) NavigationLink("Peak Limiter", destination: PeakLimiterView()) @@ -106,12 +103,13 @@ struct MasterView: View { } Group { NavigationLink("Time / Pitch", destination: TimePitchView()) - NavigationLink("Transient Shaper", destination: TransientShaperView()) + NavigationLink("Transient Shaper", destination: TransientShaperView()) NavigationLink("Tremolo", destination: TremoloView()) NavigationLink("Variable Delay", destination: VariableDelayView()) } } } + Group { DisclosureGroup("Distortion") { Group { @@ -124,6 +122,7 @@ struct MasterView: View { } } } + Group { DisclosureGroup("Reverb") { Group { @@ -132,48 +131,49 @@ struct MasterView: View { NavigationLink("Comb Filter Reverb", destination: CombFilterReverbView()) NavigationLink("Costello Reverb", destination: CostelloReverbView()) NavigationLink("Flat Frequency Response Reverb", - destination: FlatFrequencyResponseReverbView()) + destination: FlatFrequencyResponseReverbView()) NavigationLink("Zita Reverb", destination: ZitaReverbView()) } } } - Group { - DisclosureGroup("Filters") { - Group { - NavigationLink("Band Pass Butterworth Filter", - destination: BandPassButterworthFilterView()) - NavigationLink("Band Reject Butterworth Filter", - destination: BandRejectButterworthFilterView()) - NavigationLink("Equalizer Filter", destination: EqualizerFilterView()) - NavigationLink("Formant Filter", destination: FormantFilterView()) - NavigationLink("High Pass Butterworth Filter", - destination: HighPassButterworthFilterView()) - NavigationLink("High Pass Filter", destination: HighPassFilterView()) - NavigationLink("High Shelf Filter", destination: HighShelfFilterView()) - NavigationLink("High Shelf Parametric Equalizer Filter", - destination: HighShelfParametricEqualizerFilterView()) - NavigationLink("Korg Low Pass Filter", destination: KorgLowPassFilterView()) - NavigationLink("Low Pass Butterworth Filter", - destination: LowPassButterworthFilterView()) - } - Group { - NavigationLink("Low Pass Filter", destination: LowPassFilterView()) - NavigationLink("Low Shelf Filter", destination: LowShelfFilterView()) - NavigationLink("Low Shelf Parametric Equalizer Filter", - destination: LowShelfParametricEqualizerFilterView()) - NavigationLink("Modal Resonance Filter", destination: ModalResonanceFilterView()) - NavigationLink("Moog Ladder", destination: MoogLadderView()) - NavigationLink("Peaking Parametric Equalizer Filter", - destination: PeakingParametricEqualizerFilterView()) - NavigationLink("Resonant Filter", destination: ResonantFilterView()) - NavigationLink("Three Pole Lowpass Filter", destination: ThreePoleLowpassFilterView()) - NavigationLink("Tone Filter", destination: ToneFilterView()) - } - Group { - NavigationLink("Tone Complement Filter", destination: ToneComplementFilterView()) - } + + DisclosureGroup("Filters") { + Group { + NavigationLink("Band Pass Butterworth Filter", + destination: BandPassButterworthFilterView()) + NavigationLink("Band Reject Butterworth Filter", + destination: BandRejectButterworthFilterView()) + NavigationLink("Equalizer Filter", destination: EqualizerFilterView()) + NavigationLink("Formant Filter", destination: FormantFilterView()) + NavigationLink("High Pass Butterworth Filter", + destination: HighPassButterworthFilterView()) + NavigationLink("High Pass Filter", destination: HighPassFilterView()) + NavigationLink("High Shelf Filter", destination: HighShelfFilterView()) + NavigationLink("High Shelf Parametric Equalizer Filter", + destination: HighShelfParametricEqualizerFilterView()) + NavigationLink("Korg Low Pass Filter", destination: KorgLowPassFilterView()) + NavigationLink("Low Pass Butterworth Filter", + destination: LowPassButterworthFilterView()) } - + Group { + NavigationLink("Low Pass Filter", destination: LowPassFilterView()) + NavigationLink("Low Shelf Filter", destination: LowShelfFilterView()) + NavigationLink("Low Shelf Parametric Equalizer Filter", + destination: LowShelfParametricEqualizerFilterView()) + NavigationLink("Modal Resonance Filter", destination: ModalResonanceFilterView()) + NavigationLink("Moog Ladder", destination: MoogLadderView()) + NavigationLink("Peaking Parametric Equalizer Filter", + destination: PeakingParametricEqualizerFilterView()) + NavigationLink("Resonant Filter", destination: ResonantFilterView()) + NavigationLink("Three Pole Lowpass Filter", destination: ThreePoleLowpassFilterView()) + NavigationLink("Tone Filter", destination: ToneFilterView()) + } + Group { + NavigationLink("Tone Complement Filter", destination: ToneComplementFilterView()) + } + } + + Group { DisclosureGroup("Oscillators") { Group { NavigationLink("Amplitude Envelope", destination: AmplitudeEnvelopeView()) @@ -185,7 +185,7 @@ struct MasterView: View { NavigationLink("Waveform Morphing", destination: MorphingOscillatorView()) } } - + DisclosureGroup("Audio Player") { Group { NavigationLink("Completion Handler", destination: AudioPlayerCompletionHandler()) @@ -193,11 +193,36 @@ struct MasterView: View { NavigationLink("Playlist", destination: PlaylistView()) } } - + + Group { + DisclosureGroup("Additional Packages") { + Group { + NavigationLink("Controls", destination: ControlsView()) + NavigationLink("Flow", destination: FlowView()) + NavigationLink("Keyboard", destination: KeyboardView()) + NavigationLink("Piano Roll", destination: PianoRollView()) + NavigationLink("Synthesis Toolkit", destination: STKView()) + NavigationLink("Waveform", destination: WaveformView()) + } + } + } + + Group { + DisclosureGroup("Uncategorized Demos") { + Group { + NavigationLink("Audio Files View", destination: AudioFileRecipeView()) + NavigationLink("Callback Instrument", destination: CallbackInstrumentView()) + NavigationLink("Tables", destination: TableRecipeView()) + } + } + } + DisclosureGroup("WIP") { Group { NavigationLink("Base Tap Demo", destination: BaseTapDemoView()) NavigationLink("Channel/Device Routing", destination: ChannelDeviceRoutingView()) + NavigationLink("DunneAudioKit Synth", destination: DunneSynthView()) + NavigationLink("Input Device Demo", destination: InputDeviceDemoView()) NavigationLink("MIDI Port Test", destination: MIDIPortTestView()) NavigationLink("Polyphonic Oscillator", destination: PolyphonicOscillatorView()) NavigationLink("Roland Tb303 Filter", destination: RolandTB303FilterView()) @@ -207,6 +232,18 @@ struct MasterView: View { } } .navigationBarTitle("AudioKit") + .toolbar { + Button { + showingInfo = true + } label: { + Image(systemName: "info.circle") + } + } + .alert("AudioKit Cookbook", isPresented: $showingInfo) { + Button("OK", role: .cancel) { } + } message: { + Text("AudioKit is an audio synthesis, processing, and analysis platform for iOS, macOS, and tvOS.\n\nMost of the examples that were inside of AudioKit are now in this application.\n\nIn addition to the resources found here, there are various open-source example projects on GitHub and YouTube created by AudioKit contributors.") + } } } diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/ControlsView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/ControlsView.swift new file mode 100644 index 0000000..a2e3b26 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/ControlsView.swift @@ -0,0 +1,116 @@ +import Controls +import Keyboard +import SwiftUI +import Tonic + +struct ControlsView: View { + + @State var pitchBend: Float = 0.5 + @State var modulation: Float = 0 + @State var radius: Float = 0 + @State var angle: Float = 0 + @State var x: Float = 0.5 + @State var y: Float = 0.5 + + @State var octaveRange = 1 + @State var layoutType = 0 + + @State var filter: Float = 33 + @State var resonance: Float = 66 + @State var volume: Float = 80 + @State var pan: Float = 0 + + @State var smallKnobValue: Float = 0.5 + + @State var ribbon: Float = 0 + + @State var lowestNote = 48 + var hightestNote: Int { + (octaveRange + 1) * 12 + lowestNote + } + + var layout: KeyboardLayout { + let pitchRange = Pitch(intValue: lowestNote)...Pitch(intValue: hightestNote) + if layoutType == 0 { + return .piano(pitchRange: pitchRange) + } else if layoutType == 1 { + return .isomorphic(pitchRange: pitchRange) + } else { + return .guitar() + } + } + var body: some View { + GeometryReader { proxy in + HStack(spacing: 10) { + VStack { + Spacer() + HStack { + Joystick(radius: $radius, angle: $angle) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + .squareFrame(140) + XYPad(x: $x, y: $y) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + .cornerRadius(20) + .indicatorSize(CGSize(width: 15, height: 15)) + .squareFrame(140) + ArcKnob("FIL", value: $filter) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + ArcKnob("RES", value: $resonance) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + ArcKnob("PAN", value: $pan, range: -50...50) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + ArcKnob("VOL", value: $volume) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + }.frame(height: 140) + HStack { + Text("Octaves:") + .padding(.leading, 140) + IndexedSlider(index: $octaveRange, labels: ["1", "2", "3"]) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + .cornerRadius(10) + + Text("Detune:") + SmallKnob(value: $smallKnobValue) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + Text("Layout:") + .padding(.leading, 140) + IndexedSlider(index: $layoutType, labels: ["Piano", "Isomorphic", "Guitar"]) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + .cornerRadius(10) + } + .frame(height: 30) + Ribbon(position: $ribbon) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + .cornerRadius(5) + .frame(height: 15) + .padding(.leading, 140) + + HStack { + PitchWheel(value: $pitchBend) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + .cornerRadius(10) + .frame(width: 60) + ModWheel(value: $modulation) + .backgroundColor(.gray.opacity(0.5)) + .foregroundColor(.white.opacity(0.5)) + .cornerRadius(10) + .frame(width: 60) + Keyboard(layout: layout) + } + } + } + } + .navigationTitle("Controls Demo") + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/FlowView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/FlowView.swift new file mode 100644 index 0000000..44a8056 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/FlowView.swift @@ -0,0 +1,50 @@ +import SwiftUI +import Flow + +func simplePatch() -> Patch { + let generator = Node(name: "generator", titleBarColor: Color.cyan, outputs: ["out"]) + let processor = Node(name: "processor", titleBarColor: Color.red, inputs: ["in"], outputs: ["out"]) + let mixer = Node(name: "mixer", titleBarColor: Color.gray, inputs: ["in1", "in2"], outputs: ["out"]) + let output = Node(name: "output", titleBarColor: Color.purple, inputs: ["in"]) + + let nodes = [generator, processor, generator, processor, mixer, output] + + let wires = Set([Wire(from: OutputID(0, 0), to: InputID(1, 0)), + Wire(from: OutputID(1, 0), to: InputID(4, 0)), + Wire(from: OutputID(2, 0), to: InputID(3, 0)), + Wire(from: OutputID(3, 0), to: InputID(4, 1)), + Wire(from: OutputID(4, 0), to: InputID(5, 0))]) + + var patch = Patch(nodes: nodes, wires: wires) + patch.recursiveLayout(nodeIndex: 5, at: CGPoint(x: 800, y: 50)) + return patch +} + +/// Bit of a stress test to show how Flow performs with more nodes. +func randomPatch() -> Patch { + var randomNodes: [Node] = [] + for n in 0 ..< 50 { + let randomPoint = CGPoint(x: 1000 * Double.random(in: 0 ... 1), + y: 1000 * Double.random(in: 0 ... 1)) + randomNodes.append(Node(name: "node\(n)", + position: randomPoint, + inputs: ["In"], + outputs: ["Out"])) + } + + var randomWires: Set = [] + for n in 0 ..< 50 { + randomWires.insert(Wire(from: OutputID(n, 0), to: InputID(Int.random(in: 0 ... 49), 0))) + } + return Patch(nodes: randomNodes, wires: randomWires) +} + +struct FlowView: View { + @State var patch = simplePatch() + @State var selection = Set() + + var body: some View { + NodeEditor(patch: $patch, selection: $selection) + .navigationTitle("Flow Demo") + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/KeyboardView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/KeyboardView.swift new file mode 100644 index 0000000..ac76441 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/KeyboardView.swift @@ -0,0 +1,177 @@ +import Keyboard +import SwiftUI +import Tonic + +let evenSpacingInitialSpacerRatio: [Letter: CGFloat] = [ + .C: 0.0, + .D: 2.0 / 12.0, + .E: 4.0 / 12.0, + .F: 0.0 / 12.0, + .G: 1.0 / 12.0, + .A: 3.0 / 12.0, + .B: 5.0 / 12.0 +] + +let evenSpacingSpacerRatio: [Letter: CGFloat] = [ + .C: 7.0 / 12.0, + .D: 7.0 / 12.0, + .E: 7.0 / 12.0, + .F: 7.0 / 12.0, + .G: 7.0 / 12.0, + .A: 7.0 / 12.0, + .B: 7.0 / 12.0 +] + +let evenSpacingRelativeBlackKeyWidth: CGFloat = 7.0 / 12.0 + +struct KeyboardView: View { + + func noteOn(pitch: Pitch, point: CGPoint) { + print("note on \(pitch)") + } + + func noteOff(pitch: Pitch) { + print("note off \(pitch)") + } + + func noteOnWithVerticalVelocity(pitch: Pitch, point: CGPoint) { + print("note on \(pitch), midiVelocity: \(Int(point.y * 127))") + } + + func noteOnWithReversedVerticalVelocity(pitch: Pitch, point: CGPoint) { + print("note on \(pitch), midiVelocity: \(Int((1.0 - point.y) * 127))") + } + + var randomColors: [Color] = (0 ... 12).map { _ in + Color(red: Double.random(in: 0 ... 1), + green: Double.random(in: 0 ... 1), + blue: Double.random(in: 0 ... 1), opacity: 1) + } + + @State var lowNote = 24 + @State var highNote = 48 + + @State var scaleIndex = Scale.allCases.firstIndex(of: .chromatic) ?? 0 { + didSet { + if scaleIndex >= Scale.allCases.count { scaleIndex = 0 } + if scaleIndex < 0 { scaleIndex = Scale.allCases.count - 1 } + scale = Scale.allCases[scaleIndex] + } + } + + @State var scale: Scale = .chromatic + @State var root: NoteClass = .C + @State var rootIndex = 0 + @Environment(\.colorScheme) var colorScheme + + var body: some View { + HStack { + Keyboard(layout: .verticalIsomorphic(pitchRange: Pitch(48) ... Pitch(77))).frame(width: 100) + VStack { + HStack { + Stepper("Lowest Note: \(Pitch(intValue: lowNote).note(in: .C).description)", + onIncrement: { + if lowNote < 126, highNote > lowNote + 12 { + lowNote += 1 + } + }, + onDecrement: { + if lowNote > 0 { + lowNote -= 1 + } + }) + Stepper("Highest Note: \(Pitch(intValue: highNote).note(in: .C).description)", + onIncrement: { + if highNote < 126 { + highNote += 1 + } + }, + onDecrement: { + if highNote > 1, highNote > lowNote + 12 { + highNote -= 1 + } + + }) + } + Keyboard(layout: .piano(pitchRange: Pitch(intValue: lowNote) ... Pitch(intValue: highNote)), + noteOn: noteOnWithVerticalVelocity(pitch:point:), noteOff: noteOff) + .frame(minWidth: 100, minHeight: 100) + + HStack { + Stepper("Root: \(root.description)", + onIncrement: { + let allSharpNotes = (0...11).map { Note(pitch: Pitch(intValue: $0)).noteClass } + var index = allSharpNotes.firstIndex(of: root.canonicalNote.noteClass) ?? 0 + index += 1 + if index > 11 { index = 0} + if index < 0 { index = 1} + rootIndex = index + root = allSharpNotes[index] + }, + onDecrement: { + let allSharpNotes = (0...11).map { Note(pitch: Pitch(intValue: $0)).noteClass } + var index = allSharpNotes.firstIndex(of: root.canonicalNote.noteClass) ?? 0 + index -= 1 + if index > 11 { index = 0} + if index < 0 { index = 1} + rootIndex = index + root = allSharpNotes[index] + }) + + Stepper("Scale: \(scale.description)", + onIncrement: { scaleIndex += 1 }, + onDecrement: { scaleIndex -= 1 }) + } + Keyboard(layout: .isomorphic(pitchRange: + Pitch(intValue: 12 + rootIndex) ... Pitch(intValue: 84 + rootIndex), + root: root, + scale: scale), + noteOn: noteOnWithReversedVerticalVelocity(pitch:point:), noteOff: noteOff) + .frame(minWidth: 100, minHeight: 100) + + Keyboard(layout: .guitar(), + noteOn: noteOn, noteOff: noteOff) { pitch, isActivated in + KeyboardKey(pitch: pitch, + isActivated: isActivated, + text: pitch.note(in: .F).description, + pressedColor: Color(PitchColor.newtonian[Int(pitch.pitchClass)]), + alignment: .center) + } + .frame(minWidth: 100, minHeight: 100) + + Keyboard(layout: .isomorphic(pitchRange: Pitch(48) ... Pitch(65))) { pitch, isActivated in + KeyboardKey(pitch: pitch, + isActivated: isActivated, + text: pitch.note(in: .F).description, + pressedColor: Color(PitchColor.newtonian[Int(pitch.pitchClass)])) + } + .frame(minWidth: 100, minHeight: 100) + + Keyboard(latching: true, noteOn: noteOn, noteOff: noteOff) { pitch, isActivated in + if isActivated { + ZStack { + Rectangle().foregroundColor(.black) + VStack { + Spacer() + Text(pitch.note(in: .C).description).font(.largeTitle) + }.padding() + } + + } else { + Rectangle().foregroundColor(randomColors[Int(pitch.intValue) % 12]) + } + } + .frame(minWidth: 100, minHeight: 100) + } + Keyboard( + layout: .verticalPiano(pitchRange: Pitch(48) ... Pitch(77), + initialSpacerRatio: evenSpacingInitialSpacerRatio, + spacerRatio: evenSpacingSpacerRatio, + relativeBlackKeyWidth: evenSpacingRelativeBlackKeyWidth) + ).frame(width: 100) + } + .background(colorScheme == .dark ? + Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) + .navigationTitle("Keyboard Demo") + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/PianoRollView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/PianoRollView.swift new file mode 100644 index 0000000..06b0d85 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/PianoRollView.swift @@ -0,0 +1,20 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/AudioKitUI/ + +import PianoRoll +import SwiftUI + +public struct PianoRollView: View { + public init() {} + + @State var model = PianoRollModel(notes: [ + PianoRollNote(start: 1, length: 2, pitch: 3), + PianoRollNote(start: 5, length: 1, pitch: 4), + ], length: 128, height: 128) + + public var body: some View { + ScrollView([.horizontal, .vertical], showsIndicators: true) { + PianoRoll(model: $model, noteColor: .cyan, layout: .horizontal) + }.background(Color(white: 0.1)) + .navigationTitle("Piano Roll Demo").foregroundStyle(.white) + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/STKView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/STKView.swift new file mode 100644 index 0000000..e2545c1 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/STKView.swift @@ -0,0 +1,160 @@ +import AudioKit +import AudioKitEX +import AudioKitUI +import STKAudioKit +import SwiftUI + +struct ShakerMetronomeData { + var isPlaying = false + var tempo: BPM = 120 + var timeSignatureTop: Int = 4 + var downbeatNoteNumber = MIDINoteNumber(6) + var beatNoteNumber = MIDINoteNumber(10) + var beatNoteVelocity = 100.0 + var currentBeat = 0 +} + +class ShakerConductor: ObservableObject, HasAudioEngine { + let engine = AudioEngine() + let shaker = Shaker() + var callbackInst = CallbackInstrument() + let reverb: Reverb + let mixer = Mixer() + var sequencer = Sequencer() + + @Published var data = ShakerMetronomeData() { + didSet { + data.isPlaying ? sequencer.play() : sequencer.stop() + sequencer.tempo = data.tempo + updateSequences() + } + } + + func updateSequences() { + var track = sequencer.tracks.first! + + track.length = Double(data.timeSignatureTop) + + track.clear() + track.sequence.add(noteNumber: data.downbeatNoteNumber, position: 0.0, duration: 0.4) + let vel = MIDIVelocity(Int(data.beatNoteVelocity)) + for beat in 1 ..< data.timeSignatureTop { + track.sequence.add(noteNumber: data.beatNoteNumber, velocity: vel, position: Double(beat), duration: 0.1) + } + + track = sequencer.tracks[1] + track.length = Double(data.timeSignatureTop) + track.clear() + for beat in 0 ..< data.timeSignatureTop { + track.sequence.add(noteNumber: MIDINoteNumber(beat), position: Double(beat), duration: 0.1) + } + } + + init() { + let fader = Fader(shaker) + fader.gain = 20.0 + + // let delay = Delay(fader) + // delay.time = AUValue(1.5 / playRate) + // delay.dryWetMix = 0.7 + // delay.feedback = 0.2 + reverb = Reverb(fader) + + _ = sequencer.addTrack(for: shaker) + + callbackInst = CallbackInstrument(midiCallback: { _, beat, _ in + self.data.currentBeat = Int(beat) + print(beat) + }) + + _ = sequencer.addTrack(for: callbackInst) + updateSequences() + + mixer.addInput(reverb) + mixer.addInput(callbackInst) + + engine.output = mixer + } +} + +struct STKView: View { + @StateObject var conductor = ShakerConductor() + + func name(noteNumber: MIDINoteNumber) -> String { + let str = "\(ShakerType(rawValue: noteNumber)!)" + return str.titleCase() + } + + var body: some View { + VStack { + Spacer() + HStack { + Text(conductor.data.isPlaying ? "Stop" : "Start").onTapGesture { + conductor.data.isPlaying.toggle() + } + VStack { + Text("Tempo: \(Int(conductor.data.tempo))") + Slider(value: $conductor.data.tempo, in: 60.0 ... 240.0, label: { + Text("Tempo") + }) + } + + VStack { + Stepper("Downbeat: \(name(noteNumber: conductor.data.downbeatNoteNumber))", onIncrement: { + if conductor.data.downbeatNoteNumber <= 21 { + conductor.data.downbeatNoteNumber += 1 + } + }, onDecrement: { + if conductor.data.downbeatNoteNumber >= 1 { + conductor.data.downbeatNoteNumber -= 1 + } + }) + Stepper("Other beats: \(name(noteNumber: conductor.data.beatNoteNumber))", onIncrement: { + if conductor.data.beatNoteNumber <= 21 { + conductor.data.beatNoteNumber += 1 + } + }, onDecrement: { + if conductor.data.beatNoteNumber >= 1 { + conductor.data.beatNoteNumber -= 1 + } + }) + } + + VStack { + Text("Velocity") + Slider(value: $conductor.data.beatNoteVelocity, in: 0.0 ... 127.0, label: { + Text("Velocity") + }) + } + } + Spacer() + + HStack(spacing: 10) { + ForEach(0 ..< conductor.data.timeSignatureTop, id: \.self) { index in + ZStack { + Circle().foregroundColor(conductor.data.currentBeat == index ? .red : .white) + Text("\(index + 1)").foregroundColor(.black) + }.onTapGesture { + conductor.data.timeSignatureTop = index + 1 + } + } + ZStack { + Circle().foregroundColor(.white) + Text("+").foregroundColor(.black) + } + .onTapGesture { + conductor.data.timeSignatureTop += 1 + } + }.padding() + + FFTView(conductor.reverb) + } + .navigationTitle("STK Demo") + .onAppear { + conductor.start() + } + .onDisappear { + conductor.stop() + } + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/WaveformView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/WaveformView.swift new file mode 100644 index 0000000..35d38b9 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/WaveformView.swift @@ -0,0 +1,97 @@ +// Copyright AudioKit. All Rights Reserved. Revision History at http://github.com/AudioKit/Waveform/ + +import AVFoundation +import SwiftUI +import Waveform + +class WaveformModel: ObservableObject { + var samples: SampleBuffer + + init(file: AVAudioFile) { + let stereo = file.floatChannelData()! + samples = SampleBuffer(samples: stereo[0]) + } +} + +func getFile() -> AVAudioFile { + let url = Bundle.module.url(forResource: "Samples/Piano", withExtension: "mp3")! + return try! AVAudioFile(forReading: url) +} + +func clamp(_ x: Double, _ inf: Double, _ sup: Double) -> Double { + max(min(x, sup), inf) +} + +struct WaveformView: View { + @StateObject var model = WaveformModel(file: getFile()) + + @State var start = 0.0 + @State var length = 1.0 + + let formatter = NumberFormatter() + var body: some View { + VStack { + ZStack(alignment: .leading) { + Waveform(samples: model.samples).foregroundColor(.cyan) + .padding(.vertical, 5) + MinimapView(start: $start, length: $length) + } + .frame(height: 100) + .padding() + Waveform(samples: model.samples, + start: Int(start * Double(model.samples.count - 1)), + length: Int(length * Double(model.samples.count))) + .foregroundColor(.blue) + } + .padding() + .navigationTitle("Waveform Demo") + } +} + +struct MinimapView: View { + @Binding var start: Double + @Binding var length: Double + + @GestureState var initialStart: Double? + @GestureState var initialLength: Double? + + let indicatorSize = 10.0 + + var body: some View { + GeometryReader { gp in + RoundedRectangle(cornerRadius: indicatorSize) + .frame(width: length * gp.size.width) + .offset(x: start * gp.size.width) + .opacity(0.3) + .gesture(DragGesture() + .updating($initialStart) { _, state, _ in + if state == nil { + state = start + } + } + .onChanged { drag in + if let initialStart = initialStart { + start = clamp(initialStart + drag.translation.width / gp.size.width, 0, 1 - length) + } + } + ) + + RoundedRectangle(cornerRadius: indicatorSize) + .frame(width: indicatorSize).opacity(0.3) + .offset(x: (start + length) * gp.size.width) + .padding(indicatorSize) + .gesture(DragGesture() + .updating($initialLength) { _, state, _ in + if state == nil { + state = length + } + } + .onChanged { drag in + if let initialLength = initialLength { + length = clamp(initialLength + drag.translation.width / gp.size.width, 0, 1 - start) + } + } + ) + } + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift new file mode 100644 index 0000000..4cdf764 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift @@ -0,0 +1,150 @@ +import AudioKit +import AudioKitEX +import AudioKitUI +import AVFAudio +import Keyboard +import SwiftUI +import Controls +import Tonic + +class ArpeggiatorConductor: ObservableObject, HasAudioEngine { + let engine = AudioEngine() + var instrument = AppleSampler() + var sequencer: SequencerTrack! + var midiCallback: CallbackInstrument! + + var heldNotes = [Int]() + var arpUp = false + var currentNote = 0 + var sequencerNoteLength = 1.0 + + @Published var tempo : Float = 120.0 { + didSet{ + sequencer.tempo = BPM(tempo) + } + } + + @Published var noteLength : Float = 1.0 { + didSet{ + sequencerNoteLength = Double(noteLength) + sequencer.clear() + sequencer.add(noteNumber: 60, position: 0.0, duration: max(0.05, sequencerNoteLength * 0.24)) + } + } + + func noteOn(pitch: Pitch, point _: CGPoint) { + //add notes to an array + heldNotes.append(max(0,pitch.intValue)) + } + + func fireTimer() { + for i in 0...127 { + self.instrument.stop(noteNumber: MIDINoteNumber(i), channel: 0) + } + if self.heldNotes.count < 1 { + return + } + + //UP + if !arpUp { + let tempArray = heldNotes + var arrayValue = 0 + if tempArray.max() != currentNote { + arrayValue = tempArray.sorted().first(where: { $0 > currentNote }) ?? tempArray.min()! + currentNote = arrayValue + }else{ + arpUp = true + arrayValue = tempArray.sorted().last(where: { $0 < currentNote }) ?? tempArray.max()! + currentNote = arrayValue + } + + }else{ + //DOWN + let tempArray = heldNotes + var arrayValue = 0 + if tempArray.min() != currentNote { + arrayValue = tempArray.sorted().last(where: { $0 < currentNote }) ?? tempArray.max()! + currentNote = arrayValue + }else{ + arpUp = false + arrayValue = tempArray.sorted().first(where: { $0 > currentNote }) ?? tempArray.min()! + currentNote = arrayValue + } + } + instrument.play(noteNumber: MIDINoteNumber(currentNote), velocity: 120, channel: 0) + } + + func noteOff(pitch: Pitch) { + let mynote = pitch.intValue + + //remove notes from an array + for i in heldNotes { + if i == mynote { + heldNotes = heldNotes.filter { + $0 != mynote + } + } + } + } + + init() { + + midiCallback = CallbackInstrument { status, note, vel in + if status == 144 { //Note On + self.fireTimer() + } else if status == 128 { //Note Off + //all notes off + for i in 0...127 { + self.instrument.stop(noteNumber: MIDINoteNumber(i), channel: 0) + } + } + } + + engine.output = PeakLimiter(Mixer(instrument, midiCallback), attackTime: 0.001, decayTime: 0.001, preGain: 0) + + do { + if let fileURL = Bundle.main.url(forResource: "Sounds/Sampler Instruments/sawPiano1", withExtension: "exs") { + try instrument.loadInstrument(url: fileURL) + } else { + Log("Could not find file") + } + } catch { + Log("Could not load instrument") + } + + sequencer = SequencerTrack(targetNode: midiCallback) + sequencer.length = 0.25 + sequencer.loopEnabled = true + sequencer.add(noteNumber: 60, position: 0.0, duration: 0.24) + + sequencer?.playFromStart() + } +} + +struct ArpeggiatorView: View { + @StateObject var conductor = ArpeggiatorConductor() + @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) + } + .cookbookNavBarTitle("Arpeggiator") + .onAppear { + conductor.start() + } + .onDisappear { + conductor.stop() + conductor.sequencer.stop() + } + .background(colorScheme == .dark ? + Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) + } +} + diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/Effects/Audio3D.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Audio3D.swift similarity index 100% rename from Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/Effects/Audio3D.swift rename to Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Audio3D.swift diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentEXS.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentEXS.swift index b8dd329..3ffbb9a 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentEXS.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentEXS.swift @@ -9,19 +9,19 @@ import Tonic class InstrumentEXSConductor: ObservableObject, HasAudioEngine { let engine = AudioEngine() - var instrument = MIDISampler(name: "Instrument 1") - + var instrument = AppleSampler() + func noteOn(pitch: Pitch, point _: CGPoint) { instrument.play(noteNumber: MIDINoteNumber(pitch.midiNoteNumber), velocity: 90, channel: 0) } - + func noteOff(pitch: Pitch) { instrument.stop(noteNumber: MIDINoteNumber(pitch.midiNoteNumber), channel: 0) } - + init() { engine.output = instrument - + // Load EXS file (you can also load SoundFonts and WAV files too using the AppleSampler Class) do { if let fileURL = Bundle.main.url(forResource: "Sounds/Sampler Instruments/sawPiano1", withExtension: "exs") { @@ -38,19 +38,19 @@ class InstrumentEXSConductor: ObservableObject, HasAudioEngine { struct InstrumentEXSView: View { @StateObject var conductor = InstrumentEXSConductor() @Environment(\.colorScheme) var colorScheme - + var body: some View { NodeOutputView(conductor.instrument) CookbookKeyboard(noteOn: conductor.noteOn, noteOff: conductor.noteOff) .cookbookNavBarTitle("Instrument EXS") - .onAppear { - conductor.start() - } - .onDisappear { - conductor.stop() - } - .background(colorScheme == .dark ? - Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) + .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/MiniApps/InstrumentSFZ.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift index 84cb1c2..83dbf80 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift @@ -11,15 +11,15 @@ import DunneAudioKit class InstrumentSFZConductor: ObservableObject, HasAudioEngine { let engine = AudioEngine() var instrument = Sampler() - + func noteOn(pitch: Pitch, point _: CGPoint) { instrument.play(noteNumber: MIDINoteNumber(pitch.midiNoteNumber), velocity: 90, channel: 0) } - + func noteOff(pitch: Pitch) { instrument.stop(noteNumber: MIDINoteNumber(pitch.midiNoteNumber), channel: 0) } - + init() { // Load SFZ file with Dunne Sampler if let fileURL = Bundle.main.url(forResource: "Sounds/sqr", withExtension: "SFZ") { @@ -35,19 +35,40 @@ class InstrumentSFZConductor: ObservableObject, HasAudioEngine { struct InstrumentSFZView: View { @StateObject var conductor = InstrumentSFZConductor() @Environment(\.colorScheme) var colorScheme - + var body: some View { - NodeOutputView(conductor.instrument) - CookbookKeyboard(noteOn: conductor.noteOn, - noteOff: conductor.noteOff) + 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) + } .cookbookNavBarTitle("Instrument SFZ") - .onAppear { - conductor.start() - } - .onDisappear { - conductor.stop() - } - .background(colorScheme == .dark ? - Color.clear : Color(red: 0.9, green: 0.9, blue: 0.9)) + .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/MiniApps/SpriteKitAudio.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift new file mode 100644 index 0000000..51d7a0a --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift @@ -0,0 +1,111 @@ +import SwiftUI +import SpriteKit +import AudioKit +import AVFoundation + +class GameScene: SKScene, SKPhysicsContactDelegate { + var conductor: SpriteKitAudioConductor? + override func didMove(to view: SKView) { + physicsWorld.contactDelegate = self + physicsBody = SKPhysicsBody(edgeLoopFrom: frame) + self.backgroundColor = .white + for i in 1...3 { + let plat = SKShapeNode(rectOf: CGSize(width: 80, height: 10)) + plat.fillColor = .lightGray + plat.strokeColor = .lightGray + if i == 2 { + plat.zRotation = .pi / 8 + plat.position = CGPoint(x:590,y:700-75*i) + } else { + plat.zRotation = -.pi / 8 + plat.position = CGPoint(x:490,y:700-75*i) + } + plat.physicsBody = SKPhysicsBody(rectangleOf: CGSize(width: 80, height: 10)) + plat.physicsBody?.categoryBitMask = 2 + plat.physicsBody?.contactTestBitMask = 2 + plat.physicsBody?.affectedByGravity = false + plat.physicsBody?.isDynamic = false + plat.name = "platform\(i)" + addChild(plat) + } + } + + override func touchesBegan(_ touches: Set, with event: UIEvent?) { + guard let touch = touches.first else { return } + let location = touch.location(in: self) + print(location) + let box = SKShapeNode(circleOfRadius: 5) + box.fillColor = .gray + box.strokeColor = .gray + box.position = location + box.physicsBody = SKPhysicsBody(circleOfRadius: 5) + box.physicsBody?.restitution = 0.55 + box.physicsBody?.categoryBitMask = 2 + box.physicsBody?.contactTestBitMask = 2 + box.physicsBody?.affectedByGravity = true + box.physicsBody?.isDynamic = true + box.name = "ball" + addChild(box) + } + + func didBegin(_ contact: SKPhysicsContact) { + if contact.bodyB.node?.name == "platform1" || contact.bodyA.node?.name == "platform1" { + conductor!.instrument.play(noteNumber: MIDINoteNumber(60), velocity: 90, channel: 0) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.conductor!.instrument.stop(noteNumber: MIDINoteNumber(60), channel: 0) + } + } else if contact.bodyB.node?.name == "platform2" || contact.bodyA.node?.name == "platform2" { + conductor!.instrument.play(noteNumber: MIDINoteNumber(64), velocity: 90, channel: 0) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.conductor!.instrument.stop(noteNumber: MIDINoteNumber(64), channel: 0) + } + } else if contact.bodyB.node?.name == "platform3" || contact.bodyA.node?.name == "platform3" { + conductor!.instrument.play(noteNumber: MIDINoteNumber(67), velocity: 90, channel: 0) + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + self.conductor!.instrument.stop(noteNumber: MIDINoteNumber(67), channel: 0) + } + } else if contact.bodyB.node?.name != "ball" || contact.bodyA.node?.name != "ball" { + contact.bodyB.node?.removeFromParent() + } + } +} + +class SpriteKitAudioConductor: ObservableObject, HasAudioEngine { + let engine = AudioEngine() + @Published var instrument = MIDISampler(name: "Instrument 1") + init() { + engine.output = Reverb(instrument) + do { + if let fileURL = Bundle.main.url(forResource: "Sounds/Sampler Instruments/sawPiano1", withExtension: "exs") { + try instrument.loadInstrument(url: fileURL) + } else { + Log("Could not find file") + } + } catch { + Log("Could not load instrument") + } + } +} + +struct SpriteKitAudioView: View { + @StateObject var conductor = SpriteKitAudioConductor() + var scene: SKScene { + let scene = GameScene() + scene.size = CGSize(width: 1080, height: 1080) + scene.scaleMode = .aspectFit + scene.conductor = conductor + scene.backgroundColor = .lightGray + return scene + } + var body: some View { + VStack { + SpriteView(scene: scene).frame(maxWidth: .infinity, maxHeight: .infinity).ignoresSafeArea() + } + .cookbookNavBarTitle("SpriteKit Audio") + .onAppear { + conductor.start() + }.onDisappear { + conductor.stop() + } + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AudioFileView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/UncategorizedDemos/AudioFileView.swift similarity index 100% rename from Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AudioFileView.swift rename to Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/UncategorizedDemos/AudioFileView.swift diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/CallbackInstrument.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/UncategorizedDemos/CallbackInstrument.swift similarity index 100% rename from Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/CallbackInstrument.swift rename to Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/UncategorizedDemos/CallbackInstrument.swift diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/Table.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/UncategorizedDemos/Table.swift similarity index 100% rename from Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/Table.swift rename to Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/UncategorizedDemos/Table.swift diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift new file mode 100644 index 0000000..3c32bd4 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift @@ -0,0 +1,64 @@ +import AudioKit +import DunneAudioKit +import AudioKitEX +import AudioKitUI +import AVFAudio +import Keyboard +import SwiftUI +import Controls +import Tonic + +class DunneSynthConductor: ObservableObject, HasAudioEngine { + let engine = AudioEngine() + var instrument = Synth() + + func noteOn(pitch: Pitch, point _: CGPoint) { + instrument.play(noteNumber: MIDINoteNumber(pitch.midiNoteNumber), velocity: 120, channel: 0) + } + + func noteOff(pitch: Pitch) { + instrument.stop(noteNumber: MIDINoteNumber(pitch.midiNoteNumber), channel: 0) + } + + init() { + engine.output = PeakLimiter(instrument, attackTime: 0.001, decayTime: 0.001, preGain: 0) + + //Remove pops + instrument.releaseDuration = 0.01 + instrument.filterReleaseDuration = 10.0 + instrument.filterStrength = 40.0 + } +} + +struct DunneSynthView: View { + @StateObject var conductor = DunneSynthConductor() + @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) + } + .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/InputDeviceDemo.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/InputDeviceDemo.swift similarity index 100% rename from Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/InputDeviceDemo.swift rename to Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/InputDeviceDemo.swift From dd7a4296e8322a73c5a8c0b3958e6e952c6aa06e Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Fri, 8 Dec 2023 09:18:02 -0500 Subject: [PATCH 2/4] 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 + } + }) + } +} From d2c0f186d712f12dc1205246d493278e72f77c15 Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Fri, 8 Dec 2023 09:26:14 -0500 Subject: [PATCH 3/4] hounds --- .../Sources/CookbookCommon/ContentView.swift | 22 +++++++++---------- .../Recipes/MiniApps/InstrumentSFZ.swift | 8 +++---- .../Recipes/WIP/PolyphonicOscillator.swift | 8 +++---- .../Recipes/WIP/PolyphonicSTK+MIDIKit.swift | 21 ++++++++++-------- 4 files changed, 31 insertions(+), 28 deletions(-) diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift index fca6073..ee5bea7 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift @@ -38,7 +38,7 @@ struct MasterView: View { } } } - + Group { DisclosureGroup("Operations") { Group { @@ -59,7 +59,7 @@ struct MasterView: View { } } } - + Group { DisclosureGroup("Physical Models") { Group { @@ -75,7 +75,7 @@ struct MasterView: View { } } } - + Group { DisclosureGroup("Effects") { Group { @@ -109,7 +109,7 @@ struct MasterView: View { } } } - + Group { DisclosureGroup("Distortion") { Group { @@ -122,7 +122,7 @@ struct MasterView: View { } } } - + Group { DisclosureGroup("Reverb") { Group { @@ -136,7 +136,7 @@ struct MasterView: View { } } } - + DisclosureGroup("Filters") { Group { NavigationLink("Band Pass Butterworth Filter", @@ -172,7 +172,7 @@ struct MasterView: View { NavigationLink("Tone Complement Filter", destination: ToneComplementFilterView()) } } - + Group { DisclosureGroup("Oscillators") { Group { @@ -185,7 +185,7 @@ struct MasterView: View { NavigationLink("Waveform Morphing", destination: MorphingOscillatorView()) } } - + DisclosureGroup("Audio Player") { Group { NavigationLink("Completion Handler", destination: AudioPlayerCompletionHandler()) @@ -193,7 +193,7 @@ struct MasterView: View { NavigationLink("Playlist", destination: PlaylistView()) } } - + Group { DisclosureGroup("Additional Packages") { Group { @@ -206,7 +206,7 @@ struct MasterView: View { } } } - + Group { DisclosureGroup("Uncategorized Demos") { Group { @@ -216,7 +216,7 @@ struct MasterView: View { } } } - + DisclosureGroup("WIP") { Group { NavigationLink("Base Tap Demo", destination: BaseTapDemoView()) diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift index b086e14..df6805a 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/InstrumentSFZ.swift @@ -38,22 +38,22 @@ struct InstrumentSFZView: View { var body: some View { HStack { - ForEach(0...7, id: \.self){ + ForEach(0...7, id: \.self) { ParameterRow(param: conductor.instrument.parameters[$0]) } }.padding(5) HStack { - ForEach(8...15, id: \.self){ + ForEach(8...15, id: \.self) { ParameterRow(param: conductor.instrument.parameters[$0]) } }.padding(5) HStack { - ForEach(16...23, id: \.self){ + ForEach(16...23, id: \.self) { ParameterRow(param: conductor.instrument.parameters[$0]) } }.padding(5) HStack { - ForEach(24...30, id: \.self){ + ForEach(24...30, id: \.self) { ParameterRow(param: conductor.instrument.parameters[$0]) } }.padding(5) diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift index 91e8671..78d81ea 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,7 +47,7 @@ class PolyphonicOscillatorConductor: ObservableObject, HasAudioEngine { struct PolyphonicOscillatorView: View { @StateObject var conductor = PolyphonicOscillatorConductor() @Environment(\.colorScheme) var colorScheme - + var body: some View { if conductor.engine.output != nil { NodeOutputView(conductor.engine.output!) diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift index 2a10ef2..3df7121 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift @@ -53,15 +53,15 @@ class PolyphonicSTKConductor: ObservableObject, HasAudioEngine { 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() } @@ -74,7 +74,7 @@ class PolyphonicSTKConductor: ObservableObject, HasAudioEngine { } catch { print("Error starting MIDI services:", error.localizedDescription) } - + do { try midiManager.addInputConnection( to: .allOutputs, // no need to specify if we're using .allEndpoints @@ -95,18 +95,21 @@ class PolyphonicSTKConductor: ObservableObject, HasAudioEngine { ) } } - + // 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]) + 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]) + 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): @@ -124,7 +127,7 @@ extension NSNotification.Name { struct PolyphonicSTKView: View { @StateObject var conductor = PolyphonicSTKConductor() @Environment(\.colorScheme) var colorScheme - + var body: some View { if conductor.engine.output != nil { NodeOutputView(conductor.engine.output!) From a696cf2ad157316c99c82b1f07e8639591a0eb19 Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Fri, 8 Dec 2023 10:02:04 -0500 Subject: [PATCH 4/4] Update LaunchScreen.storyboard --- .../Base.lproj/LaunchScreen.storyboard | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/Cookbook/Cookbook/Base.lproj/LaunchScreen.storyboard b/Cookbook/Cookbook/Base.lproj/LaunchScreen.storyboard index 865e932..ecb7932 100644 --- a/Cookbook/Cookbook/Base.lproj/LaunchScreen.storyboard +++ b/Cookbook/Cookbook/Base.lproj/LaunchScreen.storyboard @@ -1,8 +1,10 @@ - - + + + - + + @@ -11,10 +13,24 @@ - + - + + + + + + + + + + + + + + + @@ -22,4 +38,10 @@ + + + + + +