about summary refs log tree commit homepage
path: root/lib/dtas/source.rb
blob: f6dd443d41ece5f759cdfe055f8ef97c05c13377 (plain)
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
# -*- encoding: binary -*-
# :stopdoc:
# Copyright (C) 2013, Eric Wong <normalperson@yhbt.net>
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
require_relative '../dtas'
require_relative 'command'
require_relative 'format'
require_relative 'replaygain'
require_relative 'process'
require_relative 'serialize'

# this is usually one input file
class DTAS::Source
  attr_reader :infile
  attr_reader :offset
  require_relative 'source/common'
  require_relative 'source/mp3'

  include DTAS::Command
  include DTAS::Process
  include DTAS::Source::Common
  include DTAS::Source::Mp3

  SOURCE_DEFAULTS = COMMAND_DEFAULTS.merge(
    "command" => 'exec sox "$INFILE" $SOXFMT - $TRIMFX $RGFX',
    "comments" => nil,
  )

  SIVS = %w(infile comments command env)

  def initialize(infile, offset = nil)
    command_init(SOURCE_DEFAULTS)
    @format = nil
    @infile = infile
    @offset = offset
    @comments = nil
    @samples = nil
  end

  # this exists mainly to make the mpris interface easier, but it's not
  # necessary, the mpris interface also knows the sample rate
  def offset_us
    (offset_samples / format.rate.to_f) * 1000000
  end

  # returns any offset in samples (relative to the original source file),
  # likely zero unless seek was used
  def offset_samples
    return 0 unless @offset
    case @offset
    when /\A\d+s\z/
      @offset.to_i
    else
      format.hhmmss_to_samples(@offset)
    end
  end

  def precision
    qx(%W(soxi -p #@infile), err: "/dev/null").to_i # sox.git f4562efd0aa3
  rescue # fallback to parsing the whole output
    s = qx(%W(soxi #@infile), err: "/dev/null")
    s =~ /Precision\s+:\s*(\d+)-bit/
    v = $1.to_i
    return v if v > 0
    raise TypeError, "could not determine precision for #@infile"
  end

  def format
    @format ||= begin
      fmt = DTAS::Format.new
      fmt.from_file(@infile)
      fmt.bits ||= precision
      fmt
    end
  end

  # A user may be downloading the file and start playing
  # it before the download completes, this refreshes
  def samples!
    @samples = nil
    samples
  end

  # This is the number of samples according to the samples in the source
  # file itself, not the decoded output
  def samples
    @samples ||= qx(%W(soxi -s #@infile)).to_i
  rescue => e
    warn e.message
    0
  end

  # just run soxi -a
  def __load_comments
    tmp = {}
    case @infile
    when String
      err = ""
      cmd = %W(soxi -a #@infile)
      begin
        qx(cmd, err: err).split(/\n/).each do |line|
          key, value = line.split(/=/, 2)
          key && value or next
          # TODO: multi-line/multi-value/repeated tags
          tmp[key.upcase] = value
        end
      rescue => e
        if /FAIL formats: no handler for file extension/ =~ err
          warn("#{xs(cmd)}: #{err}")
        else
          warn("#{e.message} (#{e.class})")
        end
        # TODO: fallbacks
      end
    end
    tmp
  end

  def comments
    @comments ||= __load_comments
  end

  def replaygain
    DTAS::ReplayGain.new(comments) || DTAS::ReplayGain.new(mp3gain_comments)
  end

  def spawn(format, rg_state, opts)
    raise "BUG: #{self.inspect}#spawn called twice" if @to_io
    e = format.to_env
    e["INFILE"] = @infile

    # make sure these are visible to the "current" command...
    @env["TRIMFX"] = @offset ? "trim #@offset" : nil
    @env["RGFX"] = rg_state.effect(self) || nil

    @pid = dtas_spawn(e.merge!(@env), command_string, opts)
  end

  def to_hsh
    to_hash.delete_if { |k,v| v == SOURCE_DEFAULTS[k] }
  end

  def to_hash
    rv = ivars_to_hash(SIVS)
    rv["samples"] = samples
    rv
  end
end