Skip to content

Instantly share code, notes, and snippets.

@voxels
Last active November 14, 2025 13:55
Show Gist options
  • Select an option

  • Save voxels/e9cd23e8b28a98c15783c8e6c09f4843 to your computer and use it in GitHub Desktop.

Select an option

Save voxels/e9cd23e8b28a98c15783c8e6c09f4843 to your computer and use it in GitHub Desktop.
import Foundation
import CoreMIDI
// Note: This file assumes the `MIDIAdapter.h` and `MIDIAdapter.mm` files
// from your project are available and included in the build target.
// It also assumes Core MIDI definitions like MIDIMessage_32 are available.
// --- UPDATE: We are removing the MIDIAdapter dependency for this class ---
// Define a simple Deck ID for clarity
public enum DeckID: Sendable {
case A, B
}
/// An actor to manage sending MIDI 1.0 UMP messages to Traktor.
// --- UPDATE: Now sends MIDI 1.0 Packet Lists ---
public actor TraktorMidiSender {
private let mapping: TraktorMapping
// --- OLD ---
// private let midiAdapter: MIDIAdapter
private var client = MIDIClientRef()
private var port = MIDIPortRef()
private var destinationEndpoint = MIDIEndpointRef()
/// Initializes the sender with a specific mapping and MIDI destination name.
/// - Parameters:
/// - mapping: The `TraktorMapping` struct defining what to send.
/// - destinationName: The name of the virtual MIDI port Traktor is listening to.
/// (e.g., "Traktor Virtual Input").
public init(mapping: TraktorMapping, destinationName: String) {
self.mapping = mapping
// --- OLD ---
// self.midiAdapter = MIDIAdapter(logging: false)
setupMIDI(destinationName: destinationName)
}
/// Creates the Core MIDI client and output port.
private func setupMIDI(destinationName: String) {
var status = MIDIClientCreate("com.daw.traktor-sender" as CFString, nil, nil, &client)
if status != noErr {
print("Failed to create MIDI client: \(status)")
return
}
status = MIDIOutputPortCreate(client, "DAW-Traktor-Port" as CFString, &port)
if status != noErr {
print("Failed to create MIDI output port: \(status)")
return
}
findDestination(named: destinationName)
}
/// Scans the system for a MIDI destination matching the provided name.
private func findDestination(named: String) {
let count = MIDIGetNumberOfDestinations()
print("Found \(count) MIDI destinations.")
for i in 0..<count {
let endpoint = MIDIGetDestination(i)
if endpoint != 0 {
var prop: Unmanaged<CFString>?
if MIDIObjectGetStringProperty(endpoint, kMIDIPropertyDisplayName, &prop) == noErr {
if let name = prop?.takeRetainedValue() as String? {
print("Checking destination: \(name)")
// --- NEW: Look for Network Session ---
if name.contains("Network") || name.contains("Session") || name.contains(named) {
print("Found target destination: \(name)")
// --- End NEW ---
self.destinationEndpoint = endpoint
return
}
}
}
}
}
print("Warning: Could not find MIDI destination named '\(named)'. Messages will not be sent.")
}
// MARK: - Private MIDI Message Builders
/// Constructs a 32-bit UMP for a MIDI 1.0 Note On message.
// --- OLD ---
// private func buildNoteOn(group: UInt8, channel: UInt8, note: UInt8, velocity: UInt8) -> MIDIMessage_32 {
// ...
// }
// --- NEW: Returns raw bytes ---
private func buildNoteOn(channel: UInt8, note: UInt8, velocity: UInt8) -> [UInt8] {
let status = (0x9 << 4) | (channel & 0x0F) // Note On status is 0x9n
return [status, note, velocity]
}
// --- End NEW ---
/// Constructs a 32-bit UMP for a MIDI 1.0 Control Change message.
// --- OLD ---
// private func buildControlChange(group: UInt8, channel: UInt8, controller: UInt8, value: UInt8) -> MIDIMessage_32 {
// ...
// }
// --- NEW: Returns raw bytes ---
private func buildControlChange(channel: UInt8, controller: UInt8, value: UInt8) -> [UInt8] {
let status = (0xB << 4) | (channel & 0x0F) // CC status is 0xBn
return [status, controller, value]
}
// --- End NEW ---
/// Sends a 32-bit MIDI 1.0 UMP message.
// --- OLD ---
// private func send(message: MIDIMessage_32) {
// guard destinationEndpoint != 0 else {
// // print("Cannot send MIDI: No destination.")
// return
// }
// midiAdapter.sendMIDI1UPMessage(message, port: port, destination: destinationEndpoint)
// }
// --- NEW: Sends a MIDI 1.0 Packet List, based on user's example ---
private func send(bytes: [UInt8]) {
guard destinationEndpoint != 0 else { return }
// Allocate a buffer for the MIDIPacketList.
// 1024 bytes is ample for a single 3-byte message.
let bufferSize = 1024
var packetListBuffer = [UInt8](repeating: 0, count: bufferSize)
packetListBuffer.withUnsafeMutableBufferPointer { ptr in
guard let baseAddress = ptr.baseAddress else { return }
var packetList = UnsafeMutablePointer<MIDIPacketList>(OpaquePointer(baseAddress))
// Initialize the packet list
var currentPacket = MIDIPacketListInit(packetList)
// Add our packet to the list
currentPacket = MIDIPacketListAdd(packetList,
bufferSize,
currentPacket,
0, // timestamp (send immediately)
bytes.count,
bytes)
// Send the packet list
MIDISend(port, destinationEndpoint, packetList)
}
}
// --- End NEW ---
// MARK: - Public Control Methods
// --- Transport ---
/// Toggles Play/Pause for a deck.
public func play(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.play : mapping.deckB.play
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
/// Toggles Sync for a deck.
public func sync(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.sync : mapping.deckB.sync
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
/// Toggles the main Cue button state.
public func cue(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.cue : mapping.deckB.cue
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
/// Toggles the Reverse state.
public func reverse(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.reverse : mapping.deckB.reverse
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
/// Toggles Sync for a deck.
public func sync(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.sync : mapping.deckB.sync
let velocity: UInt8 = state ? 127 : 0
let message = buildNoteOn(group: 0, channel: map.channel, note: map.note, velocity: velocity)
send(message)
}
/// Toggles the main Cue button state.
public func cue(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.cue : mapping.deckB.cue
let velocity: UInt8 = state ? 127 : 0
let message = buildNoteOn(group: 0, channel: map.channel, note: map.note, velocity: velocity)
send(message)
}
/// Toggles the Reverse state.
public func reverse(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.reverse : mapping.deckB.reverse
let velocity: UInt8 = state ? 127 : 0
let message = buildNoteOn(group: 0, channel: map.channel, note: map.note, velocity: velocity)
send(message)
}
// --- Jog Wheel ---
/// Toggles the Jog Touch (Scratch) state.
public func jogTouch(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.jogTouch : mapping.deckB.jogTouch
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
/// Sends a jog wheel turn message.
/// This uses a relative value (65 for forward, 63 for backward).
public func jogTurn(deck: DeckID, delta: Int) {
let map = (deck == .A) ? mapping.deckA.jogTurn : mapping.deckB.jogTurn
var ccValue: UInt8 = 64 // 64 = no change
if delta > 0 {
ccValue = 65 // Relative increase
} else if delta < 0 {
ccValue = 63 // Relative decrease
}
let bytes = buildControlChange(channel: map.channel, controller: map.controller, value: ccValue)
send(bytes: bytes)
}
// --- Mixer ---
/// Sets the volume for a deck (0.0 - 1.0).
public func setVolume(deck: DeckID, value: Double) {
let map = (deck == .A) ? mapping.deckA.volume : mapping.deckB.volume
let ccValue = UInt8(max(0.0, min(1.0, value)) * 127.0) // Normalize to 0-127
let bytes = buildControlChange(channel: map.channel, controller: map.controller, value: ccValue)
send(bytes: bytes)
}
/// Sets the tempo for a deck (0.0 - 1.0).
public func setTempo(deck: DeckID, value: Double) {
let map = (deck == .A) ? mapping.deckA.tempo : mapping.deckB.tempo
let ccValue = UInt8(max(0.0, min(1.0, value)) * 127.0) // Normalize to 0-127
let bytes = buildControlChange(channel: map.channel, controller: map.controller, value: ccValue)
send(bytes: bytes)
}
/// Sets the crossfader position (0.0 = A, 1.0 = B).
public func setCrossfader(value: Double) {
let map = mapping.crossfader
let ccValue = UInt8(max(0.0, min(1.0, value)) * 127.0) // Normalize to 0-127
let bytes = buildControlChange(channel: map.channel, controller: map.controller, value: ccValue)
send(bytes: bytes)
}
// --- Cues & Loops ---
/// Sets a Hot Cue state (0-7 for A-H).
public func setHotCue(deck: DeckID, cue: Int, state: Bool) {
let deckMap = (deck == .A) ? mapping.deckA : mapping.deckB
var map: MidiNoteMapping?
switch cue {
case 0: map = deckMap.hotCueA
case 1: map = deckMap.hotCueB
case 2: map = deckMap.hotCueC
case 3: map = deckMap.hotCueD
case 4: map = deckMap.hotCueE
case 5: map = deckMap.hotCueF
case 6: map = deckMap.hotCueG
case 7: map = deckMap.hotCueH
default: break
}
guard let cueMap = map else { return }
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: cueMap.channel, note: cueMap.note, velocity: velocity)
send(bytes: bytes)
}
public func loopIn(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.loopIn : mapping.deckB.loopIn
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
public func loopOut(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.loopOut : mapping.deckB.loopOut
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
public func reloopExit(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.reloopExit : mapping.deckB.reloopExit
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
public func loop4Beat(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.loop4Beat : mapping.deckB.loop4Beat
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
public func loop8Beat(deck: DeckID, state: Bool) {
let map = (deck == .A) ? mapping.deckA.loop8Beat : mapping.deckB.loop8Beat
let velocity: UInt8 = state ? 127 : 0
let bytes = buildNoteOn(channel: map.channel, note: map.note, velocity: velocity)
send(bytes: bytes)
}
// --- Beat Jump & Browser ---
public func beatJump(deck: DeckID, direction: Int) {
let map: MidiNoteMapping
if direction > 0 {
map = (deck == .A) ? mapping.deckA.beatJumpFwd : mapping.deckB.beatJumpFwd
} else {
map = (deck == .A) ? mapping.deckA.beatJumpRev : mapping.deckB.beatJumpRev
}
// Beat Jump is a momentary press (Note On, then Note Off)
let bytesOn = buildNoteOn(channel: map.channel, note: map.note, velocity: 127)
let bytesOff = buildNoteOn(channel: map.channel, note: map.note, velocity: 0)
send(bytes: bytesOn)
send(bytes: bytesOff)
}
public func browserRotate(delta: Int) {
let map = mapping.browserRotate
var ccValue: UInt8 = 64 // 64 = no change
if delta > 0 {
ccValue = 65 // Relative increase
} else if delta < 0 {
ccValue = 63 // Relative decrease
}
let bytes = buildControlChange(channel: map.channel, controller: map.controller, value: ccValue)
send(bytes: bytes)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment