1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
| | #!/usr/bin/env ruby
# Copyright (C) 2013-2019 all contributors <dtas-all@nongnu.org>
# License: GPL-3.0+ <https://www.gnu.org/licenses/gpl-3.0.txt>
# frozen_string_literal: true
#
# Note: no idea what I'm doing, especially w.r.t. curses
require 'dtas'
require 'dtas/unix_client'
require 'dtas/rg_state'
require 'dtas/sigevent'
require 'dtas/process'
require 'dtas/format'
include DTAS::Process
require 'yaml'
begin
require 'curses'
rescue LoadError
abort "please install the 'curses' RubyGem to use #$0"
end
tsec = false
se = DTAS::Sigevent.new
trap(:WINCH) { se.signal }
w = DTAS::UNIXClient.new
w.req_ok('watch')
c = DTAS::UNIXClient.new
cur = YAML.load(c.req('current'))
readable = [ se, w, $stdin ]
# current rg mode
rg_mode = DTAS::RGState::RG_MODE.keys.unshift("off")
if (rg = cur["rg"]) && (rg = rg["mode"])
rg_mode_i = rg_mode.index(cur["rg"]["mode"])
else
rg_mode_i = 0
end
def update_tfmt(prec, tsec)
if tsec
prec == 0 ? '%_8s' : "%_8s.%#{prec}N"
else
prec == 0 ? '%H:%M:%S' : "%H:%M:%S.%#{prec}N"
end
end
trap(:INT) { exit(0) }
trap(:TERM) { exit(0) }
# time precision
prec_nr = 1
prec_step = (0..9).to_a
prec_max = prec_step.size - 1
tfmt = update_tfmt(prec_step[prec_nr], tsec)
events = []
interval = 1.0 / 10 ** prec_nr
def show_events(lineno, screen, events)
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
Curses.addstr('Events:')
maxy = screen.maxy - 1
maxx = screen.maxx
events.reverse_each do |e|
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
extra = e.size/maxx
break if (lineno + extra) >= maxy
# deal with long lines
if extra
rewind = lineno
extra.times do
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
end
Curses.setpos(rewind, 0)
Curses.addstr(e)
Curses.setpos(lineno, 0)
else
Curses.addstr(e)
end
end
# discard events we can't show
nr_events = events.size
if nr_events > maxy
events = events[(nr_events - maxy)..-1]
until lineno >= screen.maxy
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
end
else
Curses.setpos(maxy + 1, 0)
Curses.clrtoeol
end
end
def fmt_to_s(f)
r = [ f['rate'], f['channels'], f['type'], f['bits'] ]
r.compact!
r.join(',')
end
def rg_string(rg, current)
rv = "rg mode=#{rg['mode']||'off'}".dup
defaults = DTAS::RGState::RG_DEFAULT
# don't show things that are too rare
%w(preamp fallback_gain).each do |param|
val = rg[param] || defaults[param]
rv << " #{param}=#{val}"
end
env = current && current["env"] and rv << " / RGFX='#{env['RGFX']}'"
rv
end
def may_fail(res, events)
events << res if res != "OK"
end
pre_mute_vol = 1.0
enc_locale = Encoding.find("locale")
$stdout.set_encoding(enc_locale)
enc_opts = { undef: :replace, invalid: :replace, replace: '?' }
begin
Curses.init_screen
Curses.nonl
Curses.cbreak
Curses.noecho
screen = Curses.stdscr
screen.scrollok(true)
screen.keypad(true)
loop do
lineno = -1
pfmt = cur['format']
elapsed = samples = 0
fmt = total = ''
if current = cur['current']
infile = current['infile'] || current['command']
elapsed = DTAS.now - current['spawn_at']
if (nr = cur['current_initial']) && (current_format = current['format'])
rate = current_format['rate'].to_f
elapsed += nr / rate
samples = current['samples']
fmt = "(#{fmt_to_s(current_format)} > #{fmt_to_s(pfmt)})"
else
fmt = fmt_to_s(pfmt)
fmt = "(#{fmt} > #{fmt})"
end
elsif cur['paused'] && infile = cur['current_paused']
fmt = "[paused] (#{fmt_to_s(pfmt)})"
infile = infile['command'] if Hash === infile
if Array === infile
infile, elapsed = infile
elapsed = elapsed.to_i
samples = rate = 0
if (bypass = cur['bypass']) && bypass.include?('rate')
rate = pfmt['rate'].to_f
else
rate = qx(%W(soxi -r #{infile}), err: DTAS.null).to_i rescue 0
end
elapsed /= rate.to_f if rate != 0
end
end
if infile
# FS encoding != locale encoding, but we need to display an FS path
# name to whatever locale the terminal is encoded to, so force it
# and risk mojibake...
infile.encode(enc_locale, enc_opts)
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
Curses.addstr(infile)
total = " [#{Time.at(samples / rate).utc.strftime(tfmt)}]" if samples != 0
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
if rate != 0
Curses.addstr("#{Time.at(elapsed).utc.strftime(tfmt)}#{total} #{fmt}")
else
Curses.addstr("#{elapsed} samples #{total} #{fmt}")
end
else
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
Curses.addstr('idle')
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
end
rg = cur['rg'] || {}
rgs = rg_string(rg, current)
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
Curses.addstr(rgs)
Curses.setpos(lineno += 1, 0)
Curses.clrtoeol
cur_vol = rg['volume'] || 1.0
extra = [ "volume=#{cur_vol}" ]
tl = cur['tracklist'] || {}
%w(repeat shuffle consume).each { |x| extra << "#{x}=#{tl[x] || 'false'}" }
trim = cur['trim'] || 'off'
extra << "trim=#{trim}"
Curses.addstr(extra.join(' '))
pre_mute_vol = cur_vol if cur_vol != 0
show_events(lineno, screen, events)
Curses.refresh # draw and wait
r = IO.select(readable, nil, nil, current ? interval : nil) or next
r[0].each do |io|
case io
when se
se.readable_iter {} # noop, just consume the event
Curses.clear
when w
event = w.res_wait
case event
when "pause"
if current
current['infile'] || current['command']
end
when %r{\Afile }
end
events << "#{Time.now.strftime(tfmt)} #{event}"
# something happened, refresh current
# we could be more intelligent here, maybe, but too much work.
cur = YAML.load(c.req('current'))
when $stdin
# keybindings taken from mplayer / vi
case key = Curses.getch
when "j" then c.req_ok("seek -5")
when "k" then c.req_ok("seek +5")
when "q" then exit(0)
when Curses::KEY_DOWN then c.req_ok("seek -60")
when Curses::KEY_UP then c.req_ok("seek +60")
when Curses::KEY_LEFT then c.req_ok("seek -10")
when Curses::KEY_RIGHT then c.req_ok("seek +10")
when Curses::KEY_BACKSPACE then c.req_ok("seek 0")
# yes, some of us have long audio files
when Curses::KEY_PPAGE then c.req_ok("seek +600")
when Curses::KEY_NPAGE then c.req_ok("seek -600")
when '9' then c.req_ok('rg volume-=0.01')
when '0' then c.req_ok('rg volume+=0.01')
when '=' then c.req_ok('rg volume=1')
when '7' then c.req_ok('rg preamp-=1')
when '8' then c.req_ok('rg preamp+=1')
when 'm' then c.req_ok("rg volume=#{cur_vol == 0 ? pre_mute_vol : 0}")
when "F" then c.req_ok("rg fallback_gain+=1")
when "f" then c.req_ok("rg fallback_gain-=1")
when ">" then c.req_ok("tl next")
when "<" then c.req_ok("tl prev")
when "!" then may_fail(c.req("cue prev"), events)
when "@" then may_fail(c.req("cue next"), events)
when "o" then tfmt = update_tfmt(prec_step[prec_nr], tsec = !tsec)
when " "
c.req("play_pause")
when "r" # cycle through replaygain modes
rg_mode_i >= 1 and c.req_ok("rg mode=#{rg_mode[rg_mode_i -= 1]}")
when "R"
rg_mode_i < (rg_mode.size - 1) and
c.req_ok("rg mode=#{rg_mode[rg_mode_i += 1]}")
when "p" # lower precision of time display
if prec_nr >= 1
prec_nr -= 1
tfmt = update_tfmt(prec_step[prec_nr], tsec)
interval = 1.0 / 10 ** prec_nr
end
when "P" # increase precision of time display
if prec_nr < prec_max
prec_nr += 1
tfmt = update_tfmt(prec_step[prec_nr], tsec)
interval = 1.0 / 10 ** prec_nr
end
when 27 # TODO readline/edit mode?
else
Curses.setpos(screen.maxy - 1, 0)
Curses.clrtoeol
Curses.addstr("unknown key=#{key.inspect}")
end
end
end
end
rescue EOFError
Curses.close_screen
abort "dtas-player exited"
ensure
Curses.close_screen
end
|