diff --git a/cimp.bas b/cimp.bas index 75c0228..c0209a1 100644 --- a/cimp.bas +++ b/cimp.bas @@ -1,428 +1,678 @@ - -declare library "terminkey" - function terminkey% () - sub echooff () - sub echoon () - function termwidth () -end declare - -$console:only - -const KEY_UP = 1001 -const KEY_DOWN = 1002 -const KEY_RIGHT = 1003 -const KEY_LEFT = 1004 - -$if WIN then -Shell "chcp 65001 > nul" -$end if - -redim file(0) as string -if command$ = "" then - print "please specify file to play." - system -end if - -echooff -cursoroff -chdir _startdir$ -dim volume as single -dim repeat as integer -dim shuffle as integer -dim nooutput as integer -dim timevis as integer -dim nyan as integer - -volume = 1 -repeat = 0 -shuffle = 0 -nooutput = 0 -timevis = 1 -nyan = 0 - -for i = 1 to _commandcount - select case command$(i) - case "-v", "--volume" - i = i + 1 - volume = val(command$(i)) / 100 - case "-s", "--shuffle" - shuffle = -1 - case "-r", "--repeat" - if command$(i + 1) = "1" then - repeat = 1 - else - repeat = -1 - end if - case "-n", "--nooutput" - nooutput = -1 - case "-N", "--nyan" - nyan = -1 - case else - if _fileexists(command$(i)) then - if lcase$(right$(command$(i), 4)) = ".m3u" then - ParseM3U command$(i), file() - else - file(ubound(file)) = command$(i) - redim _preserve file(ubound(file) + 1) - end if - end if - end select -next - -redim _preserve file(ubound(file) - 1) - -dim musichandle as long -dim oldhandle as long - -dim keyin as integer -dim playnext as integer - -dim state as string -dim songname as string -dim progress as string -dim progressbar as string - -if shuffle = -1 then shufflearray file() -i = 0 -musichandle = _sndopen(file(i)) -if musichandle = 0 then - print "Error: could not open file "; file(i) - system -end if -_sndvol musichandle, volume -_sndplay musichandle -state = "playing " -songname = beforelast(".", afterlast("/", file(i))) - -while keyin <> 27 - keyin = terminkey - select case keyin - case KEY_UP - volume = volume + (0.01 + (volume / 10)) - if volume > 1 then volume = 1 - _sndvol musichandle, volume - case KEY_DOWN - volume = volume - (0.01 + (volume / 10)) - if volume < 0 then volume = 0 - _sndvol musichandle, volume - case KEY_RIGHT - if _sndgetpos(musichandle) + 5 < _sndlen(musichandle) then - _sndsetpos musichandle, _sndgetpos(musichandle) + 5 - else - playnext = 1 - end if - case KEY_LEFT - if _sndgetpos(musichandle) - 5 > 0 then - _sndsetpos musichandle, _sndgetpos(musichandle) - 5 - else - playnext = -1 - end if - case asc("q") - keyin = 27 - case asc("z") - if _sndgetpos(musichandle) > 2 then - _sndsetpos musichandle, 0 - else - playnext = -1 - end if - case asc("x") - if _sndplaying(musichandle) then - _sndsetpos musichandle, 0 - else - _sndplay musichandle - end if - case asc("c"), asc(" ") - if _sndplaying(musichandle) then - _sndpause musichandle - state = "paused " - else - _sndplay musichandle - state = "playing " - end if - case asc("v") - _sndstop musichandle - state = "stopped " - case asc("b") - playnext = 1 - case asc("t") - timevis = -timevis - case asc("s") - shufflearray file() - case else - end select - - if _sndgetpos(musichandle) = _sndlen(musichandle) then playnext = 1 - if playnext <> 0 then - oldhandle = musichandle - i = i + playnext - if i > ubound(file) then i = 0 - if i < lbound(file) then i = ubound(file) - musichandle = _sndopen(file(i)) - if musichandle <> 0 then - _sndvol musichandle, volume - _sndplay musichandle - _sndstop oldhandle - _sndclose oldhandle - state = "playing " - songname = beforelast(".", afterlast("/", file(i))) - playnext = 0 - else - musichandle=oldhandle - end if - end if - if timevis = 1 then - progress = " -" + timeleft(musichandle) - else - progress = " " + timeelapsed(musichandle) - end if - if nooutput=0 then - if nyan = -1 then - print termcolor(7); state; AnimatedRainbowText(songname); termcolor(7); progress; clearrest - else - print termcolor(7); state; termcolor(3); songname; termcolor(7); progress; clearrest - end if - progressbar = bar(UWidth(state) + UWidth(songname) + UWidth(progress), (_sndgetpos(musichandle) / _sndlen(musichandle)) * 100, 11, 7) - print progressbar; clearrest; cursorback; - end if - _limit 30 - if _exit then goto quit -wend - -quit: -_sndclose musichandle -print clearrest -print clearrest; -print cursorback; -cursoron -echoon -system - -sub shufflearray (stringarray() as string) - randomize timer - for n = ubound(stringarray) to 1 step -1 - j = int(rnd * n) - swap stringarray(n), stringarray(j) - next -end sub - -sub ParseM3U (filename$, array$()) - for i = len(filename$) to 1 step -1 - if mid$(filename$, i, 1) = "/" or mid$(filename$, i, 1) = "\" then - basePath$ = left$(filename$, i) - exit for - end if - next - f = freefile - open filename$ for input as #f - count = 0 - do until eof(f) - line input #f, l$ - l$ = _trim$(l$) - if len(l$) > 0 and left$(l$, 1) <> "#" then - resolvedPath$ = l$ - if not _fileexists(resolvedPath$) then - resolvedPath$ = basePath$ + l$ - end if - if _fileexists(resolvedPath$) then - array$(ubound(array$)) = resolvedPath$ - redim _preserve array$(ubound(array$) + 1) - end if - end if - loop - close #f -end sub - -function timeleft$ (handle&) - dim seconds as integer - seconds = _sndlen(handle&) - _sndgetpos(handle&) - if seconds < 0 then seconds = 0 - timeleft$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2) -end function - -function timeelapsed$ (handle&) - dim seconds as integer - seconds = _sndgetpos(handle&) - if seconds < 0 then seconds = 0 - timeelapsed$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2) -end function - -function termcolor$ (colorvalue as _unsigned long) - select case colorvalue - case 0 to 7 - termcolor = chr$(27) + "[0;3" + _trim$(str$(colorvalue)) + "m" - case 8 to 15 - termcolor = chr$(27) + "[1;3" + _trim$(str$(colorvalue - 8)) + "m" - case 16 to 255 - termcolor = chr$(27) + "[38;5;" + _trim$(str$(colorvalue)) + "m" - case is > 255 - termcolor = chr$(27) + "[38;2;" + _trim$(str$(_red32(colorvalue))) + ";" + _trim$(str$(_green32(colorvalue))) + ";" + _trim$(str$(_blue32(colorvalue))) + "m" - end select -end function - - -function cursorback$ () - cursorback = chr$(27) + "[F" -end function - -function clearline$ - clearline = chr$(27) + "[2K" -end function - -function clearrest$ - clearrest = chr$(27) + "[K" -end function - -sub cursoroff () - print chr$(27); "[?25l"; -end sub - -sub cursoron () - print chr$(27); "[?25h"; -end sub - -function bar$ (length as integer, percent as integer, color1 as long, color2 as long) - dim done as string - dim notdone as string - for i = 1 to int((percent / 100) * length) - done = done + "━" - next i - for i = 1 to length - int((percent / 100) * length) - notdone = notdone + "━" - next i - bar$ = termcolor(color1) + done + termcolor(color2) + notdone -end function - -function afterlast$ (delim as string, strng as string) - afterlast = mid$(strng, _instrrev(strng, delim) + 1) -end function - -function beforelast$ (delim as string, strng as string) - beforelast = left$(strng, _instrrev(strng, delim) - 1) -end function - - - -function AnimatedRainbowText$ (text$) - static offset as double - dim result as string - dim L as long, i as long - dim r as integer, g as integer, b as integer - dim hue as double, f as double - dim sector as integer, v as integer, p as integer, q as integer, t as integer - - L = ulen(text$) - if L = 0 then exit function - offset = offset + 5.0 - if offset >= 360 then offset = offset - 360 - - for i = 1 to L - hue = MOD_Double(offset + ((i - 1) / L) * 360, 360) - sector = int(hue / 60) - f = (hue / 60) - sector - v = 255: p = 0: q = 255 * (1 - f): t = 255 * f - select case sector - case 0: r = v: g = t: b = p - case 1: r = q: g = v: b = p - case 2: r = p: g = v: b = t - case 3: r = p: g = q: b = v - case 4: r = t: g = p: b = v - case 5: r = v: g = p: b = q - end select - rgbPart$ = _trim$(str$(r)) + ";" + _trim$(str$(g)) + ";" + _trim$(str$(b)) - result = result + chr$(27) + "[38;2;" + rgbPart$ + "m" + umid$(text$, i, 1) - next i - AnimatedRainbowText$ = result + chr$(27) + "[0m" -end function - -function MOD_Double (value as double, m as double) - MOD_Double = value - (m * int(value / m)) -end function - -function ulen% (txt$) - dim count%, i%, b% - count% = 0 - for i% = 1 to len(txt$) - b% = asc(txt$, i%) - if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then - count% = count% + 1 - end if - next - ulen% = count% -end function - -function umid$ (txt$, startChar%, numChars%) - if startChar% < 1 or numChars% <= 0 or txt$ = "" then exit function - - dim byteIdx%, charCount%, startByte%, endByte% - byteIdx% = 1 - charCount% = 0 - - ' 1. Find the starting byte of the character at startChar% - while byteIdx% <= len(txt$) - b% = asc(txt$, byteIdx%) - if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then - charCount% = charCount% + 1 - if charCount% = startChar% then startByte% = byteIdx% - end if - if startByte% > 0 then exit while - byteIdx% = byteIdx% + 1 - wend - - if startByte% = 0 then exit function - - ' 2. Find the byte where the sequence ends - byteIdx% = startByte% - dim charsFound% - charsFound% = 0 - - ' We look for the start of the "lastChar + 1" to find the boundary - while byteIdx% <= len(txt$) - b% = asc(txt$, byteIdx%) - if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then - charsFound% = charsFound% + 1 - end if - ' If we found the start of the character AFTER our range, stop - if charsFound% > numChars% then exit while - byteIdx% = byteIdx% + 1 - wend - - ' byteIdx now points to the start of the next char, or LEN+1 - umid$ = mid$(txt$, startByte%, byteIdx% - startByte%) -end function - -function UWidth% (txt$) - dim totalWidth%, i%, char$, cp& - totalWidth% = 0 - for i% = 1 to ulen(txt$) - char$ = umid(txt$, i%, 1) - cp& = GetCodePoint&(char$) - - if cp& > &H1100 then - totalWidth% = totalWidth% + 2 - else - totalWidth% = totalWidth% + 1 - end if - next - UWidth% = totalWidth% -end function - -function GetCodePoint& (utf8Char$) - dim lLength as integer - lLength = len(utf8Char$) - dim b1 as _unsigned _byte, b2 as _unsigned _byte - dim b3 as _unsigned _byte, b4 as _unsigned _byte - - select case lLength - case 1 - GetCodePoint& = asc(utf8Char$, 1) - case 2 - b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2) - GetCodePoint& = (b1 and &H1F) * 64 + (b2 and &H3F) - case 3 - b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 3) - GetCodePoint& = (b1 and &H0F) * 4096 + (b2 and &H3F) * 64 + (b3 and &H3F) - case 4 - b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 3): b4 = asc(utf8Char$, 4) - GetCodePoint& = (b1 and &H07) * 262144 + (b2 and &H3F) * 4096 + (b3 and &H3F) * 64 + (b4 and &H3F) - end select -end function +declare library"terminkey" + function terminkey%() + sub echooff() + sub echoon() + function termwidth() + function ipc_init&() + function ipc_check_message%(byval buf as _offset, byval max_len as long) + sub ipc_send_message(buf as string) + sub ipc_cleanup() +end declare + +$console:only +on error goto quit +const key_up=1001 +const key_down=1002 +const key_right=1003 +const key_left=1004 +dim slash as string +slash="/" +$if win then +shell"chcp 65001 > nul" +slash="\" +$end if + +redim file(0) as string +chdir _startdir$ +if command$=""then + print"please specify file to play." + goto quit +end if + +dim ipc_status as long +ipc_status = ipc_init + +if ipc_status = 0 then + ' --- CLIENT MODE --- + dim cmd_msg as string + + Select Case command$(1) + Case "--next" + cmd_msg = "NEXT" + + Case "--prev" + cmd_msg = "PREV" + + Case "-v", "--volume" + cmd_msg = "VOL:" + command$(2) + + case "--pause" + cmd_msg = "PAUSE" + case "--stop" + cmd_msg = "STOP" + case "--play" + cmd_msg = "PLAY" + case "--quit" + cmd_msg = "QUIT" + case "--shuffle" + cmd_msg = "SHUFFLE" + case "--repeat" + cmd_msg = "REPEAT" + case "--repeat-1" + cmd_msg = "REPEAT1" + case "--nyan" + cmd_msg = "NYAN" + Case "--add" + For i = 2 To _commandcount + cmd_msg = "ADD:" + _cwd$ + slash + command$(i) + ipc_send_message cmd_msg + Next i + System + + Case "--playlist" + cmd_msg = "GET_PLAYLIST" + + Case Else + ' Default behavior: Resolve target file/playlist and replace active queue + cmd_msg = "PLAY:" + _cwd$ + slash + command$(1) + ipc_send_message cmd_msg + If _commandcount > 1 Then + For i = 2 To _commandcount + cmd_msg = "ADD:" + _cwd$ + slash + command$(i) + ipc_send_message cmd_msg + Next i + System + End If + End Select + + ' Send instruction to the main player instance + ipc_send_message cmd_msg + + ' --- Two-way Client feedback for --playlist --- + if cmd_msg = "GET_PLAYLIST" then + ipc_cleanup + _delay 0.1 + + dim client_listen as long + client_listen = ipc_init + if client_listen = 1 then + dim reply_buf as string + reply_buf = space$(4096) + chr$(0) + dim start_wait as double + start_wait = timer + + do while timer - start_wait < 2 + dim reply_len as long + reply_len = ipc_check_message(_offset(reply_buf), 4096) + if reply_len > 0 then + print left$(reply_buf, reply_len) + exit do + end if + _limit 30 + loop + ipc_cleanup + end if + end if + + system ' Exit client instance completely +elseif ipc_status = -1 then + print "Error initializing Inter-Process Communication." + goto quit +end if + +' ---SERVER MODE (Main Player Execution)--- + +echooff +cursoroff + +dim volume as single +dim repeat as integer +dim shuffle as integer +dim nooutput as integer +dim timevis as integer +dim nyan as integer +dim marqueeoffset as integer +dim i as integer + +dim musichandle as long +dim oldhandle as long +dim keyin as integer +dim playnext as integer + +dim state as string +dim songname as string +dim progress as string +dim progressbar as string + +dim tw as integer +dim fixedwidth as integer +dim maxtitlewidth as integer +dim visibletitle as string +dim currentsongwidth as integer +dim paddedtitle as string +dim paddedlength as integer +dim idx as integer +dim addedwidth as integer +dim charidx as integer +dim nextchar as string +dim marqueeframe as integer + +volume=1 +repeat=0 +shuffle=0 +nooutput=0 +timevis=1 +nyan=0 +marqueeoffset=0 + +for i=1 to _commandcount + select case command$(i) + case "-v","--volume" + i=i+1 + volume=val(command$(i))/100 + case "-s","--shuffle" + shuffle=-1 + case "-r","--repeat" + if command$(i+1)="1"then + repeat=1 + else + repeat=-1 + end if + case "-n","--nooutput" + nooutput=-1 + case "-N","--nyan" + nyan=-1 + case else + if _fileexists(command$(i)) then + if lcase$(right$(command$(i),4))=".m3u"then + parsem3u command$(i),file() + else + file(ubound(file))=command$(i) + redim _preserve file(ubound(file)+1) + end if + end if + end select +next + +redim _preserve file(ubound(file)-1) + +if shuffle=-1 then shufflearray file() +i=0 +musichandle=_sndopen(file(i)) +if musichandle=0 then + print"Error: could not open file "; file(i) + goto quit +end if +_sndvol musichandle,volume +_sndplay musichandle +state="playing " +songname=beforelast(".",afterlast("/",file(i))) + +while keyin<>27 + keyin=terminkey +' ---Poll Client Messages--- + dim incoming_buf as string + incoming_buf = space$(512) + chr$(0) + dim msg_len as long + msg_len = ipc_check_message(_offset(incoming_buf), 512) + + if msg_len > 0 then + dim client_cmd as string + client_cmd = left$(incoming_buf, msg_len) + + if client_cmd = "NEXT" then + playnext = 1 + elseif client_cmd = "PLAY" then + keyin=asc("x") + elseif client_cmd = "PAUSE" then + keyin=asc("c") + elseif client_cmd = "STOP" then + keyin=asc("v") + elseif client_cmd = "QUIT" then + keyin=27 + elseif client_cmd = "REPEAT" then + repeat=-1 + elseif client_cmd = "REPEAT1" then + repeat=1 + elseif client_cmd = "SHUFFLE" then + keyin=asc("s") + elseif client_cmd = "NYAN" then + nyan = not nyan + elseif left$(client_cmd, 4) = "VOL:" then + volume = val(mid$(client_cmd, 5)) / 100 + if volume > 1 then volume = 1 + if volume < 0 then volume = 0 + _sndvol musichandle, volume + elseif left$(client_cmd, 4) = "ADD:" then + dim new_file as string + new_file = _trim$(mid$(client_cmd, 5)) + if _fileexists(new_file) then + if lcase$(right$(new_file, 4)) = ".m3u" then + parsem3u new_file, file() + else + redim _preserve file(ubound(file) + 1) as string + file(ubound(file)) = new_file + end if + end if + elseif left$(client_cmd, 5) = "PLAY:" then + dim replace_file as string + replace_file = _trim$(mid$(client_cmd, 6)) + if _fileexists(replace_file) then + redim file(0) as string + if lcase$(right$(replace_file, 4)) = ".m3u" then + parsem3u replace_file, file() + else + file(0) = replace_file + end if + i = -1 + playnext = 1 + end if + elseif client_cmd = "GET_PLAYLIST" then + dim p_idx as long + dim playlist_payload as string + playlist_payload = "=== Current Playlist ===" + chr$(10) + for p_idx = lbound(file) to ubound(file) + if p_idx = i then + playlist_payload = playlist_payload + "-> " + file(p_idx) + chr$(10) + else + playlist_payload = playlist_payload + " " + file(p_idx) + chr$(10) + end if + next p_idx + + ipc_cleanup + _delay 0.1 + ipc_send_message playlist_payload + _delay 0.1 + dim reinit as long + reinit = ipc_init + end if + end if + ' --- End Poll Client Messages --- + + select case keyin + case key_up + volume=volume+(0.01+(volume/10)) + if volume>1 then volume=1 + _sndvol musichandle,volume + case key_down + volume=volume-(0.01+(volume/10)) + if volume<0 then volume=0 + _sndvol musichandle,volume + case key_right + if _sndgetpos(musichandle)+5<_sndlen(musichandle) then + _sndsetpos musichandle,_sndgetpos(musichandle)+5 + else + playnext=1 + end if + case key_left + if _sndgetpos(musichandle)-5>0 then + _sndsetpos musichandle,_sndgetpos(musichandle)-5 + else + playnext=-1 + end if + case asc("q") + keyin=27 + case asc("z") + if _sndgetpos(musichandle)>2 then + _sndsetpos musichandle,0 + else + playnext=-1 + end if + case asc("x") + if _sndplaying(musichandle) then + _sndsetpos musichandle,0 + else + _sndplay musichandle + end if + case asc("c"),asc(" ") + if _sndplaying(musichandle) then + _sndpause musichandle + state="paused " + else + _sndplay musichandle + state="playing " + end if + case asc("v") + _sndstop musichandle + state="stopped " + case asc("b") + playnext=1 + case asc("t") + timevis=-timevis + case asc("s") + shufflearray file() + end select + + if _sndgetpos(musichandle)=_sndlen(musichandle) then playnext=1 + if playnext<>0 then + oldhandle=musichandle + if repeat=1 and playnext=1 then + playnext=0 + else + i=i+playnext + if i>ubound(file) then + if repeat=-1 then + i=0 + else + goto quit + end if + elseif i0 then + _sndvol musichandle,volume + _sndplay musichandle + _sndstop oldhandle + _sndclose oldhandle + state="playing " + songname=beforelast(".",afterlast(slash,file(i))) + playnext=0 + else + musichandle=oldhandle + end if + end if + + if timevis=1 then + progress=" -"+timeleft(musichandle) + else + progress=" "+timeelapsed(musichandle) + end if + if nooutput=0 then + tw=termwidth + fixedwidth=uwidth(state)+uwidth(progress) + maxtitlewidth=tw-fixedwidth-2 + currentsongwidth=uwidth(songname) + + if currentsongwidth>maxtitlewidth and maxtitlewidth>4 then + paddedtitle=songname+" " + paddedlength=ulen(paddedtitle) + visibletitle="" + addedwidth=0 + idx=0 + while addedwidthmaxtitlewidth then exit while + visibletitle=visibletitle+nextchar + addedwidth=addedwidth+uwidth(nextchar) + idx=idx+1 + wend + + if addedwidth=paddedlength then marqueeoffset=0 + end if + else + ' Terminal is wide enough, no scrolling needed + visibletitle=songname + marqueeoffset=0 + end if + + ' Reset marquee offset if song changes + if playnext<>0 then marqueeoffset=0 + + ' Print the text line + if nyan=-1 then + print termcolor(7); state; animatedrainbowtext(visibletitle); termcolor(7); progress; clearrest + else + print termcolor(7); state; termcolor(3); visibletitle; termcolor(7); progress; clearrest + end if + + ' Generate and print progress bar matching the layout width + progressbar=bar(uwidth(state)+uwidth(visibletitle)+uwidth(progress),(_sndgetpos(musichandle)/_sndlen(musichandle))*100,11,7) + print progressbar; clearrest; cursorback; + end if + + _limit 30 + if _exit then goto quit + + wend + +quit: +_sndclose musichandle +ipc_cleanup +print clearrest +print clearrest; +print cursorback; +print chr$(27)+"[0m"; +cursoron +echoon +system + +sub shufflearray (stringarray() as string) + randomize timer + dim n as long,j as long + for n=ubound(stringarray) to 1 step -1 + j=int(rnd*n) + swap stringarray(n),stringarray(j) + next +end sub + +sub parsem3u (filename$,array$()) + dim i as long,f as long,count as long + dim basepath$,l$,resolvedpath$ + for i=len(filename$) to 1 step -1 + if mid$(filename$,i,1)="/"or mid$(filename$,i,1)="\"then + basepath$=left$(filename$,i) + exit for + end if + next + f=freefile + open filename$ for input as #f + count=0 + do until eof(f) + line input #f,l$ + l$=_trim$(l$) + if len(l$)>0 and left$(l$,1)<>"#"then + resolvedpath$=l$ + if not _fileexists(resolvedpath$) then + resolvedpath$=basepath$+l$ + end if + if _fileexists(resolvedpath$) then + array$(ubound(array$))=resolvedpath$ + redim _preserve array$(ubound(array$)+1) + end if + end if + loop + close #f +end sub + +function timeleft$ (handle&) + dim seconds as integer + seconds=_sndlen(handle&)-_sndgetpos(handle&) + if seconds<0 then seconds=0 + timeleft$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2) +end function + +function timeelapsed$ (handle&) + dim seconds as integer + seconds=_sndgetpos(handle&) + if seconds<0 then seconds=0 + timeelapsed$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2) +end function + +function termcolor$ (colorvalue as _unsigned long) + select case colorvalue + case 0 to 7 + termcolor=chr$(27)+"[0;3"+_trim$(str$(colorvalue))+"m" + case 8 to 15 + termcolor=chr$(27)+"[1;3"+_trim$(str$(colorvalue-8))+"m" + case 16 to 255 + termcolor=chr$(27)+"[38;5;"+_trim$(str$(colorvalue))+"m" + case is>255 + termcolor=chr$(27)+"[38;2;"+_trim$(str$(_red32(colorvalue)))+";"+_trim$(str$(_green32(colorvalue)))+";"+_trim$(str$(_blue32(colorvalue)))+"m" + end select +end function + +function cursorback$ () + cursorback=chr$(27)+"[F" +end function + +function clearline$ + clearline=chr$(27)+"[2K" +end function + +function clearrest$ + clearrest=chr$(27)+"[K" +end function + +sub cursoroff () + print chr$(27);"[?25l"; +end sub + +sub cursoron () + print chr$(27);"[?25h"; +end sub + +function bar$ (length as integer,percent as integer,color1 as long,color2 as long) + dim done as string + dim notdone as string + dim i as integer + for i=1 to int((percent/100)*length) + done=done+"━" + next i + for i=1 to length-int((percent/100)*length) + notdone=notdone+"━" + next i + bar$=termcolor(color1)+done+termcolor(color2)+notdone +end function + +function afterlast$ (delim as string,strng as string) + afterlast=mid$(strng,_instrrev(strng,delim)+1) +end function + +function beforelast$ (delim as string,strng as string) + beforelast=left$(strng,_instrrev(strng,delim)-1) +end function + +function animatedrainbowtext$ (text$) + static offset as double + dim result as string + dim l as long,i as long + dim r as integer,g as integer,b as integer + dim hue as double,f as double + dim sector as integer,v as integer,p as integer,q as integer,t as integer + dim rgbpart$ + + l=ulen(text$) + if l=0 then exit function + offset=offset+5.0 + if offset>=360 then offset=offset-360 + + for i=1 to l + hue=mod_double(offset+((i-1)/l)*360,360) + sector=int(hue/60) + f=(hue/60)-sector + v=255:p=0:q=255*(1-f):t=255*f + select case sector + case 0:r=v:g=t:b=p + case 1:r=q:g=v:b=p + case 2:r=p:g=v:b=t + case 3:r=p:g=q:b=v + case 4:r=t:g=p:b=v + case 5:r=v:g=p:b=q + end select + rgbpart$=_trim$(str$(r))+";"+_trim$(str$(g))+";"+_trim$(str$(b)) + result=result+chr$(27)+"[38;2;"+rgbpart$+"m"+umid$(text$,i,1) + next i + animatedrainbowtext$=result+chr$(27)+"[0m" +end function + +function mod_double (value as double,m as double) + mod_double=value-(m*int(value/m)) +end function + +function ulen% (txt$) + dim count%,i%,b% + count%=0 + for i%=1 to len(txt$) + b%=asc(txt$,i%) + if (b% and &h80)=0 or (b% and &hc0)=&hc0 then + count%=count%+1 + end if + next + ulen%=count% +end function + +function umid$ (txt$,startchar%,numchars%) + if startchar%<1 or numchars%<=0 or txt$=""then exit function + + dim byteidx%,charcount%,startbyte%,endbyte%,b% + byteidx%=1 + charcount%=0 + + while byteidx%<=len(txt$) + b%=asc(txt$,byteidx%) + if (b% and &h80)=0 or (b% and &hc0)=&hc0 then + charcount%=charcount%+1 + if charcount%=startchar% then startbyte%=byteidx% + end if + if startbyte%>0 then exit while + byteidx%=byteidx%+1 + wend + + if startbyte%=0 then exit function + + byteidx%=startbyte% + dim charsfound% + charsfound%=0 + + while byteidx%<=len(txt$) + b%=asc(txt$,byteidx%) + if (b% and &h80)=0 or (b% and &hc0)=&hc0 then + charsfound%=charsfound%+1 + end if + if charsfound%>numchars% then exit while + byteidx%=byteidx%+1 + wend + + umid$=mid$(txt$,startbyte%,byteidx%-startbyte%) +end function + +function uwidth% (txt$) + dim totalwidth%,i%,char$,cp& + totalwidth%=0 + for i%=1 to ulen(txt$) + char$=umid(txt$,i%,1) + cp&=getcodepoint&(char$) + + if cp&>&h1100 then + totalwidth%=totalwidth%+2 + else + totalwidth%=totalwidth%+1 + end if + next + uwidth%=totalwidth% +end function + +function getcodepoint& (utf8char$) + dim llength as integer + llength=len(utf8char$) + dim b1 as _unsigned _byte,b2 as _unsigned _byte + dim b3 as _unsigned _byte,b4 as _unsigned _byte + + select case llength + case 1 + getcodepoint&=asc(utf8char$,1) + case 2 + b1=asc(utf8char$,1):b2=asc(utf8char$,2) + getcodepoint&=(b1 and &h1f)*64+(b2 and &h3f) + case 3 + b1=asc(utf8char$,1):b2=asc(utf8char$,2):b3=asc(utf8char$,3) + getcodepoint&=(b1 and &h0f)*4096+(b2 and &h3f)*64+(b3 and &h3f) + case 4 + b1=asc(utf8char$,1):b2=asc(utf8char$,2):b3=asc(utf8char$,3):b4=asc(utf8char$,4) + getcodepoint&=(b1 and &h07)*262144+(b2 and &h3f)*4096+(b3 and &h3f)*64+(b4 and &h3f) + end select +end function diff --git a/readme.md b/readme.md index b1fffbf..198360d 100644 --- a/readme.md +++ b/readme.md @@ -2,18 +2,21 @@ **c**onsole **i**nterface **m**usic **p**layer -A lightweight, keyboard-driven music player for the terminal, written in -QuickBASIC (QB64). It plays audio files and M3U playlists directly from the -command line, with a minimal two-line display showing playback state. +A keyboard-driven music player for the terminal, written in QuickBASIC (QB64). +It plays audio files and M3U playlists directly from the command line, featuring +a minimal two-line display showing playback state and an IPC remote control +interface. ## Features -- Plays individual audio files or M3U playlists -- Real-time progress bar and time display (elapsed or remaining) -- Volume control and seeking via arrow keys -- Shuffle and repeat modes -- Cross-platform: works on Linux, macOS, and Windows -- Unicode-aware display +- Plays individual audio files or M3U playlists. +- Real-time progress bar and time display (elapsed or remaining). +- Volume control and seeking via arrow keys. +- Shuffle and repeat modes. +- **Client/Server IPC Architecture**: Control a running instance of `cimp` from + another terminal window or script. +- Cross-platform: works on Linux, macOS, and Windows. +- Unicode-aware display with support for full-width character width matching. ## Building @@ -36,9 +39,12 @@ qb64 -x cimp.bas cimp [options] [file2 ...] ``` -Pass one or more audio files or `.m3u` playlists. At least one file is required. +Pass one or more audio files or `.m3u` playlists. At least one file is required +unless sending a remote control command. -### Options +### Global & Server Options + +These flags control the primary player instance when launched. | Flag | Long form | Description | | -------- | -------------- | ---------------------------------------- | @@ -47,21 +53,65 @@ Pass one or more audio files or `.m3u` playlists. At least one file is required. | `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) | | `-n` | `--nooutput` | Run silently with no display | +### Remote Control (Client Mode) Commands + +If `cimp` is already running in a terminal, running it again with any of the +following arguments will send an instruction to the active player instead of +starting a new one: + +| Command | Description | +| ------------------------- | ---------------------------------------------------------- | +| `--play` | Resume playback | +| `--pause` | Pause playback | +| `--stop` | Stop playback | +| `--next` | Skip to the next track | +| `--prev` | Skip to the previous track | +| `-v ` / `--volume ` | Remotely adjust volume (0–100) | +| `--shuffle` | Reshuffle the current playlist queue | +| `--repeat` | Set player to repeat all | +| `--repeat-1` | Set player to repeat current track | +| `--add ` | Append a new file or playlist to the active queue | +| `--playlist` | Fetch and print the current playback queue from the server | +| `--quit` | Remotely close the running player instance | + ### Examples +#### Play a single file normally + ```shell -# Play a single file cimp song.mp3 +``` -# Play a playlist shuffled at 80% volume -cimp -s -v 80 playlist.m3u +### Remote control examples (Run from a separate terminal) -# Play multiple files with repeat -cimp -r track1.ogg track2.ogg track3.ogg +#### Pause the music + +```shell +cimp --pause +``` + +#### Lower the volume to 20% + +```shell +cimp --volume 20 +``` + +#### Add another track to the background player's active queue + +```shell +cimp --add extra_track.flac +``` + +#### View the active playlist queue + +```shell +cimp --playlist ``` ## Keybindings +When interacting directly with the player interface terminal, use these keys: + | Key | Action | | ------------- | ----------------------------------------- | | `↑` / `↓` | Volume up / down | @@ -85,13 +135,19 @@ both compilers: ## Files -| File | Description | -| ------------- | ---------------------------------------------------------------------------- | -| `cimp.bas` | Main player — QB64-PE source | -| `terminkey.h` | C library for cross-platform raw keyboard input and terminal width detection | +| File | Description | +| ------------- | --------------------------------------------------------------------------------------- | +| `cimp.bas` | Main player — QB64-PE source handling client/server states and UI | +| `terminkey.h` | C library for cross-platform raw keyboard input, IPC pipes/sockets, and terminal sizing | ## Notes +- **IPC Backend**: On Windows, IPC relies on Named Pipes (`\\.\pipe\cimp_ipc`). + On Linux/macOS, it binds to a local Unix Domain Socket at + `/tmp/cimp_ipc_.sock`. - M3U paths can be absolute or relative to the playlist file's directory. - Volume adjustment is non-linear for a more natural response and better control at lower ranges. + +``` +``` diff --git a/terminkey.h b/terminkey.h index d8e7ade..2456010 100644 --- a/terminkey.h +++ b/terminkey.h @@ -3,11 +3,23 @@ #ifdef _WIN32 #include #include + #include + #include + #include + #include #else #include #include #include #include + #include + #include + #include + #include + #include + #include + #include + #include #endif // Special key codes (unified across platforms) @@ -21,8 +33,17 @@ void echooff(); void echoon(); int termwidth(); +int ipc_init(); +int ipc_check_message(uintptr_t buffer_ptr, int max_len); +void ipc_send_message(const char* message); +void ipc_cleanup(); + #ifdef _WIN32 +static HANDLE hServerPipe = INVALID_HANDLE_VALUE; +static OVERLAPPED oOverlap; +static BOOL fPendingIO = FALSE; + int terminkey() { if (_kbhit()) { int ch = _getch(); @@ -65,8 +86,126 @@ int termwidth() { return -1; } +int ipc_init() { + // Create a regular blocking pipe, but enable FILE_FLAG_OVERLAPPED for safe async I/O + hServerPipe = CreateNamedPipe( + "\\\\.\\pipe\\cimp_ipc", + PIPE_ACCESS_DUPLEX | FILE_FLAG_OVERLAPPED, + PIPE_TYPE_BYTE | PIPE_READMODE_BYTE, + 1, + 4096, + 4096, + 0, + NULL + ); + if (hServerPipe == INVALID_HANDLE_VALUE) { + DWORD err = GetLastError(); + if (err == ERROR_ACCESS_DENIED || err == ERROR_PIPE_BUSY) { + return 0; // Already running (Client Mode) + } + return -1; // Other error + } + + // Initialize the overlapped structure and create an event + memset(&oOverlap, 0, sizeof(OVERLAPPED)); + oOverlap.hEvent = CreateEvent(NULL, TRUE, TRUE, NULL); + fPendingIO = FALSE; + + return 1; // Server mode successfully started +} + +int ipc_check_message(uintptr_t buffer_ptr, int max_len) { + if (hServerPipe == INVALID_HANDLE_VALUE) return 0; + + char* buffer = (char*)buffer_ptr; + DWORD bytesRead = 0; + + if (!fPendingIO) { + // Start an asynchronous listen operation + if (!ConnectNamedPipe(hServerPipe, &oOverlap)) { + DWORD err = GetLastError(); + if (err == ERROR_IO_PENDING) { + fPendingIO = TRUE; + } else if (err == ERROR_PIPE_CONNECTED) { + // Client already connected before we listened! + SetEvent(oOverlap.hEvent); + fPendingIO = TRUE; + } + } + } + + if (fPendingIO) { + // Check if the connection event has triggered without waiting (0ms timeout) + if (WaitForSingleObject(oOverlap.hEvent, 0) == WAIT_OBJECT_0) { + DWORD cbRet = 0; + // Connection is ready, let's see if there is data + if (GetOverlappedResult(hServerPipe, &oOverlap, &cbRet, FALSE)) { + DWORD bytesAvail = 0; + if (PeekNamedPipe(hServerPipe, NULL, 0, NULL, &bytesAvail, NULL) && bytesAvail > 0) { + // Data has arrived, read it synchronously + BOOL success = ReadFile(hServerPipe, buffer, max_len - 1, &bytesRead, NULL); + if (success && bytesRead > 0) { + buffer[bytesRead] = '\0'; + + // Clean up and reset for the next client connection + DisconnectNamedPipe(hServerPipe); + ResetEvent(oOverlap.hEvent); + fPendingIO = FALSE; + return (int)bytesRead; + } + } + } else { + // Client disconnected or pipe broke before sending + DisconnectNamedPipe(hServerPipe); + ResetEvent(oOverlap.hEvent); + fPendingIO = FALSE; + } + } + } + return 0; +} + +void ipc_send_message(const char* message) { + HANDLE hPipe = CreateFile( + "\\\\.\\pipe\\cimp_ipc", + GENERIC_WRITE, + 0, + NULL, + OPEN_EXISTING, + 0, + NULL + ); + if (hPipe != INVALID_HANDLE_VALUE) { + DWORD bytesWritten = 0; + WriteFile(hPipe, message, strlen(message), &bytesWritten, NULL); + CloseHandle(hPipe); + } +} + +void ipc_cleanup() { + if (hServerPipe != INVALID_HANDLE_VALUE) { + DisconnectNamedPipe(hServerPipe); + CloseHandle(hServerPipe); + hServerPipe = INVALID_HANDLE_VALUE; + } + if (oOverlap.hEvent != NULL) { + CloseHandle(oOverlap.hEvent); + oOverlap.hEvent = NULL; + } +} + #else +static int server_fd = -1; +static char socket_path[256] = ""; + +static void get_socket_path() { + if (socket_path[0] == '\0') { + uid_t uid = getuid(); + snprintf(socket_path, sizeof(socket_path), "/tmp/cimp_ipc_%u.sock", (unsigned int)uid); + } +} + int terminkey() { struct termios oldt, newt; int ch; @@ -126,4 +265,102 @@ int termwidth() { return -1; } +int ipc_init() { + get_socket_path(); + + int test_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (test_fd >= 0) { + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + if (connect(test_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) { + close(test_fd); + return 0; // Already running + } + close(test_fd); + } + + unlink(socket_path); + + server_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (server_fd < 0) { + return -1; + } + + int flags = fcntl(server_fd, F_GETFL, 0); + fcntl(server_fd, F_SETFL, flags | O_NONBLOCK); + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (bind(server_fd, (struct sockaddr*)&addr, sizeof(addr)) < 0) { + close(server_fd); + server_fd = -1; + return -1; + } + + if (listen(server_fd, 5) < 0) { + close(server_fd); + server_fd = -1; + unlink(socket_path); + return -1; + } + + return 1; +} + +int ipc_check_message(uintptr_t buffer_ptr, int max_len) { + if (server_fd < 0) return 0; + + char* buffer = (char*)buffer_ptr; + int client_fd = accept(server_fd, NULL, NULL); + if (client_fd < 0) { + return 0; + } + + int total_read = 0; + while (total_read < max_len - 1) { + int r = read(client_fd, buffer + total_read, max_len - 1 - total_read); + if (r <= 0) break; + total_read += r; + } + buffer[total_read] = '\0'; + close(client_fd); + return total_read; +} + +void ipc_send_message(const char* message) { + get_socket_path(); + int client_fd = socket(AF_UNIX, SOCK_STREAM, 0); + if (client_fd < 0) return; + + struct sockaddr_un addr; + memset(&addr, 0, sizeof(addr)); + addr.sun_family = AF_UNIX; + strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1); + + if (connect(client_fd, (struct sockaddr*)&addr, sizeof(addr)) == 0) { + int total_written = 0; + int len = strlen(message); + while (total_written < len) { + int w = write(client_fd, message + total_written, len - total_written); + if (w <= 0) break; + total_written += w; + } + } + close(client_fd); +} + +void ipc_cleanup() { + if (server_fd >= 0) { + close(server_fd); + server_fd = -1; + } + get_socket_path(); + unlink(socket_path); +} + #endif diff --git a/test_ipc.bas b/test_ipc.bas new file mode 100644 index 0000000..7402f88 --- /dev/null +++ b/test_ipc.bas @@ -0,0 +1,44 @@ +$console:only +declare library "terminkey" + function ipc_init&() + function ipc_check_message%(byval buf as _offset, byval max_len as long) + sub ipc_send_message(buf as string) + sub ipc_cleanup() + sub get_absolute_path(rel_path as string, byval abs_path as _offset, byval max_len as long) +end declare + +dim r as long +r = ipc_init +print "ipc_init returned:"; r + +if r = 1 then + print "We are the server! Waiting for a message..." + dim msg as string + msg = space$(100) + chr$(0) + dim start_time as double + start_time = timer + do while timer - start_time < 3 + dim n as long + n = ipc_check_message(_offset(msg), 100) + if n > 0 then + print "Received: "; left$(msg, n) + exit do + end if + _limit 10 + loop + ipc_cleanup +elseif r = 0 then + print "We are the client! Sending a message..." + ipc_send_message "Hello from client!" +else + print "Error initializing IPC" +end if + +dim rel as string +rel = "readme.md" +dim abs_path as string +abs_path = space$(512) + chr$(0) +get_absolute_path rel, _offset(abs_path), 512 +print "Absolute path of '"; rel; "' is: '"; _trim$(abs_path); "'" + +system diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..3a9631f --- /dev/null +++ b/todo.md @@ -0,0 +1,12 @@ +# todo +## ipc +add ipc functionality so it's possible while the program runs to: +- ~play next and previous track with `cimp --next` and `cimp --prev`~ +- ~change volume with `cimp --volume xx`~ +- ~add file to playlist with `cimp --add filename.ext`~ +- if started without `--add` it should clear the playlist and load and play the given file or playlist. +- if given the flag `--playlist` it should output the song array as a playlist. +- start stop pause and quit would be good, repeat as well. +the c functions are ready in terminkey.h and an example of how it works is found in test_ipc.bas + +## recurcive add all music in folder