Learn how easy it is to sync an existing GitHub or Google Code repo to a SourceForge project! See Demo

Close

[r79]: trunk / synth / riffdisp.srp Maximize Restore History

Download this file

riffdisp.srp    427 lines (387 with data), 16.4 kB

# riffdisp -- drum circle graphics
#
# Roger Dannenberg
# Feb 2014

DRUM_BPM = 100
DRUM_BEAT_PERIOD = 60 / DRUM_BPM
// 4 bars of 4 beats at tempo is this many seconds
DRUM_DELAY = 4 * 4 * DRUM_BEAT_PERIOD

PIX_PER_BEAT = PIX_PER_SEC * DRUM_BEAT_PERIOD

SOLO_TXT_OFFSET = 150

// ---------- riffs as backgrounds ---------
// type, time, dur, pitch
riff0v1 = [['n', 0, 61, 1],
           ['n', 1, 63, 1],
           ['n', 2, 66, 1],
           ['n', 4, 61, 1],
           ['n', 5, 63, 1],
           ['n', 6, 68, 1],
           ['n', 7, 66, 1],
           ['n', 8, 61, 1],
           ['n', 9, 63, 1],
           ['n', 10, 66, 1]]
riff0v2 = [['n', 2, 66, 1],
           ['n', 3, 68, 1],
           ['n', 6, 72, 1],
           ['n', 7, 70, 1],
           ['n', 10, 66, 1],
           ['n', 11, 68, 1],
           ['n', 13, 70, 2]]
riff1v1 = [['n', 0, 63, 0.5],
           ['n', 1.5, 66, 0.5],
           ['n', 2, 68, 0.5],
           ['n', 2.5, 63, 0.5],
           ['n', 3.5, 68, 0.5],
           ['n', 4.5, 68, 0.5],
           ['n', 5,   63, 1],
           ['n', 7,   66, 0.5],
           ['n', 8, 63, 0.5],
           ['n', 9.5, 66, 0.5],
           ['n', 10, 68, 0.5],
           ['n', 10.5, 63, 0.5],
           ['n', 11.5, 68, 0.5],
           ['n', 12.5, 68, 0.5],
           ['n', 13,   63, 1],
           ['n', 15,   66, 0.5]]
riff1v2 = [['n', 3.5,  72, 0.5],
           ['n', 4.5,  72, 0.5],
           ['n', 5,    70, 0.5],
           ['n', 6,    75, 0.5],
           ['n', 6.5,  73, 0.5],
           ['n', 7,    75, 0.5],
           ['n', 11.5, 72, 0.5],
           ['n', 12.5, 72, 0.5],
           ['n', 13,   70, 0.5],
           ['n', 14,   75, 0.5],
           ['n', 14.5, 73, 0.5],
           ['n', 15,   75, 0.5]]
all_riffs = [[riff0v1, riff0v2], [riff1v1, riff1v2]]


# For testing:
def solo_next_handler(rest ignore)
    riffdisp.state = 'solo_next' if riffdisp.state != 'solo_next' else nil
def solo_now_handler(rest ignore)
    riffdisp.state = 'solo' if riffdisp.state != 'solo' else nil

class Riffdisp
    var state // nil, 'preroll', 'solo_next', 'solo', 'riff'
    var solo_request_checkbox
    var start_time
    var stop_beat
    var drumming_id
    var curbeat // integer value of exact beat
    var exactbeat // keeps time
    var riff_index          // this is the riff at the bottom of the screen
    var next_riff_index     // this is the riff above riff_index
    var selected_riff_index // this one is up next
    // the "zero" beat of next_riff_index is mapped to this beat:
    var actual_beat_of_next_riff_index
    var y_for_b0 // the y coordinate for beat 0 of the riff
    var pbarPos
    var barwidth
    var wid // the width of the canvas
    var my_pitch
    var my_vel
    var more_solos

    // we could be scheduling a lot of notes and Serpent not only has
    // a linear insertion sort for scheduling, but there is a 4K limit
    // on array sizes (currently). 100 players -> 40 notes / player,
    // so only 4 notes/sec could overrun the array
    // To avoid this, we'll manually keep a queue of events for each
    // player in an array and schedule the arrays.
    var drum_note_queues

    def init()
        riff_index = 0
        next_riff_index = 0
        actual_beat_of_next_riff_index = 32
        selected_riff_index = 0 // set this one
        drumming_id = 0
        solo_next_button = Button(0, "next", 470, 135, 40, 20)
        solo_next_button.method = 'solo_next_handler'
        solo_now_button = Button(0, "solo", 510, 135, 40, 20)
        solo_now_button.method = 'solo_now_handler'

        state = nil
        exactbeat = -100.0
        curbeat = -100
        drum_note_queues = array(120) // up to 120 players for now
        for x at i in drum_note_queues:
            drum_note_queues[i] = [] // seed with empty arrays/queues
        wid = 500 // just in case someone expects a number
        my_pitch = 0
        my_vel = 0
        more_solos_ok = t

    def solo_request_handler(obj, event, x, y)
        display "solo_request_handler", x
        zmq_send_gno_msg("Q", "Y" if x else "N")

    def set_next_riff(riff_num, solo_flag)
        display "set_next_riff", riff_num, solo_flag
        selected_riff_index = riff_num
        if solo_flag and more_solos_ok
            if state == 'solo_next' or state == 'solo':
                display "**** set_next_riff ALREADY IN SOLO SEQUENCE", state
            else:
                state = 'solo_next'

    def beat_to_y(b):
        y_for_b0 - b * PIX_PER_BEAT

    def pitch_to_x(p):
        (p - 60) * 30 * wid / 530

    def inote_to_x(p)
        p * 30 * wid / 530 + (20 * wid / 1060) - 5

    def paint_riff(c, w, h, top)
        wid = w
        pbarPos = h * 0.65
        barwidth = 20 * w / 500
        // exactbeat must correspond to pbarPos
        var bottom_beat = exactbeat - ((h - pbarPos) / PIX_PER_BEAT)
        if bottom_beat > actual_beat_of_next_riff_index:
            if not riff_index and not next_riff_index
                fullstop()
            riff_index = next_riff_index
            next_riff_index = selected_riff_index
            if exactbeat > stop_beat
                if next_riff_index:
                    display "paint_riff stopping", vtsched.time, exactbeat, stop_beat
                next_riff_index = nil
            actual_beat_of_next_riff_index = actual_beat_of_next_riff_index + 16
            display "new riffs", riff_index, next_riff_index, selected_riff_index, exactbeat, stop_beat
        // y_for_b0 is the y coordinate of beat 0 of riff_index
        //     this should (almost) always be below (great than) h
        var actual_beat_of_riff_index = actual_beat_of_next_riff_index - 16
        var beats_below_bottom = bottom_beat - actual_beat_of_riff_index
        y_for_b0 = h + beats_below_bottom * PIX_PER_BEAT

        // draw beatlines
        var beat = int(bottom_beat + 1) // round up
        var beat_y = beat_to_y(beat % 16)
        c.set_pen_color("BLACK")
        while beat_y > top
            c.draw_line(0, beat_y, w, beat_y)
            beat_y = beat_y - PIX_PER_BEAT

        // iterate through all notes that could be played in current
        //     riff and paint them
        c.set_brush_color("BLACK")
        c.set_text_color("WHITE")

        var riff
        if riff_index:
            riff = all_riffs[riff_index]
            riff = riff[synth_my_usernum % len(riff)]
        else:
            riff = []
        for note in riff:
            var pp = note[NT_PIT]
            var xx = pitch_to_x(pp)
            var yy = beat_to_y(note[NT_TIM])
            var hh = note[NT_DUR] * PIX_PER_BEAT
            var pitch = note[NT_PIT]
            if yy - hh < top:
                hh = yy - top
            if hh > 0:
                c.draw_rectangle(xx, yy - hh, barwidth, hh)
                var text_y = yy - min(30, (hh / 2))
                if text_y > top:
                    c.draw_text(xx + (barwidth / 2) - 5, text_y,
                                keya[pp - 60])

        // iterate through all notes that could be played in next
        //     riff and paint them
        if next_riff_index:
            riff = all_riffs[next_riff_index]
            riff = riff[synth_my_usernum % len(riff)]
        else:
            riff = []
        for note in riff:
            pp = note[NT_PIT]
            xx = pitch_to_x(pp)
            yy = beat_to_y(note[NT_TIM] + 16)
            hh = note[NT_DUR] * PIX_PER_BEAT
            pitch = note[NT_PIT]
            if yy - hh < top:
                hh = yy - top
            if hh > 0:
                c.draw_rectangle(xx, yy - hh, barwidth, hh)
                text_y = yy - min(30, (hh / 2))
                if text_y > top:
                    c.draw_text(xx + (barwidth / 2) - 5, text_y,
                                keya[pp - 60])

        // draw the notebar
        c.set_brush_color("purple", 100)
        c.draw_rectangle(0, pbarPos, wid, h * 0.03)
        
        // if a note is playing, show it on the notebar
        // TODO: feedback based on what user should be playing
        if my_vel > 0:
            xx = pitch_to_x(my_pitch)
            c.set_brush_color ("YELLOW")
            c.draw_rectangle(xx, pbarPos, barwidth, h * 0.03)

        // draw the keys
        c.set_brush_color("CADET BLUE")
        c.draw_rectangle(0, hgt - 90 , wid, 60)
        c.set_text_color("white")
        c.set_brush_color("black")
        for k at index in keya:
            c.draw_line (inote_to_x(index) - 10, h - 90,
                         inote_to_x(index) - 10 , hgt)
            if ht_of[index] != 0:
                c.draw_rectangle(inote_to_x(index) - 5, hgt - 90, barwidth, 30)
            c.draw_text(inote_to_x(index),hgt - ht_of[index] - 60,k)

    def paint_canvas(c, w, h)
        // paint the riff first in case things go on top
        var beat_spacing = idiv(w, 17)
        var beat_size = min(30, idiv(beat_spacing, 2))
        var hbs = idiv(beat_size, 2)
        var x = beat_spacing - hbs
        var y = 20 + beat_size - hbs
        // now, bottom of beat indicators (top of scrolling notes)
        //   is y + beat_size
        paint_riff(c, w, h, y + beat_size)

        if state == 'solo_next':
            if curbeat % 2 == 0:
                c.set_brush_color("YELLOW")
                c.set_pen_color("YELLOW")
                var x_offset = max(SOLO_TXT_OFFSET, idiv(w - SAVE_BUTTON_WIDTH, 2) - 200)
                c.draw_rectangle(x_offset, 0, 400, 25)
                c.set_text_color("BLACK")
                c.draw_text(x_offset + 20, 5, "GET READY TO PLAY 4-BAR SOLO ! ! !")
        elif state == 'solo':
            if curbeat % 2 == 0:
                c.set_brush_color("GREEN")
                c.set_pen_color("GREEN")
                x_offset = max(SOLO_TXT_OFFSET, idiv(w - SAVE_BUTTON_WIDTH, 2) - 200)
                c.draw_rectangle(x_offset, 0, 400, 25)
                c.set_text_color("BLACK")
                c.draw_text(x_offset + 20, 5, "PLAY 4-BAR SOLO ! ! !")
        for i = 0 to 16:
            var color = "CADET BLUE" if i % 4 == 0 else "GREY"
            c.set_brush_color(color)
            c.set_pen_color(color)
            if i == ((curbeat + 32) % 16) and curbeat >= -24:
                c.draw_ellipse(x - hbs, y - hbs, beat_size * 2, beat_size * 2)
            else:
                c.draw_ellipse(x, y, beat_size, beat_size)
            x = x + beat_spacing

    def handle_note_event(msg):
        # handle an "N" message with timestamped notes to play
        // [timestamp, voice, pitch, vel]+, so msg len == 4 + 7N
        if (len(msg) - 4) % 7 != 0:
            display "handle_note_event - bad message", len(msg)
            return
        var i = 4
        vtsched.start_use()
        while i < len(msg):
            timestamp = get_timestamp(msg, i)
            var voice = ord(msg[i + 4])
            var pitch = ord(msg[i + 5])
            var vel = ord(msg[i + 6])
            // two level scheduling: insert in queue by voice, then schedule voice
            var when = timestamp / 1000
            # display "handle_note_event - sched drum note", vtsched.time, timestamp, when, voice, pitch, vel
            var event = [when, pitch, vel]
            if voice >= len(drum_note_queues)
                return // out of bounds, don't crash!
            var queue = drum_note_queues[voice]
            var next_time = queue.last()
            queue.append(event)
            queue.resort()
            if next_time is not queue.last():
                # display "handle_note_event scheduling", event
                // new earlier event
                the_sched.cause(absolute(when), this,
                                'check_drum_circle_note', voice)
            i = i + 7
        vtsched.finish_use()

    def check_drum_circle_note(voice)
        # display "check_drum_circle_note", voice
        var queue = drum_note_queues[voice]
        var ev = queue.last()
        if not ev:
            return // nothing to dispatch or schedule
        if the_sched.time + EPSILON >= ev[0]:
            print "play drum at ", vtsched.time, ev[0], voice, ev[1], ev[2]
            synth_set_some_state(voice, ev[1], ev[2])
            queue.unappend()
            if len(queue) > 0:
                the_sched.cause(absolute(queue.last()[0]), this,
                                'check_drum_circle_note', voice)


    // this is called when a local key-up/down occurs
    def note_on(pitch, vel):
        my_pitch = pitch
        my_vel = vel
        # display "riffdisp::note_on", pitch, vel,
        if state:
            // send notes via zmq to server
            vtsched.start_use()  // update time
            var ts = int((vtsched.time + DRUM_DELAY) * 1000)
            # display vtsched.time, ts
            ts = chr(ts & 0xFF) + chr((ts >> 8) & 0xFF) +
                 chr((ts >> 16) & 0xFF) + chr((ts >> 24) & 0xFF)
            zmq_send_gno_msg("N", ts + chr(pitch) + chr(vel))
            vtsched.finish_use()

    def start(when):
        // do this here so that it will be on top. It think if we create this
        // in init(), the chat canvas covers it and you cannot click on it
        if not solo_request_checkbox:
            solo_request_checkbox = Checkbox(pianoroll_win,
              "Request to play solo(s)", 2000, 0, 150, THIN_CTRL_H) // invisible
            solo_request_checkbox.target = this
            solo_request_checkbox.method = 'solo_request_handler'

        // actual start of beat 0 is when.
        display "riffdisp::start", when, rtsched.time
        start_time = when
        stop_beat = 10000 // 10000 beats = 100m = infinity
        pianoroll.mode = 'riff'
        solo_request_checkbox.set_position(0, CHAT_HEIGHT)
        solo_request_checkbox.set_value(0)
        pianoroll.refresh(t)
        state = 'preroll'
        riff_index = 0
        next_riff_index = 0
        selected_riff_index = 0
        actual_beat_of_next_riff_index = 32
        exactbeat = -100
        curbeat = -100
        my_pitch = 0
        my_vel = 0
        more_solos_ok = t

        // start vtsched. We're already using rtsched
        var srv_sec = get_server_time() // in seconds
        vtsched.set_vtime(srv_sec)
        vtsched.set_bps(1.0) // make sure we have 1:1 mapping
        drumming_id = drumming_id + 1
        vtsched.cause((start_time - vtsched.time),
                      this, 'beat', -24, drumming_id)
        // Now, beat -24 will happen at start_time (probably 5s from now)
        // The idea is drummer gets 8 beat countoff, then plays 16 beats,
        // then ensemble starts their riffs
        // put something here to start scrolling

    def stop(when):  // bring drumming to a controlled stop
        display "Riffdisp::stop", when, rtsched.time
        more_solos_ok = false
        stop_beat = when

    def fullstop():  // get out of riff mode, all done
        drumming_id = drumming_id + 1
        solo_request_checkbox.set_position(2000, 0)
        riffmode = nil
        pianoroll.mode = 'pianoroll'
        // make button invisible
        display "riffdisp::fullstop", pianoroll.mode, pianoroll.running
        // can't cause refresh from inside paint() so do it later
        rtsched.start_use()
        rtsched.cause(0.1, pianoroll, 'refresh', t) // we're done
        rtsched.finish_use()

    def beat(b, id):  // called on beat -24 to beat N
        exactbeat = b
        // need to round down, but int() rounds toward zero
        var old = curbeat
        curbeat = (int(b + EPSILON) if b >= 0 else int(b + EPSILON - 1))

        if old != curbeat: // a new beat
            # display "beat calls synth_set_some_state"
            synth_set_some_state(2, 61, 100)
            // turn the drum off in 1/2 beat
            vtsched.cause(DRUM_BEAT_PERIOD / 2, nil, 'synth_set_some_state', 2, 61, 0)

        if curbeat % 16 == 0 and old != curbeat: // DOWNBEAT OF 4-BAR CYCLE
            display "**** Downbeat", curbeat, state, the_sched.time
            if state == 'solo_next'
                state = 'solo'
            elif state == 'solo'
                state = 'riff'
        if drumming_id != id
            return // we're done, maybe set some state here?
        pianoroll.refresh(t)
        // 20 frames/sec = 1200 fpm. Tempo = 100 bpm, so 12 frames/beat
        vtsched.cause(DRUM_BEAT_PERIOD / 12, this, 'beat', b + 1/12, id)

riffdisp = Riffdisp()