Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Cookbook/CookbookCommon/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,16 @@ 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"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comma Spacing Violation: There should be no space before and one after any comma. (comma)

.package(url: "https://github.com/AudioKit/Tablature", from: "0.1.0"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comma Spacing Violation: There should be no space before and one after any comma. (comma)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comma Spacing Violation: There should be no space before and one after any comma. (comma)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comma Spacing Violation: There should be no space before and one after any comma. (comma)

.package(url: "https://github.com/AudioKit/Fretboard", from: "0.1.0"),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comma Spacing Violation: There should be no space before and one after any comma. (comma)
Trailing Comma Violation: Collection literals should not have trailing commas. (trailing_comma)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comma Spacing Violation: There should be no space before and one after any comma. (comma)
Trailing Comma Violation: Collection literals should not have trailing commas. (trailing_comma)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comma Spacing Violation: There should be no space before and one after any comma. (comma)
Trailing Comma Violation: Collection literals should not have trailing commas. (trailing_comma)

],
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"),
Expand Down
240 changes: 109 additions & 131 deletions Cookbook/CookbookCommon/Sources/CookbookCommon/ContentView.swift

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing Comma Violation: Collection literals should not have trailing commas. (trailing_comma)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing Comma Violation: Collection literals should not have trailing commas. (trailing_comma)

]
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
@@ -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")
}
}
Original file line number Diff line number Diff line change
@@ -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 }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Identifier Name Violation: Variable name should be between 3 and 40 characters long: 'id' (identifier_name)


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
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trailing Comma Violation: Collection literals should not have trailing commas. (trailing_comma)

]

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)
}
}
}
Loading