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.
@@ -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
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lamer
4
+ class Error < StandardError; end
5
+ class EncodingError < Error; end
6
+ class DecodingError < Error; end
7
+ class ConfigurationError < Error; end
8
+ end
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "ffi/library"
4
+ require_relative "ffi/enums"
5
+ require_relative "ffi/functions"
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Lamer
4
+ VERSION = "1.0.0"
5
+ end