dtas.git  about / heads / tags
duct tape audio suite for *nix
blob cfcec6420cbaa18f42fb50042103f73002b8d29a 4648 bytes (raw)
$ git show 0.12.x-stable:lib/dtas/format.rb	# shows this blob on the CLI

  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
 
# Copyright (C) 2013-2015 all contributors <dtas-all@nongnu.org>
# License: GPLv3 or later (https://www.gnu.org/licenses/gpl-3.0.txt)
require_relative '../dtas'
require_relative 'process'
require_relative 'serialize'

# class represents an audio format (type/bits/channels/sample rate/...)
# used throughout dtas
class DTAS::Format # :nodoc:
  include DTAS::Process
  include DTAS::Serialize

  NATIVE_ENDIAN = [1].pack("l") == [1].pack("l>") ? "big" : "little"

  attr_accessor :type # s32, f32, f64 ... any point in others?
  attr_accessor :channels # 1..666
  attr_accessor :rate     # 44100, 48000, 88200, 96000, 176400, 192000 ...
  attr_accessor :bits # only set for playback on 16-bit DACs
  attr_accessor :endian

  FORMAT_DEFAULTS = {
    "type" => "s32",
    "channels" => 2,
    "rate" => 44100,
    "bits" => nil,   # default: implied from type
    "endian" => nil, # unspecified
  }
  SIVS = FORMAT_DEFAULTS.keys

  def self.load(hash)
    fmt = new
    return fmt unless hash
    (SIVS & hash.keys).each do |k|
      fmt.instance_variable_set("@#{k}", hash[k])
    end
    fmt
  end

  # some of these are sox-only, but that's what we mainly care about
  # for audio-editing.  We only use ffmpeg/avconv for odd files during
  # playback.

  extend DTAS::Process

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

  def self.from_file(env, infile)
    fmt = new
    fmt.channels = qx(env, %W(soxi -c #{infile})).to_i
    fmt.type = qx(env, %W(soxi -t #{infile})).strip
    fmt.rate = qx(env, %W(soxi -r #{infile})).to_i
    fmt.bits ||= precision(env, infile)
    fmt
  end

  def initialize
    FORMAT_DEFAULTS.each do |k,v|
      instance_variable_set("@#{k}", v)
    end
  end

  def to_sox_arg
   rv = %W(-t#@type -c#@channels -r#@rate)
   rv.concat(%W(-b#@bits)) if @bits # needed for play(1) to 16-bit DACs
   rv
  end

  # returns 'be' or 'le' depending on endianess
  def endian2
    case e = @endian || NATIVE_ENDIAN
    when "big"
      "be"
    when "little"
      "le"
    else
      raise"unsupported endian=#{e}"
    end
  end

  # returns 1 or 0 depending on endianess
  def endian_opusenc
    case e = @endian || NATIVE_ENDIAN
    when "big" then "1"
    when "little" then "0"
    else
      raise"unsupported endian=#{e}"
    end
  end

  def to_eca_arg
    %W(-f #{@type}_#{endian2},#@channels,#@rate)
  end

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

  def to_hash
    ivars_to_hash(SIVS)
  end

  def ==(other)
    a = to_hash
    b = other.to_hash
    a["bits"] ||= bits_per_sample
    b["bits"] ||= other.bits_per_sample
    a == b
  end

  # for the _decoded_ output
  def bits_per_sample
    return @bits if @bits
    /\A[fst](8|16|24|32|64)\z/ =~ @type or
      raise TypeError, "invalid type=#@type (must be s32/f32/f64)"
    $1.to_i
  end

  def bytes_per_sample
    bits_per_sample / 8
  end

  def to_env
    rv = {
      "SOX_FILETYPE" => @type,
      "CHANNELS" => @channels.to_s,
      "RATE" => @rate.to_s,
      "ENDIAN" => @endian || NATIVE_ENDIAN,
      "SOXFMT" => to_sox_arg.join(' '),
      "ECAFMT" => to_eca_arg.join(' '),
      "ENDIAN2" => endian2,
      "ENDIAN_OPUSENC" => endian_opusenc,
    }
    begin # don't set these if we can't get them, SOX_FILETYPE may be enough
      rv["BITS_PER_SAMPLE"] = bits_per_sample.to_s
    rescue TypeError
    end
    rv
  end

  def bytes_to_samples(bytes)
    bytes / bytes_per_sample / @channels
  end

  def bytes_to_time(bytes)
    Time.at(bytes_to_samples(bytes) / @rate.to_f)
  end

  def valid_type?(type)
    !!(type =~ %r{\A[us](?:8|16|24|32)\z} || type =~ %r{\Af(?:32|64)\z})
  end

  def valid_endian?(endian)
    !!(endian =~ %r{\A(?:big|little|swap)\z})
  end

  # HH:MM:SS.frac (don't bother with more complex times, too much code)
  # part of me wants to drop this feature from playq, feels like bloat...
  def hhmmss_to_samples(hhmmss)
    Numeric === hhmmss and return hhmmss * @rate
    time = hhmmss.dup
    rv = 0
    if time.sub!(/\.(\d+)\z/, "")
      # convert fractional second to sample count:
      rv = ("0.#$1".to_f * @rate).to_i
    end

    # deal with HH:MM:SS
    t = time.split(/:/)
    raise ArgumentError, "Bad time format: #{hhmmss}" if t.size > 3

    mult = 1
    while part = t.pop
      rv += part.to_i * mult * @rate
      mult *= 60
    end
    rv
  end
end

git clone git://80x24.org/dtas.git
git clone https://80x24.org/dtas.git