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
|