Skip to content

Instantly share code, notes, and snippets.

@cellularmitosis
Last active January 15, 2022 19:28
Show Gist options
  • Save cellularmitosis/b4a3549cefd1dd8dbc97a4c0a5bdb24a to your computer and use it in GitHub Desktop.
Save cellularmitosis/b4a3549cefd1dd8dbc97a4c0a5bdb24a to your computer and use it in GitHub Desktop.
GridNotes: a half-step grid-based piano (iPad app)

Blog 2021/1/6

<- previous | index | next ->

GridNotes: a half-step grid-based piano (iPad app)

UPDATE: this is now an app in the iOS AppStore: GridNotes! https://github.com/pepaslabs/GridNotes

Having just started learning about music theory, I wanted a piano where all of the notes were laid out in a grid: seven octave rows of 12 half-step notes.

Below is the result of reading https://rollout.io/blog/building-a-midi-music-app-for-ios-in-swift/ and hacking around for an hour.

IMG_6014

Create a Xcode project (storyboard-based), delete the storyboard, then paste the below code into AppDelegate.swift.

You will also need to download this sound font and add it to the bundle: http://sourceforge.net/p/mscore/code/HEAD/tree/trunk/mscore/share/sound/TimGM6mb.sf2?format=raw (via https://musescore.org/en/handbook/soundfonts-and-sfz-files#list)

//
// 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)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment