[r80]: trunk / synth / gno-client.srp Maximize Restore History

Download this file

gno-client.srp    528 lines (472 with data), 19.3 kB

# gno-client.srp -- standard test application + voice record and playback + network

require "gnoclient-config"
client_ticket = 0
// client_ticket_as_string is used for messages to server
client_ticket_as_string = "   BAD_" 
require "gnotest"
require "sched"
require "utils"
require "netperf"
require "clientnet"
require "http"
require "strparse"
require "scoredisp"
require "riffdisp"
require "chat"
require "semicond"

riffmode = false

RIFFS_LAUNCH = 99
RIFFS_CUE_END = 98
RIFFS_FINISHED = 97

DEFAULT_STATUS = "Performer"
lastname = nil
lastpasswd = nil

// a button that turns on/off with mouse down/up
// when state changes, target.method(x) is called
// where x is true if button is down
class Momentary (Canvas):
    var color
    var text

    def init(label, parent, x, y, w, h):
        color = "WHITE"
        text = label
        super.init(parent, x, y, w, h)

    def paint(all):
        clear(color)
        draw_text(10, 5, text)

    def handle(event, x, y):
        display "Record_button", event, x, y
        if event == WXS_LEFT_DOWN:
            color = "RED"
            if target:
                send(target, method, t)
            refresh(t)
        elif event == WXS_LEFT_UP:
            color = "WHITE"
            if target:
                send(target, method, nil)
            refresh(t)


class Announcer:
    var rec_button
    var recording

    def init(win, x, y):
        rec_button = Momentary("RECORD", win, x, y, 75, 25)
        rec_button.target = this
        rec_button.method = 'record_event'
        rec_button.refresh(t)

    def record_event(flag):
        synth_record(flag)
        if not flag: // play it back
            display "time to play back"
            rtsched.start_use()
            // delay for a bit to allow audio to stop recording
            rtsched.cause(0.1, this, 'playback')
            rtsched.finish_use()

    def transfer_audio_in()
        while true
            var data = synth_get_audio()
            if not data or not push_audio_sock:
                return // maybe audio in is not open or we're not connected
            zmq_send(push_audio_sock, data)
            display "transfer_audio_in", data[0], len(data)
            if data[0] == "E":
                return

    def transfer_audio_out()
        for data in recording:
            display "transfer_audio_out", type(data)
            synth_put_audio(data)
        synth_play()
        recording = nil


    def playback():
        transfer_audio_in()
//        display "playback", len(recording)
//        transfer_audio_out()
//        display "playback started"
//        show_progress()

//    def show_progress()
//        if synth_playing():
//            print "playing..."
//            rtsched.cause(0.1, this, 'show_progress')
            

def start_piece(usernum)
    display "***** START ****", usernum, get_server_ms()


def handle_launch_event(zmq_msg)
    zmq_msg = subseq(zmq_msg, 8) // skip type and ticket
    var index = int(zmq_msg)
    var pos = find(zmq_msg, " ")
    if pos == -1:
        display "zmq_poll - bad message", msg_type, zmq_msg
        return
    zmq_msg = subseq(zmq_msg, pos + 1)
    var start = int(zmq_msg) / 1000
    display "LAUNCH", index, start, start - get_server_time()
    rtsched.start_use()
    if index == RIFFS_LAUNCH: // special case: launch drum mode
        // extra time here because we might not get any messages
        // for awhile (I think it should be 5s delay + 4.8s
        // countdown + latency to receive drums, but if drums
        // don't play, it will be another 9.6s
        time_out_next_time = rtsched.time + 60
        riffmode = true
        stop_pianoroll() // just in case anything else is running
        riffdisp.start(start)
    elif index == RIFFS_FINISHED:
        riffdisp.stop(start)
    elif index == RIFFS_CUE_END:
        // this is one case where the "when" units are integer beats, not ms
        riffdisp.stop(int(zmq_msg)) // don't use value in variable "start"
        riffmode = false  // just in case we haven't cleared it already
    elif index > 50:
        print "WARNING: handle_launch_event ignoring index", index
    else:
        start_pianoroll(start, index, synth_my_usernum) // start now
        rtsched.cause(start - get_server_time(), nil, 
                      'start_piece', synth_my_usernum)
    rtsched.finish_use()

def get_timestamp(msg, i):
    (ord(msg[i + 3]) << 24) +
    (ord(msg[i + 2]) << 16) +
    (ord(msg[i + 1]) << 8 ) +
     ord(msg[i])

def handle_drum_phrase(msg):
    // msg now consists of 4 riff numbers and solo flag
    // our riff is our user number mod 4:
    var riff = ord(msg[4 + synth_my_usernum % 4])
    var solo = ord(msg[8])
    riffdisp.set_next_riff(riff, solo == synth_my_real_index)
    display "handle_drum_phrase", riff, solo, synth_my_real_index

def zmq_poll():
    var zmq_msg = zmq_recv_noblock(subscribe_sock)
    if zmq_msg:
        time_out_next_time = rtsched.time + 20 // seconds
        var msg_type = zmq_msg[0]
        if msg_type == "A" or msg_type == "E": // Audio, End-audio
            synth_put_audio(zmq_msg)
            display "got audio to play", msg_type, len(zmq_msg)
            if msg_type == "E":
                synth_play()
        elif msg_type == "N":
            riffdisp.handle_note_event(zmq_msg)
        elif msg_type == "L": // Launch a score
            handle_launch_event(zmq_msg)
        elif msg_type == "D":
            handle_drum_phrase(zmq_msg)
        elif msg_type == "C": 
            // remove type and ticket and post string
            incoming_chat_message(zmq_msg)
        elif msg_type == "R":
            request_handler(zmq_msg)
        elif msg_type == "S": // semiconductor
            if semicond:
                semicond_incoming_message(zmq_msg)
        else:
            display "zmq_poll - unknown msg", msg_type, zmq_msg


def timer_callback()
    rtsched.poll(time_get())    // updates rtsched.time to now - offset
    now_ms = int(rtsched.time * 1000)
    if zmq_connected:
        zmq_poll()
    midi_input.poll_for_input()
    osc_server_poll()


zmq_connected = false
push_audio_sock = nil

def zmq_setup():
    // zmq_setup() is called by connect_action() when we are connected,
    //   which is evidence that server_ip is valid now (it might have
    //   been switched to "localhost"). Since connect_action() will run
    //   again if we lose connections, zmq_setup() can be called multiple
    //   times, but zmq connections save state and reconnect as needed,
    //   so we only want to run this one time.
    if zmq_connected
        return // only run zmq_setup one time
    zmq_connected = true // so that we don't run this again
    zmq_init()
    push_audio_sock = zmq_open_push()
    display "zmq_setup", server_ip
    display "zmq_setup 1", push_audio_sock, zmq_connect(push_audio_sock, "tcp", server_ip, int(server_pull_audio_port))
    subscribe_sock = zmq_open_subscribe()
    display "zmq_setup 2", subscribe_sock, zmq_connect(subscribe_sock, "tcp", server_ip, int(server_publish_port))
    zmq_subscribe(subscribe_sock, "A")  // audio
    zmq_subscribe(subscribe_sock, "E")  // end audio
    zmq_subscribe(subscribe_sock, "C")  // chat
    zmq_subscribe(subscribe_sock, "D")  // drum circle messages
    zmq_subscribe(subscribe_sock, "L")  // launch (score)
    zmq_subscribe(subscribe_sock, "N")  // notes from server
    zmq_subscribe(subscribe_sock, "Y")  // confirmation
    zmq_subscribe(subscribe_sock, "R")  // request confirmation
    zmq_subscribe(subscribe_sock, "S")  // semiconductor messages
    // Q -- set solo requested (to server only)


def client_interface_init():
    launch_buttons = []
    Statictext(0, "Start scrolling score:", 5, 150, 100, 20)
    for i = 0 to len(all_scores):
        var b = Button(0, "Start " + str(i + 1), 5 + i * 60, 170, 55, 20)
        launch_buttons.append(b)
        b.method = 'launch'

def drumming_control(obj, event, x, y):
    display "drumming_control", x
    zmq_send_gno_msg("L", 
                     str(RIFFS_LAUNCH if x else RIFFS_FINISHED) + " " +
                     str(get_server_ms() + (START_DELAY * 1000)),
                     'handle_launch_event')

def drum_stop_handler(obj, event, x, y):
    display "drum_stop_handler"
    // stop time is given in terms of beats so everyone stops on
    // the same beat. The 26 beat delay allows ~6s for transmission
    // (10 beats) + 16 beats to play out any current or pending solos
    zmq_send_gno_msg("L", str(RIFFS_CUE_END) + " " +
                     str(int(riffdisp.exactbeat + 26)),
                     'handle_launch_event')
        
drum_boost_id = 0
// schedule a drum boost message in 0.2s
def drum_boost_handler(obj, val)
    drum_boost_id = drum_boost_id + 1
    rtsched.cause(0.2, nil, 'send_drum_boost', drum_boost_id, val)

// if drum_boost was not changed for 0.2s, send it
def send_drum_boost(id, val)
    if id == drum_boost_id
        var boost = int(val)
        zmq_send_gno_msg("Q", "X" + str(boost))
        prefs.set('drum_boost', boost)
        prefs.save()

if audio_input_flag:
    announcer = Announcer(0, 370, 185)
    request_button = Button(0, "Request Conf", 460, 185, 100, 25)
    request_button.method = 'send_request'
    drumming_checkbox = Checkbox(0, "Drumming", 370, 135, 100, 20)
    drumming_checkbox.method = 'drumming_control'
    drum_stop_button = Button(0, "Stop Drums", 370, 155, 100, 20)
    drum_stop_button.method = 'drum_stop_handler'

    // drum boost slider
    var drum_boost = prefs.get('drum_boost', 0)
    drum_boost_slider = Labeled_slider(0, "Drum Boost", 15, 310, 400, 20, 85,
                                       -30, 30, drum_boost, 'linear')
    drum_boost_slider.method = 'drum_boost_handler'


def send_request(obj, event, x, y)
    zmq_send_gno_msg("R", "", 'request_handler')

confirmed = false
waiting_confirmation = false

def request_handler(msg)
    confirmed = false
    waiting_confirmation = t
    pianoroll.refresh(t)


// login should set:
//    server_ip
//    server_port1 
//    server_port2
//    server_port3
//    server_pull_audio_port
//    server_publish_port
//    client_name
//    client_ticket
//
def login():
    // you can put your name and password in a local file "credentials.srp"
    // if found, it will bypass asking you to type your name and password
    var CREDFILE = "credentials.srp"
    var probe = open(CREDFILE, "r")
    if probe
        probe.close()
        autologin = nil
        load "credentials.srp"
        if autologin: // just an extra check that the file was loaded
            get_serverinfo(myname, mypassword)
            return
    while true
        var name = wxs_get_text(
            "Please enter your GlobalNetOrchestra.com User Name " +
            "(not your email address). Type EXIT to quit.", "Login", "", 0)
        if name == "EXIT"
            wxs_message_box("You will not be connected to the network, but " +
                            "you can continue to run in stand-alone mode",
                            "Server info request failed",
                            WXS_STYLE_OK, 0)
            return
        var passwd = wxs_get_text(
            "Please enter your GlobalNetOrchestra.com password " +
            "(sorry this is not hidden, but what's to lose?).", "Login", "", 0)
        if len(name) > 0 and len(passwd) > 0:
            if get_serverinfo(name, passwd)
                return

def set_serverscore(name,passwd,score,maxscore,part,song):
    if(name == nil):
        name = lastname
        passwd = lastpasswd
    if(name == "-"): 
        return false
    if(lastname == nil):
        name = ""
        passwd = ""
        while (len(name) == 0 and len(passwd) == 0):
            name = wxs_get_text(
            "If you wish to log your scores, please enter your GlobalNetOrchestra.com User Name "  + 
                "(not your email address). Type EXIT to not log.", "Login", "", 0)
            if name == "EXIT"
                wxs_message_box("Your scores for this session will not be logged ",
                        "Server info request failed",
                        WXS_STYLE_OK, 0)
                lastname = "-"
                return false
            passwd = wxs_get_text(
                    "Please enter your GlobalNetOrchestra.com password " +
                    "(sorry this is not hidden, but what's to lose?).", "Login", "", 0)
    var request = "reportscore"
    var data = str(score) + "|" + str(maxscore)  + "|" + str(part)  + "|" + str(song)
    var req = Http_post(nil)
    req.add_field("username", name)
    req.add_field("password", passwd)
    req.add_field("request", request)
    req.add_field("data",data)
    req.post("http://globalnetorchestra.org/gnorepscore.php" 
        , "globalnetorchestra.org")
    var done = false
    while not done
        var count = 0
        while count < 150 and not done
            time_sleep(0.1)
            req.poll()
            done = req.state == 'fail' or req.state == 'done'
            count = count + 1
            default_window.set_status("Waiting on server info " +
                                      str(round((150 - count) / 10)))
        if not done:
            if wxs_message_box("Server info has not returned." +
                               "Keep trying? (YES). Press NO to stop.",
                               "Server info timeout",
                               WXS_STYLE_YES_NO, 0) != WXS_MSG_YES:
                return false
    default_window.set_status(DEFAULT_STATUS)
    if req.state == 'fail':
        wxs_message_box("Server info request encountered an error: " +
                        req.error, "Server info error",
                        WXS_STYLE_ERROR, 0)
        return false
    else:
        wxs_message_box("Score Posted: "
                        , "Server info completed",
                        WXS_STYLE_OK, 0)
        lastname = name
        lastpasswd = passwd
    if find(req.data, "error=") >= 0:
        return false
    return true
 

def get_serverinfo(name, passwd):
    var req = Http_post(nil)
    req.add_field("username", name)
    req.add_field("password", passwd)
    req.add_field("request", "serverinfo")
    req.add_field("data", "")
    req.post("http://globalnetorchestra.org/pnoinfo.php"
              ,"globalnetorchestra.org")
    var done = false
    while not done
        var count = 0
        while count < 150 and not done
            time_sleep(0.1)
            req.poll()
            done = req.state == 'fail' or req.state == 'done'
            count = count + 1
            default_window.set_status("Waiting on server info " +
                                      str(round((150 - count) / 10)))
        if not done:
            if wxs_message_box("Server info has not returned." +
                               "Keep trying? (YES). Press NO to stop.",
                               "Server info timeout",
                               WXS_STYLE_YES_NO, 0) != WXS_MSG_YES:
                return // give up, do not set any addresses
    default_window.set_status(DEFAULT_STATUS)
    if req.state == 'fail':
        wxs_message_box("Server info request encountered an error: " +
                        req.error, "Server info error",
                        WXS_STYLE_ERROR, 0)
        return false
    else:
        wxs_message_box("Server info request completed and returned: " +
                        req.data, "Server info completed",
                        WXS_STYLE_OK, 0)
        lastname = name
        lastpasswd = passwd
    if find(req.data, "error=") >= 0:
        return false
    var sp = String_parse(req.data)
    server_ip = sp.get_nonspace()
    server_port1 = sp.get_nonspace()
    server_port2 = sp.get_nonspace()
    server_port3 = sp.get_nonspace()
    server_pull_audio_port = sp.get_nonspace()
    sp.skip_space()
    server_publish_port = sp.get_delimited("|")
    // maybe not a great choice, but non-null client_name signals that
    // we're trying to connect to the server. But we might also get
    // server info here just to know what part we're supposed to practice,
    // in which case, we'll not signal clientnet.srp to connect to the server
    if not stand_alone:
        client_name = name
    sp.skip_over("ticket=")
    client_ticket = int(sp.get_integer())
    client_ticket_as_string = "   " + (chr(client_ticket & 0xFF) +
           chr((client_ticket >>  8) & 0xFF) +
           chr((client_ticket >> 16) & 0xFF) +
           chr((client_ticket >> 24) & 0xFF))
    return true


// create a message and send it via ZMQ unless we
// are not connected, in which case apply msg_handler
// to the message to deliver it locally
def zmq_send_gno_msg(type_code, msg, optional msg_handler)
    msg = type_code + client_ticket_as_string + msg
    # display "zmq_send_gno_msg", type_code, connected, msg_handler
    var ok_to_send = connected
    // if we're not the conductor, don't send Launch commands
    // to server, just send them locally for practice
    if type_code == "L" and not audio_input_flag:
        ok_to_send = false
    if ok_to_send:
        zmq_send(push_audio_sock, msg)
    elif msg_handler: // for local testing
        funcall(msg_handler, msg)


def launch(obj, event, x, y):
    var index = launch_buttons.index(obj)
    display "launch", index, get_server_ms()
    zmq_send_gno_msg("L", 
                     str(index) + " " +
                     str(get_server_ms() + (START_DELAY * 1000)),
                     'handle_launch_event')

def riff_practice_handler(obj, event, x, y)
    riffdisp.start_practice(x)

def set_up_riff_practice()
    riff_practice_checkbox = Checkbox(0, "Riffs/drumcircle practice", 250, 170, 200, 20)
    riff_practice_checkbox.method = 'riff_practice_handler'


def main():
    stand_alone = true
    if wxs_message_box("Stand alone (No) or join a network (Yes)?",
                       "Global Net Orchestra", WXS_STYLE_YES_NO, 0) ==
       WXS_MSG_YES:
        stand_alone = false
        login()
    if audio_input_flag or stand_alone
        client_interface_init() // creates start button
    if stand_alone:
        synth_my_usernum = prefs.get('usernum')
        if not synth_my_usernum:
            wxs_message_box("To practice the right part, go to " +
                            "globalnetorchestra.org, click My account " +
                            "and find the number on your folder under the " +
                            "\"Files\" tab. Enter that number using the " +
                            "Files:Set User Number... menu item of this " +
                            "program's wxWindows/Serpent window.",
                            "Need User Number",
                            WXS_STYLE_OK, 0)
            synth_my_usernum = 0
        set_up_riff_practice()
    rtsched = Scheduler()
    rtsched.init()
    vtsched = Vscheduler(rtsched)
    clientnet_init() // this will start connecting to server unless
                     // stand_alone 
    // zmq_setup() -- see clientnet.srp, connect_action()
    wxs_timer_start(10, 'timer_callback')
    sched_running = t
    rtsched.time_offset = time_get()
    synth_set_ticket(client_ticket)
    chat_init()
    display "start", synth_start(t, audio_input_flag)

main()