From 6a7804e3dff5eabeb62eaad5f86ba2b93c5046a0 Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Fri, 27 Feb 2026 11:09:44 -0500 Subject: [PATCH 1/3] Added Tablature and Fretboard examples Added Tablature and Fretboard examples. Cleaned up main view. Removed a few deprecations. --- Cookbook/CookbookCommon/Package.swift | 6 +- .../Sources/CookbookCommon/ContentView.swift | 242 ++++++++---------- .../TablatureDemoView.swift | 100 ++++++++ .../AudioPlayer/MultiSegmentPlayerView.swift | 1 - .../Recipes/WIP/ChannelDeviceRouting.swift | 1 - .../Recipes/WIP/FretboardView.swift | 51 ++++ .../Reusable Components/MIDIController.swift | 90 +++++++ .../MIDISettingsView.swift | 46 ++++ 8 files changed, 402 insertions(+), 135 deletions(-) create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/TablatureDemoView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/FretboardView.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDIController.swift create mode 100644 Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDISettingsView.swift diff --git a/Cookbook/CookbookCommon/Package.swift b/Cookbook/CookbookCommon/Package.swift index 393ba4e0..5b5d973b 100644 --- a/Cookbook/CookbookCommon/Package.swift +++ b/Cookbook/CookbookCommon/Package.swift @@ -20,13 +20,15 @@ let package = Package( .package(url: "https://github.com/AudioKit/Waveform", branch: "visionos"), .package(url: "https://github.com/AudioKit/Flow", from: "1.0.0"), .package(url: "https://github.com/AudioKit/PianoRoll", from: "1.0.0"), - .package(url: "https://github.com/orchetect/MIDIKit", from: "0.9.7"), + .package(url: "https://github.com/orchetect/MIDIKit", from: "0.11.0"), + .package(url: "https://github.com/AudioKit/Tablature", from: "0.1.0"), + .package(url: "https://github.com/AudioKit/Fretboard", from: "0.1.0"), ], targets: [ .target( name: "CookbookCommon", dependencies: ["AudioKit", "AudioKitUI", "AudioKitEX", "Keyboard", "SoundpipeAudioKit", - "SporthAudioKit", "STKAudioKit", "DunneAudioKit", "Tonic", "Controls", "Waveform", "Flow", "PianoRoll", "MIDIKit"], + "SporthAudioKit", "STKAudioKit", "DunneAudioKit", "Tonic", "Controls", "Waveform", "Flow", "PianoRoll", "MIDIKit", "Tablature", "Fretboard"], resources: [ .copy("MIDI Files"), .copy("Samples"), diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift index 1c0a3850..cca6abd7 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift @@ -14,129 +14,117 @@ struct MasterView: View { 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()) - NavigationLink("Graphic Equalizer", destination: GraphicEqualizerView()) - NavigationLink("Instrument EXS", destination: InstrumentEXSView()) - NavigationLink("Instrument SFZ", destination: InstrumentSFZView()) - } - Group { - NavigationLink("MIDI Monitor", destination: MIDIMonitorView()) - NavigationLink("MIDI Track View", destination: MIDITrackDemo()) - 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()) - } + 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()) + NavigationLink("Graphic Equalizer", destination: GraphicEqualizerView()) + NavigationLink("Instrument EXS", destination: InstrumentEXSView()) + NavigationLink("Instrument SFZ", destination: InstrumentSFZView()) + } + Group { + NavigationLink("MIDI Monitor", destination: MIDIMonitorView()) + NavigationLink("MIDI Track View", destination: MIDITrackDemo()) + 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("Operations") { - Group { - NavigationLink("Crossing Signal", destination: CrossingSignalView()) - NavigationLink("Drone Operation", destination: DroneOperationView()) - NavigationLink("Instrument Operation", destination: InstrumentOperationView()) - NavigationLink("LFO Operation", destination: LFOOperationView()) - NavigationLink("Phasor Operation", destination: PhasorOperationView()) - NavigationLink("Pitch Shift Operation", destination: PitchShiftOperationView()) - NavigationLink("Segment Operation", destination: SegmentOperationView()) - NavigationLink("Smooth Delay Operation", destination: SmoothDelayOperationView()) - NavigationLink("Stereo Operation", destination: StereoOperationView()) - NavigationLink("Stereo Delay Operation", destination: StereoDelayOperationView()) - } - Group { - NavigationLink("Variable Delay Operation", destination: VariableDelayOperationView()) - NavigationLink("Vocal Fun", destination: VocalTractOperationView()) - } + + DisclosureGroup("Operations") { + Group { + NavigationLink("Crossing Signal", destination: CrossingSignalView()) + NavigationLink("Drone Operation", destination: DroneOperationView()) + NavigationLink("Instrument Operation", destination: InstrumentOperationView()) + NavigationLink("LFO Operation", destination: LFOOperationView()) + NavigationLink("Phasor Operation", destination: PhasorOperationView()) + NavigationLink("Pitch Shift Operation", destination: PitchShiftOperationView()) + NavigationLink("Segment Operation", destination: SegmentOperationView()) + NavigationLink("Smooth Delay Operation", destination: SmoothDelayOperationView()) + NavigationLink("Stereo Operation", destination: StereoOperationView()) + NavigationLink("Stereo Delay Operation", destination: StereoDelayOperationView()) + } + Group { + NavigationLink("Variable Delay Operation", destination: VariableDelayOperationView()) + NavigationLink("Vocal Fun", destination: VocalTractOperationView()) } } - Group { - DisclosureGroup("Physical Models") { - Group { - NavigationLink(destination: PluckedStringView()) { - Text("Plucked String") - } - Text("More at STKAudioKit").onTapGesture { - if let url = URL(string: "https://www.audiokit.io/STKAudioKit/") { - UIApplication.shared.open(url) - } - } - .foregroundColor(.blue) + DisclosureGroup("Physical Models") { + Group { + NavigationLink(destination: PluckedStringView()) { + Text("Plucked String") } + Text("More at STKAudioKit").onTapGesture { + if let url = URL(string: "https://www.audiokit.io/STKAudioKit/") { + UIApplication.shared.open(url) + } + }.foregroundColor(.blue) } } - Group { - DisclosureGroup("Effects") { - Group { - NavigationLink("Auto Panner", destination: AutoPannerView()) - NavigationLink("Auto Wah", destination: AutoWahView()) - NavigationLink("Balancer", destination: BalancerView()) - NavigationLink("Chorus", destination: ChorusView()) - NavigationLink("Compressor", destination: CompressorView()) - NavigationLink("Convolution", destination: ConvolutionView()) - NavigationLink("Delay", destination: DelayView()) - NavigationLink("Dynamic Range Compressor", destination: DynamicRangeCompressorView()) - NavigationLink("Expander", destination: ExpanderView()) - } - Group { - NavigationLink("Flanger", destination: FlangerView()) - NavigationLink("MultiTap Delay", destination: MultiTapDelayView()) - NavigationLink("Panner", destination: PannerView()) - NavigationLink("Peak Limiter", destination: PeakLimiterView()) - NavigationLink("Phaser", destination: PhaserView()) - NavigationLink("Phase-Locked Vocoder", destination: PhaseLockedVocoderView()) - NavigationLink("Playback Speed", destination: PlaybackSpeedView()) - NavigationLink("Pitch Shifter", destination: PitchShifterView()) - NavigationLink("Stereo Delay", destination: StereoDelayView()) - NavigationLink("String Resonator", destination: StringResonatorView()) - } - Group { - NavigationLink("Talkbox", destination: TalkboxView()) - NavigationLink("Time / Pitch", destination: TimePitchView()) - NavigationLink("Transient Shaper", destination: TransientShaperView()) - NavigationLink("Tremolo", destination: TremoloView()) - NavigationLink("Variable Delay", destination: VariableDelayView()) - NavigationLink("Vocoder", destination: VocoderView()) - } + DisclosureGroup("Effects") { + Group { + NavigationLink("Auto Panner", destination: AutoPannerView()) + NavigationLink("Auto Wah", destination: AutoWahView()) + NavigationLink("Balancer", destination: BalancerView()) + NavigationLink("Chorus", destination: ChorusView()) + NavigationLink("Compressor", destination: CompressorView()) + NavigationLink("Convolution", destination: ConvolutionView()) + NavigationLink("Delay", destination: DelayView()) + NavigationLink("Dynamic Range Compressor", destination: DynamicRangeCompressorView()) + NavigationLink("Expander", destination: ExpanderView()) + } + Group { + NavigationLink("Flanger", destination: FlangerView()) + NavigationLink("MultiTap Delay", destination: MultiTapDelayView()) + NavigationLink("Panner", destination: PannerView()) + NavigationLink("Peak Limiter", destination: PeakLimiterView()) + NavigationLink("Phaser", destination: PhaserView()) + NavigationLink("Phase-Locked Vocoder", destination: PhaseLockedVocoderView()) + NavigationLink("Playback Speed", destination: PlaybackSpeedView()) + NavigationLink("Pitch Shifter", destination: PitchShifterView()) + NavigationLink("Stereo Delay", destination: StereoDelayView()) + NavigationLink("String Resonator", destination: StringResonatorView()) + } + Group { + NavigationLink("Talkbox", destination: TalkboxView()) + NavigationLink("Time / Pitch", destination: TimePitchView()) + NavigationLink("Transient Shaper", destination: TransientShaperView()) + NavigationLink("Tremolo", destination: TremoloView()) + NavigationLink("Variable Delay", destination: VariableDelayView()) + NavigationLink("Vocoder", destination: VocoderView()) } } - Group { - DisclosureGroup("Distortion") { - Group { - NavigationLink("Apple Distortion", destination: DistortionView()) - NavigationLink("Bit Crusher", destination: BitCrusherView()) - NavigationLink("Clipper", destination: ClipperView()) - NavigationLink("Decimator", destination: DecimatorView()) - NavigationLink("Ring Modulator", destination: RingModulatorView()) - NavigationLink("Tanh Distortion", destination: TanhDistortionView()) - } + DisclosureGroup("Distortion") { + Group { + NavigationLink("Apple Distortion", destination: DistortionView()) + NavigationLink("Bit Crusher", destination: BitCrusherView()) + NavigationLink("Clipper", destination: ClipperView()) + NavigationLink("Decimator", destination: DecimatorView()) + NavigationLink("Ring Modulator", destination: RingModulatorView()) + NavigationLink("Tanh Distortion", destination: TanhDistortionView()) } } - Group { - DisclosureGroup("Reverb") { - Group { - NavigationLink("Apple Reverb", destination: ReverbView()) - NavigationLink("Chowning Reverb", destination: ChowningReverbView()) - NavigationLink("Comb Filter Reverb", destination: CombFilterReverbView()) - NavigationLink("Costello Reverb", destination: CostelloReverbView()) - NavigationLink("Flat Frequency Response Reverb", - destination: FlatFrequencyResponseReverbView()) - NavigationLink("Zita Reverb", destination: ZitaReverbView()) - } + DisclosureGroup("Reverb") { + Group { + NavigationLink("Apple Reverb", destination: ReverbView()) + NavigationLink("Chowning Reverb", destination: ChowningReverbView()) + NavigationLink("Comb Filter Reverb", destination: CombFilterReverbView()) + NavigationLink("Costello Reverb", destination: CostelloReverbView()) + NavigationLink("Flat Frequency Response Reverb", + destination: FlatFrequencyResponseReverbView()) + NavigationLink("Zita Reverb", destination: ZitaReverbView()) } } @@ -197,34 +185,33 @@ 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()) - } + 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("Tablature", destination: TablatureDemoView()) + 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("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("Fretboard", destination: FretboardView()) NavigationLink("Input Device Demo", destination: InputDeviceDemoView()) NavigationLink("MIDI Port Test", destination: MIDIPortTestView()) NavigationLink("Polyphonic Oscillator", destination: PolyphonicOscillatorView()) @@ -238,13 +225,6 @@ struct MasterView: View { .navigationBarTitle("AudioKit") .navigationBarTitleDisplayMode(.inline) .toolbar { - // This leading ToolbarItem is required to center the AudioKit logo on iPhones. - ToolbarItem(placement: .topBarLeading) { - Rectangle() - .frame(minWidth: 30) - .hidden() - .accessibilityHidden(true) - } ToolbarItem(placement: .principal) { Image("audiokit-logo") .resizable() diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/TablatureDemoView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/TablatureDemoView.swift new file mode 100644 index 00000000..8e437fb2 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AdditionalPackages/TablatureDemoView.swift @@ -0,0 +1,100 @@ +import Tablature +import SwiftUI + +struct TablatureDemoView: View { + @StateObject private var midiController = MIDIController() + @StateObject private var liveModel = LiveTablatureModel(instrument: .guitar) + @State private var isPlaying = false + + var body: some View { + ScrollView { + VStack(alignment: .leading, spacing: 24) { + // MARK: - Live Tablature (MIDI) + Text("Live Tablature") + .font(.headline) + LiveTablatureView(model: liveModel) + .frame(height: 140) + .background(Color.secondary.opacity(0.1)) + .cornerRadius(8) + + MIDISettingsView( + midiController: midiController, + instrument: $midiController.instrument, + timeWindow: $liveModel.timeWindow, + onReset: { + isPlaying = false + liveModel.reset() + } + ) + + HStack { + Button(isPlaying ? "Stop Simulation" : "Simulate Input") { + isPlaying.toggle() + if isPlaying { + liveModel.reset() + startSimulatedInput() + } + } + Text("or connect a MIDI guitar") + .foregroundColor(.secondary) + .font(.caption) + } + + Divider() + + // MARK: - Static Examples + Text("Smoke on the Water") + .font(.headline) + TablatureView(sequence: .smokeOnTheWater) + + Text("C Major Scale") + .font(.headline) + TablatureView(sequence: .cMajorScale) + + Text("E Minor Chord") + .font(.headline) + TablatureView(sequence: .eMinorChord) + + Text("Blues Lick (Articulations)") + .font(.headline) + TablatureView(sequence: .bluesLick) + + Text("Custom Styled") + .font(.headline) + TablatureView(sequence: .smokeOnTheWater) + .tablatureStyle(TablatureStyle( + stringSpacing: 24, + measureWidth: 400, + lineThickness: 2, + fretColor: .blue, + lineColor: .gray + )) + } + .padding() + } + .onAppear { + midiController.noteHandler = { [weak liveModel] string, fret, articulation in + liveModel?.addNote(string: string, fret: fret, articulation: articulation) + } + } + .navigationTitle("Tablature Demo") + } + + private func startSimulatedInput() { + let pattern: [(string: Int, fret: Int)] = [ + (0, 0), (0, 3), (1, 0), (1, 2), (2, 0), (2, 2), + (3, 0), (3, 2), (4, 0), (4, 3), (5, 0), (5, 3), + ] + var index = 0 + + Timer.scheduledTimer(withTimeInterval: 0.35, repeats: true) { timer in + guard isPlaying else { + timer.invalidate() + return + } + let entry = pattern[index % pattern.count] + liveModel.addNote(string: entry.string, fret: entry.fret) + index += 1 + } + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AudioPlayer/MultiSegmentPlayerView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AudioPlayer/MultiSegmentPlayerView.swift index ccc6183b..52798d3d 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AudioPlayer/MultiSegmentPlayerView.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AudioPlayer/MultiSegmentPlayerView.swift @@ -108,7 +108,6 @@ class MultiSegmentPlayerConductor: ObservableObject, HasAudioEngine { try AudioKit.Settings.session.setCategory(.playAndRecord, options: [.defaultToSpeaker, .mixWithOthers, - .allowBluetooth, .allowBluetoothA2DP]) try AudioKit.Settings.session.setActive(true) } catch { diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/ChannelDeviceRouting.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/ChannelDeviceRouting.swift index cb7e5de5..557b56ec 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/ChannelDeviceRouting.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/ChannelDeviceRouting.swift @@ -15,7 +15,6 @@ class ChannelDeviceRoutingConductor: ObservableObject, HasAudioEngine { try Settings.setSession(category: .playAndRecord, with: [.defaultToSpeaker, .mixWithOthers, - .allowBluetooth, .allowBluetoothA2DP]) try Settings.session.setActive(true) } catch let err { diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/FretboardView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/FretboardView.swift new file mode 100644 index 00000000..7a1f3ce4 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/FretboardView.swift @@ -0,0 +1,51 @@ +import Fretboard +import SwiftUI +import Tonic + +struct FretboardView: View { + @State var playedFrets: [Int?] = [nil, 5, 7, 7, 5, nil] + @State var bends: [Double] = [0, 0, 0, 0, 0, 0] + @State var bassMode = false + @State var selectedKey: Key = .C + + var body: some View { + VStack(spacing: 20) { + Text("Fretboard Demo") + .font(.largeTitle) + + Fretboard( + playedFrets: playedFrets, + bends: bends, + bassMode: bassMode, + noteKey: selectedKey + ) + .frame(height: 150) + .padding(.horizontal) + + Toggle("Bass Mode", isOn: $bassMode) + .frame(width: 200) + + HStack { + Text("Example Chords:") + Button("Am") { + playedFrets = [0, 1, 2, 2, 0, nil] + bends = Array(repeating: 0, count: 6) + } + Button("C") { + playedFrets = [0, 1, 0, 2, 3, nil] + bends = Array(repeating: 0, count: 6) + } + Button("G") { + playedFrets = [3, 0, 0, 0, 2, 3] + bends = Array(repeating: 0, count: 6) + } + Button("Clear") { + playedFrets = Array(repeating: nil, count: 6) + bends = Array(repeating: 0, count: 6) + } + } + } + .padding() + .navigationTitle("Fretboard Demo") + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDIController.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDIController.swift new file mode 100644 index 00000000..2c2d3e53 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDIController.swift @@ -0,0 +1,90 @@ +import Foundation +import MIDIKit +import Tablature + +class MIDIController: ObservableObject { + let midiManager = MIDIManager( + clientName: "TablatureDemoMIDIManager", + model: "TablatureDemo", + manufacturer: "AudioKit" + ) + + enum ChannelMapPreset: String, CaseIterable, Identifiable { + case channels1to6 = "Ch 1–6" + case channels11to16 = "Ch 11–16" + + var id: String { rawValue } + + var mapping: [UInt4: Int] { + switch self { + case .channels1to6: + return [0: 5, 1: 4, 2: 3, 3: 2, 4: 1, 5: 0] + case .channels11to16: + return [10: 5, 11: 4, 12: 3, 13: 2, 14: 1, 15: 0] + } + } + } + + @Published var channelMapPreset: ChannelMapPreset = .channels1to6 { + didSet { channelMap = channelMapPreset.mapping } + } + + @Published var channelMap: [UInt4: Int] + + @Published var instrument: StringInstrument = .guitar + + /// Called on the main thread with (string, fret, articulation) after fret lookup. + var noteHandler: ((Int, Int, Articulation?) -> Void)? + + /// Tracks the last note-on MIDI note per string for pitch bend context. + private var lastMIDINote: [Int: UInt8] = [:] + + init() { + channelMap = ChannelMapPreset.channels1to6.mapping + + do { + setMIDINetworkSession(policy: .anyone) + try midiManager.start() + try midiManager.addInputConnection( + to: .allOutputs, + tag: "inputConnections", + receiver: .events { [weak self] events, _, _ in + DispatchQueue.main.async { [weak self] in + self?.handleEvents(events) + } + } + ) + } catch { + print("MIDI did not start. Error: \(error)") + } + } + + private func handleEvents(_ events: [MIDIEvent]) { + for event in events { + switch event { + case .noteOn(let payload): + guard payload.velocity.midi1Value > 0 else { continue } + guard let stringIndex = channelMap[payload.channel] else { continue } + let midiNote = payload.note.number.uInt8Value + lastMIDINote[stringIndex] = midiNote + if let fret = instrument.fret(for: midiNote, onString: stringIndex) { + noteHandler?(stringIndex, fret, nil) + } + + case .pitchBend(let payload): + guard let stringIndex = channelMap[payload.channel] else { continue } + let centered = Int(payload.value.midi1Value) - 8192 + let semitones = Double(centered) / 8192.0 * 2.0 + if abs(semitones) > 1.0 { + guard let lastNote = lastMIDINote[stringIndex], + let fret = instrument.fret(for: lastNote, onString: stringIndex) + else { continue } + noteHandler?(stringIndex, fret, .pitchBendArrow) + } + + default: + break + } + } + } +} diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDISettingsView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDISettingsView.swift new file mode 100644 index 00000000..f9349ce1 --- /dev/null +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/Reusable Components/MIDISettingsView.swift @@ -0,0 +1,46 @@ +import SwiftUI +import Tablature + +struct MIDISettingsView: View { + @ObservedObject var midiController: MIDIController + @Binding var instrument: StringInstrument + @Binding var timeWindow: Double + var onReset: () -> Void + + private static let instruments: [StringInstrument] = [ + .guitar, .guitar7String, .guitarDropD, + .bass, .bass5String, .ukulele, .banjo, + ] + + var body: some View { + HStack(spacing: 16) { + Picker("Instrument", selection: $instrument) { + ForEach(Self.instruments) { preset in + Text(preset.name).tag(preset) + } + } + .frame(maxWidth: 200) + + Picker("Channels", selection: $midiController.channelMapPreset) { + ForEach(MIDIController.ChannelMapPreset.allCases) { preset in + Text(preset.rawValue).tag(preset) + } + } + .pickerStyle(.segmented) + .frame(maxWidth: 200) + + HStack(spacing: 4) { + Text("Window:") + Slider(value: $timeWindow, in: 2...15, step: 1) + .frame(maxWidth: 120) + .accessibilityLabel("Time window") + .accessibilityValue("\(Int(timeWindow)) seconds") + Text("\(Int(timeWindow))s") + .monospacedDigit() + .frame(width: 28, alignment: .trailing) + } + + Button("Reset", action: onReset) + } + } +} From 9994089512fde63319a5909a745eef188bbc12ee Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Fri, 27 Feb 2026 11:14:11 -0500 Subject: [PATCH 2/3] Hounds --- Cookbook/CookbookCommon/Package.swift | 7 ++++--- .../Sources/CookbookCommon/ContentView.swift | 2 -- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Cookbook/CookbookCommon/Package.swift b/Cookbook/CookbookCommon/Package.swift index 5b5d973b..208024e9 100644 --- a/Cookbook/CookbookCommon/Package.swift +++ b/Cookbook/CookbookCommon/Package.swift @@ -21,14 +21,15 @@ let package = Package( .package(url: "https://github.com/AudioKit/Flow", from: "1.0.0"), .package(url: "https://github.com/AudioKit/PianoRoll", from: "1.0.0"), .package(url: "https://github.com/orchetect/MIDIKit", from: "0.11.0"), - .package(url: "https://github.com/AudioKit/Tablature", from: "0.1.0"), - .package(url: "https://github.com/AudioKit/Fretboard", from: "0.1.0"), + .package(url: "https://github.com/AudioKit/Tablature", from: "0.1.0"), + .package(url: "https://github.com/AudioKit/Fretboard", from: "0.1.0"), ], targets: [ .target( name: "CookbookCommon", dependencies: ["AudioKit", "AudioKitUI", "AudioKitEX", "Keyboard", "SoundpipeAudioKit", - "SporthAudioKit", "STKAudioKit", "DunneAudioKit", "Tonic", "Controls", "Waveform", "Flow", "PianoRoll", "MIDIKit", "Tablature", "Fretboard"], + "SporthAudioKit", "STKAudioKit", + "DunneAudioKit", "Tonic", "Controls", "Waveform", "Flow", "PianoRoll", "MIDIKit", "Tablature", "Fretboard"], resources: [ .copy("MIDI Files"), .copy("Samples"), diff --git a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift index cca6abd7..148110b0 100644 --- a/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift +++ b/Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift @@ -38,7 +38,6 @@ struct MasterView: View { } } - DisclosureGroup("Operations") { Group { NavigationLink("Crossing Signal", destination: CrossingSignalView()) @@ -204,7 +203,6 @@ struct MasterView: View { NavigationLink("Tables", destination: TableRecipeView()) } } - DisclosureGroup("WIP") { Group { From 9d36c4efdcbb916a9c060c565a9b1cb6bb164ba6 Mon Sep 17 00:00:00 2001 From: NickCulbertson Date: Fri, 27 Feb 2026 11:16:01 -0500 Subject: [PATCH 3/3] Hounds --- Cookbook/CookbookCommon/Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cookbook/CookbookCommon/Package.swift b/Cookbook/CookbookCommon/Package.swift index 208024e9..e84aeebb 100644 --- a/Cookbook/CookbookCommon/Package.swift +++ b/Cookbook/CookbookCommon/Package.swift @@ -28,8 +28,8 @@ let package = Package( .target( name: "CookbookCommon", dependencies: ["AudioKit", "AudioKitUI", "AudioKitEX", "Keyboard", "SoundpipeAudioKit", - "SporthAudioKit", "STKAudioKit", - "DunneAudioKit", "Tonic", "Controls", "Waveform", "Flow", "PianoRoll", "MIDIKit", "Tablature", "Fretboard"], + "SporthAudioKit", "STKAudioKit", "DunneAudioKit", "Tonic", "Controls", + "Waveform", "Flow", "PianoRoll", "MIDIKit", "Tablature", "Fretboard"], resources: [ .copy("MIDI Files"), .copy("Samples"),