Advertise Bluetooth MIDI on iOS manually, without CABTMIDILocalPeripheralViewController
Asked Answered
W

4

10

I've just discovered CABTMIDILocalPeripheralViewController for iOS which handles user settings for enabling Bluetooth MIDI discoverability. This is fine and good but in order to integrate bluetooth into the rest of my app's Network MIDI connectivity it would be good to be able to handle the enabling directly from my app's code rather than rely on this opaque VC. Does anyone know whether that is possible?

Warnock answered 5/10, 2015 at 20:35 Comment(4)
Did you ever figure out how to use these things? I'm using CABTMIDICentralViewController and I really want to check which MIDI device the user connected in my code (so I can connect to the correct MIDI device automatically). Is there any way to do that, or should we file some bugs with Apple?Outherod
I'm using the Peripheral version but based on that I'd guess that once it is connected it becomes a MIDIEndpointRef so you can query for it with the CoreMIDI api. It should have the name like "Hari Karam's iPhone Bluetooth Connection". You can't tweak the internals of the VC at all. And there's no other CABTMIDI... api as far as I can seeWarnock
Ok. I pieced together a terrible hack solution which I will post below.Outherod
Also, I filed a bug with Apple about this, hopefully they can make a nice delegate API to tell us which devices were connected. You may want to do the same so we can get some visibilityOutherod
W
2

There's no public API to manage this functionality. Upon investigation with Instruments, it appears that the switch causes an instantiation of CBPeripheralManager. I presume it sets up the device as a Bluetooth peripheral and manually channels the data to and from a MIDIEndpointRef which is has also created.

In other words, there's no one-liner solution. If I go down this route further I'll post the code unless someone else wants to have a go...

UPDATE

The magic code...

- (instancetype)init
{
    self = [super init];
    if (self) {
        _peripheralManager = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];
    }
    return self;
}

//---------------------------------------------------------------------

- (void)peripheralManagerDidUpdateState:(CBPeripheralManager *)peripheral
{
    if (peripheral.state != CBPeripheralManagerStatePoweredOn) {
        return;
    }

    info(@"_peripheralManager powered on.");

//    CBMutableCharacteristic *tx = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:RBL_TX_UUID] properties:CBCharacteristicPropertyWriteWithoutResponse value:nil permissions:CBAttributePermissionsWriteable];
//    
    rx = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:@"7772E5DB-3868-4112-A1A9-F2669D106BF3"] properties:CBCharacteristicPropertyRead|CBCharacteristicPropertyWriteWithoutResponse|CBCharacteristicPropertyNotify value:nil permissions:CBAttributePermissionsReadable|CBAttributePermissionsWriteable];

    CBMutableService *s = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:@"03B80E5A-EDE8-4B33-A751-6CE34EC4C700"] primary:YES];
    s.characteristics = @[rx];

    [_peripheralManager addService:s];

    NSDictionary *advertisingData = @{CBAdvertisementDataLocalNameKey : BLE_NAME, CBAdvertisementDataServiceUUIDsKey : @[[CBUUID UUIDWithString:@"03B80E5A-EDE8-4B33-A751-6CE34EC4C700"]]};
    [_peripheralManager startAdvertising:advertisingData];
}

It's those IDs that define a MIDI peripheral. More info:

Apple used to have a doc at

...which is no longer. I'd love to find an old copy as it turns out the receiver code (emulating CABTMIDICentralViewController) is even harder to crack...

Warnock answered 21/10, 2015 at 11:25 Comment(0)
V
2

This is swift 5 version. this works fine.

import SpriteKit
import CoreBluetooth

class GameScene: SKScene {
    var manager: CBPeripheralManager!
    var service: CBMutableService!
    // MIDI Service UUID
    let serviceID = CBUUID(string: "03B80E5A-EDE8-4B33-A751-6CE34EC4C700")
    // MIDI I/O Characteristic UUID
    let characteristicID = CBUUID(string: "7772E5DB-3868-4112-A1A9-F2669D106BF3")

    override func didMove(to view: SKView) {
        super.didMove(to: view)
        self.manager = CBPeripheralManager(delegate : self, queue : nil, options: nil)
    }
}

extension GameScene: CBPeripheralManagerDelegate {
    // Notify the peripheral state
    func peripheralManagerDidUpdateState(_ peripheral: CBPeripheralManager) {
        guard peripheral.state == .poweredOn else {
            print("error: \(peripheral.state)")
            return
        }
        
        print("bluetooth pwoer on")
        // Add service and characteristic
        self.service = CBMutableService(type: self.serviceID,
                                       primary: true)

        let properties: CBCharacteristicProperties = [.notify, .read, .writeWithoutResponse]
        let permissions: CBAttributePermissions = [.readable, .writeable]
        let characteristic = CBMutableCharacteristic(type: self.characteristicID,
                                                     properties: properties,
                                                     value: nil, permissions: permissions)
        
        self.service.characteristics = [characteristic]
        self.manager.add(self.service)
    }
    // Notify add the service
    func peripheralManager(_ peripheral: CBPeripheralManager, didAdd service: CBService, error: Error?) {
        guard (error == nil) else {
            print("Add service failed")
            return
        }
        print("Add service succeeded")
        
        // Start advertising
        let advertisementData = [CBAdvertisementDataLocalNameKey: "Penguin Drums",
                                 CBAdvertisementDataServiceUUIDsKey: [self.serviceID]] as [String : Any]
        manager.startAdvertising(advertisementData)
        
        print("Service starts advertising!")
    }
}
Virchow answered 12/9, 2020 at 13:49 Comment(0)
O
1

So I pieced together a pretty hacky solution for discovering which MIDI device the user clicked on once inside the CABTMIDICentralViewController. I'm not sure if this is a good idea—if Apple changes the internals of the controller it won't work anymore. Also I'm not sure if it's "legal" concerning App Store guidelines. Anyone know more info on this?

DPBleMidiDeviceManager.h:

#import <CoreAudioKit/CoreAudioKit.h>

@protocol MidiDeviceConnectedDelegate <NSObject>

-(void) onMidiDeviceConnected: (NSString*) deviceName;

@end


@interface DPBleMidiDeviceManager : CABTMIDICentralViewController

@property (weak, nonatomic) id<MidiDeviceConnectedDelegate> midiDeviceDelegate;

@end

DPBleMidiDeviceManager.m:

#import "DPBleMidiDeviceManager.h"

@implementation DPBleMidiDeviceManager


- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    NSLog(@"midi device selected %@", indexPath);

    [super tableView:tableView didSelectRowAtIndexPath:indexPath];

    // TODO: this is very bad. apple may change their internal API and this will break.
    UITableViewCell* cell = [tableView cellForRowAtIndexPath:indexPath];
    if ([cell respondsToSelector:@selector(deviceNameLabel)]) {
        UILabel* deviceLabel = [cell performSelector:@selector(deviceNameLabel)];

        NSLog(@"midi device named %@", deviceLabel.text);

        // must wait a couple seconds for it to actually connect.
        [self performSelector:@selector(sendMidiDeviceConnected:) withObject:deviceLabel.text afterDelay: 3];
    }
}


- (void) sendMidiDeviceConnected: (NSString*) deviceName
{
    [self.midiDeviceDelegate onMidiDeviceConnected:deviceName];
}
@end

Then, in your parent view controller, you can get the result from the delegate and look for a new MIDI device matching that name:

...
    DPBleMidiDeviceManager *controller = [DPBleMidiDeviceManager new];
    controller.midiDeviceDelegate = self;
    // now present the VC as usual
...


-(void) onMidiDeviceConnected: (NSString*) deviceName
{
    [self connectMidiDevice: deviceName];
}


/**
 Connects to a MIDI source with the given name,
 and interprets all notes from that source as notes;

 */
- (void) connectMidiDevice: (NSString*) deviceName
{
    NSLog(@"Connecting to MIDI device: %@", deviceName);

    PGMidi* midi = [[PGMidi alloc] init];

    if (midi != NULL) {
        NSArray* sources = midi.sources;
        for (PGMidiSource* src in sources) {
            NSLog(@"Found midi source: %@", src.name);

            if ([src.name containsString: deviceName]) {

                NSLog(@"Connecting to midi source: %@", src.name);
                [src addDelegate:self];
            }
        }
    }

}

The only other alternative I can think of is to scan for MIDI devices before showing the controller, save the list of devices, then open the controller. When it closes, scan MIDI devices again, and diff that new list with the old one. Any new MIDI devices that show up would be the ones the user selected. Not sure why Apple didn't make this easier for us...

Outherod answered 31/10, 2015 at 23:18 Comment(0)
B
0

I think you could be looking for this : CABTMIDICentralViewController more info on this page : https://developer.apple.com/library/ios/qa/qa1831/_index.html basicaly this helps you scan and connect to devices through your app. I'm not sure if you only want to be discovered or also to be the one that scans. I hope this helps

Byronbyrum answered 20/10, 2015 at 12:36 Comment(3)
That's where I learned about CABTMIDILocalPeripheralViewController, which is definitely the one to use in my case. Either way, this page doesn't describe how to accomplish the task without using these VCs which is what I want to know.Warnock
It's actually pretty limited to these, maybe due to security mesures.Byronbyrum
There are apps (e.g. Apollo Bluetooth) which accomplish this. I suspect they do it manually w/o the CABT... VCsWarnock

© 2022 - 2024 — McMap. All rights reserved.