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