[r80]: / trunk / synth / gnotest.srp  Maximize  Restore  History

Download this file

589 lines (509 with data), 21.2 kB

# gnotest.srp - for GNO synth

require "debug"
require "wxserpent"
require "keyboard"
require "slider"
require "prefs"
require "version"
require "chat"

# --- subclass window and make default window with
#     special handler for Quit an close --------------

//class Mainwin (Window):
//    def init(title, x, y, w, h):
//        super.init(title, x, y, w, h)
//
//    def 

default_window.method = 'main_window_event'

play_immediately = true // locally generated notes bypass server

def main_window_event(obj, event, x, y):
    if event == WXS_CLOSE_WINDOW:
        quit() // defined in clientnet.srp or init.srp

default_window.set_size(DEFAULT_WINDOW_WIDTH, DEFAULT_WINDOW_HEIGHT)

prefs = Prefs("GNO")

// if input is not from keyboard, we'll use this flag to
// say we can color the keys based on other input
kbd_is_note_source = false
def keyboard_handler(obj, down, pitch):
    kbd_is_note_source = true
    midi_input.note_on(pitch, text_velocity if down else 0)
    kbd_is_note_source = false


def set_default_keymap():
    the_keymap = []
    for k in "AWSEDFTGYHUJKOLP;'"
        the_keymap.append(ord(k))

keymapping_mode = nil
keymapping_keys = nil
the_keymap = prefs.get('keymap')
if not the_keymap:
    set_default_keymap()
else:
    keymap_as_string = ""
    display "Key mapping is not default", the_keymap,
    for k in the_keymap:
        keymap_as_string = keymap_as_string + chr(k)
    display keymap_as_string

text_pitch = 0
text_velocity = 100
def text_handler(obj, event, x, y):
    var ordx = x // retain the number - is this ever non-8-bit?
    x = chr(x)
    if event == WXS_CHAR:
        // see if it's a confirmation (+) character
        if x == "+":
            waiting_confirmation = false
            confirmed = t
            if pianoroll:
                pianoroll.refresh(t)
            zmq_send_gno_msg("Y", "")
        elif audio_input_flag and semicond:
            // see if it is a semiconductor command
            semicond.handle_char(x)
    if event == WXS_KEYDOWN:
        down = t
        if keymapping_mode and keymapping_keys.last() != ordx:
            keymapping_keys.append(ordx)
            midi_input.note_on(59 + len(keymapping_keys), 100)
            return
    elif event == WXS_KEYUP:
        down = false
        if keymapping_mode:
            midi_input.note_on(59 + len(keymapping_keys), 0)
            if len(keymapping_keys) >= 18:
                keymap_done()
            return
    else:
        return // not a key event we're interested in
    var pos = the_keymap.index(ordx)
    if pos == -1:
        // see if we get a volume control
        pos = find("1234567890", x)
        if pos >= 0:
            text_velocity = (pos * 14) + 1 // 9 * 14 + 1 = 127
            //setuvol(str(x) + ":" + str(text_velocity))
            setuvol(str(x))
        return // not a key designated for playing notes
    var pitch = pos + 60
    midi_input.note_on(pitch, text_velocity if down else 0)

def load_samples(rest ignore):
    resp = synth_read(wxs_file_selector("Select sample file",
                          "../../..", "", ".wav",
                          "WAV files (*.wav)|*.wav|OGG files (*.ogg)|*.ogg",
                          WXS_FILE_OPEN, 0), 200)
    if resp == "OK":
        wxs_message_box("New samples loaded", "Status", WXS_STYLE_OK, 0)
    else
        wxs_message_box("Error: " + resp, "Status", WXS_STYLE_ERROR, 0)

def create_state_strings(i):
    allon = chr(60 + i % 18) + chr(30)
    alloff = chr(60) + chr(0)
    while len(allon) < 100: // each iteration doubles the strings
        allon = allon + allon
        alloff = alloff + alloff
    // we want the final length to be 200
    allon = allon + subseq(allon, 0, 200 - len(allon))
    alloff = alloff + subseq(alloff, 0, 200 - len(alloff))


def performance_test(rest ignore):
    default_window.show_status(t)
    for i = 0 to 100:
        default_window.set_status("loading samples " + str(i + 1) + " of 100")
        synth_read_to("samples.wav", 1, i)
    default_window.set_status("playing some notes")
    for i = 0 to 180: // 180 * 0.3 = 54s, 54 + extra 5s tone -> 59s runtime
        create_state_strings(i)
        synth_set_state(allon, len(allon))
        time_sleep(0.2)
        synth_set_state(alloff, len(alloff))
        time_sleep(0.1)
    create_state_strings(0)
    synth_set_state(allon, len(allon))
    time_sleep(5.1)
    synth_set_state(alloff, len(alloff))
    default_window.set_status("performance test complete")


def help_handler(obj, event, x, y):
    if x == HELP_SAMPLES_ITEM:
        help_samples()
    elif x == HELP_PERFORMANCE_ITEM:
        help_performance()

def keymap_start():
    wxs_message_box("You will now play a chromatic scale from C4 to F5." +
                    "The keys you type will replace the current keys AWSEDF...",
                    "Start Key Mapping", WXS_STYLE_OK, 0)
    keymapping_keys = []
    keymapping_mode = true

def keymap_done():
    if wxs_message_box("Accept the keys just played as the key assignment?",
                       "Finish Key Mapping", WXS_STYLE_YES_NO, 0) == WXS_MSG_YES:
        the_keymap = keymapping_keys
        prefs.set('keymap', the_keymap)
        prefs.save()
    keymapping_keys = []
    keymapping_mode = false
    
def file_handler(obj, event, x, y):
    display "file_handler", obj, event, x, y
    if x == FILE_EXIT_ITEM:
        quit()
    elif x == FILE_LOAD_ITEM:
        load_samples()
    elif x == FILE_PERFORMANCE_ITEM:
        performance_test()
    elif x == FILE_IMMEDIATE_ITEM:
        play_immediately = (y and (y != 0))
        // tell synth whether to use local or server state for synth_index
        display "synth_set_my_state", not play_immediately
        synth_set_my_state(my_pitch, my_velocity, not play_immediately)
    elif x == FILE_SET_INDEX:
        display "file_handler, FILE_SET_INDEX", synth_myindex
        if type(synth_myindex) != 'Integer':
            synth_myindex = 0
        synth_myindex = wxs_get_number("Sample index (assigned = " +
                                       str(synth_my_real_index),
            "Index",
            "Set Sample Index", synth_myindex, 0, 120, 0)
        update_volume() // transfer synth_myindex to synth
    elif x == FILE_SPECIAL_ITEM:
        var ins = stdin.readvalue()
        display "ignore", ins
        if ins == 'gnc':
            audio_input_selection = Audio_menu(nil)
        else
            prefs.prefs.remove('audio_default_input')
            prefs.save()
    elif x == FILE_TIME_ITEM:
        var local_time = time_get()
        print "Local time:", local_time
        print "Local ms:", local_time * 1000
        print "rtsched.time:", rtsched.time
        print "now_ms:", now_ms
        print "Estimated Server Time:", get_server_time()
        print "Estimated Server ms:", get_server_ms()
        print "server_time_offset:", server_time_offset
        print "synchronized:", "true" if synchronized else "false"
        print "Next clock sync time:", clock_sync_time
        print "Next sync in:", clock_sync_time - rtsched.time
    elif x == FILE_REMAP_ITEM:
        keymap_start()
    elif x == FILE_RESETMAP_ITEM:
        set_default_keymap()
    elif x == FILE_SET_USERNUM:
        var usernum = wxs_get_number("Enter your user number",
                                 "User number", "User number", synth_my_usernum, 0, 999, 0)
        if usernum >= 0 and usernum != synth_my_usernum:
            prefs.set('usernum', usernum)                            
            prefs.save()
            synth_my_usernum = usernum
        // else the user clicked 'cancel' and wxs_get_number returned -1
            
def help_win_handler(obj, event, x, y):
    if event == WXS_CLOSE_WINDOW:
        help_window = nil
        for item in help_window_items:
            item.delete()
        help_window_items = []
        help_y = 5
        return nil // means close the window now

help_window = nil
help_window_items = []
help_vspace = 17
help_y = 5


synth_volume = -18.0
synth_myvolume = -6.0
synth_voice = -10.0
synth_myindex = 0
synth_my_real_index = 0
synth_my_usernum = 0
orch_revb_db = -6
my_revb_db = -24

def vol_handler(slider, vol_db):
    synth_volume = vol_db
    update_volume()
def myvol_handler(slider, vol_db):
    synth_myvolume = vol_db
    update_volume()
def voice_handler(slider, vol_db):
    synth_voice = vol_db
    update_volume()
def update_volume()
    display "update", synth_volume, exp(log_of_10_over_20 * synth_volume)
    synth_set_volume(exp(log_of_10_over_20 * synth_volume),
                     exp(log_of_10_over_20 * synth_myvolume),
                     synth_myindex,
                     exp(log_of_10_over_20 * synth_voice))
    prefs.set('synth_volume', synth_volume)
    prefs.set('synth_myvolume', synth_myvolume)
    prefs.set('synth_voice', synth_voice)
    prefs.save()
    
def orch_revb_handler(slider, db)
    orch_revb_db = db
    update_reverb()
def my_revb_handler(slider, db)
    my_revb_db = db
    update_reverb()
def update_reverb()
    synth_set_reverb(exp(log_of_10_over_20 * orch_revb_db),
                     exp(log_of_10_over_20 * my_revb_db))
    prefs.set('orch_revb_db', orch_revb_db)
    prefs.set('my_revb_db', my_revb_db)

def make_help_window():
    var save_window = help_window
    help_win_handler(nil, WXS_CLOSE_WINDOW, nil, nil) // clean up old window
    // but the window if any is not deleted; now, we restore help_window
    help_window = save_window
    // we really do need a window, so if it does not exist, create it
    if not help_window:
        help_window = Window("Help", 100, 300, 400, 300)
        help_window.method = 'help_win_handler'
        help_window.set_color("GOLDENROD")


def add_help_text(text, x):
    help_window_items.append(Statictext(help_window, text, x, help_y, 400, 20))
    help_y = help_y + help_vspace

def help_samples():
    make_help_window()
    add_help_text("Test your sample file.", 5)
    add_help_text("", 5) // blank line
    add_help_text("Make a chromatic scale of ", 5)
    add_help_text("exactly 18 notes,", 30)
    add_help_text("exactly 5 seconds per note,", 30)
    add_help_text("mono (not stereo),", 30)
    add_help_text("44100 Hz, 16-bit,", 30)
    add_help_text(".wav format", 30)
    add_help_text("Use the File:Load Samples... menu item to load the file.", 5)
    add_help_text("Then try playing your samples.", 5)

def help_performance():
    make_help_window()
    add_help_text("Test performance on your machine.", 5)
    add_help_text("", 5) // blank line
    add_help_text("Use the File:Test Performance menu item to start.", 5)
    add_help_text("It may take some time to load 800MB of samples.", 5)
    add_help_text("After loading, sounds will play for 60 seconds.", 5)
    add_help_text("If the sound is choppy during the test, ", 5)
    add_help_text("close as many applications as possible and try again.", 20)
    add_help_text("If the program reports an error or there are many", 5)
    add_help_text("pauses in the sound, you probably need more memory.", 20)
    add_help_text("You might also want to look at CPU utilization", 5)
    add_help_text("while playing -- it should be less than 50%.", 20)


#evaluate = Evaluate(0, 0, 0, 400, 100)

default_window.set_rgb(220, 220, 220)

keyboard = Keyboard(0, KBD_X, KBD_Y, 195, 80)
keyboard.octave_offset = 5
keyboard.method = 'keyboard_handler'

if VERBOSE_GUI:
    text1 = Statictext(0, "Click on keys above, or...", 5, 115, 200, 20)
 
if VERBOSE_GUI:
    text2 = Statictext(0,
        "Click in the box below and type AWSEDFTGYHUJKOLP;' to play notes.",
        5, 150, 400, 20)
 
# maybe if we make this first, we'll get key events here
textpanel = Canvas(0, PLAYBOX_X, PLAYBOX_Y, PLAYBOX_W, PLAYBOX_H)
textpanel.set_color("CADET BLUE")
textpanel.method = 'text_handler'
 
if VERBOSE_GUI:
    text3 = Statictext(0, "Use Help menu for further instructions",
                       5, 215, 400, 20)

synth_volume = prefs.get('synth_volume', -18.0)
vol_slider = Labeled_slider(0, "Volume (dB)", 15, 210, 400, 20, 85, -120, 0, synth_volume, 'db')
vol_slider.method = 'vol_handler'

synth_myvolume = prefs.get('synth_volume', -6.0)
myvol_slider = Labeled_slider(0, "My Volume", 15, 230, 400, 20, 85, -120, 0, synth_myvolume, 'db')
myvol_slider.method = 'myvol_handler'

synth_voice = prefs.get('synth_volume', -10.0)
voice_slider = Labeled_slider(0, "Voice (dB)", 15, 250, 400, 20, 85, -120, 0, synth_voice, 'db')
voice_slider.method = 'voice_handler'

update_volume()

orch_revb_db = prefs.get('orch_revb_db', -18.0)
orch_revb = Labeled_slider(0, "Orch Reverb", 15, 270, 400, 20, 85, -120, 0, orch_revb_db, 'db')
orch_revb.method = 'orch_revb_handler'

my_revb_db = prefs.get('my_revb_db', -24.0)
my_revb = Labeled_slider(0, "My Reverb", 15, 290, 400, 20, 85, -120, 0, my_revb_db, 'db')
my_revb.method = 'my_revb_handler'

update_reverb()


help_menu = Menu(0, "Help")
HELP_SAMPLES_ITEM = help_menu.item("Load Samples Help",
                                   "Get help on loading samples")
HELP_PERFORMANCE_ITEM = help_menu.item("Performance Test Help",
                                       "Get help on performance testing")
help_menu.method = 'help_handler'

file_menu = Menu(0, "File")
FILE_EXIT_ITEM = 0 // built-in

FILE_LOAD_ITEM = file_menu.item("Load Samples...",
                                "Load samples from a file")

FILE_PERFORMANCE_ITEM = file_menu.item("Performance Test",
                                       "Test system performance")

FILE_IMMEDIATE_ITEM = file_menu.item("Play local notes immediately",
         "Play notes immediately (or wait until they are returned from the server)",
         t)
file_menu.set_and_act(FILE_IMMEDIATE_ITEM, t)

FILE_SET_INDEX = file_menu.item("Select Voice",
                                "Choose sample set index")
FILE_SPECIAL_ITEM = file_menu.item("Ignore", "Please ignore this item")

FILE_TIME_ITEM = file_menu.item("Show Times", "Debugging info on time")

FILE_REMAP_ITEM = file_menu.item("Reassign Keyboard", "Choose key for each pitch")

FILE_RESETMAP_ITEM = file_menu.item("Default Key Assignments", "Reset default key for each pitch")

FILE_SET_USERNUM = file_menu.item("Set user number...",
                                  "Set your user number -- used to select parts")

file_menu.method = 'file_handler'

wxs_set_about(
   "Global Net Orchestra Sample File and Audio Performance Tester - Version " +
   serpent_version)

wxs_message_box("About to load samples. This may take a minute." +
                " Program will be unresponsive while loading",
                "Notice", WXS_STYLE_OK, 0)
display "read", synth_read("sample-set.ogg", 200)
audio_talk = nil


def adi_trim(str, pos): // remove leading spaces after pos
    while pos < len(str) and isspace(str[pos]):
        pos = pos + 1
    return subseq(str, pos)

def adi_parse(str): // parse string of form "api ; name ; ins ; outs"
    var pos = find(str, ";") + 1
    if pos > 0:
        pos = find(str, ";", pos) + 1
        if pos > 0:
            str = adi_trim(str, pos)
            var ins = int(str)
            pos = find(str, ";") + 1
            if pos > 0:
                str = adi_trim(str, pos)
                var outs = int(str)
                return [ins, outs]
    return nil


class Audio_menu:
    var out_flag
    var prefname
    var menu
    var devices
    var default_device
    var default_device_num

    def init(out):
        prefname = 'audio_default_output' if out else 'audio_default_input'
        default_device = prefs.get(prefname)
        default_device_num = nil // none unless we find one from prefs
        out_flag = out
        menu = Menu(0, "Audio Device" if out else "Audio Input")
        menu.target = this
        menu.method = 'handler'
        devices = {}
        var device = 0
        var device_info = audio_get_device_info(device)
        while device_info != ""
            display "listing audio devices", device_info
            // see if the device supports input (1 chan) or output (2 chan)
            var ins_outs = adi_parse(device_info)
            if ins_outs and ins_outs[1 if out else 0] >= (2 if out else 1):
                var item_num = menu.item(device_info, 
                               "Set audio device in preferences")
                devices[item_num] = device_info
                if default_device == device_info
                    default_device_num = device
            device = device + 1
            device_info = audio_get_device_info(device)
        if not out:
            item_num = menu.item("None", "Remove audio input")
            devices[item_num] = "None"
        if default_device_num:
            if out:
                audio_set_output_device(default_device_num)
                print "AUDIO OUTPUT DEVICE:", default_device
            else:
                audio_set_input_device(default_device_num)
                print "AUDIO INPUT DEVICE:", default_device

    def handler(obj, event, x, y):
        if devices.has_key(x)
            var device = devices[x]
            wxs_message_box("When you restart this application, " +
                "your audio device will be " + device, 
                "Status", WXS_STYLE_OK, 0)
            if device == "None":
                device = nil
            prefs.set(prefname, device)
            prefs.save()
        
audio_output_selection = Audio_menu(t)
audio_input_flag = prefs.get('audio_default_input')
display "audio_setup", audio_input_flag, type(audio_input_flag)
if audio_input_flag:
    audio_input_selection = Audio_menu(nil)

########### MIDI INPUT #############

midi_status_noteoff = 0x80
midi_status_noteon = 0x90

class Midi_input:
    var midi_menu
    var midi_in_num
    var midi_in
    var cur_pitch

    def init(win):
        cur_pitch = 60
        midi_menu = Menu(win, "Midi Device")
        midi_menu.target = this
        midi_menu.method = 'midi_handler'
        midi_menu.item("-1 No MIDI Input", "Do not open MIDI input")
        var n = midi_count_devices()
        for i = 0 to n:
            var info = midi_get_device_info(i)
            if info[2]: // is an input
                midi_menu.item(str(i) + " " + info[0] + " : " + info[1],
                               "Select this MIDI device for input", nil)
            print i, info
        midi_in_num = prefs.get('midi_in_device', -1)
        midi_in = nil
        if midi_in_num >= 0:
            midi_in = midi_create()
            if midi_open_input(midi_in, midi_in_num, 100) != 0:
                midi_close(midi_in)
                midi_in = nil
                wxs_message_box("Could not open MIDI Input device " +
                                str(midi_in_num) + ". Try selecting another " +
                                "device using the Midi Device menu.",
                                "Warning", WXS_STYLE_ERROR, 0)

    def midi_handler(obj, event, x, y):
        var selection = obj.item_string(x)
        if selection:
            var devno = int(selection)
            wxs_message_box("When you restart this application, your MIDI " +
                            "INPUT device will be " + selection,
                            "Status", WXS_STYLE_OK, 0)
            prefs.set('midi_in_device', devno)
            prefs.save()

    def poll_for_input()
        if midi_in
            var msg = midi_read(midi_in)
            if msg:
                msg = msg[1] // ignore timestamp
                var status = ord(msg[0])
                var cmd = status & 0xF0
                var data1 = ord(msg[1])
                var data2 = ord(msg[2])
                if cmd == midi_status_noteon:
                    note_on(data1, data2)
                elif cmd == midi_status_noteoff:
                    note_on(data1, 0)
            
    // this is a bit tricky -- translate on/off messages to final
    // state; e.g. if there are overlapping notes, the state matches
    // the most recent note-on, so need to ignore note-off; otherwise,
    // the note-off on the old note will cut off the current note
    def note_on(pitch, vel):
        while pitch < 60:
            pitch = pitch + 12
        while pitch > 77:
            pitch = pitch - 12
        // compress midi velocity part way to 127
        if vel > 0:
            vel = vel + idiv(127 - vel, 3)
        vel = min(127, max(0, vel))
        if vel == 0 and cur_pitch != pitch:
            return // ignore note off on other pitch
        if not kbd_is_note_source:
            // in case we're just changing pitch and missed note
            // off, turn off current pitch
            keyboard.highlight_key(cur_pitch, false, "CADET BLUE")
            keyboard.highlight_key(pitch, vel > 0, "CADET BLUE")
        synth_set_my_state(pitch, vel, not play_immediately)
        if riffdisp and riffmode:
            riffdisp.note_on(pitch, vel)
        my_note_timestamp = int(get_server_time() * 1000)
        my_velocity = vel
        my_pitch = pitch
        cur_pitch = pitch
        if pianoroll and not riffmode:
            if vel > 0:
                pianoroll.setCurrentUserPitch(pitch)
            else:
                pianoroll.unsetCurrentUserPitch(pitch)            


midi_input = Midi_input(default_window)