InterAppAudioHost/InterAppAudioHost/UI/CAUIKeyboardView.mm

/*
     File: CAUIKeyboardView.mm
 Abstract: 
  Version: 1.1.2
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 
 */
 
#import "CAUIKeyboardView.h"
#import "AppDelegate.h"
 
#import <UIKit/UIGestureRecognizer.h>
 
#define kWhiteKeyWidthRatio     0.268f
#define kBlackKeyHeightRatio    0.634f
#define kBlackKeyWidthRatio     0.159f
#define kDbKeyOffsetRatio       0.201f
#define kEbKeyOffsetRatio       0.232f
#define kGbKeyOffsetRatio       0.146f
#define kAbKeyOffsetRatio       0.195f
#define kBbKeyOffsetRatio       0.234f
 
#pragma mark - Utility functions
typedef enum NoteIdentifier {
    C = 0, Db= 1, D = 2, Eb= 3, E = 4, F = 5, Gb= 6, G = 7, Ab= 8, A = 9, Bb= 10, B = 11
} NoteIdentifier;
 
BOOL IsWhiteKey(NSInteger note) {
    NSInteger value = note % 12;
    
    switch (value) {
        case 0: case 2: case 4: case 5: case 7: case 9: case 11:
            return YES;
        default:
            return NO;
    }
}
 
BOOL IsNoteSharp(NSInteger note) {
    NSInteger value = note % 12;
    
    switch (value) {
        case 0: case 2: case 4: case 5: case 7: case 9: case 11:
            return false;
        case 1: case 3: case 6: case 8: case 10:
            return true;
    }
    return false;
}
 
NoteIdentifier IdentifierForNote(NSInteger note) {
    NSInteger value = note % 12;
    return (NoteIdentifier)value;
}
 
NSInteger NumWhiteKeysBetweenNotes(NSInteger startNote, NSInteger endNote) {
    // figure out how many octaves between notes
    if (startNote == endNote)
        return 0;
    
    NSInteger keys = 0;
    NSInteger octaves = ((endNote - startNote) / 12);
    if (octaves > 0)
        keys = octaves * 7;
    
    if (IsWhiteKey(startNote))
        keys--;
    
    NSInteger extraNotes = (endNote - startNote) % 12;
    for (NSInteger indexKey = endNote - extraNotes; indexKey < endNote; indexKey++) {
        if (IsWhiteKey(indexKey))
            keys++;
    }
    
    return keys;
}
    
BOOL CGPointInRect(CGPoint aPoint, CGRect aRect) {
    return aPoint.x >= aRect.origin.x &&
    aPoint.x < (aRect.origin.x + aRect.size.width) &&
    aPoint.y >= aRect.origin.y &&
    aPoint.y < (aRect.origin.y + aRect.size.height);
}
 
#pragma mark - CAUIKeyboardView implementation
@implementation CAUIKeyboardView
 
@synthesize whiteKeyWidth, whiteKeyHeight, blackKeyWidth, blackKeyHeight;
@synthesize noteDownColor;
@synthesize engine = _engine;
 
#pragma mark Initialization
- (void) initialize {
    _engine = NULL;
    whiteKeyHeight  = floorf(self.frame.size.height);
    whiteKeyWidth   = roundf(whiteKeyHeight * kWhiteKeyWidthRatio);
    blackKeyHeight  = roundf(whiteKeyHeight * kBlackKeyHeightRatio);
    blackKeyWidth   = roundf(whiteKeyHeight * kBlackKeyWidthRatio);
    
    dbKeyOffset     = roundf(whiteKeyHeight * kDbKeyOffsetRatio);
    ebKeyOffset     = roundf(whiteKeyHeight * kEbKeyOffsetRatio);
    gbKeyOffset     = roundf(whiteKeyHeight * kGbKeyOffsetRatio);
    abKeyOffset     = roundf(whiteKeyHeight * kAbKeyOffsetRatio);
    bbKeyOffset     = roundf(whiteKeyHeight * kBbKeyOffsetRatio);
    
    self.noteDownColor = [[UIColor redColor] colorWithAlphaComponent: .7];
    notesDown = [NSMutableSet setWithCapacity: 0];
}
 
- (id) initWithCoder:(NSCoder *) aDecoder {
    self = [super initWithCoder: aDecoder];
    if (self) 
        [self initialize];
 
    return self;
}
 
#pragma mark Utility methods
- (NSInteger) offsetForBlackKey:(NSInteger) note {
    NoteIdentifier identifier = IdentifierForNote(note);
    switch (identifier) {
        case Db:
            return dbKeyOffset;
        case Eb:
            return ebKeyOffset;
        case Gb:
            return gbKeyOffset;
        case Ab:
            return abKeyOffset;
        case Bb:
            return bbKeyOffset;
        default:
            return 0;
    }
}
 
- (CGRect) noteRectForNote:(NSInteger) note inRect:(CGRect) rect {
    // starting with the starting note, calculate the rect that the note to be drawn is in
    // if the starting note is a sharp, we need to get the previous white key
    // if the starting note is a white key, we can calculate the location of the new note
    
    CGRect firstNoteRect = CGRectZero;
    CGRect firstWhiteNoteRect = CGRectZero;
    
    BOOL startingNoteSharp = IsNoteSharp(displayKeyStart);
    firstWhiteNoteRect = CGRectMake(rect.origin.x, rect.origin.y, self.whiteKeyWidth, self.whiteKeyHeight);
    
    if (startingNoteSharp) {
        firstNoteRect = CGRectMake(rect.origin.x, rect.origin.y, self.blackKeyWidth, self.blackKeyHeight);
        
        // we need to calculate the starting location of the previous white key
        NSInteger offset = [self offsetForBlackKey: note];
        firstWhiteNoteRect.origin.x -= offset;
    }
    else
        firstNoteRect = firstWhiteNoteRect;
    
    if (displayKeyStart == note)
        return firstNoteRect;
    
    // calculate distance in white keys between note and displayKeyStart
    NSInteger whiteInterval = NumWhiteKeysBetweenNotes(displayKeyStart, note) * self.whiteKeyWidth;
    NSInteger lastWhiteStart = firstWhiteNoteRect.origin.x + whiteInterval;
    
    BOOL endingNoteSharp = IsNoteSharp(note);
    if (!endingNoteSharp)
        return CGRectMake(lastWhiteStart+self.whiteKeyWidth, rect.origin.y, self.whiteKeyWidth, self.whiteKeyHeight);
    else
        return CGRectMake(lastWhiteStart+ [self offsetForBlackKey:note], rect.origin.y, self.blackKeyWidth, self.blackKeyHeight);
}
 
- (CGFloat) leftEdgeOfNote:(NSInteger) note {
    CGRect rect = [self noteRectForNote: note inRect: self.bounds];
    return rect.origin.x;
}
 
- (CGFloat) rightEdgeOfNote:(NSInteger) note {
    CGRect rect = [self noteRectForNote: note inRect: self.bounds];
    return rect.origin.x + rect.size.width;
}
 
- (NSInteger) displayKeyStart {
    return displayKeyStart;
}
 
- (void) setDisplayKeyStart:(NSInteger) note {
    if (note >= 0 && note < 128 && note != displayKeyStart) {
        displayKeyStart = note;
        
        [self prepareBackground];
        [self setNeedsDisplay];
    }
}
 
- (NSInteger) noteAtPoint: (CGPoint) point {
    // check to see which white key we are in
    CGRect firstWhiteNoteRect = CGRectZero;
    
    BOOL startingNoteSharp = IsNoteSharp(displayKeyStart);
    firstWhiteNoteRect = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, whiteKeyWidth, whiteKeyHeight);
    
    if (startingNoteSharp) {
        // we need to calculate the starting location of the previous white key
        NSInteger offset = [self offsetForBlackKey: displayKeyStart];
        firstWhiteNoteRect.origin.x -= offset;
    }
    
    NSInteger whiteNoteNumber = (point.x - firstWhiteNoteRect.origin.x)/whiteKeyWidth;
    NSInteger noteNumber = displayKeyStart + (((float)whiteNoteNumber/7) * 12);
    if (IsNoteSharp(noteNumber))
        noteNumber++;
    
    if (point.y <= blackKeyHeight) {    // we could be on a black key, so we need to check the black keys surrounding the white key
        // if we are not on a c or and f, check the previous flat
        BOOL foundFlat = NO;
        NoteIdentifier identifier = IdentifierForNote(noteNumber);
        if (identifier != C && identifier != F) {
            if (CGPointInRect(point, [self noteRectForNote:noteNumber-1 inRect:self.bounds])) {
                noteNumber--;
                foundFlat = YES;
            }
        }
        // check the next flat
        if (!foundFlat && CGPointInRect(point, [self noteRectForNote:noteNumber+1 inRect:self.bounds]))
            noteNumber++;
    }
    
    return noteNumber;
}
 
#pragma mark Drawing methods
- (UIBezierPath *) bezierPathForNote: (NSInteger) note {
    if (note < 0 || note > 127)
        return nil;
    
    if (!IsWhiteKey(note))
        return [UIBezierPath bezierPathWithRect: [self noteRectForNote: note inRect: self.bounds]];
    else {
        UIBezierPath *path = [UIBezierPath bezierPath];
        CGFloat edge;
        if (note == 0 || IsWhiteKey(note-1)) { // special case the very first note
            edge = [self leftEdgeOfNote: note] + 1;
            
            // if the previous key is a white key, then the left edge is the same as the edge of the note
            [path moveToPoint: CGPointMake(edge, whiteKeyHeight-1)];
            [path addLineToPoint: CGPointMake(edge + whiteKeyWidth-1, whiteKeyHeight-1)];
            [path addLineToPoint: CGPointMake(edge + whiteKeyWidth-1, blackKeyHeight)];
            
            edge = [self leftEdgeOfNote: note+1];
            [path addLineToPoint: CGPointMake(edge, blackKeyHeight)];
            [path addLineToPoint: CGPointMake(edge, 1)];
            edge = [self leftEdgeOfNote: note] + 1;
        } else {
            edge = [self rightEdgeOfNote: note-1];
            [path moveToPoint: CGPointMake(edge, 1)];
            [path addLineToPoint: CGPointMake(edge, blackKeyHeight)];
            
            edge = [self leftEdgeOfNote: note];
            [path addLineToPoint: CGPointMake(edge, blackKeyHeight)];
            [path addLineToPoint: CGPointMake(edge, whiteKeyHeight-1)];
            [path addLineToPoint: CGPointMake(edge + whiteKeyWidth-1, whiteKeyHeight-1)];
            
            // if the next key is a white key, then the right edge is the same as the edge of the note
            if (note == 127 || IsWhiteKey(note+1)) {
                edge = [self rightEdgeOfNote: note]-1;
                [path addLineToPoint: CGPointMake(edge, 1)];
            } else {
                [path addLineToPoint: CGPointMake(edge+ whiteKeyWidth-1, blackKeyHeight)];
                
                edge = [self leftEdgeOfNote: note+1];
                [path addLineToPoint: CGPointMake(edge, blackKeyHeight)];
            }
        }
        [path addLineToPoint: CGPointMake(edge, 1)];
        [path closePath];
        
        return path;
    }
    
    return nil;
}
 
- (void) drawRect:(CGRect) rect {
    [super drawRect: rect];
    
    CGRect myFrame = self.bounds;
    
    // prepares the image cache of the keyboard
    if (!imageCache)
        [self prepareBackground];
    
    [imageCache drawInRect: myFrame];
    
    [noteDownColor set];
    
    for (NSNumber *noteDown in notesDown) {
        NSInteger note = [noteDown integerValue];
        if (IsNoteSharp(note))
            UIRectFill([self noteRectForNote: note inRect: self.bounds]);
        else {
            UIBezierPath *path = [self bezierPathForNote: note];
            if (path)
                [path fill];
        }
    }
}
 
/* We draw the keyboard into a cache for performance reasons so that our draw rect only has to draw the notes that are currently pressed */
- (void) prepareBackground {
    UIGraphicsBeginImageContext(self.bounds.size);
    
    CGContextRef ctx = UIGraphicsGetCurrentContext();
    
    displayKeyEnd = 127;
    CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
    CGContextFillRect(ctx, self.bounds);
    
    CGContextSetFillColorWithColor(ctx, [UIColor blackColor].CGColor);
    CGContextStrokeRect(ctx, self.bounds);
    
    CGRect noteRect = CGRectZero;
    NSInteger keyIndex, startKey = displayKeyStart;
    if (startKey > 0 == !IsWhiteKey(startKey-1))
        startKey--;
    
    for (keyIndex = startKey; keyIndex < 128; keyIndex++) {
        if ([notesDown containsObject: @(keyIndex)])
            continue;
        
        noteRect = [self noteRectForNote: keyIndex inRect: self.bounds];
        
        if (noteRect.origin.x > self.frame.origin.x + self.frame.size.width) {
            displayKeyEnd = keyIndex-1;
            break;
        }
        if (IsNoteSharp(keyIndex))
            CGContextFillRect(ctx, noteRect);
        else {
            CGContextStrokeRect(ctx, noteRect);
            
            // draw the note label if this is a C
            NoteIdentifier identifier = IdentifierForNote(keyIndex);
            if (identifier == C) {
                if (!labelAttributes) {
                    UIFont *font = [UIFont systemFontOfSize: 12];
                    NSMutableParagraphStyle *paraStyle = [NSMutableParagraphStyle new];
                    paraStyle.alignment = NSTextAlignmentCenter;
                    
                    labelAttributes = [[NSDictionary alloc] initWithObjectsAndKeys: font, NSFontAttributeName,
                                       paraStyle, NSParagraphStyleAttributeName,
                                       [UIColor grayColor], NSForegroundColorAttributeName, nil];
                }
                NSAttributedString *label = [[NSAttributedString alloc]initWithString:[NSString stringWithFormat: @"C%d", (int)(keyIndex / 12) - 1] attributes: labelAttributes];
                [label drawInRect: CGRectMake(noteRect.origin.x + 1, noteRect.origin.y + self.whiteKeyHeight - 16, noteRect.size.width -2, 12)];
            }
            [[UIColor blackColor] set];
        }
    }
    
    if (noteRect.origin.x + noteRect.size.width < self.bounds.size.width) {
        CGContextSetFillColorWithColor(ctx, [UIColor darkGrayColor].CGColor);
        CGFloat rightEdge = noteRect.origin.x + noteRect.size.width;
        CGContextFillRect(ctx, CGRectMake(rightEdge, 0, self.bounds.size.width - rightEdge, self.bounds.size.height));
    }
    
    imageCache = UIGraphicsGetImageFromCurrentImageContext();
    
    UIGraphicsEndImageContext();
    [self setNeedsDisplay];
}
 
#pragma mark Event handling
- (OSStatus) playNote:(UInt32) note velocity: (UInt32) velocity {
    const UInt32 noteOnCommand = kMidiMessage_NoteOn << 4 | 0;
    OSStatus result =  -1;
    if (self.engine){
        AudioUnit au = [self.engine getAudioUnitInstrument];
        result = MusicDeviceMIDIEvent(au, noteOnCommand, note, 100, 0);
    }
    return result;
}
 
- (OSStatus) stopNote:(UInt32) note {
    const UInt32 noteOffCommand = kMidiMessage_NoteOff << 4 | 0;
    OSStatus result =  -1;
    if (self.engine)
        result = MusicDeviceMIDIEvent([self.engine getAudioUnitInstrument], noteOffCommand, note, 100, 0);
    
    return result;
}
 
- (OSStatus) stopAllNotes {
    const UInt32 allNotesOffCommand = kMidiController_AllNotesOff << 4 | 0;
    OSStatus result =  -1;
    if (self.engine)
        result = MusicDeviceMIDIEvent([self.engine getAudioUnitInstrument], allNotesOffCommand, 0, 0, 0);
    
    return result;
}
 
- (void) touchesBegan:(NSSet *) touches withEvent:(UIEvent *) event {
    
    for (UITouch *touch in touches) {
        CGPoint loc = [touch locationInView: self];
        NSNumber * note = @([self noteAtPoint: loc]);
        
        if (![notesDown containsObject: note])
            [notesDown addObject: note];
        
        CGRect noteRect = [self noteRectForNote:[note intValue] inRect:self.bounds];
        
        // generate a 1 to 127 value based on the vertical position of the tap
        NSInteger margin = 6;
        Float32 velocity = 127;
        
        CGFloat adjustedPoint = loc.y - noteRect.origin.y;
        if (adjustedPoint > noteRect.size.height - margin)
            velocity = 127;
        else if (adjustedPoint > margin) {
            velocity = (adjustedPoint - margin) / (noteRect.size.height - margin * 2);
            velocity = velocity * 127;
        }
        [self playNote: [note unsignedIntValue] velocity: velocity];
    }
    [self setNeedsDisplay];
}
 
- (void) touchesMoved:(NSSet *) touches withEvent:(UIEvent *) event {
    
    for (UITouch *touch in touches) {
        CGPoint previousLoc = [touch previousLocationInView: self];
        CGPoint currentLoc  = [touch locationInView: self];
        
        BOOL previousInView = CGPointInRect(previousLoc, self.bounds);
        BOOL currentInView  = CGPointInRect(currentLoc, self.bounds);
        
        NSInteger previousNote, currentNote;
        
        previousNote = previousInView ? [self noteAtPoint: previousLoc] : -1;
        currentNote  = currentInView  ? [self noteAtPoint: currentLoc]  : -1;
        
        // if the previous and current note match, don't do anything
        // otherwise, if the note has changed, end previous note, and start new note
        if (previousNote != currentNote) {
            if (previousNote > -1) {
                
                [notesDown removeObject: @(previousNote)];
                // end the note
                OSStatus result = [self stopNote: (UInt32)previousNote];
                if (result != noErr)
                    NSLog(@"Error stopping note %ld: %d", (long)previousNote, (int)result);
            }
            
            if (currentNote >= -1) {
                if (![notesDown containsObject: @(currentNote)])
                    [notesDown addObject: @(currentNote)];
                
                // start the new note
                OSStatus result = [self playNote: (UInt32)currentNote velocity: 100];
                if (result != noErr)
                    NSLog(@"Error playing note %ld: %d", (long)currentNote, (int)result);
            }
        }
    }
    [self setNeedsDisplay];
}
 
- (void) touchesEnded:(NSSet *) touches withEvent:(UIEvent *) event {
    for (UITouch *touch in touches) {
        CGPoint currentLoc  = [touch locationInView: self];
        CGPoint previousLoc = [touch previousLocationInView:self];
        
        NSNumber *note = @([self noteAtPoint: currentLoc]);
        NSNumber *prevNote = @([self noteAtPoint: previousLoc]);
        
        if ([notesDown containsObject: note]) {
            [notesDown removeObject: note];
        } else  if ([notesDown containsObject: prevNote]){
            [notesDown removeObject: prevNote];
            note = prevNote;
        }
        
        // end the note
        OSStatus result = [self stopNote: [note unsignedIntValue]];
        if (result != noErr)
            NSLog(@"Error stopping note %lu: %d", [note unsignedLongValue], (int)result);
    }
    [self setNeedsDisplay];
}
 
- (void) touchesCancelled:(NSSet *) touches withEvent:(UIEvent *) event {
    [notesDown removeAllObjects];
    [self setNeedsDisplay];
    
    // end all playing notes
    OSStatus result = [self stopAllNotes];
    if (result != noErr)
        NSLog(@"Error stopping all notes: %d", (int)result);
}
 
@end