|
// |
|
// AppDelegate.swift |
|
// GridNotes |
|
// |
|
// Created by Jason Pepas on 1/6/21. |
|
// |
|
|
|
import UIKit |
|
|
|
@main |
|
class AppDelegate: UIResponder, UIApplicationDelegate { |
|
|
|
var window: UIWindow? |
|
|
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { |
|
window = UIWindow() |
|
window?.makeKeyAndVisible() |
|
window?.rootViewController = ViewController() |
|
return true |
|
} |
|
|
|
} |
|
|
|
|
|
class ViewController: UIViewController, KeyRowDelegate { |
|
|
|
var rows: [KeyRow] = [KeyRow(), KeyRow(), KeyRow(), KeyRow(), KeyRow(), KeyRow(), KeyRow()] |
|
|
|
override func viewDidLoad() { |
|
super.viewDidLoad() |
|
view.backgroundColor = UIColor.white |
|
|
|
let guide = view.layoutMarginsGuide |
|
for row in rows { |
|
view.addSubview(row) |
|
guide.leadingAnchor.constraint(equalTo: row.leadingAnchor).isActive = true |
|
guide.trailingAnchor.constraint(equalTo: row.trailingAnchor).isActive = true |
|
} |
|
guide.topAnchor.constraint(equalTo: rows.first!.topAnchor).isActive = true |
|
guide.bottomAnchor.constraint(equalTo: rows.last!.bottomAnchor).isActive = true |
|
for i in [0,1,2,3,4,5] { |
|
rows[i].bottomAnchor.constraint(equalTo: rows[i+1].topAnchor).isActive = true |
|
} |
|
for i in [1,2,3,4,5,6] { |
|
rows[i].heightAnchor.constraint(equalTo: rows[0].heightAnchor).isActive = true |
|
} |
|
for i in [6,5,4,3,2,1,0] { |
|
rows[i].noteOffset = ((6-i) * 12) + 24 |
|
} |
|
for i in [0,1,2,3,4,5,6] { |
|
rows[i].delegate = self |
|
} |
|
|
|
initAudio() |
|
} |
|
|
|
func keyDidGetPressed(note: Int) { |
|
startNote(note: UInt8(note)) |
|
} |
|
|
|
func keyDidGetReleased(note: Int) { |
|
endNote(note: UInt8(note)) |
|
} |
|
} |
|
|
|
|
|
protocol KeyRowDelegate { |
|
func keyDidGetPressed(note: Int) |
|
func keyDidGetReleased(note: Int) |
|
} |
|
|
|
class KeyRow: UIView { |
|
var delegate: KeyRowDelegate? = nil |
|
|
|
var noteOffset: Int = 0 |
|
|
|
let key1 = UIButton() |
|
let key2 = UIButton() |
|
let key3 = UIButton() |
|
let key4 = UIButton() |
|
let key5 = UIButton() |
|
let key6 = UIButton() |
|
let key7 = UIButton() |
|
let key8 = UIButton() |
|
let key9 = UIButton() |
|
let key10 = UIButton() |
|
let key11 = UIButton() |
|
let key12 = UIButton() |
|
|
|
var keys: [UIButton] { |
|
return [key1, key2, key3, key4, key5, key6, key7, key8, key9, key10, key11, key12] |
|
} |
|
|
|
override init(frame: CGRect) { |
|
super.init(frame: frame) |
|
translatesAutoresizingMaskIntoConstraints = false |
|
for k in keys { |
|
k.translatesAutoresizingMaskIntoConstraints = false |
|
addSubview(k) |
|
k.layer.borderWidth = 1 |
|
k.layer.borderColor = UIColor.blue.cgColor |
|
k.addTarget(self, action: #selector(keyDidGetPressed(key:)), for: .touchDown) |
|
k.addTarget(self, action: #selector(keyDidGetReleased(key:)), for: .touchUpInside) |
|
k.addTarget(self, action: #selector(keyDidGetReleased(key:)), for: .touchDragExit) |
|
k.addTarget(self, action: #selector(keyDidGetReleased(key:)), for: .touchCancel) |
|
} |
|
|
|
for i in [0,1,2,3,4,5,6,7,8,9,10,11] { |
|
keys[i].tag = i |
|
} |
|
|
|
setNeedsUpdateConstraints() |
|
} |
|
|
|
required init?(coder: NSCoder) { |
|
fatalError("init(coder:) has not been implemented") |
|
} |
|
|
|
private var hasSetUpConstraints: Bool = false |
|
override func updateConstraints() { |
|
super.updateConstraints() |
|
if !hasSetUpConstraints { |
|
hasSetUpConstraints = true |
|
|
|
for k in keys { |
|
topAnchor.constraint(equalTo: k.topAnchor).isActive = true |
|
bottomAnchor.constraint(equalTo: k.bottomAnchor).isActive = true |
|
} |
|
|
|
leadingAnchor.constraint(equalTo: key1.leadingAnchor).isActive = true |
|
for i in [0,1,2,3,4,5,6,7,8,9,10] { |
|
keys[i+1].leadingAnchor.constraint(equalTo: keys[i].trailingAnchor).isActive = true |
|
} |
|
trailingAnchor.constraint(equalTo: key12.trailingAnchor).isActive = true |
|
|
|
for k in keys { |
|
key1.widthAnchor.constraint(equalTo: k.widthAnchor).isActive = true |
|
} |
|
} |
|
} |
|
|
|
@objc func keyDidGetPressed(key: UIButton) { |
|
key.backgroundColor = UIColor.yellow |
|
delegate?.keyDidGetPressed(note: noteOffset + key.tag) |
|
} |
|
|
|
@objc func keyDidGetReleased(key: UIButton) { |
|
key.backgroundColor = UIColor.clear |
|
delegate?.keyDidGetReleased(note: noteOffset + key.tag) |
|
} |
|
} |
|
|
|
|
|
// The below code was adapted from https://rollout.io/blog/building-a-midi-music-app-for-ios-in-swift/ |
|
|
|
import AudioToolbox |
|
|
|
enum MIDICommand { |
|
static let noteOff: UInt32 = 0x80 |
|
static let noteOn: UInt32 = 0x90 |
|
static let patchChange: UInt32 = 0xC0 |
|
} |
|
|
|
var graph: AUGraph? |
|
var synthNode: AUNode = AUNode() |
|
var outputNode: AUNode = AUNode() |
|
var synthUnit: AudioUnit? |
|
|
|
func initAudio() { |
|
var ret: OSStatus |
|
|
|
ret = NewAUGraph(&graph) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
var desc = AudioComponentDescription( |
|
componentType: OSType(kAudioUnitType_Output), |
|
componentSubType: OSType(kAudioUnitSubType_RemoteIO), |
|
componentManufacturer: OSType(kAudioUnitManufacturer_Apple), |
|
componentFlags: 0, |
|
componentFlagsMask: 0 |
|
) |
|
ret = AUGraphAddNode(graph!, &desc, &outputNode) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
desc = AudioComponentDescription( |
|
componentType: OSType(kAudioUnitType_MusicDevice), |
|
componentSubType: OSType(kAudioUnitSubType_MIDISynth), |
|
componentManufacturer: OSType(kAudioUnitManufacturer_Apple), |
|
componentFlags: 0, |
|
componentFlagsMask: 0 |
|
) |
|
ret = AUGraphAddNode(graph!, &desc, &synthNode) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
ret = AUGraphOpen(graph!) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
ret = AUGraphNodeInfo(graph!, synthNode, nil, &synthUnit) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
let synthOutElement: AudioUnitElement = 0 |
|
let ioInputElement: AudioUnitElement = 0 |
|
ret = AUGraphConnectNodeInput(graph!, synthNode, synthOutElement, outputNode, ioInputElement) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
ret = AUGraphInitialize(graph!) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
ret = AUGraphStart(graph!) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
// load a sound font. |
|
var soundFont: URL = Bundle.main.url(forResource: "TimGM6mb", withExtension: "sf2")! |
|
ret = AudioUnitSetProperty( |
|
synthUnit!, |
|
AudioUnitPropertyID(kMusicDeviceProperty_SoundBankURL), |
|
AudioUnitScope(kAudioUnitScope_Global), |
|
0, |
|
&soundFont, |
|
UInt32(MemoryLayout<URL>.size) |
|
) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
// load a patch. |
|
let channel: UInt32 = 0 |
|
var disabled: UInt32 = 0 |
|
var enabled: UInt32 = 1 |
|
let patch: UInt32 = 0 |
|
|
|
ret = AudioUnitSetProperty( |
|
synthUnit!, |
|
AudioUnitPropertyID(kAUMIDISynthProperty_EnablePreload), |
|
AudioUnitScope(kAudioUnitScope_Global), |
|
0, |
|
&enabled, |
|
UInt32(MemoryLayout<UInt32>.size) |
|
) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
let command = UInt32(MIDICommand.patchChange | channel) |
|
ret = MusicDeviceMIDIEvent( |
|
synthUnit!, |
|
command, |
|
patch, |
|
0, |
|
0 |
|
) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
ret = AudioUnitSetProperty( |
|
synthUnit!, |
|
AudioUnitPropertyID(kAUMIDISynthProperty_EnablePreload), |
|
AudioUnitScope(kAudioUnitScope_Global), |
|
0, |
|
&disabled, |
|
UInt32(MemoryLayout<UInt32>.size) |
|
) |
|
precondition(ret == kAudioServicesNoError) |
|
|
|
ret = MusicDeviceMIDIEvent(synthUnit!, command, patch, 0, 0) |
|
precondition(ret == kAudioServicesNoError) |
|
} |
|
|
|
func startNote(note: UInt8) { |
|
print("startNote \(note)") |
|
var ret: OSStatus |
|
let channel: UInt32 = 0 |
|
let command: UInt32 = (MIDICommand.noteOn | channel) |
|
let base: UInt8 = note |
|
let octave: UInt32 = 0 |
|
let pitch: UInt32 = UInt32(base) + (octave * 12) |
|
let velocity: UInt32 = 128 |
|
ret = MusicDeviceMIDIEvent(synthUnit!, command, pitch, velocity, 0) |
|
precondition(ret == kAudioServicesNoError) |
|
} |
|
|
|
func endNote(note: UInt8) { |
|
print("endNote \(note)") |
|
var ret: OSStatus |
|
let channel: UInt32 = 0 |
|
let command: UInt32 = (MIDICommand.noteOff | channel) |
|
let base: UInt8 = note |
|
let octave: UInt32 = 0 |
|
let pitch: UInt32 = UInt32(base) + (octave * 12) |
|
ret = MusicDeviceMIDIEvent(synthUnit!, command, pitch, 0, 0) |
|
precondition(ret == kAudioServicesNoError) |
|
} |