Allihoopa-macOS 1.2.1

Allihoopa-macOS 1.2.1

TestsTested
LangLanguage Obj-CObjective C
License MIT
ReleasedLast Release Jun 2017

Maintained by Ali Baaba.



  • By
  • mhallin

Allihoopa SDK for macOS


Objective-C/Swift SDK to interface with Allihoopa from macOS.

If you're working on an iOS app, check out our iOS SDK.

Installation

There are multiple ways of installing the Allihoopa SDK depending on how your setup looks like.

If you use CocoaPods, you can simply add this SDK to your Podfile:

target 'TargetName' do
  pod 'Allihoopa-macOS', '~> 1.2.1'
end

And then running the following command to download the dependency:

pod install

After this, you run carthage to build the framework, and then drag the resulting Allihoopa.framework from the Carthage/Build folder into your project.

Alternatively, you can simply download the latest built framework from the Releases tab.

Manual build

If you want, you can include the Allihoopa-macOS project as a sub-project to your application and build the the framework as a dependency. In this case, you will need to include AllihoopaCore-ObjC too as a dependency since this project share a lot of code and functionality with the iOS SDK.

If you use one of the methods described above, this dependency is managed automatically for you.

Configuration

You need to add a URL scheme to your app's Info.plist: ah-{APP_IDENTIFIER}, e.g. ah-figure if your application identifier is figure. You will receive your application identifier and API key when your register your application with Allihoopa. If you want to get on board, please send an email to [email protected].

Development setup

Look in the SDKExample folder for instructions how to work on this SDK.

API documentation

Generated API documentation for the latest release can be found on http://cocoadocs.org/docsets/Allihoopa-macOS.

Setting up the SDK

You need a class, e.g. your app delegate, to implement the AHAAllihoopaSDKDelegate to support the "open in" feature. See below how to implement this feature.

import Allihoopa

// In your UIApplicationDelegate implementation
func applicationDidFinishLaunching(_ aNotification: Notification) {
    AHAAllihoopaSDK.shared().setup([
        .applicationIdentifier: "your-application-identifier",
        .apiKey: "your-api-key",
        .delegate: self,
    ])
}
#import <Allihoopa/Allihoopa.h>

// In your UIApplicationDelegate implementation
- (void)applicationDidFinishLaunching:(NSNotification*)aNotification {
    [[AHAAllihoopaSDK sharedInstance] setupWithConfiguration:@{
        AHAConfigKeyApplicationIdentifier: @"your-application-identifier",
        AHAConfigKeyAPIKey: @"your-api-key",
        AHAConfigKeySDKDelegate: self,
    }];
}

setupWithConfiguration: must be called before any other API calls can be made. It will throw an exception if the SDK is improperly setup: if the credentials are missing or if you've not set up the URL scheme properly. For more information, see the "Steting up the SDK" heading above.

The configuration dictionary supports the following keys - names in Objective-C vs. Swift:

  • AHAConfigKeyApplicationIdentifier/.applicationIdentifier: required string containing the application identifier provided by Allihoopa.
  • AHAConfigKeyAPIKey/.apiKey: required string containing the app's API key.
  • AHAConfigKeySDKDelegate/.sdkDelegate: optional instance used to notify the application when a user tries to import a piece into this app. If provided, the instance must conform to the AHAAllihoopaSDKDelegate protocol.
  • AHAConfigKeyFacebookAppID/.facebookAppID: optional string containing the Facebook App ID of the application. This will enable secondary social sharing through the Accounts and Social frameworks built into iOS.
  • AHAConfigKeyRegisterURLEventHandler/.registerURLEventHandler: optional boolean indicating whether the SDK should register its own URL opener handler. For convenience, this defaults to true and thus must be set to false if you want your own URL event handler. If you set this to false, you must call -handleOpenURL:(NSURL*)/handleOpen(url) in your own event handler.

Dropping pieces

let piece = try! AHADropPieceData(
    defaultTitle: "Default title", // The default title of the piece
    lengthMicroseconds: 40000000, // Length of the piece, in microseconds
    tempo: nil,
    loopMarkers: nil,
    timeSignature: nil,
    basedOn: [])

let vc = AHAAllihoopaSDK.shared().dropViewController(forPiece: piece, delegate: self)

self.presentViewControllerAsSheet(vc)
extension ViewController : AHADropDelegate {
    // The drop view controller will ask your application for audio data.
    // You should perform work in the background and call the completion
    // handler on the main queue. If you already have the data available,
    // you can just call the completion handler directly.
    //
    // This method *must* call completion with a data bundle for the drop to
    // succeed. If it doesn't, an error screen will be shown.
    func renderMixStem(forPiece piece: AHADropPieceData, completion: @escaping (AHAAudioDataBundle?, Error?) -> Void) {
        DispatchQueue.global().async {
            // Render Wave data into an NSData object
            let bundle = AHAAudioDataBundle(format: .wave, data: data)
            DispatchQueue.main.async {
                completion(bundle, nil)
            }
        }
    }

    // You are responsible for dismissing the drop view controller in a
    // suitable way, matching how you presented it.
    func dropViewController(_ sender: NSViewController, willClose piece: AHADropPieceData, successfulDrop: Bool) {
        self.dismissViewController(sender)
    }
}

NSError* error;
AHADropPieceData* piece = [[AHADropPieceData alloc] initWithDefaultTitle:@"Default title"
                                                      lengthMicroseconds:40000000
                                                                   tempo:nil
                                                             loopMarkers:nil
                                                           timeSignature:nil
                                                         basedOnPieceIDs:@[]
                                                                   error:&error];
if (piece) {
    UIViewController* vc = [[AHAAllihoopaSDK sharedInstance] dropViewControllerForPiece:piece delegate:self];
    [self presentViewControllerAsSheet:vc];
}
@implementation ViewController

- (void)renderMixStemForPiece:(AHADropPieceData *)piece
                   completion:(void (^)(AHAAudioDataBundle * _Nullable, NSError * _Nullable))completion {
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        // Render wave data into an NSData object
        AHAAudioDataBundle* bundle = [[AHAAudioDataBundle alloc] initWithFormat:AHAAudioFormatWave data:data];

        dispatch_async(dispatch_get_main_queue(), ^{
            completion(bundle, nil);
        });
    });
}

- (void)dropViewController:(NSViewController*)sender
         forPieceWillClose:(AHADropPieceData*)piece
       afterSuccessfulDrop:(BOOL)successfulDrop {
    [self dismissViewController:sender];
}

@end

dropViewController creates a view controller responsible for dropping the piece you supplied with the help of the delegate object. If the user is not logged in, the SDK will open the login dialog in the user's default browser and proceed after the user logs in. When the user wants to close the drop view, the delegate will be notified and must dismiss the view controller.

A piece can contain different kinds of metadata. The above example shows off the minimum amount of data we require: a default title, the length of the piece audio data, and a delegate method that renders the audio into a known format.

The AHADropPieceData performs basic validation on the inputs: it will return a NSError containing information on what went wrong. Errors can include things like the loop markers being inverted or the length outside of reasonable limits. These are usually programmer errors - not runtime errors that can be handled in a meaningful way.

If your application knows about it, it can supply a lot more metadata to AHADropPieceData more information, as well as implementing more methods on AHADropDelegate than shown above. Here's a complete example showing all data you can set:

let piece = try! AHADropPieceData(
    defaultTitle: "Default title",
    lengthMicroseconds: 100000000,
    // The fixed tempo in BPM. Allowed range: 1 - 999.999 BPM
    tempo: AHAFixedTempo(fixedTempo: 128), 

    // If the piece is a loop, you can provide the loop markers.
    loopMarkers: AHALoopMarkers(startMicroseconds: 0, endMicroseconds: 500000),

    // If the time signature is available and fixed, you can provide a time
    // signature object.
    //
    // The upper numeral can range from 1 to 16, incnlusive. The lower numeral
    // must be one of 2, 4, 8, and 16.
    timeSignature: AHATimeSignature(upper: 8, lower: 4),

    // If the piece is based on other pieces, provide a list of the IDs of those
    // pieces here.
    basedOn: [],

    // If the piece has a known tonality, provide the scale and root here.
    // Other values are AHATonality.unknown() (default if omitted), and
    // AHATonality.atonal() for pieces that contain audio that doesn't have a
    // tonality, e.g. drum loops.
    tonality: AHATonality(tonalScale: AHAGetMajorScale(4), root: 4)
)
extension ViewController : AHADropDelegate {
    // The "mix stem" is the audio data that should be used to place the piece
    // on a timeline. Call the completion handler with a data bundle instance
    // on the main queue when data is available.
    //
    // The mix stem is mandatory.
    func renderMixStem(forPiece piece: AHADropPieceData, completion: @escaping (AHAAudioDataBundle?, Error?) -> Void) {
    }

    // You can supply a default cover image that the user can upload or change.
    // Call the completion handler with an image of size 640x640 px, or nil.
    func renderCoverImage(forPiece piece: AHADropPieceData, completion: @escaping (UIImage?) -> Void) {
        completion(nil)
    }

    // If the audio to be placed on the timeline is different from what users
    // should listen to, use this delegate method to provide a "preview"
    // audio bundle.
    //
    // For example, if you're providing a short loop you can supply only the
    // loop data in a lossless format as the mix stem, and then a longer track
    // containing a few loops with fade in/out in a lossy format in the
    // preview audio.
    //
    // The preview audio is what's going to be played on the website.
    //
    // If no preview audio is provided, the mix stem will be used instead. This
    // replacement is done server-side, the mix stem data will only be uploaded
    // once from the client.
    func renderPreviewAudio(forPiece piece: AHADropPieceData, completion: @escaping (AHAAudioDataBundle?, Error?) -> Void) {
    }
    
    // You can include an application specific attachment, which can be of 
    // arbitrary file type referred by a registered MIME type
    func renderAttachment(forPiece piece: AHADropPieceData, completion: @escaping (AHAAttachmentBundle?, Error?) -> Void) {
    }
}

For more information on tonality and other fields, please see the iOS SDK documentation.

Importing pieces

When a user picks "Open in [your app]" on the website, the SDK will pick up the request, fetch piece metadata, and call the openPieceFromAllihoopa:error: with the piece the user wanted to open. The AHAPiece instance has methods for downloading the audio data in the specified format, which you can use to import the audio into the current document, or save it for later. AHAPiece also contain metadata, similar to AHADropPiece.

func openPiece(fromAllihoopa piece: AHAPiece?, error: Error?) {
    if let piece = piece {
        // The user wanted to open a piece

        // Download the mix stem audio in Ogg format. You can also use .wave
        piece.downloadMixStem(format: .oggVorbis, completion: { (data, error) in
            if let data = data {
                // Data downloaded successfully
            }
        })
    }
    else {
        // Handle the error
        //
        // This should not *usually* happen, but if the piece was removed after
        // opening, or if there were some connection issues we can end up here
    }
}
- (void)openPieceFromAllihoopa:(AHAPiece*)piece error:(NSError*)error {
    if (piece != nil) {
        // The user wanted to open a piece
        [piece downloadMixStemWithFormat:AHAAudioFormatOggVorbis completion:^(NSData* data, NSError* error) {
            if (data != nil) {
                // Data downloaded successfully
            }
        }];
    }
    else {
        // Handle the error
    }
}

Attachments

You can also include an application specific attachment when dropping a piece and similarly download that attachment when importing a piece. An attachment can be of arbitrary file type and is referred by a MIME type string. The MIME type must be registered to the Allihoopa service in advance, i.e. if your application needs to use attachments, register them by sending an email to [email protected].

Application specific attachment can be included in the drop delegate object.

extension ViewController : AHADropDelegate {
    // ...
    
    // You can include an application specific attachment along other asset files
    func renderAttachment(forPiece piece: AHADropPieceData, completion: @escaping (AHAAttachmentBundle?, Error?) -> Void) {
        DispatchQueue.global().async {
            do {
                let data = try Data(contentsOf: Bundle.main.url(forResource: "figure_song", withExtension: "xml")!)
                let bundle = AHAAttachmentBundle(mimeType: "application/figure", data: data)
                
                DispatchQueue.main.async {
                    completion(bundle, nil)
                }
            }
            catch let error {
                DispatchQueue.main.async {
                    completion(nil, error)
                }
            }
        }
    }
}

And when importing a piece, you can download the attachment using the same MIME type.

func openPiece(fromAllihoopa piece: AHAPiece?, error: Error?) {
    if let piece = piece {
        // ...
    
        // Download an attachment based on specific MIME type
        piece.downloadAttachment(mimeType: "application/figure", completion: { (data, error) in
            if let data = data {
                // Data downloaded successfully
            }
        })
    }
}