Compare commits
23 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
82c859317b | ||
|
|
bd11dfb7ca | ||
| 4cf569b01e | |||
|
|
52c0e73659 | ||
|
|
14d3e11ac1 | ||
|
|
58844384f2 | ||
| cd290f0d2b | |||
|
|
a3c7f66b2f | ||
|
|
a781ef1453 | ||
|
|
96c694f78d | ||
|
|
c2d86b6f46 | ||
|
|
301a6423db | ||
|
|
19701af4ec | ||
|
|
19bf4b5288 | ||
|
|
46cdaf4e63 | ||
|
|
c1d109e198 | ||
|
|
b52fdf512c | ||
|
|
abc48259bd | ||
|
|
0de770ce99 | ||
|
|
a894f36ffa | ||
|
|
8af3bc9649 | ||
|
|
765372aa6b | ||
|
|
b6c1a1c1c0 |
5 changed files with 1047 additions and 448 deletions
742
cimp.bas
742
cimp.bas
|
|
@ -1,79 +1,141 @@
|
||||||
|
declare library"terminkey"
|
||||||
declare library "terminkey"
|
function terminkey%()
|
||||||
function terminkey% ()
|
sub echooff()
|
||||||
sub echooff ()
|
sub echoon()
|
||||||
sub echoon ()
|
function termwidth()
|
||||||
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
|
end declare
|
||||||
|
|
||||||
$console:only
|
$console:only
|
||||||
|
on error goto quit
|
||||||
const KEY_UP = 1001
|
const key_up=1001
|
||||||
const KEY_DOWN = 1002
|
const key_down=1002
|
||||||
const KEY_RIGHT = 1003
|
const key_right=1003
|
||||||
const KEY_LEFT = 1004
|
const key_left=1004
|
||||||
|
dim slash as string
|
||||||
$if WIN then
|
slash="/"
|
||||||
Shell "chcp 65001 > nul"
|
$if win then
|
||||||
|
shell"chcp 65001 > nul"
|
||||||
|
slash="\"
|
||||||
$end if
|
$end if
|
||||||
|
|
||||||
redim file(0) as string
|
redim file(0) as string
|
||||||
if command$ = "" then
|
chdir _startdir$
|
||||||
print "please specify file to play."
|
if command$=""then
|
||||||
system
|
print"please specify file to play."
|
||||||
|
goto quit
|
||||||
end if
|
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
|
echooff
|
||||||
cursoroff
|
cursoroff
|
||||||
chdir _startdir$
|
|
||||||
dim volume as single
|
dim volume as single
|
||||||
dim repeat as integer
|
dim repeat as integer
|
||||||
dim shuffle as integer
|
dim shuffle as integer
|
||||||
dim nooutput as integer
|
dim nooutput as integer
|
||||||
dim timevis as integer
|
dim timevis as integer
|
||||||
dim nyan as integer
|
dim nyan as integer
|
||||||
|
dim marqueeoffset as integer
|
||||||
volume = 1
|
dim i as integer
|
||||||
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 musichandle as long
|
||||||
dim oldhandle as long
|
dim oldhandle as long
|
||||||
|
|
||||||
dim keyin as integer
|
dim keyin as integer
|
||||||
dim playnext as integer
|
dim playnext as integer
|
||||||
|
|
||||||
|
|
@ -82,150 +144,344 @@ dim songname as string
|
||||||
dim progress as string
|
dim progress as string
|
||||||
dim progressbar as string
|
dim progressbar as string
|
||||||
|
|
||||||
if shuffle = -1 then shufflearray file()
|
dim tw as integer
|
||||||
i = 0
|
dim fixedwidth as integer
|
||||||
musichandle = _sndopen(file(i))
|
dim maxtitlewidth as integer
|
||||||
if musichandle = 0 then
|
dim visibletitle as string
|
||||||
print "Error: could not open file "; file(i)
|
dim currentsongwidth as integer
|
||||||
system
|
dim paddedtitle as string
|
||||||
end if
|
dim paddedlength as integer
|
||||||
_sndvol musichandle, volume
|
dim idx as integer
|
||||||
_sndplay musichandle
|
dim addedwidth as integer
|
||||||
state = "playing "
|
dim charidx as integer
|
||||||
songname = beforelast(".", afterlast("/", file(i)))
|
dim nextchar as string
|
||||||
|
dim marqueeframe as integer
|
||||||
|
|
||||||
while keyin <> 27
|
volume=1
|
||||||
keyin = terminkey
|
repeat=0
|
||||||
select case keyin
|
shuffle=0
|
||||||
case KEY_UP
|
nooutput=0
|
||||||
volume = volume + (0.01 + (volume / 10))
|
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 > 1 then volume = 1
|
||||||
_sndvol musichandle, volume
|
|
||||||
case KEY_DOWN
|
|
||||||
volume = volume - (0.01 + (volume / 10))
|
|
||||||
if volume < 0 then volume = 0
|
if volume < 0 then volume = 0
|
||||||
_sndvol musichandle, volume
|
_sndvol musichandle, volume
|
||||||
case KEY_RIGHT
|
elseif left$(client_cmd, 4) = "ADD:" then
|
||||||
if _sndgetpos(musichandle) + 5 < _sndlen(musichandle) then
|
dim new_file as string
|
||||||
_sndsetpos musichandle, _sndgetpos(musichandle) + 5
|
new_file = _trim$(mid$(client_cmd, 5))
|
||||||
else
|
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
|
playnext = 1
|
||||||
end if
|
end if
|
||||||
case KEY_LEFT
|
elseif client_cmd = "GET_PLAYLIST" then
|
||||||
if _sndgetpos(musichandle) - 5 > 0 then
|
dim p_idx as long
|
||||||
_sndsetpos musichandle, _sndgetpos(musichandle) - 5
|
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
|
else
|
||||||
playnext = -1
|
playnext=1
|
||||||
|
end if
|
||||||
|
case key_left
|
||||||
|
if _sndgetpos(musichandle)-5>0 then
|
||||||
|
_sndsetpos musichandle,_sndgetpos(musichandle)-5
|
||||||
|
else
|
||||||
|
playnext=-1
|
||||||
end if
|
end if
|
||||||
case asc("q")
|
case asc("q")
|
||||||
keyin = 27
|
keyin=27
|
||||||
case asc("z")
|
case asc("z")
|
||||||
if _sndgetpos(musichandle) > 2 then
|
if _sndgetpos(musichandle)>2 then
|
||||||
_sndsetpos musichandle, 0
|
_sndsetpos musichandle,0
|
||||||
else
|
else
|
||||||
playnext = -1
|
playnext=-1
|
||||||
end if
|
end if
|
||||||
case asc("x")
|
case asc("x")
|
||||||
if _sndplaying(musichandle) then
|
if _sndplaying(musichandle) then
|
||||||
_sndsetpos musichandle, 0
|
_sndsetpos musichandle,0
|
||||||
else
|
else
|
||||||
_sndplay musichandle
|
_sndplay musichandle
|
||||||
end if
|
end if
|
||||||
case asc("c"), asc(" ")
|
case asc("c"),asc(" ")
|
||||||
if _sndplaying(musichandle) then
|
if _sndplaying(musichandle) then
|
||||||
_sndpause musichandle
|
_sndpause musichandle
|
||||||
state = "paused "
|
state="paused "
|
||||||
else
|
else
|
||||||
_sndplay musichandle
|
_sndplay musichandle
|
||||||
state = "playing "
|
state="playing "
|
||||||
end if
|
end if
|
||||||
case asc("v")
|
case asc("v")
|
||||||
_sndstop musichandle
|
_sndstop musichandle
|
||||||
state = "stopped "
|
state="stopped "
|
||||||
case asc("b")
|
case asc("b")
|
||||||
playnext = 1
|
playnext=1
|
||||||
case asc("t")
|
case asc("t")
|
||||||
timevis = -timevis
|
timevis=-timevis
|
||||||
case asc("s")
|
case asc("s")
|
||||||
shufflearray file()
|
shufflearray file()
|
||||||
case else
|
|
||||||
end select
|
end select
|
||||||
|
|
||||||
if _sndgetpos(musichandle) = _sndlen(musichandle) then playnext = 1
|
if _sndgetpos(musichandle)=_sndlen(musichandle) then playnext=1
|
||||||
if playnext <> 0 then
|
if playnext<>0 then
|
||||||
oldhandle = musichandle
|
oldhandle=musichandle
|
||||||
i = i + playnext
|
if repeat=1 and playnext=1 then
|
||||||
if i > ubound(file) then i = 0
|
playnext=0
|
||||||
if i < lbound(file) then i = ubound(file)
|
else
|
||||||
musichandle = _sndopen(file(i))
|
i=i+playnext
|
||||||
if musichandle <> 0 then
|
if i>ubound(file) then
|
||||||
_sndvol musichandle, volume
|
if repeat=-1 then
|
||||||
|
i=0
|
||||||
|
else
|
||||||
|
goto quit
|
||||||
|
end if
|
||||||
|
elseif i<lbound(file) then
|
||||||
|
if repeat=-1 then
|
||||||
|
i=ubound(file)
|
||||||
|
else
|
||||||
|
i=lbound(file)
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
end if
|
||||||
|
musichandle=_sndopen(file(i))
|
||||||
|
if musichandle<>0 then
|
||||||
|
_sndvol musichandle,volume
|
||||||
_sndplay musichandle
|
_sndplay musichandle
|
||||||
_sndstop oldhandle
|
_sndstop oldhandle
|
||||||
_sndclose oldhandle
|
_sndclose oldhandle
|
||||||
state = "playing "
|
state="playing "
|
||||||
songname = beforelast(".", afterlast("/", file(i)))
|
songname=beforelast(".",afterlast(slash,file(i)))
|
||||||
playnext = 0
|
playnext=0
|
||||||
else
|
else
|
||||||
musichandle=oldhandle
|
musichandle=oldhandle
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
if timevis = 1 then
|
|
||||||
progress = " -" + timeleft(musichandle)
|
if timevis=1 then
|
||||||
|
progress=" -"+timeleft(musichandle)
|
||||||
else
|
else
|
||||||
progress = " " + timeelapsed(musichandle)
|
progress=" "+timeelapsed(musichandle)
|
||||||
end if
|
end if
|
||||||
if nooutput=0 then
|
if nooutput=0 then
|
||||||
if nyan = -1 then
|
tw=termwidth
|
||||||
print termcolor(7); state; AnimatedRainbowText(songname); termcolor(7); progress; clearrest
|
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 addedwidth<maxtitlewidth
|
||||||
|
charidx=((marqueeoffset+idx) mod paddedlength)+1
|
||||||
|
nextchar=umid(paddedtitle,charidx,1)
|
||||||
|
if addedwidth+uwidth(nextchar)>maxtitlewidth then exit while
|
||||||
|
visibletitle=visibletitle+nextchar
|
||||||
|
addedwidth=addedwidth+uwidth(nextchar)
|
||||||
|
idx=idx+1
|
||||||
|
wend
|
||||||
|
|
||||||
|
if addedwidth<maxtitlewidth then
|
||||||
|
visibletitle=visibletitle+space$(maxtitlewidth-addedwidth)
|
||||||
|
end if
|
||||||
|
|
||||||
|
marqueeframe=marqueeframe+1
|
||||||
|
if marqueeframe mod 4=0 then
|
||||||
|
marqueeoffset=marqueeoffset+1
|
||||||
|
if marqueeoffset>=paddedlength then marqueeoffset=0
|
||||||
|
end if
|
||||||
else
|
else
|
||||||
print termcolor(7); state; termcolor(3); songname; termcolor(7); progress; clearrest
|
' Terminal is wide enough, no scrolling needed
|
||||||
|
visibletitle=songname
|
||||||
|
marqueeoffset=0
|
||||||
end if
|
end if
|
||||||
progressbar = bar(UWidth(state) + UWidth(songname) + UWidth(progress), (_sndgetpos(musichandle) / _sndlen(musichandle)) * 100, 11, 7)
|
|
||||||
|
' 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;
|
print progressbar; clearrest; cursorback;
|
||||||
end if
|
end if
|
||||||
|
|
||||||
_limit 30
|
_limit 30
|
||||||
if _exit then goto quit
|
if _exit then goto quit
|
||||||
wend
|
|
||||||
|
wend
|
||||||
|
|
||||||
quit:
|
quit:
|
||||||
_sndclose musichandle
|
_sndclose musichandle
|
||||||
|
ipc_cleanup
|
||||||
print clearrest
|
print clearrest
|
||||||
print clearrest;
|
print clearrest;
|
||||||
print cursorback;
|
print cursorback;
|
||||||
|
print chr$(27)+"[0m";
|
||||||
cursoron
|
cursoron
|
||||||
echoon
|
echoon
|
||||||
system
|
system
|
||||||
|
|
||||||
sub shufflearray (stringarray() as string)
|
sub shufflearray (stringarray() as string)
|
||||||
randomize timer
|
randomize timer
|
||||||
for n = ubound(stringarray) to 1 step -1
|
dim n as long,j as long
|
||||||
j = int(rnd * n)
|
for n=ubound(stringarray) to 1 step -1
|
||||||
swap stringarray(n), stringarray(j)
|
j=int(rnd*n)
|
||||||
|
swap stringarray(n),stringarray(j)
|
||||||
next
|
next
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub ParseM3U (filename$, array$())
|
sub parsem3u (filename$,array$())
|
||||||
for i = len(filename$) to 1 step -1
|
dim i as long,f as long,count as long
|
||||||
if mid$(filename$, i, 1) = "/" or mid$(filename$, i, 1) = "\" then
|
dim basepath$,l$,resolvedpath$
|
||||||
basePath$ = left$(filename$, i)
|
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
|
exit for
|
||||||
end if
|
end if
|
||||||
next
|
next
|
||||||
f = freefile
|
f=freefile
|
||||||
open filename$ for input as #f
|
open filename$ for input as #f
|
||||||
count = 0
|
count=0
|
||||||
do until eof(f)
|
do until eof(f)
|
||||||
line input #f, l$
|
line input #f,l$
|
||||||
l$ = _trim$(l$)
|
l$=_trim$(l$)
|
||||||
if len(l$) > 0 and left$(l$, 1) <> "#" then
|
if len(l$)>0 and left$(l$,1)<>"#"then
|
||||||
resolvedPath$ = l$
|
resolvedpath$=l$
|
||||||
if not _fileexists(resolvedPath$) then
|
if not _fileexists(resolvedpath$) then
|
||||||
resolvedPath$ = basePath$ + l$
|
resolvedpath$=basepath$+l$
|
||||||
end if
|
end if
|
||||||
if _fileexists(resolvedPath$) then
|
if _fileexists(resolvedpath$) then
|
||||||
array$(ubound(array$)) = resolvedPath$
|
array$(ubound(array$))=resolvedpath$
|
||||||
redim _preserve array$(ubound(array$) + 1)
|
redim _preserve array$(ubound(array$)+1)
|
||||||
end if
|
end if
|
||||||
end if
|
end if
|
||||||
loop
|
loop
|
||||||
|
|
@ -234,195 +490,189 @@ end sub
|
||||||
|
|
||||||
function timeleft$ (handle&)
|
function timeleft$ (handle&)
|
||||||
dim seconds as integer
|
dim seconds as integer
|
||||||
seconds = _sndlen(handle&) - _sndgetpos(handle&)
|
seconds=_sndlen(handle&)-_sndgetpos(handle&)
|
||||||
if seconds < 0 then seconds = 0
|
if seconds<0 then seconds=0
|
||||||
timeleft$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2)
|
timeleft$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2)
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function timeelapsed$ (handle&)
|
function timeelapsed$ (handle&)
|
||||||
dim seconds as integer
|
dim seconds as integer
|
||||||
seconds = _sndgetpos(handle&)
|
seconds=_sndgetpos(handle&)
|
||||||
if seconds < 0 then seconds = 0
|
if seconds<0 then seconds=0
|
||||||
timeelapsed$ = right$("0" + ltrim$(str$(seconds \ 60)), 2) + ":" + right$("0" + ltrim$(str$(seconds mod 60)), 2)
|
timeelapsed$=right$("0"+ltrim$(str$(seconds \ 60)),2)+":"+right$("0"+ltrim$(str$(seconds mod 60)),2)
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function termcolor$ (colorvalue as _unsigned long)
|
function termcolor$ (colorvalue as _unsigned long)
|
||||||
select case colorvalue
|
select case colorvalue
|
||||||
case 0 to 7
|
case 0 to 7
|
||||||
termcolor = chr$(27) + "[0;3" + _trim$(str$(colorvalue)) + "m"
|
termcolor=chr$(27)+"[0;3"+_trim$(str$(colorvalue))+"m"
|
||||||
case 8 to 15
|
case 8 to 15
|
||||||
termcolor = chr$(27) + "[1;3" + _trim$(str$(colorvalue - 8)) + "m"
|
termcolor=chr$(27)+"[1;3"+_trim$(str$(colorvalue-8))+"m"
|
||||||
case 16 to 255
|
case 16 to 255
|
||||||
termcolor = chr$(27) + "[38;5;" + _trim$(str$(colorvalue)) + "m"
|
termcolor=chr$(27)+"[38;5;"+_trim$(str$(colorvalue))+"m"
|
||||||
case is > 255
|
case is>255
|
||||||
termcolor = chr$(27) + "[38;2;" + _trim$(str$(_red32(colorvalue))) + ";" + _trim$(str$(_green32(colorvalue))) + ";" + _trim$(str$(_blue32(colorvalue))) + "m"
|
termcolor=chr$(27)+"[38;2;"+_trim$(str$(_red32(colorvalue)))+";"+_trim$(str$(_green32(colorvalue)))+";"+_trim$(str$(_blue32(colorvalue)))+"m"
|
||||||
end select
|
end select
|
||||||
end function
|
end function
|
||||||
|
|
||||||
|
|
||||||
function cursorback$ ()
|
function cursorback$ ()
|
||||||
cursorback = chr$(27) + "[F"
|
cursorback=chr$(27)+"[F"
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function clearline$
|
function clearline$
|
||||||
clearline = chr$(27) + "[2K"
|
clearline=chr$(27)+"[2K"
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function clearrest$
|
function clearrest$
|
||||||
clearrest = chr$(27) + "[K"
|
clearrest=chr$(27)+"[K"
|
||||||
end function
|
end function
|
||||||
|
|
||||||
sub cursoroff ()
|
sub cursoroff ()
|
||||||
print chr$(27); "[?25l";
|
print chr$(27);"[?25l";
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
sub cursoron ()
|
sub cursoron ()
|
||||||
print chr$(27); "[?25h";
|
print chr$(27);"[?25h";
|
||||||
end sub
|
end sub
|
||||||
|
|
||||||
function bar$ (length as integer, percent as integer, color1 as long, color2 as long)
|
function bar$ (length as integer,percent as integer,color1 as long,color2 as long)
|
||||||
dim done as string
|
dim done as string
|
||||||
dim notdone as string
|
dim notdone as string
|
||||||
for i = 1 to int((percent / 100) * length)
|
dim i as integer
|
||||||
done = done + "━"
|
for i=1 to int((percent/100)*length)
|
||||||
|
done=done+"━"
|
||||||
next i
|
next i
|
||||||
for i = 1 to length - int((percent / 100) * length)
|
for i=1 to length-int((percent/100)*length)
|
||||||
notdone = notdone + "━"
|
notdone=notdone+"━"
|
||||||
next i
|
next i
|
||||||
bar$ = termcolor(color1) + done + termcolor(color2) + notdone
|
bar$=termcolor(color1)+done+termcolor(color2)+notdone
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function afterlast$ (delim as string, strng as string)
|
function afterlast$ (delim as string,strng as string)
|
||||||
afterlast = mid$(strng, _instrrev(strng, delim) + 1)
|
afterlast=mid$(strng,_instrrev(strng,delim)+1)
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function beforelast$ (delim as string, strng as string)
|
function beforelast$ (delim as string,strng as string)
|
||||||
beforelast = left$(strng, _instrrev(strng, delim) - 1)
|
beforelast=left$(strng,_instrrev(strng,delim)-1)
|
||||||
end function
|
end function
|
||||||
|
|
||||||
|
function animatedrainbowtext$ (text$)
|
||||||
|
|
||||||
function AnimatedRainbowText$ (text$)
|
|
||||||
static offset as double
|
static offset as double
|
||||||
dim result as string
|
dim result as string
|
||||||
dim L as long, i as long
|
dim l as long,i as long
|
||||||
dim r as integer, g as integer, b as integer
|
dim r as integer,g as integer,b as integer
|
||||||
dim hue as double, f as double
|
dim hue as double,f as double
|
||||||
dim sector as integer, v as integer, p as integer, q as integer, t as integer
|
dim sector as integer,v as integer,p as integer,q as integer,t as integer
|
||||||
|
dim rgbpart$
|
||||||
|
|
||||||
L = ulen(text$)
|
l=ulen(text$)
|
||||||
if L = 0 then exit function
|
if l=0 then exit function
|
||||||
offset = offset + 5.0
|
offset=offset+5.0
|
||||||
if offset >= 360 then offset = offset - 360
|
if offset>=360 then offset=offset-360
|
||||||
|
|
||||||
for i = 1 to L
|
for i=1 to l
|
||||||
hue = MOD_Double(offset + ((i - 1) / L) * 360, 360)
|
hue=mod_double(offset+((i-1)/l)*360,360)
|
||||||
sector = int(hue / 60)
|
sector=int(hue/60)
|
||||||
f = (hue / 60) - sector
|
f=(hue/60)-sector
|
||||||
v = 255: p = 0: q = 255 * (1 - f): t = 255 * f
|
v=255:p=0:q=255*(1-f):t=255*f
|
||||||
select case sector
|
select case sector
|
||||||
case 0: r = v: g = t: b = p
|
case 0:r=v:g=t:b=p
|
||||||
case 1: r = q: g = v: b = p
|
case 1:r=q:g=v:b=p
|
||||||
case 2: r = p: g = v: b = t
|
case 2:r=p:g=v:b=t
|
||||||
case 3: r = p: g = q: b = v
|
case 3:r=p:g=q:b=v
|
||||||
case 4: r = t: g = p: b = v
|
case 4:r=t:g=p:b=v
|
||||||
case 5: r = v: g = p: b = q
|
case 5:r=v:g=p:b=q
|
||||||
end select
|
end select
|
||||||
rgbPart$ = _trim$(str$(r)) + ";" + _trim$(str$(g)) + ";" + _trim$(str$(b))
|
rgbpart$=_trim$(str$(r))+";"+_trim$(str$(g))+";"+_trim$(str$(b))
|
||||||
result = result + chr$(27) + "[38;2;" + rgbPart$ + "m" + umid$(text$, i, 1)
|
result=result+chr$(27)+"[38;2;"+rgbpart$+"m"+umid$(text$,i,1)
|
||||||
next i
|
next i
|
||||||
AnimatedRainbowText$ = result + chr$(27) + "[0m"
|
animatedrainbowtext$=result+chr$(27)+"[0m"
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function MOD_Double (value as double, m as double)
|
function mod_double (value as double,m as double)
|
||||||
MOD_Double = value - (m * int(value / m))
|
mod_double=value-(m*int(value/m))
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function ulen% (txt$)
|
function ulen% (txt$)
|
||||||
dim count%, i%, b%
|
dim count%,i%,b%
|
||||||
count% = 0
|
count%=0
|
||||||
for i% = 1 to len(txt$)
|
for i%=1 to len(txt$)
|
||||||
b% = asc(txt$, i%)
|
b%=asc(txt$,i%)
|
||||||
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then
|
if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
|
||||||
count% = count% + 1
|
count%=count%+1
|
||||||
end if
|
end if
|
||||||
next
|
next
|
||||||
ulen% = count%
|
ulen%=count%
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function umid$ (txt$, startChar%, numChars%)
|
function umid$ (txt$,startchar%,numchars%)
|
||||||
if startChar% < 1 or numChars% <= 0 or txt$ = "" then exit function
|
if startchar%<1 or numchars%<=0 or txt$=""then exit function
|
||||||
|
|
||||||
dim byteIdx%, charCount%, startByte%, endByte%
|
dim byteidx%,charcount%,startbyte%,endbyte%,b%
|
||||||
byteIdx% = 1
|
byteidx%=1
|
||||||
charCount% = 0
|
charcount%=0
|
||||||
|
|
||||||
' 1. Find the starting byte of the character at startChar%
|
while byteidx%<=len(txt$)
|
||||||
while byteIdx% <= len(txt$)
|
b%=asc(txt$,byteidx%)
|
||||||
b% = asc(txt$, byteIdx%)
|
if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
|
||||||
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then
|
charcount%=charcount%+1
|
||||||
charCount% = charCount% + 1
|
if charcount%=startchar% then startbyte%=byteidx%
|
||||||
if charCount% = startChar% then startByte% = byteIdx%
|
|
||||||
end if
|
end if
|
||||||
if startByte% > 0 then exit while
|
if startbyte%>0 then exit while
|
||||||
byteIdx% = byteIdx% + 1
|
byteidx%=byteidx%+1
|
||||||
wend
|
wend
|
||||||
|
|
||||||
if startByte% = 0 then exit function
|
if startbyte%=0 then exit function
|
||||||
|
|
||||||
' 2. Find the byte where the sequence ends
|
byteidx%=startbyte%
|
||||||
byteIdx% = startByte%
|
dim charsfound%
|
||||||
dim charsFound%
|
charsfound%=0
|
||||||
charsFound% = 0
|
|
||||||
|
|
||||||
' We look for the start of the "lastChar + 1" to find the boundary
|
while byteidx%<=len(txt$)
|
||||||
while byteIdx% <= len(txt$)
|
b%=asc(txt$,byteidx%)
|
||||||
b% = asc(txt$, byteIdx%)
|
if (b% and &h80)=0 or (b% and &hc0)=&hc0 then
|
||||||
if (b% and &H80) = 0 or (b% and &HC0) = &HC0 then
|
charsfound%=charsfound%+1
|
||||||
charsFound% = charsFound% + 1
|
|
||||||
end if
|
end if
|
||||||
' If we found the start of the character AFTER our range, stop
|
if charsfound%>numchars% then exit while
|
||||||
if charsFound% > numChars% then exit while
|
byteidx%=byteidx%+1
|
||||||
byteIdx% = byteIdx% + 1
|
|
||||||
wend
|
wend
|
||||||
|
|
||||||
' byteIdx now points to the start of the next char, or LEN+1
|
umid$=mid$(txt$,startbyte%,byteidx%-startbyte%)
|
||||||
umid$ = mid$(txt$, startByte%, byteIdx% - startByte%)
|
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function UWidth% (txt$)
|
function uwidth% (txt$)
|
||||||
dim totalWidth%, i%, char$, cp&
|
dim totalwidth%,i%,char$,cp&
|
||||||
totalWidth% = 0
|
totalwidth%=0
|
||||||
for i% = 1 to ulen(txt$)
|
for i%=1 to ulen(txt$)
|
||||||
char$ = umid(txt$, i%, 1)
|
char$=umid(txt$,i%,1)
|
||||||
cp& = GetCodePoint&(char$)
|
cp&=getcodepoint&(char$)
|
||||||
|
|
||||||
if cp& > &H1100 then
|
if cp&>&h1100 then
|
||||||
totalWidth% = totalWidth% + 2
|
totalwidth%=totalwidth%+2
|
||||||
else
|
else
|
||||||
totalWidth% = totalWidth% + 1
|
totalwidth%=totalwidth%+1
|
||||||
end if
|
end if
|
||||||
next
|
next
|
||||||
UWidth% = totalWidth%
|
uwidth%=totalwidth%
|
||||||
end function
|
end function
|
||||||
|
|
||||||
function GetCodePoint& (utf8Char$)
|
function getcodepoint& (utf8char$)
|
||||||
dim lLength as integer
|
dim llength as integer
|
||||||
lLength = len(utf8Char$)
|
llength=len(utf8char$)
|
||||||
dim b1 as _unsigned _byte, b2 as _unsigned _byte
|
dim b1 as _unsigned _byte,b2 as _unsigned _byte
|
||||||
dim b3 as _unsigned _byte, b4 as _unsigned _byte
|
dim b3 as _unsigned _byte,b4 as _unsigned _byte
|
||||||
|
|
||||||
select case lLength
|
select case llength
|
||||||
case 1
|
case 1
|
||||||
GetCodePoint& = asc(utf8Char$, 1)
|
getcodepoint&=asc(utf8char$,1)
|
||||||
case 2
|
case 2
|
||||||
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2)
|
b1=asc(utf8char$,1):b2=asc(utf8char$,2)
|
||||||
GetCodePoint& = (b1 and &H1F) * 64 + (b2 and &H3F)
|
getcodepoint&=(b1 and &h1f)*64+(b2 and &h3f)
|
||||||
case 3
|
case 3
|
||||||
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 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)
|
getcodepoint&=(b1 and &h0f)*4096+(b2 and &h3f)*64+(b3 and &h3f)
|
||||||
case 4
|
case 4
|
||||||
b1 = asc(utf8Char$, 1): b2 = asc(utf8Char$, 2): b3 = asc(utf8Char$, 3): b4 = asc(utf8Char$, 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)
|
getcodepoint&=(b1 and &h07)*262144+(b2 and &h3f)*4096+(b3 and &h3f)*64+(b4 and &h3f)
|
||||||
end select
|
end select
|
||||||
end function
|
end function
|
||||||
|
|
|
||||||
96
readme.md
96
readme.md
|
|
@ -2,18 +2,21 @@
|
||||||
|
|
||||||
**c**onsole **i**nterface **m**usic **p**layer
|
**c**onsole **i**nterface **m**usic **p**layer
|
||||||
|
|
||||||
A lightweight, keyboard-driven music player for the terminal, written in
|
A keyboard-driven music player for the terminal, written in QuickBASIC (QB64).
|
||||||
QuickBASIC (QB64). It plays audio files and M3U playlists directly from the
|
It plays audio files and M3U playlists directly from the command line, featuring
|
||||||
command line, with a minimal two-line display showing playback state.
|
a minimal two-line display showing playback state and an IPC remote control
|
||||||
|
interface.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Plays individual audio files or M3U playlists
|
- Plays individual audio files or M3U playlists.
|
||||||
- Real-time progress bar and time display (elapsed or remaining)
|
- Real-time progress bar and time display (elapsed or remaining).
|
||||||
- Volume control and seeking via arrow keys
|
- Volume control and seeking via arrow keys.
|
||||||
- Shuffle and repeat modes
|
- Shuffle and repeat modes.
|
||||||
- Cross-platform: works on Linux, macOS, and Windows
|
- **Client/Server IPC Architecture**: Control a running instance of `cimp` from
|
||||||
- Unicode-aware display
|
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
|
## Building
|
||||||
|
|
||||||
|
|
@ -36,9 +39,12 @@ qb64 -x cimp.bas
|
||||||
cimp [options] <file|playlist> [file2 ...]
|
cimp [options] <file|playlist> [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 |
|
| 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`) |
|
| `-r [1]` | `--repeat [1]` | Repeat all (`-r`) or repeat one (`-r 1`) |
|
||||||
| `-n` | `--nooutput` | Run silently with no display |
|
| `-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 <n>` / `--volume <n>` | 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 <file>` | 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
|
### Examples
|
||||||
|
|
||||||
|
#### Play a single file normally
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
# Play a single file
|
|
||||||
cimp song.mp3
|
cimp song.mp3
|
||||||
|
```
|
||||||
|
|
||||||
# Play a playlist shuffled at 80% volume
|
### Remote control examples (Run from a separate terminal)
|
||||||
cimp -s -v 80 playlist.m3u
|
|
||||||
|
|
||||||
# Play multiple files with repeat
|
#### Pause the music
|
||||||
cimp -r track1.ogg track2.ogg track3.ogg
|
|
||||||
|
```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
|
## Keybindings
|
||||||
|
|
||||||
|
When interacting directly with the player interface terminal, use these keys:
|
||||||
|
|
||||||
| Key | Action |
|
| Key | Action |
|
||||||
| ------------- | ----------------------------------------- |
|
| ------------- | ----------------------------------------- |
|
||||||
| `↑` / `↓` | Volume up / down |
|
| `↑` / `↓` | Volume up / down |
|
||||||
|
|
@ -85,13 +135,19 @@ both compilers:
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|
||||||
| File | Description |
|
| File | Description |
|
||||||
| ------------- | ---------------------------------------------------------------------------- |
|
| ------------- | --------------------------------------------------------------------------------------- |
|
||||||
| `cimp.bas` | Main player — QB64-PE source |
|
| `cimp.bas` | Main player — QB64-PE source handling client/server states and UI |
|
||||||
| `terminkey.h` | C library for cross-platform raw keyboard input and terminal width detection |
|
| `terminkey.h` | C library for cross-platform raw keyboard input, IPC pipes/sockets, and terminal sizing |
|
||||||
|
|
||||||
## Notes
|
## 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_<uid>.sock`.
|
||||||
- M3U paths can be absolute or relative to the playlist file's directory.
|
- 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
|
- Volume adjustment is non-linear for a more natural response and better control
|
||||||
at lower ranges.
|
at lower ranges.
|
||||||
|
|
||||||
|
```
|
||||||
|
```
|
||||||
|
|
|
||||||
237
terminkey.h
237
terminkey.h
|
|
@ -3,11 +3,23 @@
|
||||||
#ifdef _WIN32
|
#ifdef _WIN32
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <conio.h>
|
#include <conio.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <stdint.h>
|
||||||
#else
|
#else
|
||||||
#include <termios.h>
|
#include <termios.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <sys/ioctl.h>
|
#include <sys/ioctl.h>
|
||||||
|
#include <sys/socket.h>
|
||||||
|
#include <sys/un.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdint.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Special key codes (unified across platforms)
|
// Special key codes (unified across platforms)
|
||||||
|
|
@ -21,8 +33,17 @@ void echooff();
|
||||||
void echoon();
|
void echoon();
|
||||||
int termwidth();
|
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
|
#ifdef _WIN32
|
||||||
|
|
||||||
|
static HANDLE hServerPipe = INVALID_HANDLE_VALUE;
|
||||||
|
static OVERLAPPED oOverlap;
|
||||||
|
static BOOL fPendingIO = FALSE;
|
||||||
|
|
||||||
int terminkey() {
|
int terminkey() {
|
||||||
if (_kbhit()) {
|
if (_kbhit()) {
|
||||||
int ch = _getch();
|
int ch = _getch();
|
||||||
|
|
@ -65,8 +86,126 @@ int termwidth() {
|
||||||
return -1;
|
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
|
#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() {
|
int terminkey() {
|
||||||
struct termios oldt, newt;
|
struct termios oldt, newt;
|
||||||
int ch;
|
int ch;
|
||||||
|
|
@ -126,4 +265,102 @@ int termwidth() {
|
||||||
return -1;
|
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
|
#endif
|
||||||
|
|
|
||||||
44
test_ipc.bas
Normal file
44
test_ipc.bas
Normal file
|
|
@ -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
|
||||||
12
todo.md
Normal file
12
todo.md
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue