#!/usr/bin/env ruby # Copyright (C) 2013, Eric Wong and all contributors # License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) # # Note: no idea what I'm doing, especially w.r.t. curses require 'dtas/unix_client' require 'dtas/rg_state' require 'dtas/sigevent' require 'yaml' begin require 'curses' rescue LoadError abort "please install the 'curses' RubyGem to use #$0" end 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) prec == 0 ? '%H:%M:%S' : "%H:%M:%S.%#{prec}N" 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]) events = [] interval = 1.0 / 10 ** prec_nr pause = nil 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'}" 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 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'] if current = cur['current'] Curses.setpos(lineno += 1, 0) Curses.clrtoeol Curses.addstr(current['infile'] || current['command']) elapsed = Time.now.to_f - current['spawn_at'] if (nr = cur['current_initial']) && (current_format = current['format']) rate = current_format['rate'].to_f elapsed += nr / rate total = " [#{Time.at(current['samples'] / rate).strftime(tfmt)}]" fmt = "(#{fmt_to_s(current_format)} > #{fmt_to_s(pfmt)})" else total = "" fmt = fmt_to_s(pfmt) fmt = "(#{fmt} > #{fmt})" end Curses.setpos(lineno += 1, 0) Curses.clrtoeol Curses.addstr("#{Time.at(elapsed).strftime(tfmt)}#{total} #{fmt}") else Curses.setpos(lineno += 1, 0) Curses.clrtoeol Curses.addstr(cur['paused'] ? "paused #{pause}" : 'idle') Curses.setpos(lineno += 1, 0) Curses.clrtoeol end rgs = rg_string(cur["rg"] || {}, current) Curses.setpos(lineno += 1, 0) Curses.clrtoeol Curses.addstr(rgs) 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 pause = current['infile'] || current['command'] end when %r{\Afile } pause = nil 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 preamp-=1") when "0" then c.req_ok("rg preamp+=1") when "F" then c.req_ok("rg fallback_gain+=1") when "f" then c.req_ok("rg fallback_gain-=1") 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]) 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]) 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