lamer 0.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +5 -5
- data/README.md +222 -30
- data/lib/lamer/decoder.rb +165 -0
- data/lib/lamer/encoder.rb +258 -0
- data/lib/lamer/error.rb +8 -0
- data/lib/lamer/ffi/enums.rb +47 -0
- data/lib/lamer/ffi/functions.rb +150 -0
- data/lib/lamer/ffi/library.rb +26 -0
- data/lib/lamer/ffi.rb +5 -0
- data/lib/lamer/version.rb +5 -0
- data/lib/lamer.rb +217 -86
- metadata +53 -57
- data/Rakefile +0 -33
- data/VERSION +0 -1
- data/lamer.gemspec +0 -55
- data/spec/decoding_spec.rb +0 -18
- data/spec/encoding_spec.rb +0 -161
- data/spec/lamer_spec.rb +0 -126
- data/spec/output.mp3 +0 -0
- data/spec/spec_helper.rb +0 -16
- data/spec/test.mp3 +0 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lamer
|
|
4
|
+
class Encoder
|
|
5
|
+
FLUSH_BUFFER_SIZE = 7200
|
|
6
|
+
OUTPUT_BUFFER_SAFETY_MARGIN = 1.25
|
|
7
|
+
|
|
8
|
+
attr_reader :global_flags
|
|
9
|
+
|
|
10
|
+
def initialize(options = {})
|
|
11
|
+
@global_flags = FFI.lame_init
|
|
12
|
+
raise Error, "Failed to initialize LAME encoder" if @global_flags.null?
|
|
13
|
+
|
|
14
|
+
@initialized = false
|
|
15
|
+
configure(options)
|
|
16
|
+
|
|
17
|
+
ObjectSpace.define_finalizer(self, self.class.release(@global_flags))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.release(gfp)
|
|
21
|
+
proc { FFI.lame_close(gfp) unless gfp.null? }
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure(options)
|
|
25
|
+
raise ConfigurationError, "Cannot configure after encoding has started" if @initialized
|
|
26
|
+
|
|
27
|
+
FFI.lame_set_brate(@global_flags, options[:bitrate]) if options[:bitrate]
|
|
28
|
+
FFI.lame_set_in_samplerate(@global_flags, options[:in_samplerate]) if options[:in_samplerate]
|
|
29
|
+
FFI.lame_set_out_samplerate(@global_flags, options[:out_samplerate]) if options[:out_samplerate]
|
|
30
|
+
FFI.lame_set_num_channels(@global_flags, options[:num_channels]) if options[:num_channels]
|
|
31
|
+
FFI.lame_set_quality(@global_flags, options[:quality]) if options[:quality]
|
|
32
|
+
|
|
33
|
+
if options[:mode]
|
|
34
|
+
mode = case options[:mode]
|
|
35
|
+
when :mono then :mono
|
|
36
|
+
when :stereo then :stereo
|
|
37
|
+
when :joint, :joint_stereo then :joint_stereo
|
|
38
|
+
when :dual, :dual_channel then :dual_channel
|
|
39
|
+
else options[:mode]
|
|
40
|
+
end
|
|
41
|
+
FFI.lame_set_mode(@global_flags, mode)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if options[:vbr]
|
|
45
|
+
FFI.lame_set_VBR(@global_flags, :vbr_mtrh)
|
|
46
|
+
FFI.lame_set_VBR_q(@global_flags, options[:vbr_quality]) if options[:vbr_quality]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
FFI.lame_set_lowpassfreq(@global_flags, options[:lowpass]) if options[:lowpass]
|
|
50
|
+
FFI.lame_set_highpassfreq(@global_flags, options[:highpass]) if options[:highpass]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def apply_id3(id3_options)
|
|
54
|
+
raise ConfigurationError, "Cannot set ID3 tags after encoding has started" if @initialized
|
|
55
|
+
|
|
56
|
+
FFI.id3tag_init(@global_flags)
|
|
57
|
+
FFI.id3tag_add_v2(@global_flags)
|
|
58
|
+
|
|
59
|
+
FFI.id3tag_set_title(@global_flags, id3_options[:title].to_s) if id3_options[:title]
|
|
60
|
+
FFI.id3tag_set_artist(@global_flags, id3_options[:artist].to_s) if id3_options[:artist]
|
|
61
|
+
FFI.id3tag_set_album(@global_flags, id3_options[:album].to_s) if id3_options[:album]
|
|
62
|
+
FFI.id3tag_set_year(@global_flags, id3_options[:year].to_s) if id3_options[:year]
|
|
63
|
+
FFI.id3tag_set_comment(@global_flags, id3_options[:comment].to_s) if id3_options[:comment]
|
|
64
|
+
FFI.id3tag_set_track(@global_flags, id3_options[:track_number].to_s) if id3_options[:track_number]
|
|
65
|
+
FFI.id3tag_set_genre(@global_flags, id3_options[:genre].to_s) if id3_options[:genre]
|
|
66
|
+
|
|
67
|
+
if id3_options[:version] == 1
|
|
68
|
+
FFI.id3tag_v1_only(@global_flags)
|
|
69
|
+
elsif id3_options[:version] == 2
|
|
70
|
+
FFI.id3tag_v2_only(@global_flags)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def init_params!
|
|
75
|
+
return if @initialized
|
|
76
|
+
|
|
77
|
+
ret = FFI.lame_init_params(@global_flags)
|
|
78
|
+
raise ConfigurationError, "Failed to initialize LAME parameters (error code: #{ret})" if ret < 0
|
|
79
|
+
|
|
80
|
+
@initialized = true
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def encode_short(left_samples, right_samples = nil)
|
|
84
|
+
init_params!
|
|
85
|
+
|
|
86
|
+
num_samples = left_samples.size
|
|
87
|
+
right_samples ||= left_samples
|
|
88
|
+
|
|
89
|
+
mp3buf_size = calculate_buffer_size(num_samples)
|
|
90
|
+
mp3buf = ::FFI::MemoryPointer.new(:uchar, mp3buf_size)
|
|
91
|
+
|
|
92
|
+
left_ptr = ::FFI::MemoryPointer.new(:short, num_samples)
|
|
93
|
+
left_ptr.write_array_of_int16(left_samples)
|
|
94
|
+
|
|
95
|
+
right_ptr = ::FFI::MemoryPointer.new(:short, num_samples)
|
|
96
|
+
right_ptr.write_array_of_int16(right_samples)
|
|
97
|
+
|
|
98
|
+
bytes = FFI.lame_encode_buffer(@global_flags, left_ptr, right_ptr, num_samples, mp3buf, mp3buf_size)
|
|
99
|
+
raise EncodingError, "Encoding failed with error code: #{bytes}" if bytes < 0
|
|
100
|
+
|
|
101
|
+
mp3buf.read_bytes(bytes)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def encode_short_interleaved(interleaved_samples)
|
|
105
|
+
init_params!
|
|
106
|
+
|
|
107
|
+
num_samples = interleaved_samples.size / 2
|
|
108
|
+
|
|
109
|
+
mp3buf_size = calculate_buffer_size(num_samples)
|
|
110
|
+
mp3buf = ::FFI::MemoryPointer.new(:uchar, mp3buf_size)
|
|
111
|
+
|
|
112
|
+
pcm_ptr = ::FFI::MemoryPointer.new(:short, interleaved_samples.size)
|
|
113
|
+
pcm_ptr.write_array_of_int16(interleaved_samples)
|
|
114
|
+
|
|
115
|
+
bytes = FFI.lame_encode_buffer_interleaved(@global_flags, pcm_ptr, num_samples, mp3buf, mp3buf_size)
|
|
116
|
+
raise EncodingError, "Encoding failed with error code: #{bytes}" if bytes < 0
|
|
117
|
+
|
|
118
|
+
mp3buf.read_bytes(bytes)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def encode_float(left_samples, right_samples = nil)
|
|
122
|
+
init_params!
|
|
123
|
+
|
|
124
|
+
num_samples = left_samples.size
|
|
125
|
+
right_samples ||= left_samples
|
|
126
|
+
|
|
127
|
+
mp3buf_size = calculate_buffer_size(num_samples)
|
|
128
|
+
mp3buf = ::FFI::MemoryPointer.new(:uchar, mp3buf_size)
|
|
129
|
+
|
|
130
|
+
left_ptr = ::FFI::MemoryPointer.new(:float, num_samples)
|
|
131
|
+
left_ptr.write_array_of_float(left_samples)
|
|
132
|
+
|
|
133
|
+
right_ptr = ::FFI::MemoryPointer.new(:float, num_samples)
|
|
134
|
+
right_ptr.write_array_of_float(right_samples)
|
|
135
|
+
|
|
136
|
+
bytes = FFI.lame_encode_buffer_ieee_float(@global_flags, left_ptr, right_ptr, num_samples, mp3buf, mp3buf_size)
|
|
137
|
+
raise EncodingError, "Encoding failed with error code: #{bytes}" if bytes < 0
|
|
138
|
+
|
|
139
|
+
mp3buf.read_bytes(bytes)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def flush
|
|
143
|
+
init_params!
|
|
144
|
+
|
|
145
|
+
mp3buf = ::FFI::MemoryPointer.new(:uchar, FLUSH_BUFFER_SIZE)
|
|
146
|
+
bytes = FFI.lame_encode_flush(@global_flags, mp3buf, FLUSH_BUFFER_SIZE)
|
|
147
|
+
raise EncodingError, "Flush failed with error code: #{bytes}" if bytes < 0
|
|
148
|
+
|
|
149
|
+
mp3buf.read_bytes(bytes)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def encode_file(input_path, output_path, decode_mp3: false)
|
|
153
|
+
pcm_data = if decode_mp3 || input_path.end_with?(".mp3")
|
|
154
|
+
read_mp3_file(input_path)
|
|
155
|
+
else
|
|
156
|
+
read_wav_file(input_path)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
File.open(output_path, "wb") do |output|
|
|
160
|
+
pcm_data[:samples].each_slice(pcm_data[:chunk_size]) do |chunk|
|
|
161
|
+
if pcm_data[:channels] == 2
|
|
162
|
+
mp3_data = encode_short_interleaved(chunk)
|
|
163
|
+
else
|
|
164
|
+
mp3_data = encode_short(chunk)
|
|
165
|
+
end
|
|
166
|
+
output.write(mp3_data)
|
|
167
|
+
end
|
|
168
|
+
output.write(flush)
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
private
|
|
173
|
+
|
|
174
|
+
def read_mp3_file(path)
|
|
175
|
+
decoder = Lamer::Decoder.new
|
|
176
|
+
result = decoder.decode_buffer(File.binread(path))
|
|
177
|
+
|
|
178
|
+
channels = result[:channels] > 0 ? result[:channels] : 2
|
|
179
|
+
sample_rate = result[:sample_rate] > 0 ? result[:sample_rate] : 44100
|
|
180
|
+
|
|
181
|
+
samples = if channels == 2 && result[:right]
|
|
182
|
+
result[:left].zip(result[:right]).flatten
|
|
183
|
+
else
|
|
184
|
+
result[:left]
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
FFI.lame_set_in_samplerate(@global_flags, sample_rate)
|
|
188
|
+
FFI.lame_set_num_channels(@global_flags, channels)
|
|
189
|
+
|
|
190
|
+
{
|
|
191
|
+
samples: samples,
|
|
192
|
+
channels: channels,
|
|
193
|
+
sample_rate: sample_rate,
|
|
194
|
+
chunk_size: sample_rate * channels
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def calculate_buffer_size(num_samples)
|
|
199
|
+
((num_samples * OUTPUT_BUFFER_SAFETY_MARGIN) + FLUSH_BUFFER_SIZE).ceil
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def read_wav_file(path)
|
|
203
|
+
File.open(path, "rb") do |file|
|
|
204
|
+
riff = file.read(4)
|
|
205
|
+
raise Error, "Not a valid WAV file: missing RIFF header" unless riff == "RIFF"
|
|
206
|
+
|
|
207
|
+
file.read(4) # file size
|
|
208
|
+
wave = file.read(4)
|
|
209
|
+
raise Error, "Not a valid WAV file: missing WAVE format" unless wave == "WAVE"
|
|
210
|
+
|
|
211
|
+
fmt_chunk = nil
|
|
212
|
+
data_chunk = nil
|
|
213
|
+
|
|
214
|
+
while !file.eof?
|
|
215
|
+
chunk_id = file.read(4)
|
|
216
|
+
break if chunk_id.nil? || chunk_id.size < 4
|
|
217
|
+
|
|
218
|
+
chunk_size = file.read(4).unpack1("V")
|
|
219
|
+
|
|
220
|
+
case chunk_id
|
|
221
|
+
when "fmt "
|
|
222
|
+
fmt_data = file.read(chunk_size)
|
|
223
|
+
audio_format, channels, sample_rate, byte_rate, block_align, bits_per_sample = fmt_data.unpack("vvVVvv")
|
|
224
|
+
fmt_chunk = {
|
|
225
|
+
audio_format: audio_format,
|
|
226
|
+
channels: channels,
|
|
227
|
+
sample_rate: sample_rate,
|
|
228
|
+
byte_rate: byte_rate,
|
|
229
|
+
block_align: block_align,
|
|
230
|
+
bits_per_sample: bits_per_sample
|
|
231
|
+
}
|
|
232
|
+
when "data"
|
|
233
|
+
data_chunk = file.read(chunk_size)
|
|
234
|
+
break
|
|
235
|
+
else
|
|
236
|
+
file.seek(chunk_size, IO::SEEK_CUR)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
raise Error, "Invalid WAV file: missing fmt chunk" unless fmt_chunk
|
|
241
|
+
raise Error, "Invalid WAV file: missing data chunk" unless data_chunk
|
|
242
|
+
raise Error, "Only 16-bit PCM WAV files are supported" unless fmt_chunk[:bits_per_sample] == 16
|
|
243
|
+
|
|
244
|
+
FFI.lame_set_in_samplerate(@global_flags, fmt_chunk[:sample_rate])
|
|
245
|
+
FFI.lame_set_num_channels(@global_flags, fmt_chunk[:channels])
|
|
246
|
+
|
|
247
|
+
samples = data_chunk.unpack("s*")
|
|
248
|
+
|
|
249
|
+
{
|
|
250
|
+
samples: samples,
|
|
251
|
+
channels: fmt_chunk[:channels],
|
|
252
|
+
sample_rate: fmt_chunk[:sample_rate],
|
|
253
|
+
chunk_size: fmt_chunk[:sample_rate] * fmt_chunk[:channels]
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
data/lib/lamer/error.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lamer
|
|
4
|
+
module FFI
|
|
5
|
+
# VBR mode enumeration from lame.h
|
|
6
|
+
VBR_MODE = enum :vbr_mode, [
|
|
7
|
+
:vbr_off, 0,
|
|
8
|
+
:vbr_mt,
|
|
9
|
+
:vbr_rh,
|
|
10
|
+
:vbr_abr,
|
|
11
|
+
:vbr_mtrh,
|
|
12
|
+
:vbr_max_indicator
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
# MPEG mode enumeration from lame.h
|
|
16
|
+
MPEG_MODE = enum :mpeg_mode, [
|
|
17
|
+
:stereo, 0,
|
|
18
|
+
:joint_stereo,
|
|
19
|
+
:dual_channel,
|
|
20
|
+
:mono,
|
|
21
|
+
:not_set,
|
|
22
|
+
:max_indicator
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
# Preset modes
|
|
26
|
+
PRESET_MODE = enum :preset_mode, [
|
|
27
|
+
:v9, 410,
|
|
28
|
+
:v8, 420,
|
|
29
|
+
:v7, 430,
|
|
30
|
+
:v6, 440,
|
|
31
|
+
:v5, 450,
|
|
32
|
+
:v4, 460,
|
|
33
|
+
:v3, 470,
|
|
34
|
+
:v2, 480,
|
|
35
|
+
:v1, 490,
|
|
36
|
+
:v0, 500,
|
|
37
|
+
:r3mix, 1000,
|
|
38
|
+
:standard, 1001,
|
|
39
|
+
:extreme, 1002,
|
|
40
|
+
:insane, 1003,
|
|
41
|
+
:standard_fast, 1004,
|
|
42
|
+
:extreme_fast, 1005,
|
|
43
|
+
:medium, 1006,
|
|
44
|
+
:medium_fast, 1007
|
|
45
|
+
]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class Lamer
|
|
4
|
+
module FFI
|
|
5
|
+
# Core lifecycle functions
|
|
6
|
+
attach_function :lame_init, [], :pointer
|
|
7
|
+
attach_function :lame_init_params, [:pointer], :int
|
|
8
|
+
attach_function :lame_close, [:pointer], :int
|
|
9
|
+
|
|
10
|
+
# Version info
|
|
11
|
+
attach_function :get_lame_version, [], :string
|
|
12
|
+
attach_function :get_lame_short_version, [], :string
|
|
13
|
+
|
|
14
|
+
# Input settings
|
|
15
|
+
attach_function :lame_set_num_samples, [:pointer, :ulong], :int
|
|
16
|
+
attach_function :lame_get_num_samples, [:pointer], :ulong
|
|
17
|
+
attach_function :lame_set_in_samplerate, [:pointer, :int], :int
|
|
18
|
+
attach_function :lame_get_in_samplerate, [:pointer], :int
|
|
19
|
+
attach_function :lame_set_num_channels, [:pointer, :int], :int
|
|
20
|
+
attach_function :lame_get_num_channels, [:pointer], :int
|
|
21
|
+
|
|
22
|
+
# Output settings
|
|
23
|
+
attach_function :lame_set_out_samplerate, [:pointer, :int], :int
|
|
24
|
+
attach_function :lame_get_out_samplerate, [:pointer], :int
|
|
25
|
+
|
|
26
|
+
# Quality and mode settings
|
|
27
|
+
attach_function :lame_set_quality, [:pointer, :int], :int
|
|
28
|
+
attach_function :lame_get_quality, [:pointer], :int
|
|
29
|
+
attach_function :lame_set_brate, [:pointer, :int], :int
|
|
30
|
+
attach_function :lame_get_brate, [:pointer], :int
|
|
31
|
+
attach_function :lame_set_mode, [:pointer, :mpeg_mode], :int
|
|
32
|
+
attach_function :lame_get_mode, [:pointer], :mpeg_mode
|
|
33
|
+
|
|
34
|
+
# VBR settings
|
|
35
|
+
attach_function :lame_set_VBR, [:pointer, :vbr_mode], :int
|
|
36
|
+
attach_function :lame_get_VBR, [:pointer], :vbr_mode
|
|
37
|
+
attach_function :lame_set_VBR_q, [:pointer, :int], :int
|
|
38
|
+
attach_function :lame_get_VBR_q, [:pointer], :int
|
|
39
|
+
attach_function :lame_set_VBR_quality, [:pointer, :float], :int
|
|
40
|
+
attach_function :lame_get_VBR_quality, [:pointer], :float
|
|
41
|
+
attach_function :lame_set_VBR_min_bitrate_kbps, [:pointer, :int], :int
|
|
42
|
+
attach_function :lame_set_VBR_max_bitrate_kbps, [:pointer, :int], :int
|
|
43
|
+
|
|
44
|
+
# Filtering
|
|
45
|
+
attach_function :lame_set_lowpassfreq, [:pointer, :int], :int
|
|
46
|
+
attach_function :lame_get_lowpassfreq, [:pointer], :int
|
|
47
|
+
attach_function :lame_set_highpassfreq, [:pointer, :int], :int
|
|
48
|
+
attach_function :lame_get_highpassfreq, [:pointer], :int
|
|
49
|
+
|
|
50
|
+
# Psychoacoustic settings
|
|
51
|
+
attach_function :lame_set_ATHonly, [:pointer, :int], :int
|
|
52
|
+
attach_function :lame_set_ATHshort, [:pointer, :int], :int
|
|
53
|
+
attach_function :lame_set_noATH, [:pointer, :int], :int
|
|
54
|
+
|
|
55
|
+
# Encoding functions - 16-bit signed integer PCM
|
|
56
|
+
attach_function :lame_encode_buffer, [
|
|
57
|
+
:pointer, # gfp (global flags pointer)
|
|
58
|
+
:pointer, # buffer_l (left channel, short*)
|
|
59
|
+
:pointer, # buffer_r (right channel, short*)
|
|
60
|
+
:int, # nsamples
|
|
61
|
+
:pointer, # mp3buf (output buffer)
|
|
62
|
+
:int # mp3buf_size
|
|
63
|
+
], :int
|
|
64
|
+
|
|
65
|
+
attach_function :lame_encode_buffer_interleaved, [
|
|
66
|
+
:pointer, # gfp
|
|
67
|
+
:pointer, # pcm (interleaved short*)
|
|
68
|
+
:int, # nsamples (per channel)
|
|
69
|
+
:pointer, # mp3buf
|
|
70
|
+
:int # mp3buf_size
|
|
71
|
+
], :int
|
|
72
|
+
|
|
73
|
+
# Encoding functions - IEEE float PCM
|
|
74
|
+
attach_function :lame_encode_buffer_ieee_float, [
|
|
75
|
+
:pointer, # gfp
|
|
76
|
+
:pointer, # buffer_l (float*)
|
|
77
|
+
:pointer, # buffer_r (float*)
|
|
78
|
+
:int, # nsamples
|
|
79
|
+
:pointer, # mp3buf
|
|
80
|
+
:int # mp3buf_size
|
|
81
|
+
], :int
|
|
82
|
+
|
|
83
|
+
attach_function :lame_encode_buffer_interleaved_ieee_float, [
|
|
84
|
+
:pointer, # gfp
|
|
85
|
+
:pointer, # pcm (interleaved float*)
|
|
86
|
+
:int, # nsamples (per channel)
|
|
87
|
+
:pointer, # mp3buf
|
|
88
|
+
:int # mp3buf_size
|
|
89
|
+
], :int
|
|
90
|
+
|
|
91
|
+
# Flush encoder
|
|
92
|
+
attach_function :lame_encode_flush, [
|
|
93
|
+
:pointer, # gfp
|
|
94
|
+
:pointer, # mp3buf
|
|
95
|
+
:int # mp3buf_size
|
|
96
|
+
], :int
|
|
97
|
+
|
|
98
|
+
attach_function :lame_encode_flush_nogap, [
|
|
99
|
+
:pointer, # gfp
|
|
100
|
+
:pointer, # mp3buf
|
|
101
|
+
:int # mp3buf_size
|
|
102
|
+
], :int
|
|
103
|
+
|
|
104
|
+
# ID3 tag functions
|
|
105
|
+
attach_function :id3tag_init, [:pointer], :void
|
|
106
|
+
attach_function :id3tag_add_v2, [:pointer], :void
|
|
107
|
+
attach_function :id3tag_v1_only, [:pointer], :void
|
|
108
|
+
attach_function :id3tag_v2_only, [:pointer], :void
|
|
109
|
+
attach_function :id3tag_set_title, [:pointer, :string], :void
|
|
110
|
+
attach_function :id3tag_set_artist, [:pointer, :string], :void
|
|
111
|
+
attach_function :id3tag_set_album, [:pointer, :string], :void
|
|
112
|
+
attach_function :id3tag_set_year, [:pointer, :string], :void
|
|
113
|
+
attach_function :id3tag_set_comment, [:pointer, :string], :void
|
|
114
|
+
attach_function :id3tag_set_track, [:pointer, :string], :void
|
|
115
|
+
attach_function :id3tag_set_genre, [:pointer, :string], :void
|
|
116
|
+
|
|
117
|
+
# HIP (LAME's Hip Is a Player) decoder functions for MP3 decoding
|
|
118
|
+
attach_function :hip_decode_init, [], :pointer
|
|
119
|
+
attach_function :hip_decode_exit, [:pointer], :int
|
|
120
|
+
attach_function :hip_decode, [
|
|
121
|
+
:pointer, # hip (decoder handle)
|
|
122
|
+
:pointer, # mp3buf (input)
|
|
123
|
+
:size_t, # len (input size)
|
|
124
|
+
:pointer, # pcm_l (output left channel, short*)
|
|
125
|
+
:pointer # pcm_r (output right channel, short*)
|
|
126
|
+
], :int
|
|
127
|
+
attach_function :hip_decode_headers, [
|
|
128
|
+
:pointer, # hip
|
|
129
|
+
:pointer, # mp3buf
|
|
130
|
+
:size_t, # len
|
|
131
|
+
:pointer, # pcm_l
|
|
132
|
+
:pointer, # pcm_r
|
|
133
|
+
:pointer # mp3data (mp3data_struct*)
|
|
134
|
+
], :int
|
|
135
|
+
|
|
136
|
+
# MP3 data structure for decoder
|
|
137
|
+
class Mp3Data < ::FFI::Struct
|
|
138
|
+
layout :header_parsed, :int,
|
|
139
|
+
:stereo, :int,
|
|
140
|
+
:samplerate, :int,
|
|
141
|
+
:bitrate, :int,
|
|
142
|
+
:mode, :int,
|
|
143
|
+
:mode_ext, :int,
|
|
144
|
+
:framesize, :int,
|
|
145
|
+
:nsamp, :ulong,
|
|
146
|
+
:totalframes, :int,
|
|
147
|
+
:framenum, :int
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ffi"
|
|
4
|
+
|
|
5
|
+
class Lamer
|
|
6
|
+
module FFI
|
|
7
|
+
extend ::FFI::Library
|
|
8
|
+
|
|
9
|
+
LIBRARY_NAMES = case RbConfig::CONFIG["host_os"]
|
|
10
|
+
when /darwin/
|
|
11
|
+
%w[libmp3lame.dylib libmp3lame.0.dylib]
|
|
12
|
+
when /linux/
|
|
13
|
+
%w[libmp3lame.so.0 libmp3lame.so]
|
|
14
|
+
when /mswin|mingw/
|
|
15
|
+
%w[libmp3lame.dll mp3lame.dll lame_enc.dll]
|
|
16
|
+
else
|
|
17
|
+
%w[libmp3lame.so libmp3lame.dylib mp3lame]
|
|
18
|
+
end.freeze
|
|
19
|
+
|
|
20
|
+
begin
|
|
21
|
+
ffi_lib LIBRARY_NAMES
|
|
22
|
+
rescue LoadError => e
|
|
23
|
+
raise LoadError, "Could not load libmp3lame. Please install LAME (e.g., `brew install lame` on macOS). Original error: #{e.message}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/lamer/ffi.rb
ADDED