2

I'm trying to use the receivedMIDINoteOn function to flash a UILabel when the sequencer is playing a note. I have tried using the AKMIDIListener protocol with no success. Also I have made a sub class of AKMIDISampler and send midi to it from the sequencer. It plays the midi but the receivedMIDINoteOn is not called.

This is what I have in the init() of the conductor:

init() {

    [ahSampler, beeSampler, gooSampler,flasher] >>> samplerMixer
    AudioKit.output = samplerMixer
    AudioKit.start()



    let midi = AKMIDI()
    midi.createVirtualPorts()
    midi.openInput("Session 1")
    midi.addListener(self)
}

The conductor follows AKMIDIListener protocol

this is the function: it is never called

func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel)
{
    print("got it")
}

And this is the sub class of AKMIDISampler, it gets midi and plays the sine synth, but the receivedMIDINoteOn is never called.

class Flasher: AKMIDISampler
{
    override func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity:    MIDIVelocity, channel: MIDIChannel)
    {
        print("Flasher got it!")
    }
}

edit: I should have been using the AKCallbackInstrument class instead, and overriding it's start() function.

Ben Spector
  • 319
  • 2
  • 9

2 Answers2

5

Ben,

Without seeing your entire project, I would guess that if your project is able to receive and trigger MIDI notes, then it's an issue with only sending its output to the UILabel. I recommend using NotificationCenter to inform the ViewController when a MIDI event has been received in the Conductor class. Be sure to add the DispatchQueue.main.async code, or else the text won't update as expected. This was noted in the AudioKit Google Group here.

Example:

DispatchQueue.main.async(execute: {
        nc.post(name: NSNotification.Name(rawValue: "outputMessage"),
                object: nil,
                userInfo: [
                    "message": self.outputMIDIMessage,
                    "midiSignalReceived": self.midiSignalReceived,
                    "midiTypeReceived": self.midiTypeReceived
        ])
})

I would also recommend the following:

Move let midi = AKMIDI() into an instance variable outside of the init() at the top of your Conductor class, rather than inside of it. It looks like you're attempting to create it after AudioKit.start().

I posted a sample project that demonstrates how you can change a UILabel's color whenever a MIDI note number has been received via AudioKit:

https://github.com/markjeschke/AKMidiReceiver

Conductor class:

import AudioKit

enum MidiEventType: String {
    case
        noteNumber          = "Note Number",
        continuousControl   = "Continuous Control",
        programChange       = "Program Change"
}

class Conductor: AKMIDIListener {

    // Globally accessible
    static let sharedInstance = Conductor()

    // Set the instance variables outside of the init()
    let midi = AKMIDI()

    var demoSampler = SamplerAudioFileLoader()
    var samplerMixer = AKMixer()
    var outputMIDIMessage = ""
    var midiSignalReceived = false
    var midiTypeReceived: MidiEventType = .noteNumber

    init() {

        // Session settings
        AKSettings.bufferLength = .medium
        AKSettings.defaultToSpeaker = true

        // Allow audio to play while the iOS device is muted.
        AKSettings.playbackWhileMuted = true

        do {
            try AKSettings.setSession(category: .playAndRecord, with: [.defaultToSpeaker, .allowBluetooth, .mixWithOthers])
        } catch {
            AKLog("Could not set session category.")
        }

        // File path options are:
        // "TX Brass"
        // "TX LoTine81z"
        // "TX Metalimba"
        // "TX Pluck Bass"
        demoSampler.loadEXS24Sample(filePath: "TX Brass")

        // If you wish to load a wav file, comment the `loadEXS24` method and uncomment this one:
//      demoSampler.loadWavSample(filePath: "Kick") // Load Kick wav file

        [demoSampler] >>> samplerMixer
        AudioKit.output = samplerMixer
        AudioKit.start()

        // MIDI Configure
        midi.createVirtualInputPort(98909, name: "AKMidiReceiver")
        midi.createVirtualOutputPort(97789, name: "AKMidiReceiver")
        midi.openInput()
        midi.openOutput()
        midi.addListener(self)

    }

    // Capture the MIDI Text within a DispatchQueue, so that it's on the main thread.
    // Otherwise, it won't display.
    func captureMIDIText() {
        let nc = NotificationCenter.default
        DispatchQueue.main.async(execute: {
            nc.post(name: NSNotification.Name(rawValue: "outputMessage"),
                    object: nil,
                    userInfo: [
                        "message": self.outputMIDIMessage,
                        "midiSignalReceived": self.midiSignalReceived,
                        "midiTypeReceived": self.midiTypeReceived
                ])
        })
    }

    // MARK: MIDI received

    // Note On Number + Velocity + MIDI Channel
    func receivedMIDINoteOn(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel) {
        midiTypeReceived = .noteNumber
        outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1)  noteOn: \(noteNumber)  velocity: \(velocity)"
        print(outputMIDIMessage)
        midiSignalReceived = true
        captureMIDIText()
        playNote(note: noteNumber, velocity: velocity, channel: channel)
    }

    // Note Off Number + Velocity + MIDI Channel
    func receivedMIDINoteOff(noteNumber: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel) {
        midiTypeReceived = .noteNumber
        outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1)  noteOff: \(noteNumber)  velocity: \(velocity)"
        print(outputMIDIMessage)
        midiSignalReceived = false
        captureMIDIText()
        stopNote(note: noteNumber, channel: channel)
    }

    // Controller Number + Value + MIDI Channel
    func receivedMIDIController(_ controller: MIDIByte, value: MIDIByte, channel: MIDIChannel) {
        // If the controller value reaches 127 or above, then trigger the `demoSampler` note.
        // If the controller value is less, then stop the note.
        // This creates an on/off type of "momentary" MIDI messaging.
        if value >= 127 {
            playNote(note: 30 + controller, velocity: 80, channel: channel)
        } else {
            stopNote(note: 30 + controller, channel: channel)
        }
        midiTypeReceived = .continuousControl
        outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1)  controller: \(controller)  value: \(value)"
        midiSignalReceived = true
        captureMIDIText()
    }

    // Program Change Number + MIDI Channel
    func receivedMIDIProgramChange(_ program: MIDIByte, channel: MIDIChannel) {
        // Trigger the `demoSampler` note and release it after half a second (0.5), since program changes don't have a note off release.
        triggerSamplerNote(program, channel: channel)
        midiTypeReceived = .programChange
        outputMIDIMessage = "\(midiTypeReceived.rawValue)\nChannel: \(channel+1)  programChange: \(program)"
        midiSignalReceived = true
        captureMIDIText()
    }

    func receivedMIDISetupChange() {
        print("midi setup change")
        print("midi.inputNames: \(midi.inputNames)")

        let listInputNames = midi.inputNames

        for inputNames in listInputNames {
            print("inputNames: \(inputNames)")
            midi.openInput(inputNames)
        }
    }

    func playNote(note: MIDINoteNumber, velocity: MIDIVelocity, channel: MIDIChannel) {
        demoSampler.play(noteNumber: note, velocity: velocity, channel: channel)
    }

    func stopNote(note: MIDINoteNumber, channel: MIDIChannel) {
        demoSampler.stop(noteNumber: note, channel: channel)
    }

    func triggerSamplerNote(_ program: MIDIByte, channel: MIDIChannel) {
        playNote(note: 60 + program, velocity: 80, channel: channel)
        let releaseNoteDelay = DispatchTime.now() + 0.5 // Change 0.5 to desired number of seconds
        DispatchQueue.main.asyncAfter(deadline: releaseNoteDelay) {
            self.stopNote(note: 60 + program, channel: channel)
            self.midiSignalReceived = false
        }
    }

}

ViewController with the UILabel:

import UIKit
import AudioKit

class ViewController: UIViewController {

    @IBOutlet weak var outputTextLabel: UILabel!

    var conductor = Conductor.sharedInstance
    var midiSignalReceived = false
    var midiTypeReceived: MidiEventType = .noteNumber

    override func viewDidLoad() {
        super.viewDidLoad()

        let nc = NotificationCenter.default
        nc.addObserver(forName:NSNotification.Name(rawValue: "outputMessage"), object:nil, queue:nil, using:catchNotification)
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        flashBackgroundColor()
        midiSignalReceived = false
        self.outputTextLabel.text = "Listening for MIDI events..."
    }

    @objc func catchNotification(notification:Notification) -> Void {
        guard
            let userInfo = notification.userInfo,
            let message  = userInfo["message"] as? String,
            let midiSignalReceived = userInfo["midiSignalReceived"] as? Bool,
            let midiTypeReceived = userInfo["midiTypeReceived"] as? MidiEventType else {
                print("No userInfo found in notification")
                return
        }
        DispatchQueue.main.async(execute: {
            self.outputTextLabel.text = message
            self.midiSignalReceived = midiSignalReceived
            self.midiTypeReceived = midiTypeReceived
            self.flashBackgroundColor()
        })
    }

    @objc func flashBackgroundColor() {
        if midiSignalReceived {
            self.outputTextLabel.backgroundColor = UIColor.green
            self.view.backgroundColor = UIColor.lightGray
            if midiTypeReceived != .noteNumber {
                self.perform(#selector(dismissFlashBackgroundColor), with: nil, afterDelay: 0.5)
            }
        } else {
            dismissFlashBackgroundColor()
        }
    }

    @objc func dismissFlashBackgroundColor() {
        UIView.animate(withDuration: 0.5) {
            self.outputTextLabel.backgroundColor = UIColor.clear
            self.view.backgroundColor = UIColor.white
            self.midiSignalReceived = false
            self.conductor.midiSignalReceived = false
        }
    }

    deinit {
        NotificationCenter.default.removeObserver(self,
                                                  name: NSNotification.Name(rawValue: "outputMessage"),
                                                  object: nil)
    }

}

SamplerAudioFileLoader.swift:

import AudioKit

class SamplerAudioFileLoader: AKMIDISampler {

    internal func loadWavSample(filePath: String) {
        do {
            try self.loadWav("Sounds/\(filePath)")
        } catch {
            print("Could not locate the Wav file.")
        }
    }

    internal func loadEXS24Sample(filePath: String) {
        do {
            try self.loadEXS24("Sounds/Sampler Instruments/\(filePath)")
        } catch {
            print("Could not locate the EXS24 file.")
        }
    }

}

I hope this helps. Please let me know if you have any questions about this.

Take care,
Mark

P.S. If you clone this AKMidiReceiver example, open the Workspace, and no scheme appears in the Xcode project, please follow these steps that were found here:

  1. Click on No Scheme
  2. Click on Manage Scheme
  3. Click on Autocreate Schemes Now
Mark Jeschke
  • 274
  • 1
  • 6
  • 1
    Your project is helpful in so many way! So first of all thanks for sharing it. I'm actually trying to do something different: I want to use the midi output of an AKSequencer to flash the UILabel, so there is no midi input from an external devise involved. I've implemented a sequencer in your project and noticed that even though the sampler is playing the receivedMIDINoteOn function is not called, so I guess maybe I'm in the wrong direction. Do you know if the receivedMIDINoteOn function could be called when the midi is coming from a sequencer? And if not, what else could I do? – Ben Spector Feb 02 '18 at 20:46
  • 1
    Apparently I was going in the wrong direction, should have been using the AKCallbackInstrument instead. And thanx for the DispatchQueue.main.async tip! It did come handy at the end, now I got flashing alright :) – Ben Spector Feb 02 '18 at 21:43
  • I'm glad I could help, and that you were able to solve flashing indicator via the AKCallbackInstrument. I would love to see your sequencer in action, if and when it becomes public. – Mark Jeschke Feb 02 '18 at 23:48
  • Sure thing. I’d love to. – Ben Spector Feb 03 '18 at 00:02
  • 1
    @BenSpector, I updated this GitHub example with Xcode 11.2.1 and AudioKit 4.9.2: https://github.com/markjeschke/AKMidiReceiver – Mark Jeschke Nov 10 '19 at 08:08
  • Thanks for sharing this @mark, at the moment I have some local [issue](https://stackoverflow.com/questions/58521803/audiokit-akmidiinstrument-crashes-on-init-after-upgrading-to-xcode-11) preventing me from running the latest AudioKit, will be solved soon hopefully so I can run your project. – Ben Spector Nov 19 '19 at 09:31
1

Depending on how you initialize flasher, you may have to run flasher.enableMIDI() optionally with names.

Aurelius Prochazka
  • 4,510
  • 2
  • 11
  • 34
  • Thanks for the answer, I added **flasher.enableMIDI()** line after **AudioKit.start()** but no change. not sure how to use the one with names, could really use some help there. also how can I get the AKMIDIListener to call receivedMIDINoteOn function? thank you so much. – Ben Spector Jan 30 '18 at 23:56
  • BTW, flasher **is** playing the midi that I send it, it just does not call the override function receivedMIDINoteOn. – Ben Spector Jan 31 '18 at 00:00