From 3e09ac0c10c95bb24a08af62393b4f761e2743d0 Mon Sep 17 00:00:00 2001 From: Eric Wong Date: Sat, 24 Aug 2013 09:54:45 +0000 Subject: initial commit --- bin/dtas-console | 160 ++++++++++++++++++++++++++++++++++++++++++++++ bin/dtas-ctl | 10 +++ bin/dtas-cueedit | 78 ++++++++++++++++++++++ bin/dtas-enq | 13 ++++ bin/dtas-graph | 129 +++++++++++++++++++++++++++++++++++++ bin/dtas-msinkctl | 51 +++++++++++++++ bin/dtas-play-xover-delay | 85 ++++++++++++++++++++++++ bin/dtas-player | 34 ++++++++++ bin/dtas-sinkedit | 46 +++++++++++++ bin/dtas-sourceedit | 39 +++++++++++ 10 files changed, 645 insertions(+) create mode 100755 bin/dtas-console create mode 100755 bin/dtas-ctl create mode 100755 bin/dtas-cueedit create mode 100755 bin/dtas-enq create mode 100755 bin/dtas-graph create mode 100755 bin/dtas-msinkctl create mode 100755 bin/dtas-play-xover-delay create mode 100755 bin/dtas-player create mode 100755 bin/dtas-sinkedit create mode 100755 bin/dtas-sourceedit (limited to 'bin') diff --git a/bin/dtas-console b/bin/dtas-console new file mode 100755 index 0000000..3693c99 --- /dev/null +++ b/bin/dtas-console @@ -0,0 +1,160 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# 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 'curses' +require 'yaml' + +w = DTAS::UNIXClient.new +w.req_ok('watch') +c = DTAS::UNIXClient.new +cur = YAML.load(c.req('current')) +readable = [ w, $stdin ] + +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 + +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 + +begin + Curses.init_screen + Curses.nonl + Curses.cbreak + Curses.noecho + screen = Curses.stdscr + screen.scrollok(true) + screen.keypad(true) + loop do + lineno = -1 + if current = cur['current'] + Curses.setpos(lineno += 1, 0) + Curses.clrtoeol + Curses.addstr(current['infile']) + + 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)}]" + else + total = "" + end + + Curses.setpos(lineno += 1, 0) + Curses.clrtoeol + Curses.addstr("#{Time.at(elapsed).strftime(tfmt)}#{total}") + else + Curses.setpos(lineno += 1, 0) + Curses.clrtoeol + Curses.addstr(cur['paused'] ? 'paused' : 'idle') + Curses.setpos(lineno += 1, 0) + Curses.clrtoeol + end + + 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 w + event = w.res_wait + 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 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 " " + c.req("play_pause") + 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 diff --git a/bin/dtas-ctl b/bin/dtas-ctl new file mode 100755 index 0000000..c05dc28 --- /dev/null +++ b/bin/dtas-ctl @@ -0,0 +1,10 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'dtas/unix_client' + +# Unix paths are encoding agnostic +ARGV.map! { |arg| arg.b } +c = DTAS::UNIXClient.new +puts c.req(ARGV) diff --git a/bin/dtas-cueedit b/bin/dtas-cueedit new file mode 100755 index 0000000..2a205ac --- /dev/null +++ b/bin/dtas-cueedit @@ -0,0 +1,78 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'tempfile' +require 'shellwords' +usage = "Usage: #$0 FILENAME" +editor = ENV["VISUAL"] || ENV["EDITOR"] +ARGV.size > 0 or abort usage + +def err_msg(cmd, status) + err_cmd = cmd.map { |f| Shellwords.escape(f) }.join(' ') + "E: #{err_cmd} failed: #{status.inspect}" +end + +def x!(*cmd) + system(*cmd) or abort err_msg(cmd, $?) +end + +def tmpfile(file, suffix) + tmp = Tempfile.new([File.basename(file), suffix]) + tmp.sync = true + tmp.binmode + tmp +end + +ARGV.each do |file| + # Unix paths are encoding agnostic + file = file.b + file =~ /\.flac\z/i or warn "Unsupported suffix, assuming FLAC" + tmp = tmpfile(file, '.cue') + begin + # export the temporary file for the user to edit + if system(*%W(metaflac --export-cuesheet-to=#{tmp.path} #{file})) + remove_existing = true + backup = tmpfile(file, '.backup.cue') + else + remove_existing = false + backup = nil + tmp.puts 'FILE "dtas-cueedit.tmp.flac" FLAC' + tmp.puts ' TRACK 01 AUDIO' + tmp.puts ' INDEX 01 00:00:00' + end + + # keep a backup, in case the user screws up the edit + original = File.binread(tmp.path) + backup.write(original) if backup + + # user edits the file + x!("#{editor} #{tmp.path}") + + # avoid an expensive update if the user didn't change anything + current = File.binread(tmp.path) + if current == original + $stderr.puts "tags for #{Shellwords.escape(file)} unchanged" if $DEBUG + next + end + + # we must remove existing tags before importing again + if remove_existing + x!(*%W(metaflac --remove --block-type=CUESHEET #{file})) + end + + # try to import the new file but restore from the original backup if the + # user wrote an improperly formatted cue sheet + cmd = %W(metaflac --import-cuesheet-from=#{tmp.path} #{file}) + if ! system(*cmd) && backup + warn err_msg(cmd, $?) + warn "E: restoring original from backup" + x!(*%W(metaflac --import-cuesheet-from=#{backup.path} #{file})) + warn "E: backup cuesheet restored, #{Shellwords.escape(file)} unchanged" + exit(false) + end + ensure + tmp.close! + backup.close! if backup + end +end diff --git a/bin/dtas-enq b/bin/dtas-enq new file mode 100755 index 0000000..e8ebd66 --- /dev/null +++ b/bin/dtas-enq @@ -0,0 +1,13 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'dtas/unix_client' +c = DTAS::UNIXClient.new + +ARGV.each do |path| + # Unix paths are encoding agnostic + path = File.expand_path(path.b) + res = c.req_ok(%W(enq #{path})) + puts "#{path} #{res}" +end diff --git a/bin/dtas-graph b/bin/dtas-graph new file mode 100755 index 0000000..a668d47 --- /dev/null +++ b/bin/dtas-graph @@ -0,0 +1,129 @@ +#!/usr/bin/perl -w +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +use strict; +use Graph::Easy; # for ASCII-art graphs +$^O =~ /linux/ or print STDERR "$0 probably only works on Linux...\n"; +scalar @ARGV or die "Usage: $0 PID [PID ...]"; +our $procfs = $ENV{PROCFS} || "/proc"; +my $cull_self_pipe = 1; + +# returns a list of PIDs which are children of the given PID +sub children_of { + my ($ppid) = @_; + my %rv = map { + s/\A\s*//g; + s/\s*\z//g; + my ($pid, $cmd) = split(/\s+/, $_, 2); + $pid => $cmd; + } `ps h -o pid,cmd --ppid=$ppid`; + \%rv; +} + +# pid => [ child pids ] +my %pids; + +# pipe_ino => { r => [ [pid, fd], [pid, fd] ], w => [ [pid, fd], ... ] } +my %pipes; + +# pid => argv +my %cmds; + +my $pipe_nr = 0; +# pipe_id -> pipe_ino (we use short pipe IDs to save space on small terms) +my %graphed; + +my @to_scan = (@ARGV); + +sub cmd_of { + my ($pid) = @_; + my $cmd = `ps h -o cmd $pid`; + chomp $cmd; + $cmd; +} + +while (my $pid = shift @to_scan) { + my $children = children_of($pid); + my @child_pids = keys %$children; + push @to_scan, @child_pids; + $pids{$pid} = \@child_pids; + foreach my $child (keys @child_pids) { + $cmds{$child} = $children->{$child}; + } +} + +# build up a hash of pipes and their connectivity to processes: +# +foreach my $pid (keys %pids) { + my @out = `lsof -p $pid`; + # output is like this: + # play 12739 ew 0r FIFO 0,7 0t0 36924019 pipe + foreach my $l (@out) { + my @l = split(/\s+/, $l); + $l[4] eq "FIFO" or next; + + my $fd = $l[3]; + my $pipe_ino = $l[7]; + my $info = $pipes{$pipe_ino} ||= { r => [], w => [] }; + if ($fd =~ s/r\z//) { + push @{$info->{r}}, [ $pid, $fd ]; + } elsif ($fd =~ s/w\z//) { + push @{$info->{w}}, [ $pid, $fd ]; + } + + } +} + +my $graph = Graph::Easy->new(); +foreach my $pid (keys %pids) { + $graph->add_node($pid); +} + +foreach my $pipe_ino (keys %pipes) { + my $info = $pipes{$pipe_ino}; + my %pairs; + my $pipe_node; + + foreach my $rw (qw(r w)) { + foreach my $pidfd (@{$info->{$rw}}) { + my ($pid, $fd) = @$pidfd; + my $pair = $pairs{$pid} ||= {}; + my $fds = $pair->{$rw} ||= []; + push @$fds, $fd; + } + } + # use Data::Dumper; + # print Dumper(\%pairs); + my $nr_pids = scalar keys %pairs; + + foreach my $pid (keys %pairs) { + my $pair = $pairs{$pid}; + my $r = $pair->{r} || []; + my $w = $pair->{w} || []; + next if $cull_self_pipe && $nr_pids == 1 && @$r && @$w; + + unless ($pipe_node) { + my $pipe_id = $pipe_nr++; + $graphed{$pipe_id} = $pipe_ino; + $pipe_node = "|$pipe_id"; + $graph->add_node($pipe_node); + } + + $graph->add_edge($pipe_node, $pid, join(',', @$r)) if @$r; + $graph->add_edge($pid, $pipe_node, join(',', @$w)) if @$w; + } +} + +print " PID COMMAND\n"; +foreach my $pid (sort { $a <=> $b } keys %pids) { + printf "% 6d", $pid; + print " ", $cmds{$pid} || cmd_of($pid), "\n"; +} + +print "\nPIPEID PIPE_INO\n"; +foreach my $pipe_id (sort { $a <=> $b } keys %graphed) { + printf "% 6s", "|$pipe_id"; + print " ", $graphed{$pipe_id}, "\n"; +} + +print $graph->as_ascii; diff --git a/bin/dtas-msinkctl b/bin/dtas-msinkctl new file mode 100755 index 0000000..a3cb8ce --- /dev/null +++ b/bin/dtas-msinkctl @@ -0,0 +1,51 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'yaml' +require 'dtas/unix_client' +usage = "#$0 SINK" +c = DTAS::UNIXClient.new +action = ARGV.shift +sink_args = ARGV + +buf = c.req("sink ls") +abort(buf) if buf =~ /\AERR/ +player_sinks = buf.split(/ /) + +non_existent = sink_args - player_sinks +non_existent[0] and + abort "non-existent sink(s): #{non_existent.join(' ')}" + +def activate_sinks(c, sink_names) + sink_names.each { |name| c.req_ok("sink ed #{name} active=true") } +end + +def deactivate_sinks(c, sink_names) + sink_names.each { |name| c.req_ok("sink ed #{name} active=false") } +end + +def filter(c, player_sinks, key) + rv = [] + player_sinks.each do |name| + buf = c.req("sink cat #{name}") + sink = YAML.load(buf) + rv << sink["name"] if sink[key] + end + rv +end + +case action +when "active-set" + activate_sinks(c, sink_args) + deactivate_sinks(c, player_sinks - sink_args) +when "active-add" # idempotent + activate_sinks(c, sink_args) +when "active-sub" + deactivate_sinks(c, sink_args) +when "active", "nonblock" + abort "`#$0 #{action}' takes no arguments" if sink_args[0] + puts filter(c, player_sinks, action).join(' ') +else + abort usage +end diff --git a/bin/dtas-play-xover-delay b/bin/dtas-play-xover-delay new file mode 100755 index 0000000..d5b6533 --- /dev/null +++ b/bin/dtas-play-xover-delay @@ -0,0 +1,85 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +USAGE = "Usage: #$0 [-x FREQ] [-l] /dev/fd/LO /dev/fd/HI DELAY [DELAY ...]" +require 'optparse' +dryrun = false +xover = '80' +delay_lo = [] +delay_hi = [] +adj_delay = delay_hi +out_channels = out_rate = out_type = nil + +lowpass = 'lowpass %s lowpass %s' +highpass = 'highpass %s highpass %s' + +op = OptionParser.new('', 24, ' ') do |opts| + opts.banner = USAGE + opts.on('-x', '--crossover-frequency FREQ') do |freq| + xover = freq + end + opts.on('-l', '--lowpass-delay') { adj_delay = delay_lo } + opts.on('-c', '--channels INTEGER') { |val| out_channels = val } + opts.on('-r', '--rate RATE') { |val| out_rate = val } + opts.on('-t', '--type FILE-TYPE') { |val| out_type = val } + opts.on('-n', '--dry-run') { dryrun = true } + opts.on('--lowpass FORMAT_STRING') { |s| lowpass = s } + opts.on('--highpass FORMAT_STRING') { |s| highpass = s } + opts.parse!(ARGV) +end + +dev_fd_lo = ARGV.shift +dev_fd_hi = ARGV.shift +if ARGV.delete('-') + # we re-add the '-' below + out_channels && out_rate && out_type or + abort "-c, -r, and -t must all be specified for standard output" + cmd = "sox" +elsif out_channels || out_rate || out_type + abort "standard output (`-') must be specified with -c, -r, or -t" +else + cmd = "play" +end +soxfmt = ENV["SOXFMT"] or abort "#$0 SOXFMT undefined" + +# configure the sox "delay" effect +delay = ARGV.dup +delay[0] or abort USAGE +channels = ENV['CHANNELS'] or abort "#$0 CHANNELS env must be set" +channels = channels.to_i +adj_delay.replace(delay.dup) +until adj_delay.size == channels + adj_delay << delay.last +end +adj_delay.unshift("delay") + +# prepare two inputs: +delay_lo = delay_lo.join(' ') +delay_hi = delay_hi.join(' ') + +lowpass_args = [] +lowpass.gsub('%s') { |s| lowpass_args << xover; s } +highpass_args = [] +highpass.gsub('%s') { |s| highpass_args << xover; s } + +lo = "|exec sox #{soxfmt} #{dev_fd_lo} -p " \ + "#{sprintf(lowpass, *lowpass_args)} #{delay_lo}".strip +hi = "|exec sox #{soxfmt} #{dev_fd_hi} -p " \ + "#{sprintf(highpass, *highpass_args)} #{delay_hi}".strip + +args = [ "-m", "-v1", lo, "-v1", hi ] +case cmd +when "sox" + args.unshift "sox" + args.concat(%W(-t#{out_type} -c#{out_channels} -r#{out_rate} -)) +when "play" + args.unshift "play" +else + abort "BUG: bad cmd=#{cmd.inspect}" +end +if dryrun + p args +else + exec *args, close_others: false +end diff --git a/bin/dtas-player b/bin/dtas-player new file mode 100755 index 0000000..8b78771 --- /dev/null +++ b/bin/dtas-player @@ -0,0 +1,34 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +Thread.abort_on_exception = $stderr.sync = $stdout.sync = true +require 'yaml' +require 'dtas/player' +sock = (ENV["DTAS_PLAYER_SOCK"] || + File.expand_path("~/.dtas/player.sock")).b +state = (ENV["DTAS_PLAYER_STATE"] || + File.expand_path("~/.dtas/player_state.yml")).b +[ sock, state ].each do |file| + dir = File.dirname(file) + next if File.directory?(dir) + require 'fileutils' + FileUtils.mkpath(dir) +end + +state = DTAS::StateFile.new(state) +if tmp = state.tryload + tmp["socket"] ||= sock + player = DTAS::Player.load(tmp) + player.state_file ||= state +else + player = DTAS::Player.new + player.state_file = state + player.socket = sock +end + +at_exit { player.close } +player.bind +trap(:INT) { exit } +trap(:TERM) { exit } +player.run diff --git a/bin/dtas-sinkedit b/bin/dtas-sinkedit new file mode 100755 index 0000000..d1e8fb4 --- /dev/null +++ b/bin/dtas-sinkedit @@ -0,0 +1,46 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'dtas/unix_client' +require 'dtas/disclaimer' +require 'tempfile' +require 'yaml' +editor = ENV["VISUAL"] || ENV["EDITOR"] +c = DTAS::UNIXClient.new +usage = "#$0 SINKNAME" +ARGV.size == 1 or abort usage +name = ARGV[0] + +tmp = Tempfile.new(%w(dtas-sinkedit .yml)) +tmp.sync = true +tmp.binmode + +buf = c.req(%W(sink cat #{name})) +abort(buf) if buf =~ /\AERR/ +buf << DTAS_DISCLAIMER + +tmp.write(buf) +cmd = "#{editor} #{tmp.path}" +system(cmd) or abort "#{cmd} failed: #$?" +tmp.rewind +sink = YAML.load(tmp.read) + +cmd = %W(sink ed #{name}) +if env = sink["env"] + env.each do |k,v| + cmd << (v.nil? ? "env##{k}" : "env.#{k}=#{v}") + end +end + +%w(nonblock active).each do |field| + if sink.key?(field) + cmd << "#{field}=#{sink[field] ? 'true' : 'false'}" + end +end + +%w(prio pipe_size command).each do |field| + value = sink[field] and cmd << "#{field}=#{value}" +end + +c.req_ok(cmd) diff --git a/bin/dtas-sourceedit b/bin/dtas-sourceedit new file mode 100755 index 0000000..9d329d7 --- /dev/null +++ b/bin/dtas-sourceedit @@ -0,0 +1,39 @@ +#!/usr/bin/env ruby +# -*- encoding: binary -*- +# Copyright (C) 2013, Eric Wong +# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt) +require 'tempfile' +require 'yaml' +require 'dtas/unix_client' +require 'dtas/disclaimer' +editor = ENV["VISUAL"] || ENV["EDITOR"] +c = DTAS::UNIXClient.new +usage = $0 +ARGV.size == 0 or abort usage +name = ARGV[0] + +tmp = Tempfile.new(%w(dtas-sourceedit .yml)) +tmp.sync = true +tmp.binmode + +buf = c.req(%W(source cat)) +abort(buf) if buf =~ /\AERR/ + +tmp.write(buf << DTAS_DISCLAIMER) +cmd = "#{editor} #{tmp.path}" +system(cmd) or abort "#{cmd} failed: #$?" +tmp.rewind +source = YAML.load(tmp.read) + +cmd = %W(source ed) +if env = source["env"] + env.each do |k,v| + cmd << (v.nil? ? "env##{k}" : "env.#{k}=#{v}") + end +end + +%w(command).each do |field| + value = source[field] and cmd << "#{field}=#{value}" +end + +c.req_ok(cmd) -- cgit v1.2.3-24-ge0c7