Community discussions

MikroTik App
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3447
Joined: Sun May 01, 2016 7:12 pm
Location: California

$PIANO - interactive "player piano" & studio-quality recorder using :beep

Sun Feb 18, 2024 4:47 am

Another thread gave me an idea for a "interactive piano" using /terminal/inkey. It got more complex than I intended... But essentially everything that's played is saved to an array & played. A status bar shows the current octave, note length, last note played — even a "tape counter" for recording length. The help should explain most things, so here is a screenshot:

Image

To use it, you need to either cut-and-paste the following script, or add it to /system/script etc as desired. To invoke the piano, you just use $PIANO at the CLI (after loading the script). After quit via "q", a valid script will be output to play it back. Obviously this all requires a RouterOS device with a "beeper" (e.g. :beep does something) – most ARM devices do NOT have one. I'll add more examples later, but add a comment if you find any bug or have suggestions.
# version 1.2

:global PIANO do={
    # required for recussion later
    :global PIANO
    # default note time is 200ms for 1/8 note (1x) 
    :local nms 200ms
    # change note  length by arg using $PIANO ms=250ms
    :if ([:typeof $ms]="str") do={:set nms [:totime "0.$ms"]}
    :if ([:typeof $ms]="time") do={:set nms $ms}
    # or, use BPM via bpm= to control the length of 1/4 note
    :local lbpm (60000 / (([:tonsec [:totime $nms]] / 1000000) * 2))
    :if ([:typeof $bpm]~"(num|str)") do={
        :set lbpm [:tonum $bpm]
        :set nms [:totime "0.$(60000 / $bpm / 2)"]
    }
    # handle silent=yes (for output recording without playing)
    :local lsilent "no"
    :if ($silent="yes") do={ :set lsilent "yes" }
    # handle 'as-value' to return array, instead of output script
    :local asvalue 0
    :if ([:tostr $1]="as-value") do={:set asvalue 1}

    # array map of keypress to the Hz values for octaves 1 to 9
    #   note: k o l are +1 octave, so those are shifted by 1
    :local scalearr {"a"=("C",33,65,131,262,523,1047,2093,4186,8372) ; 
    "w"=("C#",35,69,139,277,554,1109,2217,4435,8870) ;
    "s"=("D",37,73,147,294,587,1175,2349,4699,9397) ;
    "e"=("D#",39,78,156,311,622,1245,2489,4978,9956) ;
    "d"=("E",41,82,165,330,659,1319,2637,5274,10548) ;
    "f"=("F",44,87,175,349,698,1397,2794,5588,11175) ;
    "t"=("F#",46,92,185,370,740,1480,2960,5920,11840) ; 
    "g"=("G",49,98,196,392,784,1568,3136,6272,12544) ;
    "y"=("G#",52,104,208,415,831,1661,3322,6645,13290) ;
    "h"=("A",55,110,220,440,880,1760,3520,7040,14080) ;
    "u"=("A#",58,117,233,466,932,1865,3729,7459,14917) ;
    "j"=("B",62,123,247,494,988,1976,3951,7902,15804) ;
    "k"=("+C",65,131,262,523,1047,2093,4186,8372,16744) ; 
    "o"=("+C#",139,277,554,1109,2217,4435,8870,17739) ;
    "l"=("+D",73,147,294,587,1175,2349,4699,9397,18795) ;
    }
    # script needs to map numeric ASCII keycode to a string type with letter
    :local asciimap {"";"";"";"";"";"";"";"";"back";"";"tab";"";"";"enter";"return";"";"";"";"";"";"";"";"";"";"";"";"";"ESC";"";"";"";"";"space";"!";"\"";"";"\$";"%";"&";"";"(";")";"*";"+";",";"-";".";"/";"0";"1";"2";"3";"4";"5";"6";"7";"8";"9";":";";";"<";"=";">";"\?";"@";"A";"B";"C";"D";"E";"F";"G";"H";"I";"J";"K";"L";"M";"N";"O";"P";"Q";"R";"S";"T";"U";"V";"W";"X";"Y";"Z";"[";"\\";"]";"^";"_";"`";"a";"b";"c";"d";"e";"f";"g";"h";"i";"j";"k";"l";"m";"n";"o";"p";"q";"r";"s";"t";"u";"v";"w";"x";"y";"z";"{";"|";"}";"~";"delete"}
    # current note size in ms - can be adjusted using 1-8 keys while playing
    :local lnms $nms
    # current octave, default is 4
    :local octave 4
    # current "eighth"
    :local neighth 1 
    # store if "recording" and notes played
    :local record 1
    :local played [:toarray ""] 
    # ...recording stopped when $record is set to 0, on with 1
    #    notes are pushed to a array "list" 
    #    with each element in "outer" list being a list of two values:
    #    ($freq,$lnms) e.g. {(440,125),(440,125)}

    # helper function to format note as C3
    :local getnotename do={
        :local notename $2
        :if ([:len $2] != 0) do={
            :if ([:pick $notename 0 1]="+") do={
                # used higher octave keys like j i k
                :set notename "$[:pick $notename 1 8]$($1 + 1)"
            } else={
                :set notename "$notename$[:tostr $1]"
            }
        } else={:set notename ""}
        :return $notename
    }
    # helper function to print status line on update
    :local printstatus do={
        :local reconoff ""
        :local reccount ""
        # recording ON or OFF
        :if ([:tonum $3]!=0) do={:set reconoff "\1B[1;35mON "} else={:set reconoff "\1B[2;35mOFF"}
        # pretty display of record counter (length of played)
        :for lrec from=1 to=(4-[:len [:tostr [:len $4]]]) do={:set reccount "0$reccount"}
        :set reccount "$reccount$[:tostr [:len $4]]"
        :local notename $7
        # replace last status line, with new status line
        /terminal cuu
        :local notelenstr "$6/8"
        :if ("$6" = "4") do={:set notelenstr "1/2"}
        :if ("$6" = "2") do={:set notelenstr "1/4"}
        :if ("$6" = "6") do={:set notelenstr "3/4"}
        :if ("$6" = "8") do={:set notelenstr " 1 "}
        :put "\t\1B[1;34m$[:pick $2 7 16]s\1B[0m   \1B[2;31mOCTAVE\1B[0m \1B[1;31m$1\1B[0m  \1B[1;34m$notelenstr\1B[0m   \1B[1;35m$reccount\1B[0m \1B[2;35mRECORD\1B[0m $reconoff\1B[0m  \1B[1;7m$notename\1B[0m \1B[1;1m$5\1B[0m      "
    }

    # IF \$PIANO is called with a play=$myrecording, play that and exit
    :if ([:typeof $1]="array") do={
        # optional: store the saved recording, so it's returned again
        # :set played $play
        :set played $1
        :put "" 
        :put " # SCRIPT TO PLAY RECORDING"
        :foreach rnote in=$played do={
            :if ([:typeof ($rnote->0)]="num" && [:typeof ($rnote->1)]~"(time|num)") do={
                :if (($rnote->0) > 19) do={
                    # play regular note
                    :if ($lsilent!="yes") do={
                        /beep freq=($rnote->0) length=($rnote->1)
                        /terminal/cuu
                    }
                    :put "     \1B[1;35m /beep\1B[0m  \1B[2;34mlength=\1B[0m\1B[1;34m$[:pick [:tostr ($rnote->1)] 7 16]\1B[0m \1B[2;31mfreq=\1B[0m\1B[1;31m$($rnote->0)\1B[0m \1B[2;31m; (\"\1B[0m\1B[1;7m$($rnote->2)\1B[0m\1B[2;31m\")\1B[0m \1B[2;35m; :delay $[:pick [:tostr ($rnote->1)] 7 16]\1B[0m     "
                } else={
                    # either "marker" - no delay, but comment
                    :if (($rnote->1) = 0) do={
                        :put "\t\t\t# MARK"
                    } else={
                        # or a "rest" - output the delay command
                        :put "\t\1B[1;35m     :delay $[:pick [:tostr ($rnote->1)] 7 16]\1B[0m     "
                    }
                }
                :if ($lsilent!="yes") do={
                    :delay ($rnote->1)
                }
            } else={
                :error "$[:tostr $rnote] contains invalid data"
            }
        }
        :return $played
    }

    # help screen
    :put "\1B[1;7m                    ROUTEROS PLAYER PIANO                    \1B[0m"
    :put "\1B[1;36mType a key to play a note...  1/8th note is $[:pick $nms 7 16]s."
    :put "\1B[2;36m\$PIANO takes ms= and bpm= to set the default note length"
    :put "\1B[1;36mTo play a longer note, use number key with #/8th of a note"
    :put "\1B[2;36m  1 == 1/8  2 == 1/4  4 = 1/2 ... 8 = whole note"
    :put "\1B[1;36mTo quit, hit 'q'"
    :put "\1B[1;36mUse 'x' for next higher octave, or 'z' to lower octave"
    :put "\1B[1;36mTo record, use ','|'.' to start|stop, <BS> to clear"
    :put "\1B[1;36mAny recording will be output as script after 'q'"
    :put "\1B[2;36m  to skip recording output use '\$PIANO as-value ms=120'"
    :put "\1B[2;36m  which will return an array of saved notes/rests/marks"
    :put "\1B[2;36m  e.g. ':global myrecording [\$PIANO as-value bpm=120]'"
    :put "\1B[1;36mTo later playback from var, use '\$PIANO \$myrecording'"
    :put "\1B[2;36m  with the array defined as {{freq;len};{freq;len},...}"
    :put "\1B[0m"
    :put "      \1B[2;34mLEN \1B[1;34m#\1B[2;34m/8\1B[0m \1B[1;34m 1\1B[0m \1B[1;34m 2\1B[0m \1B[1;34m 3\1B[0m \1B[1;34m 4\1B[0m \1B[1;34m 5\1B[0m \1B[1;34m 6\1B[0m \1B[1;34m 7\1B[0m \1B[1;34m 8\1B[0m   \1B[2;34mBPM \1B[1;34m$lbpm  \1B[1;35mCLEAR\1B[0m" 
    :put "\t\1B[0m    `  1  2  3  4  5  6  7  8  9  0  -  = del"
    :put "\t      \1B[1;31mQUIT\1B[0m \1B[1;7mC#\1B[0m \1B[1;7mD#\1B[0m    \1B[1;7mF#\1B[0m \1B[1;7mG#\1B[0m \1B[1;7mA#\1B[0m    \1B[1;7mC#\1B[0m        \1B[1;35mMARK\1B[0m" 
    :put "\t\1B[0m   tab  q  w  e  r  t  y  u  i  o  p  [  ]  \\"
    :put "\t         \1B[1;7mC\1B[0m  \1B[1;7mD\1B[0m  \1B[1;7mE\1B[0m  \1B[1;7mF\1B[0m  \1B[1;7mG\1B[0m  \1B[1;7mA\1B[0m  \1B[1;7mB\1B[0m  \1B[1;7mC\1B[0m  \1B[1;7mD\1B[0m        \1B[1mREST\1B[0m"
    :put "\t\1B[0m   caps  a  s  d  f  g  h  j  k  l  ;  '  ret"
    :put "\t\1B[1;31m          <  >               \1B[1;35mREC\1B[0m \1B[1;35mSTOP\1B[0m"        
    :put "\t\1B[0m   shft   z  x  c  v  b  n  m  ,  .  /  shft"
    :put ""
    # first print of the status line
    $printstatus $octave $lnms $record $played 0 $neighth 
    # start live player...
    # - loops per note time, plays notes per stored octave and keypress 
    #   ends with q is pressed (ascii code 113)
    :local lastkey 65535
    :local lastfq 0
    :local notename ""
    :while ($lastkey != 113) do={
        # collect input
        :set lastkey [/terminal inkey]
        # 65535 means no keyboard input recieved before input timeout
        :if ($lastkey = 65535) do={:delay $nms} else={
            # if a number, use that as the multiplier for notes per second
            :if ($lastkey > 48 && $lastkey < 57) do={
                :set $neighth ($lastkey - 48)
                :set $lnms ($nms*$neighth)
                # update the display with new time per note
                $printstatus $octave $lnms $record $played $lastfq $neighth $notename
            }
            # convert the keypress ASCII code (num type) to a actual str type with a letter
            :local lastascii ($asciimap->$lastkey)
            :if ($lastkey = 60929) do={:set lastascii "left"}
            :if ($lastkey = 60930) do={:set lastascii "right"}
            :if ($lastkey = 60931) do={:set lastascii "up"}
            :if ($lastkey = 60932) do={:set lastascii "down"}
            # change octave via x or z
            :if ($lastascii ~ "x|z|left|right") do={
                :local newoctave 
                :if ($lastascii~"z|left") do={ :set newoctave ($octave-1) } else={ :set newoctave ($octave +1) }
                :if ($newoctave > 0 && $newoctave < 10) do={
                    :set octave $newoctave
                } 
            }
            # handle recording start/resume stop/start
            :if ($lastascii = ",") do={:set record 1}
            :if ($lastascii = ".") do={:set record 0}
            # handle clear recording
            :if ($lastascii = "back") do={:set played [:toarray ""]}
            # if enter key, that's a rest, which is stored as freq==0
            :if ($lastascii = "enter") do={
                # add the rest to stored recording
                :if ($record!=0) do={ :set $played ($played,{{0;$lnms}}) }
            }
            # handle mark recording
            :if ($lastascii = "\\") do={
                :if ($record!=0) do={ :set $played ($played,{{0;0}}) }
            }
            # fetch the actual Hz freq for the note, array is 0-based, so ->0 is 1st octave
            :local freq ($scalearr->$lastascii->($octave))
            # actually play the note
            :if ([:typeof $freq]="num") do={
                :if ($freq > 20) do={
                    :if ($silent!="yes") do={
                        :beep frequency=$freq length=$lnms
                    }
                    :set lastfq $freq
                    :set notename [$getnotename $octave ($scalearr->$lastascii->0)]
                    # if recording
                    :if ($record!=0) do={ :set $played ($played,{{$freq;$lnms;$notename}}) }
                }
                :delay $lnms
                /terminal cuu
            } 
            $printstatus $octave $lnms $record $played $lastfq $neighth $notename
        }
    }
    :if ($asvalue=1) do={
        :return $played
    } else={
        $PIANO $played silent="yes"
        # in theory, should return nothing without "as-value" 
        # but return anyway for easy-of-use
        :return $played
    }
}

:global myrecording [$PIANO bpm=150]


Known Issues/Bugs
- support file serialization – should store ms as num in output array – not a typeof time... issue is JSON de/serialize does not store milliseconds
- perhaps should store note name in output array – as third element – to aid "debugging" since note name is known – use ;("note"); in output which is a NOOP.
- BPM math may be calculate wrong, need to test more [scripting has no floating point numbers, so division is tricky/error-prone – only time type support fractional numbers but time types do not support division...]
- support up/down arrow keys for "splice"/replacing a past note — e.g. walk the array stack, to replace an entry [requires using counter as index into a "sparce array" instead of just appending to array]
- similar with left/right arrows to change octave (easier than above...)
- silent=yes has re-draw issue – does not need a /terminal/cuu if silent=yes
- status bar/help should show note sizes as 1/8 ... 1/4 ... 1/2 ... 1 – not 2/8 ... 4/8 ... 8/8
- "open quesion" – unclear if showing a "note name" in script output as a NOOP ; ("A4") ; or as comment # A4 – problem is comment do not always cut-and-paste well...
- support setting "mark name" – perhaps a | (e.g. shift \ on US Mac keyboard) — on a mark, 3rd element in $played array could some text to display
- support changing BPM with + / - key – ideally also noting as a mark (with BPM change noted) since recording will change BPM mid-stream
- fix typos in help/comments — generally code still a bit messy [although limited since DRY is hard since scripting does not allow local function to call other local functions in a single global function]
- perhaps add a "$PIANO help" that will show full docs — current help text is too long
- could do more with coloring/ANSI codes to make sure they work with both black and white terminal backgrounds – e.g. on Mac via ssh on black, "dim" is pretty dim...
- octave was misspelled octive, everywhere (var, help, screen)
Last edited by Amm0 on Sat Feb 24, 2024 11:01 pm, edited 3 times in total.
 
User avatar
Sertik
Member
Member
Posts: 435
Joined: Fri Sep 25, 2020 3:30 pm
Location: Russia, Moscow

Re: $PIANO - interactive "player piano" & studio-quality recorder using :beep

Tue Feb 20, 2024 7:48 pm

In terms of design and implementation, this is very cool! Thank you very much to the author!
And also a very good tutorial for writing scripts!
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3447
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: $PIANO - interactive "player piano" & studio-quality recorder using :beep

Tue Feb 20, 2024 9:14 pm

In terms of design and implementation, this is very cool! Thank you very much to the author!
And also a very good tutorial for writing scripts!

LOL. I was thinking of you when I added the "todo":
perhaps add a "$PIANO help" that will show full docs — current help text is too long

Code could be cleaned up, but it does use quite a few tricks. The central one is using /terminal/inkey – but that is the basis for any interactive UI in RouterOS scripting. A more simple example of using /terminal/inkey is here: viewtopic.php?t=165722&hilit=inkey#p896227

And more complex version be my $ROKU function with a CLI interface to control a Roku TV, which is where some of the code here comes from: viewtopic.php?p=957512&hilit=ROKU#p957512

But everytime I make a big script I do get limited on what can be cleaned up since local function cannot call other local functions. I use recursion, but it's more confusing (e.g. call the same global function from within it – that's how script gets output after you quit here e.g. "$PIANO $played silent=yes").
 
mykytalvov
just joined
Posts: 10
Joined: Thu Feb 15, 2024 5:06 pm
Location: Czech Republic

Re: $PIANO - interactive "player piano" & studio-quality recorder using :beep

Sat Feb 24, 2024 9:00 pm

Recording feature is amazing! Bravo sir!

I have only one note - you should correct octive to octave :D
 
User avatar
Amm0
Forum Guru
Forum Guru
Topic Author
Posts: 3447
Joined: Sun May 01, 2016 7:12 pm
Location: California

Re: $PIANO - interactive "player piano" & studio-quality recorder using :beep

Sat Feb 24, 2024 10:49 pm

I have only one note - you should correct octive to octave :D
LOL. My spelling was in a higher octave of vowels. Thanks, fixed octAve spelling in script/text.

Also added support for the left and right arrow keys to change octAve & also now check octAve they are between 1 and 9. Plus, note sizes are "reduced" 1/8 ... 1/4 ... 1/2 ... 1 (not 2/8 ... 4/8 ... 8/8 )
 
User avatar
rextended
Forum Guru
Forum Guru
Posts: 12001
Joined: Tue Feb 25, 2014 12:49 pm
Location: Italy
Contact:

Re: $PIANO - interactive "player piano" & studio-quality recorder using :beep

Sun Feb 25, 2024 1:41 pm

$PIANO - interactive "player piano" & studio-quality recorder using :beep

Bravo. 👏👏👏👏👏👏
 
mykytalvov
just joined
Posts: 10
Joined: Thu Feb 15, 2024 5:06 pm
Location: Czech Republic

Re: $PIANO - interactive "player piano" & studio-quality recorder using :beep

Sun Feb 25, 2024 2:27 pm

I have only one note - you should correct octive to octave :D
LOL. My spelling was in a higher octave of vowels. Thanks, fixed octAve spelling in script/text.

Also added support for the left and right arrow keys to change octAve & also now check octAve they are between 1 and 9. Plus, note sizes are "reduced" 1/8 ... 1/4 ... 1/2 ... 1 (not 2/8 ... 4/8 ... 8/8 )
Massive! Thanks

Who is online

Users browsing this forum: rextended and 37 guests