Hi Matt,

Here's my opinion... :)

On Dec 31, 2008, at 05:35, Matt Rajca wrote:

I am trying to figure out how to implement a 'Piano Roll' view in Cocoa. Attached is a screenshot of what I'm trying to accomplish. Disregard the keyboard on the left of the window and let's just focus on the area with the colorful rectangles, symbolizing notes.

1. When drawing the light and dark grey rows which make up a background, should I just use a loop with Quartz 2D/Cocoa Drawing calls?

1: Find out what color occupies the most of the view - the dark or the light color. Erase the whole view using that color. 2: Set the other color [[NSColor colorWithCalibratedRed:green:blue:alpha:] set]; or the like 3: Set up rectangle: rect.origin = NSZeroPoint; rect.size = [self frame].size; rect.size.height = barHeight; 4: Draw rectangles for(i = 0; i < positions; i++) { rect. NSRectFill(rect); rect.origin.y = positions[i++]; }

Note: The fewer NSRectFill you do, the faster your code will be. Don't use NSBezierpath, as NSRectFill is way faster!

2. When drawing the vertical lines, should I just use a loop with Quartz 2D/Cocoa Drawing calls?

Use NSRectFill again, because NSBezierPath is way too slow. :)

3. Should I put the notes (colorful rectangles) each in a separate view (or Core Animation layer), or is it effective to just draw them directly in the main view (same view in which the rows are drawn)? I am eventually going to want to resize them and change their position by dragging them around.

Make one pianoview (eg. MRPianoRollView), which contains only the pianoroll (all the notes), not the keyboard (put the keyboard in its own view)

4. How would I go about changing the view's width as more notes (rectangles) are added to the view? When enclosed in a NSScrollView, would manipulating the width of the view also hide/show the horizontal scrollbar appropriately?

You're in luck, just resize the view, and the NSScrollView does the rest. You may even feel like looking at TextEdit, especially the "ScalingScrollView", in case you want to add a zoom feature (I don't know why you'd want that, though). =)

Everything mentioned above is easy.

The most "difficult" thing you will experience about this, is the notes themselves. I'd go and make my own note objects (as there will be many), containing something like a SMPTE for position, some data [eg. finetune/pitchbend]. I'd then make an array of pointers to this structure, as you usually don't have more than 127 notes; one could keep a fixed array size here.

In my drawRect, I'd set up a NSRect with Y position, width and height. The x position will be calculated from the SMPTE position. The color will be looked up as well.

A prototype implementation could look something like the following:

enum                                                            /* flags, 
currently only 'selected' is used */
{
        MRNoteSelected  = 0x01,
        MRNoteLocked    = 0x02                  /* not movable */
};

#define MRMinimumDuration (0.1)         /* change to what suits you best */
#define MRSideSize      (1.0)                   /* same here */

@class MRNoteData;
typedef struct NoteStruct NoteStruct; /* good old habit*/
struct NoteStruct
{
        NoteStruct      *prev;
        NoteStruct      *next;
        NoteStruct      *master;
        long            flags;
        double          smpte;
        double          duration;
        double          velocity;
        double          attack;

        /* and whatever else you might want here */

        MRNoteData      *data;                  /* other data not related to 
drawing */
};

@interface MRPianoRollView : NSView
{
        NoteStruct      notes[128];
        NoteStruct      *lost;
        float                   barHeight;
        float                   scale;
}
+ (float)defaultBarHeight
+ (void)setDefaultBarHeight:(float)aDefaultBarHeight
+ (float)defaultScale;
+ (void)setDefaultScale:(float)aDefaultScale;

- (void)setScale:(float)aScale;
- (float)scale;
- (void)setBarHeight:(float)aBarHeight;
- (float)barHeight;
- (void)addNote:(NoteStruct *)aNote key:(int)aKey;

@end

@implementation MRPianoRollView

static NSColor *bgColor = NULL; // never release; used across instances.
static NSColor *fgColor = NULL;         // same here.

static float defaultScale = 10.0; // I'd probably make SMPTE in seconds.
static float defaultBarHeight = 16.0;

#define MASTERNOTE(a) (((a)->master) ? ((a)->master) : (a))

+ (float)defaultBarHeight
{
        return(defaultBarHeight);
}

+ (void)setDefaultBarHeight:(float)aDefaultBarHeight
{
        defaultBarHeight = aDefaultBarHeight;
}

+ (float)defaultScale
{
        return(defaultScale);
}

+ (void)setDefaultScale:(float)aDefaultScale
{
        defaultScale = aDefaultScale;
}

- (void)awakeFromNib
{
        if(NULL == bgColor)
        {
bgColor = [[NSColor colorWithCalibratedRed:0.4 green:0.4 blue:0.4 alpha:1.0] retain];
        }
        if(NULL == fgColor)
        {
fgColor = [[NSColor colorWithCalibratedRed:0.7 green:0.7 blue:0.7 alpha:1.0] retain];
        }
        for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
        {
notes[i] = NULL; // good practice, but not necessary, as alloc is zeroing the instance data.
        }
        barHeight = [MRPianoRollView defaultBarHeight];
        scale = [MRPianoRollView defaultScale];
}

- (void)setScale:(float)aScale
{
        scale = aScale;
}

- (float)scale
{
        return(scale);
}

- (void)setBarHeight:(float)aBarHeight
{
        barHeight = aBarHeight;
}

- (float)barHeight
{
        return(barHeight);
}

- (NoteStruct *)newNote
{
        NoteStruct      *result;

        result = (NoteStruct *)malloc(sizeof(NoteStruct));
        if(result)
        {
                memset(result, 0, sizeof(*result));
                result->prev = NULL;
                result->next = NULL;
                result->master = NULL;
        }
        return(result);
}

- (void)freeNote:(NoteStruct *)aNote // releases memory. Key must be unlinked (removed) first
{
        free(aNote);
}

- (void)removeNote:(NoteStruct *)aNote // only removes note, keeps it allocated
{
        if(aNote->next)
        {
                aNote->next->prev = aNote->prev;
        }
        if(aNote->prev)
        {
                aNote->prev->next = aNote->next;
        }
        aNote->prev = NULL;
        aNote->next = NULL;
}

- (void)deleteNote:(NoteStruct *)aNote  // removes and releases memory
{
        [self removeNote:aNote];
        [self freeNote:aNote];
}

- (void)addNote:(NoteStruct *)aNote key:(int)aKey
{
        NoteStruct      *travel;
        BOOL            lose;

        lose = (aKey < 0 || aKey >= (sizeof(notes) / sizeof(*notes)));
        aNote->prev = NULL;
        aNote->next = NULL;
        travel = lose ? lost : notes[aKey];
        if(travel)
        {
                while(travel->next)
                {
                        if(travel->smpte >= aNote->smpte)
                        {
                                break;
                        }
                        travel = travel->next;
                }
                if(travel->smpte >= aNote->smpte)
                {
                        aNote->next = travel;
                        aNote->prev = travel->prev;
                        if(travel->prev)
                        {
                                travel->prev->next = aNote;
                        }
                }
                else
                {
                        travel->next = aNote;
                }
        }
        else if(lose)
        {
                lost = aNote;
        }
        else
        {
                notes[key] = aNote;
        }
}

- (NoteStruct *)findNoteForKey:(int)aKey time:(double)aTime
{
        NoteStruct      *travel;
        NoteStruct      *next;
        float                   start;

        if(aKey >= 0 && aKey < (sizeof(notes) / sizeof(*notes)))
        {
                travel = notes[aKey];
                while(travel)
                {
                        next = travel->next;
                        start = travel->smpte;
                        if(aTime >= start && aTime < (start + travel->duration))
                        {
                                return(travel);
                        }
                        travel = next;
                }
        }
        return(NULL);
}

- (double)timeFromPosition:(float)aPosition
{
        return(aPosition / [self scale]);
}

- (int)keyFromPosition:(float)aPosition
{
        return((int) (aPoint.y / [self barHeight]));
}

- (NoteStruct *)findNoteAtPoint:(NSPoint)aPoint
{
        double  time;
        int             key;
        float           bh;
        float           y;

        time = [self timeFromPosition:aPoint.x];
        key = [self keyFromPosition:aPoint.y];

        bh = [self barHeight];
        y = aPoint - (((float) key) * bh);
        if(y >= 1.0 && y < (bh - 2.0))
        {
                if(key >= 0)
                {
                        return([self findNoteForKey:key time:time]);
                }
        }
        return(NULL);
}

void drawRect:(NSRect)aRect
{
        long            i;
        NSRect          rect;
        NSRect          r;
        NSColor         *col;
        NoteStruct      *note;
        NoteStruct      *next;
        NoteStruct      *original;
        float                   bh;
        float                   sc;

sc = [self scale]; // Good practice (speed): only use accessor method once.
        bh = [self barHeight];

        rect.origin = NSZeroPoint;
        rect.size = [self frame].size;

        [bgColor set];
        NSRectFill(rect);

        rect.size.height = bh;                                          // 
height of each line
        [fgColor set];
        for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
        {
                rect.origin.y = ((double) i) * bh + 1.0;        // y position 
of each bar
                switch(i % 12)                                          // 
black keys
                {
                  case 1:
                  case 3:
                  case 6:
                  case 8:
                  case 10:
                        NSRectFill(rect);
                break;
        }
        for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
        {
                rect.origin.y = ((double) i) * bh;                      // y 
position of each bar divider

                col = NULL;                                                     
// assume we won't draw a separator line
                switch(i % 12)
                {
                  case 0:
                        rect.size.height = 2.0;                         // 
thick line when switching octave
col = [NSColor blackColor]; // (maybe you want the line to be 1 pixel and only change the color)
                        break;
                  case 7:
                        rect.size.height = 1.0;                         // thin 
line between E and F.
                        col = [NSColor grayColor];
                        break;
                }
                if(col)
                {
                        [col set];
                        NSRectFill(rect);                                       
// draw the separator line
                }

                rect.origin.y++;                                                
// y position of each note
                rect.size.height = bh - 2.0;                            // 
height of each note

                note = notes[i];
                while(note)
                {
                        note = note->next;                           // point 
to next note (good practice)
original = note; // inspired by Cubase (it's such a cool feature, having to change only the master)
                        if(note->master)
                        {
                                original = note->master;
                        }
// (the above line allows you to unlink the note and delete it in the loop)
                        col = [NSColor cyanColor];
                        if(original->velocity > 120)
                        {
                                col = [NSColor yellowColor];
                        }
                        else if(original->velocity > 100)
                        {
                                col = [NSColor greenColor];
                        }
                        rect.origin.x = note->smpte * sc;            // 
calculate x position of note
                        rect.size.width = original->duration * sc;   // 
calculate width of note
                        if(note->selected)
                        {
                                r = rect;
                                r.origin.x++;
                                r.origin.y++;
[[NSColor colorWithCalibratedRed:0.0 green:0.0 blue:0.0 alpha:0.5] set]; NSRectFillUsingOperation(rect, NSCompositeSourceOver); // draw note shadow (draw transparant; this is slow)
                        }
                        [col set];
                        NSRectFill(rect);                                       
        // show note
                        note = next;                                            
        // next note.
                }
        }
}

- (BOOL)toggleNoteSelectionAtPoint:(NSPoint)aPoint
{
        NoteStruct      *note;

        note = [self findNoteAtPoint:aPoint];
        if(note)
        {
                note->flags ^= MRNoteSelected;
                return(YES);
        }
        return(NO);
}

- (BOOL)selectNoteAtPoint:(NSPoint)aPoint
{
        NoteStruct      *note;

        note = [self findNoteAtPoint:aPoint];
        if(note && !(note->flags & MRNoteSelected))
        {
                note->flags |= MRNoteSelected;
                return(YES);
        }
        return(NO);
}

- (BOOL)deselectNoteAtPoint:(NSPoint)aPoint
{
        NoteStruct      *note;

        note = [self findNoteAtPoint:aPoint];
        if(note && (note->flags & MRNoteSelected))
        {
                note->flags &= ~MRNoteSelected;
                return(YES);
        }
        return(NO);
}

- (BOOL)moveSelectedNotes:(NSPoint)aRelativePoint
{
        BOOL            changed;
        NoteStruct      *travel;
        NoteStruct      *next;
        int                     deltaKey;
        double          deltaTime;
        long            i;
        long            newKey;

        deltaTime = [self timeFromPosition:aRelativePoint.x];
        deltaKey = [self keyFromPosition:aRelativePoint.y];
        changed = NO;
        if(deltaTime && deltaKey)                                               
        // if we have a change
        {
                for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
                {
                        travel = notes[i];
                        while(travel)
                        {
                                next = travel->next;
                                if(travel->selected)
                                {
                                        changed = YES;
                                        travel->smpte += deltaTime;
                                        if(travel->smpte < 0.0)
                                        {
                                                travel->smpte = 0.0;
                                        }
                                        newKey = deltaKey + i;
                                        if(newKey < 0 || newKey > 
(sizeof(notes) / sizeof(*notes)))
                                        {
                                                [self addNote:travel key:-1];
                                        }
                                        else if(deltaKey)
                                        {
                                                [self removeNote:travel];
                                                [self addNote:travel 
key:newKey];
                                        }
                                }
                                travel = next;
                        }
                }
        }
        return(changed);
}

- (BOOL)changeStartOfSelectedNotes:(double)aDeltaTime
{
        BOOL            changed;
        NoteStruct      *travel;
        NoteStruct      *next;
        long            i;
        double          oldValue;

        changed = NO;
        for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
        {
                travel = notes[i];
                while(travel)
                {
                        next = travel->next;
                        if(travel->flags & MRNoteSelected)
                        {
                                oldValue = travel->smpte;
                                travel->smpte += aDeltaTime;
                                if(travel->smpte < 0.0)
                                {
                                        travel->smpte = 0.0;
                                }
                                if(oldValue != travel->smpte)
                                {
                                        changed = YES;
                                }
                        }
                        travel = next;
                }
        }
        return(changed);
}

- (BOOL)changeDurationOfSelectedNotes:(double)aDeltaTime
{
        BOOL            changed;
        NoteStruct      *travel;
        NoteStruct      *next;
        long            i;
        double          oldValue;

        changed = NO;
        for(i = 0; i < (sizeof(notes) / sizeof(*notes)); i++)
        {
                travel = notes[i];
                while(travel)
                {
                        next = travel->next;
                        if(travel->flags & MRNoteSelected)
                        {
                                oldValue = travel->duration;
                                travel->duration += aDeltaTime;
                                if(travel->duration <= MRMinimumDuration)
                                {
                                        travel->duration = MRMinimumDuration;
                                }
                                if(oldValue != travel->duration)
                                {
                                        changed = YES;
                                }
                        }
                        travel = next;
                }
        }
        return(changed);
}


- (void)clicked:(NSPoint)aPoint withFlags:(NSUInteger)aFlags
{
        NoteStruct      *note;
        double          startTime;
        double          endTime;
        double          time;

        note = [self findNoteAtPoint:pt];

        if(note)
        {
                if(note->flags & MRNoteSelected)
                {
                        time = [self timeFromPosition:aPoint.x];
                        startTime = note->smpte;
                        endTime = startTime + note->duration;
                        if(startTime - MRSideSize < time && startTime + MRSideSize 
> time)
                        {
                                dragKind = MRDragKindChangeStart;
                        }
                        else if(endTime - MRSideSize < time && endTime + 
MRSideSize > time)
                        {
                                dragKind = MRDragKindChangeDuration;
                        }
                        else
                        {
                                dragKind = MRDragKindMove;
                        }
                }
        }

        if(aFlags & NSShiftKeyMask)
        {
                if([self toggleNoteSelectionAtPoint:aPoint])
                {
                        [self setNeedsDisplay:YES];
                }
        }
        else
        {
if(NULL == note || !(note->flags & MRNoteSelected)) // make sure we don't deselect when dragging multiple selections
                {
                        if([self selectOneNoteAtPoint:aPoint])
                        {
                                [self setNeedsDisplay:YES];
                        }
                }
        }
}

- (void)mouseDown:(NSEvent *)aEvent // note: when I use 'a' in front of the name it's a short for 'a'rgument.
{
        NSPoint         pt;
        NSUInteger      modifiers;

        modifiers = [aEvent modifierFlags];
        pt = [self convertPoint:[aEvent locationInWindow] fromView:NULL];
        [self clicked:pt withFlags:modifiers];
}

- (void)mouseDragged:(NSEvent *)aEvent
{
        NSPoint pt;
        double  deltaTime;
        BOOL    update;

//      pt = [self convertPoint:[aEvent locationInWindow] fromView:NULL];

        pt.x = [aEvent deltaX];
        pt.y = [aEvent deltaY];

        deltaTime = [self timeFromPosition:pt.x];
        update = NO;
        switch(dragKind)
        {
          case MRDragKindChangeStart:
                update = [self changeStartOfSelectedNotes:deltaTime];
                break;
          case MRDragKindChangeDuration:
                update = [self changeDurationOfSelectedNotes:deltaTime];
                break;
          case MRDragKindMove:
                update = [self moveSelectedNotes:pt];
                break;
        }
        if(update)
        {
                [self setNeedsDisplay:YES];
        }
}

@end

The above is just a "what came to mind" example. I don't expect the code to compile out-of-the-box, but it will probably give you a basic idea on how you could implement it. I guess it grew quite large, but it should keep you occupied for a few minutes. ;)
Hint: Don't do many cocoa invocations ["calls"] in a loop. C is faster.
As the NoteStruct object is a C structure, the preferred way of allocating/deallocating would be malloc/free as I did above.

I hope this can be of some use to you, and perhaps bring you some new ideas as well.

As you'll probably notice, I haven't made any 'demo' for the above; you could add some test-notes in the -awakeFromNib method.

Happy coding! =)


Love,
Jens

_______________________________________________

Cocoa-dev mailing list (Cocoa-dev@lists.apple.com)

Please do not post admin requests or moderator comments to the list.
Contact the moderators at cocoa-dev-admins(at)lists.apple.com

Help/Unsubscribe/Update your Subscription:
http://lists.apple.com/mailman/options/cocoa-dev/archive%40mail-archive.com

This email sent to arch...@mail-archive.com

Reply via email to