
The task is to implement a   metronome.

The metronome should be capable of producing high and low audio beats, accompanied by a visual beat indicator, and the beat pattern and tempo should be configurable.

For the purpose of this task, it is acceptable to play sound files for production of the beat notes, and an external player may be used.

However, the playing of the sounds should not interfere with the timing of the metronome.

The visual indicator can simply be a blinking red or green area of the screen (depending on whether a high or low beat is being produced), and the metronome can be implemented using a terminal display, or optionally, a graphical display, depending on the language capabilities.

If the language has no facility to output sound, then it is permissible for this to implemented using just the visual indicator.




--
-- demo\rosetta\virtunome.exw
--
--  Originally by ghaberek
--  Translated from win32lib by Pete Lomax
--
--  I will note that accuracy drops sharply above 5 beats per second.
--
with javascript_semantics   -- needs some work though, usual sizing stuff [DEV]
                            -- NB: don't break Morse_code.exw when fixing this!
include pGUI.e
Ihandle dlg, frame_1, radio,
             frame_2, val_lbl,
             frame_3, bpm_lbl, spb_lbl, act_lbl, acc_lbl,
             onbtn, onoff, timer
sequence notes

function rle_decode_image(sequence data)
-- (not my best work, may benefit from a rethink...)
    sequence img = {}
    for i=1 to length(data) do
        sequence rle = data[i], line = {}, val
        integer rpt = rle[1]
        for j=2 to length(rle)-1 by 2 do
            integer RGB = rle[j],
                    count = rle[j+1]
            if RGB=-1 then
                string RGBs = IupGetGlobal("DLGBGCOLOR")
                {val} = scanf(RGBs,"%d %d %d")
            elsif RGB=0 then
                val = repeat(0,3)
            else
                ?9/0
            end if
            val &= #FF
            line &= flatten(repeat(val,count))
        end for
        img &= flatten(repeat(line,rpt))
    end for
    return img
end function

constant 
Whole_note = {
    {13,-1,32},
    {1,-1,12,0,9,-1,11},
    {1,-1,10,0,4,-1,3,0,6,-1,9},
    {1,-1,9,0,5,-1,4,0,6,-1,8},
    {1,-1,8,0,5,-1,6,0,5,-1,8},
    {2,-1,8,0,5,-1,6,0,6,-1,7},
    {1,-1,8,0,6,-1,5,0,6,-1,7},
    {1,-1,8,0,6,-1,5,0,5,-1,8},
    {1,-1,9,0,6,-1,3,0,5,-1,9},
    {1,-1,11,0,11,-1,10},
    {9,-1,32},
},
Half_note = {
    {30,-1,21,0,1,-1,10},
    {1,-1,14,0,8,-1,10},
    {1,-1,12,0,10,-1,10},
    {1,-1,11,0,6,-1,3,0,2,-1,10},
    {1,-1,10,0,5,-1,5,0,2,-1,10},
    {1,-1,10,0,4,-1,5,0,3,-1,10},
    {1,-1,10,0,3,-1,5,0,4,-1,10},
    {1,-1,10,0,2,-1,5,0,5,-1,10},
    {1,-1,10,0,2,-1,3,0,6,-1,11},
    {1,-1,10,0,10,-1,12},
    {1,-1,11,0,7,-1,14}
},
Eigth_note = {
    {2,-1,17,0,1,-1,14},
    {2,-1,17,0,2,-1,13},
    {1,-1,17,0,3,-1,12},
    {1,-1,17,0,4,-1,11},
    {1,-1,17,0,5,-1,10},
    {1,-1,17,0,6,-1,9},
    {1,-1,17,0,1,-1,1,0,5,-1,8},
    {1,-1,17,0,1,-1,3,0,4,-1,7},
    {1,-1,17,0,1,-1,4,0,4,-1,6},
    {1,-1,17,0,1,-1,5,0,3,-1,6},
    {1,-1,17,0,1,-1,6,0,3,-1,5},
    {1,-1,17,0,1,-1,7,0,2,-1,5},
    {1,-1,17,0,1,-1,7,0,3,-1,4},
    {2,-1,17,0,1,-1,8,0,2,-1,4},
    {4,-1,17,0,1,-1,9,0,1,-1,4},
    {3,-1,17,0,1,-1,8,0,1,-1,5},
    {6,-1,17,0,1,-1,14},
    {1,-1,11,0,7,-1,14},
    {1,-1,9,0,9,-1,14},
    {1,-1,8,0,10,-1,14},
    {1,-1,7,0,11,-1,14},
    {2,-1,6,0,12,-1,14},
    {2,-1,6,0,11,-1,15},
    {1,-1,6,0,10,-1,16},
    {1,-1,7,0,7,-1,18},
    {1,-1,9,0,2,-1,21}
},
Quarter_note = {
    {30,-1,21,0,1,-1,10},
    {1,-1,15,0,7,-1,10},
    {1,-1,13,0,9,-1,10},
    {1,-1,12,0,10,-1,10},
    {1,-1,11,0,11,-1,10},
    {2,-1,10,0,12,-1,10},
    {2,-1,10,0,11,-1,11},
    {1,-1,10,0,10,-1,12},
    {1,-1,11,0,7,-1,14},
    {1,-1,13,0,2,-1,17}
},
Sixteenth_note = {
    {2,-1,17,0,1,-1,14},
    {2,-1,17,0,2,-1,13},
    {1,-1,17,0,3,-1,12},
    {1,-1,17,0,4,-1,11},
    {1,-1,17,0,5,-1,10},
    {1,-1,17,0,6,-1,9},
    {1,-1,17,0,7,-1,8},
    {1,-1,17,0,2,-1,2,0,4,-1,7},
    {1,-1,17,0,2,-1,3,0,4,-1,6},
    {1,-1,17,0,2,-1,4,0,3,-1,6},
    {1,-1,17,0,3,-1,4,0,3,-1,5},
    {1,-1,17,0,4,-1,4,0,2,-1,5},
    {1,-1,17,0,5,-1,3,0,3,-1,4},
    {1,-1,17,0,6,-1,3,0,2,-1,4},
    {1,-1,17,0,7,-1,2,0,2,-1,4},
    {1,-1,17,0,1,-1,2,0,5,-1,1,0,2,-1,4},
    {1,-1,17,0,1,-1,4,0,6,-1,4},
    {1,-1,17,0,1,-1,5,0,5,-1,4},
    {1,-1,17,0,1,-1,6,0,4,-1,4},
    {2,-1,17,0,1,-1,8,0,2,-1,4},
    {1,-1,17,0,1,-1,9,0,1,-1,4},
    {4,-1,17,0,1,-1,9,0,2,-1,3},
    {2,-1,17,0,1,-1,9,0,1,-1,4},
    {1,-1,11,0,7,-1,8,0,2,-1,4},
    {1,-1,9,0,9,-1,8,0,1,-1,5},
    {1,-1,8,0,10,-1,8,0,1,-1,5},
    {1,-1,7,0,11,-1,14},
    {2,-1,6,0,12,-1,14},
    {2,-1,6,0,11,-1,15},
    {1,-1,6,0,10,-1,16},
    {1,-1,7,0,7,-1,18},
    {1,-1,9,0,2,-1,21}
}

integer note = 4 -- quarter initially
atom vLastTime = 0.0 -- for time resolution

constant
    -- in bpm
    MIN_TEMPO   =   1,
    DEF_TEMPO   =  90,
    MAX_TEMPO   = 200

integer vMSPB = 667     -- default milliseconds per beat

constant Tempos = {"Grave", "Largo", "Adagio", "Lento", "Adante", "Moderato",
                   "Allegretto", "Allegro", "Presto", "Vivance", "Prestissimo"}

function set_tempo(integer pBPM, atom pNote)
-- returns tempo index
    integer index = floor(((length(Tempos)-1)*pBPM)/(MAX_TEMPO-MIN_TEMPO))+1
    atom lSPB = 60 / pBPM / pNote   -- seconds per beat
    vMSPB = floor( lSPB * 1000 )

    IupSetStrAttribute(spb_lbl,"TITLE","%.2f",{lSPB})
    IupSetInt(timer,"TIME",vMSPB)
    if IupGetInt(timer,"RUN") then
        -- restart needed to apply new TIME (not doc?)
        IupSetInt(timer,"RUN",false)
        IupSetInt(timer,"RUN",true)
    end if
    return index
end function

procedure tempo_change()
    integer lBPM = IupGetInt(val_lbl,"TITLE"),
            lIndex = set_tempo(lBPM, note/4)
    IupSetStrAttribute(frame_2, "TITLE", "Tempo: %s ", {Tempos[lIndex]})
    vLastTime = time()
end procedure

function toggle_state_cb(Ihandle ih, integer state)
    if state then
        note = power(2,find(ih,notes)-1) -- 1/2/4/8/16
        tempo_change()
    end if
    -- and shift focus away, since it looks ugly w/o any text
    IupSetFocus(onbtn)
    return IUP_DEFAULT
end function

function valuechanged_cb(Ihandle val)
    integer v = IupGetInt(val,"VALUE")
    IupSetInt(val_lbl,"TITLE",v)
    IupSetStrAttribute(bpm_lbl,"TITLE","%.2f",{v})
    tempo_change()
    return IUP_DEFAULT
end function

include builtins\beep.e

function timer_cb(Ihandle /*ih*/)
    beep(#200,20)

    atom lThisTime = time()
    if vLastTime > 0.0 then
        atom lDiff = (lThisTime - vLastTime),
             lResolution = ((lDiff * 1000)/ vMSPB) * 100
        IupSetStrAttribute(act_lbl, "TITLE", "%0.2f", {lDiff})
        IupSetStrAttribute(acc_lbl, "TITLE", "%d%%", {lResolution})
    end if
    vLastTime = lThisTime

    return IUP_DEFAULT
end function

function button_cb(Ihandle ih)
    bool active = not IupGetInt(timer,"RUN")
    IupSetInt(timer,"RUN",active)
    IupSetAttribute(ih,"TITLE",{"Off","On"}[active+1])
    return IUP_DEFAULT
end function

IupOpen()

notes = {IupImageRGBA(32, 32, rle_decode_image(Whole_note)),
         IupImageRGBA(32, 40, rle_decode_image(Half_note)),
         IupImageRGBA(32, 40, rle_decode_image(Quarter_note)),
         IupImageRGBA(32, 40, rle_decode_image(Eigth_note)),
         IupImageRGBA(32, 40, rle_decode_image(Sixteenth_note))}

sequence btns = {}
for i=1 to length(notes) do
    Ihandle btn = IupToggle(NULL, Icallback("toggle_state_cb"), "CANFOCUS=NO"),
            lbl = IupLabel()
    IupSetAttributeHandle(lbl,"IMAGE",notes[i])
    btns &= {btn,lbl}
    notes[i] = btn
end for
radio = IupRadio(IupHbox(btns,"GAP=20"))
frame_1 = IupFrame(radio,"MARGIN=20x10")
IupSetAttribute(frame_1,"TITLE","Note ")

val_lbl = IupLabel(" 200","ALIGNMENT=ARIGHT")
Ihandle val = IupValuator("HORIZONTAL","EXPAND=HORIZONTAL, CANFOCUS=NO")
IupSetInt(val,"MIN",MIN_TEMPO)
IupSetInt(val,"MAX",MAX_TEMPO)
IupSetInt(val,"VALUE",DEF_TEMPO)
IupSetCallback(val, "VALUECHANGED_CB", Icallback("valuechanged_cb"))

frame_2 = IupFrame(IupHbox({val_lbl,val}),`TITLE="Tempo: "`)

bpm_lbl = IupLabel("90.00","ALIGNMENT=ARIGHT, EXPAND=HORIZONTAL")
act_lbl = IupLabel("0.00","ALIGNMENT=ARIGHT, EXPAND=HORIZONTAL")
spb_lbl = IupLabel("0.67","ALIGNMENT=ARIGHT, EXPAND=HORIZONTAL")
acc_lbl = IupLabel("0%","ALIGNMENT=ARIGHT, EXPAND=HORIZONTAL")

frame_3 = IupFrame(IupHbox({IupVbox({IupHbox({IupLabel("Beats Per Minute:"),bpm_lbl}),
                                     IupHbox({IupLabel("Seconds Per Beat:"),spb_lbl})},
                                    "GAP=10,MARGIN=10x0"),
                            IupVbox({IupHbox({IupLabel("Actual Seconds Per Beat:"),act_lbl}),
                                     IupHbox({IupLabel("Accuracy:"),acc_lbl})},
                                    "GAP=10,MARGIN=10x0")}),
                   `TITLE="Statistics ",MARGIN=4x8`)

onbtn = IupButton("On",Icallback("button_cb"),"PADDING=30x0")
onoff = IupHbox({IupFill(),onbtn},"MARGIN=0x20")

dlg = IupDialog(IupVbox({frame_1,
                         frame_2,
                         frame_3,
                         onoff}, "MARGIN=10x5, GAP=5"),
--              `TITLE="Virtunome",RASTERSIZE=500x330`)
                `TITLE="Virtunome"`)

IupShow(dlg)

-- The TIME and RUN attributes are set dynamically:
timer = IupTimer(Icallback("timer_cb"), vMSPB, active:=false)

IupSetInt(val_lbl,"TITLE",DEF_TEMPO)
IupSetAttributeHandle(radio,"VALUE",btns[5])
tempo_change()
if platform()!=JS then
    IupMainLoop()
    IupClose()
end if
