Skip to content

Commit 10fa15b

Browse files
Added Tablature and Fretboard examples
Added Tablature and Fretboard examples. Cleaned up main view. Removed a few deprecations.
2 parents 86f829c + 9d36c4e commit 10fa15b

File tree

8 files changed

+401
-135
lines changed

8 files changed

+401
-135
lines changed

Cookbook/CookbookCommon/Package.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,16 @@ let package = Package(
2020
.package(url: "https://github.com/AudioKit/Waveform", branch: "visionos"),
2121
.package(url: "https://github.com/AudioKit/Flow", from: "1.0.0"),
2222
.package(url: "https://github.com/AudioKit/PianoRoll", from: "1.0.0"),
23-
.package(url: "https://github.com/orchetect/MIDIKit", from: "0.9.7"),
23+
.package(url: "https://github.com/orchetect/MIDIKit", from: "0.11.0"),
24+
.package(url: "https://github.com/AudioKit/Tablature", from: "0.1.0"),
25+
.package(url: "https://github.com/AudioKit/Fretboard", from: "0.1.0"),
2426
],
2527
targets: [
2628
.target(
2729
name: "CookbookCommon",
2830
dependencies: ["AudioKit", "AudioKitUI", "AudioKitEX", "Keyboard", "SoundpipeAudioKit",
29-
"SporthAudioKit", "STKAudioKit", "DunneAudioKit", "Tonic", "Controls", "Waveform", "Flow", "PianoRoll", "MIDIKit"],
31+
"SporthAudioKit", "STKAudioKit", "DunneAudioKit", "Tonic", "Controls",
32+
"Waveform", "Flow", "PianoRoll", "MIDIKit", "Tablature", "Fretboard"],
3033
resources: [
3134
.copy("MIDI Files"),
3235
.copy("Samples"),

Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift

Lines changed: 109 additions & 131 deletions
Large diffs are not rendered by default.
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import Tablature
2+
import SwiftUI
3+
4+
struct TablatureDemoView: View {
5+
@StateObject private var midiController = MIDIController()
6+
@StateObject private var liveModel = LiveTablatureModel(instrument: .guitar)
7+
@State private var isPlaying = false
8+
9+
var body: some View {
10+
ScrollView {
11+
VStack(alignment: .leading, spacing: 24) {
12+
// MARK: - Live Tablature (MIDI)
13+
Text("Live Tablature")
14+
.font(.headline)
15+
LiveTablatureView(model: liveModel)
16+
.frame(height: 140)
17+
.background(Color.secondary.opacity(0.1))
18+
.cornerRadius(8)
19+
20+
MIDISettingsView(
21+
midiController: midiController,
22+
instrument: $midiController.instrument,
23+
timeWindow: $liveModel.timeWindow,
24+
onReset: {
25+
isPlaying = false
26+
liveModel.reset()
27+
}
28+
)
29+
30+
HStack {
31+
Button(isPlaying ? "Stop Simulation" : "Simulate Input") {
32+
isPlaying.toggle()
33+
if isPlaying {
34+
liveModel.reset()
35+
startSimulatedInput()
36+
}
37+
}
38+
Text("or connect a MIDI guitar")
39+
.foregroundColor(.secondary)
40+
.font(.caption)
41+
}
42+
43+
Divider()
44+
45+
// MARK: - Static Examples
46+
Text("Smoke on the Water")
47+
.font(.headline)
48+
TablatureView(sequence: .smokeOnTheWater)
49+
50+
Text("C Major Scale")
51+
.font(.headline)
52+
TablatureView(sequence: .cMajorScale)
53+
54+
Text("E Minor Chord")
55+
.font(.headline)
56+
TablatureView(sequence: .eMinorChord)
57+
58+
Text("Blues Lick (Articulations)")
59+
.font(.headline)
60+
TablatureView(sequence: .bluesLick)
61+
62+
Text("Custom Styled")
63+
.font(.headline)
64+
TablatureView(sequence: .smokeOnTheWater)
65+
.tablatureStyle(TablatureStyle(
66+
stringSpacing: 24,
67+
measureWidth: 400,
68+
lineThickness: 2,
69+
fretColor: .blue,
70+
lineColor: .gray
71+
))
72+
}
73+
.padding()
74+
}
75+
.onAppear {
76+
midiController.noteHandler = { [weak liveModel] string, fret, articulation in
77+
liveModel?.addNote(string: string, fret: fret, articulation: articulation)
78+
}
79+
}
80+
.navigationTitle("Tablature Demo")
81+
}
82+
83+
private func startSimulatedInput() {
84+
let pattern: [(string: Int, fret: Int)] = [
85+
(0, 0), (0, 3), (1, 0), (1, 2), (2, 0), (2, 2),
86+
(3, 0), (3, 2), (4, 0), (4, 3), (5, 0), (5, 3),
87+
]
88+
var index = 0
89+
90+
Timer.scheduledTimer(withTimeInterval: 0.35, repeats: true) { timer in
91+
guard isPlaying else {
92+
timer.invalidate()
93+
return
94+
}
95+
let entry = pattern[index % pattern.count]
96+
liveModel.addNote(string: entry.string, fret: entry.fret)
97+
index += 1
98+
}
99+
}
100+
}

Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/AudioPlayer/MultiSegmentPlayerView.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,6 @@ class MultiSegmentPlayerConductor: ObservableObject, HasAudioEngine {
108108
try AudioKit.Settings.session.setCategory(.playAndRecord,
109109
options: [.defaultToSpeaker,
110110
.mixWithOthers,
111-
.allowBluetooth,
112111
.allowBluetoothA2DP])
113112
try AudioKit.Settings.session.setActive(true)
114113
} catch {

Cookbook/CookbookCommon/Sources/CookbookCommon/Recipes/WIP/ChannelDeviceRouting.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class ChannelDeviceRoutingConductor: ObservableObject, HasAudioEngine {
1515
try Settings.setSession(category: .playAndRecord,
1616
with: [.defaultToSpeaker,
1717
.mixWithOthers,
18-
.allowBluetooth,
1918
.allowBluetoothA2DP])
2019
try Settings.session.setActive(true)
2120
} catch let err {
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import Fretboard
2+
import SwiftUI
3+
import Tonic
4+
5+
struct FretboardView: View {
6+
@State var playedFrets: [Int?] = [nil, 5, 7, 7, 5, nil]
7+
@State var bends: [Double] = [0, 0, 0, 0, 0, 0]
8+
@State var bassMode = false
9+
@State var selectedKey: Key = .C
10+
11+
var body: some View {
12+
VStack(spacing: 20) {
13+
Text("Fretboard Demo")
14+
.font(.largeTitle)
15+
16+
Fretboard(
17+
playedFrets: playedFrets,
18+
bends: bends,
19+
bassMode: bassMode,
20+
noteKey: selectedKey
21+
)
22+
.frame(height: 150)
23+
.padding(.horizontal)
24+
25+
Toggle("Bass Mode", isOn: $bassMode)
26+
.frame(width: 200)
27+
28+
HStack {
29+
Text("Example Chords:")
30+
Button("Am") {
31+
playedFrets = [0, 1, 2, 2, 0, nil]
32+
bends = Array(repeating: 0, count: 6)
33+
}
34+
Button("C") {
35+
playedFrets = [0, 1, 0, 2, 3, nil]
36+
bends = Array(repeating: 0, count: 6)
37+
}
38+
Button("G") {
39+
playedFrets = [3, 0, 0, 0, 2, 3]
40+
bends = Array(repeating: 0, count: 6)
41+
}
42+
Button("Clear") {
43+
playedFrets = Array(repeating: nil, count: 6)
44+
bends = Array(repeating: 0, count: 6)
45+
}
46+
}
47+
}
48+
.padding()
49+
.navigationTitle("Fretboard Demo")
50+
}
51+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import Foundation
2+
import MIDIKit
3+
import Tablature
4+
5+
class MIDIController: ObservableObject {
6+
let midiManager = MIDIManager(
7+
clientName: "TablatureDemoMIDIManager",
8+
model: "TablatureDemo",
9+
manufacturer: "AudioKit"
10+
)
11+
12+
enum ChannelMapPreset: String, CaseIterable, Identifiable {
13+
case channels1to6 = "Ch 1–6"
14+
case channels11to16 = "Ch 11–16"
15+
16+
var id: String { rawValue }
17+
18+
var mapping: [UInt4: Int] {
19+
switch self {
20+
case .channels1to6:
21+
return [0: 5, 1: 4, 2: 3, 3: 2, 4: 1, 5: 0]
22+
case .channels11to16:
23+
return [10: 5, 11: 4, 12: 3, 13: 2, 14: 1, 15: 0]
24+
}
25+
}
26+
}
27+
28+
@Published var channelMapPreset: ChannelMapPreset = .channels1to6 {
29+
didSet { channelMap = channelMapPreset.mapping }
30+
}
31+
32+
@Published var channelMap: [UInt4: Int]
33+
34+
@Published var instrument: StringInstrument = .guitar
35+
36+
/// Called on the main thread with (string, fret, articulation) after fret lookup.
37+
var noteHandler: ((Int, Int, Articulation?) -> Void)?
38+
39+
/// Tracks the last note-on MIDI note per string for pitch bend context.
40+
private var lastMIDINote: [Int: UInt8] = [:]
41+
42+
init() {
43+
channelMap = ChannelMapPreset.channels1to6.mapping
44+
45+
do {
46+
setMIDINetworkSession(policy: .anyone)
47+
try midiManager.start()
48+
try midiManager.addInputConnection(
49+
to: .allOutputs,
50+
tag: "inputConnections",
51+
receiver: .events { [weak self] events, _, _ in
52+
DispatchQueue.main.async { [weak self] in
53+
self?.handleEvents(events)
54+
}
55+
}
56+
)
57+
} catch {
58+
print("MIDI did not start. Error: \(error)")
59+
}
60+
}
61+
62+
private func handleEvents(_ events: [MIDIEvent]) {
63+
for event in events {
64+
switch event {
65+
case .noteOn(let payload):
66+
guard payload.velocity.midi1Value > 0 else { continue }
67+
guard let stringIndex = channelMap[payload.channel] else { continue }
68+
let midiNote = payload.note.number.uInt8Value
69+
lastMIDINote[stringIndex] = midiNote
70+
if let fret = instrument.fret(for: midiNote, onString: stringIndex) {
71+
noteHandler?(stringIndex, fret, nil)
72+
}
73+
74+
case .pitchBend(let payload):
75+
guard let stringIndex = channelMap[payload.channel] else { continue }
76+
let centered = Int(payload.value.midi1Value) - 8192
77+
let semitones = Double(centered) / 8192.0 * 2.0
78+
if abs(semitones) > 1.0 {
79+
guard let lastNote = lastMIDINote[stringIndex],
80+
let fret = instrument.fret(for: lastNote, onString: stringIndex)
81+
else { continue }
82+
noteHandler?(stringIndex, fret, .pitchBendArrow)
83+
}
84+
85+
default:
86+
break
87+
}
88+
}
89+
}
90+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import SwiftUI
2+
import Tablature
3+
4+
struct MIDISettingsView: View {
5+
@ObservedObject var midiController: MIDIController
6+
@Binding var instrument: StringInstrument
7+
@Binding var timeWindow: Double
8+
var onReset: () -> Void
9+
10+
private static let instruments: [StringInstrument] = [
11+
.guitar, .guitar7String, .guitarDropD,
12+
.bass, .bass5String, .ukulele, .banjo,
13+
]
14+
15+
var body: some View {
16+
HStack(spacing: 16) {
17+
Picker("Instrument", selection: $instrument) {
18+
ForEach(Self.instruments) { preset in
19+
Text(preset.name).tag(preset)
20+
}
21+
}
22+
.frame(maxWidth: 200)
23+
24+
Picker("Channels", selection: $midiController.channelMapPreset) {
25+
ForEach(MIDIController.ChannelMapPreset.allCases) { preset in
26+
Text(preset.rawValue).tag(preset)
27+
}
28+
}
29+
.pickerStyle(.segmented)
30+
.frame(maxWidth: 200)
31+
32+
HStack(spacing: 4) {
33+
Text("Window:")
34+
Slider(value: $timeWindow, in: 2...15, step: 1)
35+
.frame(maxWidth: 120)
36+
.accessibilityLabel("Time window")
37+
.accessibilityValue("\(Int(timeWindow)) seconds")
38+
Text("\(Int(timeWindow))s")
39+
.monospacedDigit()
40+
.frame(width: 28, alignment: .trailing)
41+
}
42+
43+
Button("Reset", action: onReset)
44+
}
45+
}
46+
}

0 commit comments

Comments
 (0)