diff --git a/Cookbook/Cookbook.xcodeproj/project.pbxproj b/Cookbook/Cookbook.xcodeproj/project.pbxproj index b06e66e..a75d527 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,11 @@ 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 */; }; + 5A7F40572B22774A000A28F9 /* MIDIKit in Frameworks */ = {isa = PBXBuildFile; productRef = 5A7F40562B22774A000A28F9 /* MIDIKit */; }; C446DE542528D8E700138D0A /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = C446DE522528D8E700138D0A /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ @@ -36,6 +41,11 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 5A7F40572B22774A000A28F9 /* MIDIKit in Frameworks */, + 5A7F404C2B220667000A28F9 /* STKAudioKit in Frameworks */, + 5A7F40432B21F314000A28F9 /* Flow in Frameworks */, + 5A7F40492B21FE34000A28F9 /* PianoRoll in Frameworks */, + 5A7F40462B21FD06000A28F9 /* Waveform in Frameworks */, 29215CE827CC30CF005B706C /* CookbookCommon in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -121,6 +131,11 @@ name = Cookbook; packageProductDependencies = ( 29215CE727CC30CF005B706C /* CookbookCommon */, + 5A7F40422B21F314000A28F9 /* Flow */, + 5A7F40452B21FD06000A28F9 /* Waveform */, + 5A7F40482B21FE34000A28F9 /* PianoRoll */, + 5A7F404B2B220667000A28F9 /* STKAudioKit */, + 5A7F40562B22774A000A28F9 /* MIDIKit */, ); productName = Cookbook; productReference = C446DE442528D8E600138D0A /* Cookbook.app */; @@ -150,6 +165,11 @@ ); mainGroup = C446DE3B2528D8E600138D0A; packageReferences = ( + 5A7F40412B21F314000A28F9 /* XCRemoteSwiftPackageReference "Flow" */, + 5A7F40442B21FD06000A28F9 /* XCRemoteSwiftPackageReference "Waveform" */, + 5A7F40472B21FE34000A28F9 /* XCRemoteSwiftPackageReference "PianoRoll" */, + 5A7F404A2B220667000A28F9 /* XCRemoteSwiftPackageReference "STKAudioKit" */, + 5A7F40552B22774A000A28F9 /* XCRemoteSwiftPackageReference "MIDIKit" */, ); productRefGroup = C446DE452528D8E600138D0A /* Products */; projectDirPath = ""; @@ -382,11 +402,79 @@ }; /* 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; + }; + }; + 5A7F40552B22774A000A28F9 /* XCRemoteSwiftPackageReference "MIDIKit" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/orchetect/MIDIKit"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 0.9.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; + }; + 5A7F40562B22774A000A28F9 /* MIDIKit */ = { + isa = XCSwiftPackageProductDependency; + package = 5A7F40552B22774A000A28F9 /* XCRemoteSwiftPackageReference "MIDIKit" */; + productName = MIDIKit; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = C446DE3C2528D8E600138D0A /* Project object */; 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 @@ + + + + + + diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift index deee93e..ee5bea7 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()) @@ -194,12 +194,38 @@ struct MasterView: View { } } + 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("Polyphonic STK + MIDIKit", destination: PolyphonicSTKView()) NavigationLink("Roland Tb303 Filter", destination: RolandTB303FilterView()) } } @@ -207,6 +233,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..a46c04e --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Arpeggiator.swift @@ -0,0 +1,148 @@ +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 { + 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 98% rename from Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/Effects/Audio3D.swift rename to Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/Audio3D.swift index b5c3996..d25944c 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/Effects/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/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..df6805a 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,38 @@ class InstrumentSFZConductor: ObservableObject, HasAudioEngine { struct InstrumentSFZView: View { @StateObject var conductor = InstrumentSFZConductor() @Environment(\.colorScheme) var colorScheme - + var body: some View { - NodeOutputView(conductor.instrument) + 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..3849545 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/MiniApps/SpriteKitAudio.swift @@ -0,0 +1,110 @@ +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() + } + .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..34b9dd3 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/DunneSynth.swift @@ -0,0 +1,63 @@ +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 { + 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() + } + .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 97% rename from Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/InputDeviceDemo.swift rename to Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/InputDeviceDemo.swift index 9bccc20..38c8127 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/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..78d81ea 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicOscillator.swift @@ -49,21 +49,19 @@ struct PolyphonicOscillatorView: View { @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..3df7121 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/PolyphonicSTK+MIDIKit.swift @@ -0,0 +1,147 @@ +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 + } + }) + } +}