ffi-sndfile 0.1.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,194 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SndFile
4
+ # FFI::Struct definitions for the structs in sndfile.h, in the order of
5
+ # that header. Field names follow the C names.
6
+ module Native
7
+ # Helpers for structs with fixed-size char arrays: read them as Ruby
8
+ # strings (up to the first NUL) and write Ruby strings into them.
9
+ module CharArrayFields
10
+ # The char array +field+ as a String, up to the first NUL byte.
11
+ def string(field)
12
+ self[field].to_ptr.get_bytes(0, self[field].size).unpack1("Z*")
13
+ end
14
+
15
+ # Writes +value+ into the char array +field+, NUL-padded. A value may
16
+ # fill the array exactly; a longer one raises ArgumentError.
17
+ def set_string(field, value)
18
+ bytes = value.to_s.b
19
+ size = self[field].size
20
+ raise ArgumentError, "#{field} is at most #{size} bytes, got #{bytes.bytesize}" if bytes.bytesize > size
21
+
22
+ self[field].to_ptr.put_bytes(0, bytes.ljust(size, "\0"))
23
+ value
24
+ end
25
+ end
26
+
27
+ # SF_INFO: passed to sf_open, which fills it in when reading and reads
28
+ # it when writing.
29
+ class Info < FFI::Struct
30
+ layout :frames, :int64,
31
+ :samplerate, :int,
32
+ :channels, :int,
33
+ :format, :int,
34
+ :sections, :int,
35
+ :seekable, :int
36
+ end
37
+
38
+ # SF_FORMAT_INFO: describes a format (SFC_GET_FORMAT_INFO and friends).
39
+ class FormatInfo < FFI::Struct
40
+ layout :format, :int,
41
+ :name, :string,
42
+ :extension, :string
43
+ end
44
+
45
+ # SF_DITHER_INFO (reserved for future use by libsndfile).
46
+ class DitherInfo < FFI::Struct
47
+ layout :type, :int,
48
+ :level, :double,
49
+ :name, :string
50
+ end
51
+
52
+ # SF_EMBED_FILE_INFO: where a sound file sits inside a larger file.
53
+ class EmbedFileInfo < FFI::Struct
54
+ layout :offset, :int64,
55
+ :length, :int64
56
+ end
57
+
58
+ # SF_CUE_POINT: one cue marker.
59
+ class CuePoint < FFI::Struct
60
+ include CharArrayFields
61
+
62
+ layout :indx, :int32,
63
+ :position, :uint32,
64
+ :fcc_chunk, :int32,
65
+ :chunk_start, :int32,
66
+ :block_start, :int32,
67
+ :sample_offset, :uint32,
68
+ :name, [:char, 256]
69
+ end
70
+
71
+ # SF_CUES: up to 100 cue markers (SFC_GET_CUE / SFC_SET_CUE).
72
+ class Cues < FFI::Struct
73
+ MAX_CUE_POINTS = 100
74
+
75
+ layout :cue_count, :uint32,
76
+ :cue_points, [CuePoint, MAX_CUE_POINTS]
77
+ end
78
+
79
+ # SF_INSTRUMENT: sampler instrument data (SFC_GET/SET_INSTRUMENT).
80
+ class Instrument < FFI::Struct
81
+ # One entry of SF_INSTRUMENT's loops array.
82
+ class Loop < FFI::Struct
83
+ layout :mode, :int,
84
+ :start, :uint32,
85
+ :end, :uint32,
86
+ :count, :uint32
87
+ end
88
+
89
+ MAX_LOOPS = 16
90
+
91
+ layout :gain, :int,
92
+ :basenote, :char,
93
+ :detune, :char,
94
+ :velocity_lo, :char,
95
+ :velocity_hi, :char,
96
+ :key_lo, :char,
97
+ :key_hi, :char,
98
+ :loop_count, :int,
99
+ :loops, [Loop, MAX_LOOPS]
100
+ end
101
+
102
+ # SF_LOOP_INFO: loop/tempo information (SFC_GET_LOOP_INFO).
103
+ class LoopInfo < FFI::Struct
104
+ layout :time_sig_num, :short,
105
+ :time_sig_den, :short,
106
+ :loop_mode, :int,
107
+ :num_beats, :int,
108
+ :bpm, :float,
109
+ :root_key, :int,
110
+ :future, [:int, 6]
111
+ end
112
+
113
+ # SF_BROADCAST_INFO: the Broadcast WAV "bext" chunk, with 256 bytes of
114
+ # coding history (SFC_GET/SET_BROADCAST_INFO).
115
+ class BroadcastInfo < FFI::Struct
116
+ include CharArrayFields
117
+
118
+ layout :description, [:char, 256],
119
+ :originator, [:char, 32],
120
+ :originator_reference, [:char, 32],
121
+ :origination_date, [:char, 10],
122
+ :origination_time, [:char, 8],
123
+ :time_reference_low, :uint32,
124
+ :time_reference_high, :uint32,
125
+ :version, :short,
126
+ :umid, [:char, 64],
127
+ :loudness_value, :int16,
128
+ :loudness_range, :int16,
129
+ :max_true_peak_level, :int16,
130
+ :max_momentary_loudness, :int16,
131
+ :max_shortterm_loudness, :int16,
132
+ :reserved, [:char, 180],
133
+ :coding_history_size, :uint32,
134
+ :coding_history, [:char, 256]
135
+ end
136
+
137
+ # SF_CART_TIMER: one post timer of SF_CART_INFO.
138
+ class CartTimer < FFI::Struct
139
+ include CharArrayFields
140
+
141
+ layout :usage, [:char, 4],
142
+ :value, :int32
143
+ end
144
+
145
+ # SF_CART_INFO: the AES46 "cart" chunk, with 256 bytes of tag text
146
+ # (SFC_GET/SET_CART_INFO).
147
+ class CartInfo < FFI::Struct
148
+ include CharArrayFields
149
+
150
+ layout :version, [:char, 4],
151
+ :title, [:char, 64],
152
+ :artist, [:char, 64],
153
+ :cut_id, [:char, 64],
154
+ :client_id, [:char, 64],
155
+ :category, [:char, 64],
156
+ :classification, [:char, 64],
157
+ :out_cue, [:char, 64],
158
+ :start_date, [:char, 10],
159
+ :start_time, [:char, 8],
160
+ :end_date, [:char, 10],
161
+ :end_time, [:char, 8],
162
+ :producer_app_id, [:char, 64],
163
+ :producer_app_version, [:char, 64],
164
+ :user_def, [:char, 64],
165
+ :level_reference, :int32,
166
+ :post_timers, [CartTimer, 8],
167
+ :reserved, [:char, 276],
168
+ :url, [:char, 1024],
169
+ :tag_text_size, :uint32,
170
+ :tag_text, [:char, 256]
171
+ end
172
+
173
+ # SF_VIRTUAL_IO: the callbacks sf_open_virtual uses instead of a file.
174
+ # Every callback gets the user_data pointer passed to sf_open_virtual.
175
+ class VirtualIO < FFI::Struct
176
+ layout :get_filelen, callback([:pointer], :int64),
177
+ :seek, callback(%i[int64 int pointer], :int64),
178
+ :read, callback(%i[pointer int64 pointer], :int64),
179
+ :write, callback(%i[pointer int64 pointer], :int64),
180
+ :tell, callback([:pointer], :int64)
181
+ end
182
+
183
+ # SF_CHUNK_INFO: a chunk of a sound file (sf_set_chunk and the chunk
184
+ # iterator functions).
185
+ class ChunkInfo < FFI::Struct
186
+ include CharArrayFields
187
+
188
+ layout :id, [:char, 64],
189
+ :id_size, :uint,
190
+ :datalen, :uint,
191
+ :data, :pointer
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sndfile/constants"
4
+ require "sndfile/library_version"
5
+
6
+ module SndFile
7
+ # The libsndfile bindings: an FFI::Library with every function in
8
+ # sndfile.h, plus the structs and enums those functions use.
9
+ module Native
10
+ extend FFI::Library
11
+
12
+ ffi_lib ["sndfile", "libsndfile.so.1", "libsndfile.1.dylib", "libsndfile-1"]
13
+
14
+ typedef :pointer, :sndfile
15
+ typedef :int64, :sf_count_t
16
+
17
+ # Attached first, so the version check below can run before the other
18
+ # functions: attaching a function that an older libsndfile lacks would
19
+ # fail with an unclear FFI::NotFoundError.
20
+ attach_function :sf_version_string, [], :string
21
+ LibraryVersion.ensure_supported!
22
+ end
23
+ end
24
+
25
+ require "sndfile/native/enums"
26
+ require "sndfile/native/structs"
27
+ require "sndfile/native/handle"
28
+ require "sndfile/native/functions"
@@ -0,0 +1,289 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SndFile
4
+ # A sound file opened for reading, writing or both.
5
+ #
6
+ # Samples are flat Arrays with the channels interleaved: left, right, left,
7
+ # right, ... for stereo. They are :float (the default, -1.0..1.0), :double,
8
+ # :short or :int; libsndfile converts them from and to the file's encoding.
9
+ class SoundFile
10
+ include Commands
11
+
12
+ SAMPLE_TYPES = { short: :short, int: :int, float: :float, double: :double }.freeze
13
+
14
+ # Frames per read in #each_buffer and #read without a frame count.
15
+ DEFAULT_BUFFER_FRAMES = 4096
16
+
17
+ # The path, or the IO object for files opened through VirtualIO.
18
+ attr_reader :target
19
+
20
+ # :read, :write or :rdwr.
21
+ attr_reader :mode
22
+
23
+ # Opens a sound file. Without a block, returns the SoundFile. With a
24
+ # block, yields it, closes it afterwards (also when the block raises)
25
+ # and returns the block's value.
26
+ def self.open(target, mode = :read, **, &)
27
+ file = new(target, mode, **)
28
+ return file unless block_given?
29
+
30
+ begin
31
+ yield file
32
+ ensure
33
+ file.close
34
+ end
35
+ end
36
+
37
+ # Opens +target+, a path or an IO-like object (see VirtualIO), in +mode+
38
+ # :read, :write or :rdwr.
39
+ #
40
+ # Writing needs +samplerate+, +channels+ and a +format+: anything
41
+ # Format.coerce accepts, with an optional +subtype+ and +endian+. Without
42
+ # a format, it follows the path's extension. Reading RAW files needs the
43
+ # same options. +strings+ are set before any audio is written.
44
+ def initialize(target, mode = :read, format: nil, subtype: nil, endian: nil, samplerate: nil, channels: nil,
45
+ strings: nil)
46
+ @target = target
47
+ @mode = Native.enum_type(:sf_mode).find(Native.enum_type(:sf_mode).to_native(mode, nil))
48
+ info = native_info(format_for(format, subtype, endian), samplerate, channels)
49
+ @handle = open_handle(target, info)
50
+ self.strings.update(strings) if strings
51
+ end
52
+
53
+ # The file's current Info (SFC_GET_CURRENT_SF_INFO), which includes the
54
+ # frames written so far.
55
+ def info
56
+ struct = Native::Info.new
57
+ command(SFC_GET_CURRENT_SF_INFO, struct)
58
+ Info.from_native(struct)
59
+ end
60
+
61
+ def frames = info.frames
62
+ def samplerate = info.samplerate
63
+ def channels = info.channels
64
+ def format = info.format
65
+ def sections = info.sections
66
+ def seekable? = info.seekable?
67
+ def duration = info.duration
68
+
69
+ # The string metadata (title, artist, ...); see Strings.
70
+ def strings
71
+ @strings ||= Strings.new(self)
72
+ end
73
+
74
+ # The current byte rate (bytes of audio per second), or nil if unknown.
75
+ def byterate
76
+ rate = Native.sf_current_byterate(handle)
77
+ rate unless rate.negative?
78
+ end
79
+
80
+ # Reads up to +frames+ frames and returns their samples as a flat,
81
+ # interleaved Array (empty at the end of the file). Without +frames+,
82
+ # reads everything up to the end of the file.
83
+ def read(frames = nil, type: :float)
84
+ return read_all(type) if frames.nil?
85
+
86
+ ffi_type = ffi_type_for(type)
87
+ channel_count = channels
88
+ buffer = FFI::MemoryPointer.new(ffi_type, frames * channel_count)
89
+ read_frames = Native.public_send(:"sf_readf_#{type}", handle, buffer, frames)
90
+ check_io!
91
+ buffer.public_send(:"get_array_of_#{ffi_type}", 0, read_frames * channel_count)
92
+ end
93
+
94
+ # Yields the samples of each buffer of up to +frames+ frames until the
95
+ # end of the file. Returns an Enumerator without a block.
96
+ def each_buffer(frames = DEFAULT_BUFFER_FRAMES, type: :float)
97
+ return enum_for(:each_buffer, frames, type: type) unless block_given?
98
+
99
+ loop do
100
+ samples = read(frames, type: type)
101
+ break if samples.empty?
102
+
103
+ yield samples
104
+ end
105
+ self
106
+ end
107
+
108
+ # Reads up to +bytes+ bytes of the encoded audio data (sf_read_raw).
109
+ def read_raw(bytes)
110
+ buffer = FFI::MemoryPointer.new(:char, bytes)
111
+ count = Native.sf_read_raw(handle, buffer, bytes)
112
+ check_io!
113
+ buffer.get_bytes(0, count)
114
+ end
115
+
116
+ # Writes +samples+, a flat interleaved Array whose size is a multiple of
117
+ # the channel count. Returns the number of frames written.
118
+ def write(samples, type: :float)
119
+ ffi_type = ffi_type_for(type)
120
+ frames = frame_count(samples)
121
+ return 0 if frames.zero?
122
+
123
+ buffer = FFI::MemoryPointer.new(ffi_type, samples.size)
124
+ buffer.public_send(:"put_array_of_#{ffi_type}", 0, samples)
125
+ written = Native.public_send(:"sf_writef_#{type}", handle, buffer, frames)
126
+ check_io!(failed: written < frames)
127
+ written
128
+ end
129
+
130
+ # Writes +samples+ as floats and returns the file, for chaining.
131
+ def <<(samples)
132
+ write(samples)
133
+ self
134
+ end
135
+
136
+ # Writes +data+, already encoded in the file's format (sf_write_raw).
137
+ # Returns the number of bytes written.
138
+ def write_raw(data)
139
+ bytes = data.to_s.b
140
+ buffer = FFI::MemoryPointer.new(:char, bytes.bytesize)
141
+ buffer.put_bytes(0, bytes)
142
+ written = Native.sf_write_raw(handle, buffer, bytes.bytesize)
143
+ check_io!(failed: written < bytes.bytesize)
144
+ written
145
+ end
146
+
147
+ # Forces written data to disk (sf_write_sync).
148
+ def sync
149
+ Native.sf_write_sync(handle)
150
+ self
151
+ end
152
+
153
+ # Moves to frame +frames+, relative to +whence+ (:set, :cur or :end).
154
+ # Returns the new position in frames.
155
+ def seek(frames, whence = :set)
156
+ position = Native.sf_seek(handle, frames, whence)
157
+ check_io!(failed: position.negative?)
158
+ position
159
+ end
160
+
161
+ # The current position in frames.
162
+ def tell
163
+ seek(0, :cur)
164
+ end
165
+ alias pos tell
166
+
167
+ def rewind
168
+ seek(0, :set)
169
+ self
170
+ end
171
+
172
+ # Closes the file, writing any buffered data and the final header. Does
173
+ # nothing when it is already closed.
174
+ def close
175
+ return if closed?
176
+
177
+ result = Native.sf_close(handle)
178
+ @closed = true
179
+ @virtual_io&.raise_error!
180
+ Error.check_error(result)
181
+ nil
182
+ end
183
+
184
+ def closed?
185
+ @closed == true
186
+ end
187
+
188
+ # libsndfile's description of the last error on this file.
189
+ def error_message
190
+ Native.sf_strerror(handle)
191
+ end
192
+
193
+ # The Native::Handle. Raises ClosedFileError once the file is closed.
194
+ def handle
195
+ raise ClosedFileError, "#{inspect} is closed" if closed?
196
+
197
+ @handle
198
+ end
199
+
200
+ def inspect
201
+ state = closed? ? "closed" : "#{format} #{channels}ch #{samplerate}Hz"
202
+ "#<#{self.class.name} #{target_name} (#{mode}, #{state})>"
203
+ end
204
+
205
+ private
206
+
207
+ def format_for(format, subtype, endian)
208
+ return Format.coerce(format, subtype, endian) if format
209
+ return unless mode == :write
210
+
211
+ return Format.for_path(target) if path?(target)
212
+
213
+ raise ArgumentError, "A format is needed to write to #{target.class}"
214
+ end
215
+
216
+ def native_info(format, samplerate, channels)
217
+ check_write_options(format, samplerate, channels) if mode == :write
218
+ Native::Info.new.tap do |info|
219
+ info[:format] = format.to_i if format
220
+ info[:samplerate] = samplerate.to_i if samplerate
221
+ info[:channels] = channels.to_i if channels
222
+ end
223
+ end
224
+
225
+ def check_write_options(format, samplerate, channels)
226
+ raise ArgumentError, "Cannot tell the format of #{target_name}; pass format:" unless format
227
+
228
+ missing = { samplerate: samplerate, channels: channels }.select { |_, value| value.nil? }.keys
229
+ raise ArgumentError, "Writing needs #{missing.join(" and ")}" if missing.any?
230
+ return if format.valid?(samplerate: samplerate, channels: channels)
231
+
232
+ raise ArgumentError, "libsndfile cannot write #{format} with #{samplerate} Hz and #{channels} channel(s)"
233
+ end
234
+
235
+ def open_handle(target, info)
236
+ if path?(target)
237
+ Native.sf_open(target.to_s, mode, info)
238
+ elsif target.respond_to?(:read) || target.respond_to?(:write)
239
+ @virtual_io = VirtualIO.new(target)
240
+ open_virtual(info)
241
+ else
242
+ raise ArgumentError, "Expected a path or an IO, got #{target.class}"
243
+ end
244
+ end
245
+
246
+ def open_virtual(info)
247
+ Native.sf_open_virtual(@virtual_io.struct, mode, info, nil)
248
+ rescue SndFileError
249
+ @virtual_io.raise_error!
250
+ raise
251
+ end
252
+
253
+ def read_all(type)
254
+ each_buffer(DEFAULT_BUFFER_FRAMES, type: type).reduce([], :concat)
255
+ end
256
+
257
+ def ffi_type_for(type)
258
+ SAMPLE_TYPES.fetch(type) do
259
+ raise ArgumentError, "Unknown sample type #{type.inspect}; expected one of #{SAMPLE_TYPES.keys.join(", ")}"
260
+ end
261
+ end
262
+
263
+ def frame_count(samples)
264
+ channel_count = channels
265
+ unless (samples.size % channel_count).zero?
266
+ raise ArgumentError, "#{samples.size} samples is not a whole number of #{channel_count}-channel frames"
267
+ end
268
+
269
+ samples.size / channel_count
270
+ end
271
+
272
+ # Raises an IO callback's exception, or libsndfile's error when +failed+
273
+ # (a short write or failed seek) or when libsndfile recorded one.
274
+ def check_io!(failed: false)
275
+ @virtual_io&.raise_error!
276
+ Error.check_sndfile(handle)
277
+ raise SndFileError, error_message if failed
278
+ end
279
+
280
+ # Strings and Pathnames are paths; anything else is opened as an IO.
281
+ def path?(target)
282
+ target.is_a?(String) || (defined?(::Pathname) && target.is_a?(::Pathname))
283
+ end
284
+
285
+ def target_name
286
+ path?(target) ? target.to_s.inspect : target.class.name
287
+ end
288
+ end
289
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SndFile
4
+ # The string metadata of a sound file, such as its title and artist
5
+ # (sf_get_string / sf_set_string). libsndfile stores them as the format's
6
+ # own tags: Vorbis comments in Ogg and FLAC, the INFO chunk in WAV. Most
7
+ # formats need them set before any audio is written.
8
+ class Strings
9
+ include Enumerable
10
+
11
+ # The string types, in libsndfile's order (SF_STR_*).
12
+ NAMES = Native.enum_type(:sf_str).symbols.freeze
13
+
14
+ def initialize(sound_file)
15
+ @sound_file = sound_file
16
+ end
17
+
18
+ # The string +name+ (one of NAMES), or nil when the file has none. It is
19
+ # returned as UTF-8, or as binary when it isn't valid UTF-8.
20
+ def [](name)
21
+ value = Native.sf_get_string(@sound_file.handle, name_for(name))
22
+ return if value.nil?
23
+
24
+ utf8 = value.dup.force_encoding(Encoding::UTF_8)
25
+ utf8.valid_encoding? ? utf8 : value
26
+ end
27
+
28
+ # Sets the string +name+ (one of NAMES). Raises a SndFileError when the
29
+ # file format doesn't support it or audio data was already written.
30
+ def []=(name, value)
31
+ handle = @sound_file.handle
32
+ Error.check_error(Native.sf_set_string(handle, name_for(name), value.to_s.encode(Encoding::UTF_8)), handle)
33
+ end
34
+
35
+ NAMES.each do |name|
36
+ define_method(name) { self[name] }
37
+ define_method(:"#{name}=") { |value| self[name] = value }
38
+ end
39
+ alias track_number tracknumber
40
+ alias track_number= tracknumber=
41
+
42
+ # Sets every string in +hash+, e.g. update(title: "Song", artist: "Me").
43
+ def update(hash)
44
+ hash.each { |name, value| self[name] = value }
45
+ self
46
+ end
47
+
48
+ # Yields each string that is set, as name and value.
49
+ def each(&)
50
+ return enum_for(:each) unless block_given?
51
+
52
+ to_h.each(&)
53
+ self
54
+ end
55
+
56
+ def to_h
57
+ NAMES.filter_map { |name| (value = self[name]) && [name, value] }.to_h
58
+ end
59
+
60
+ def inspect
61
+ "#<#{self.class.name} #{to_h.inspect}>"
62
+ end
63
+
64
+ private
65
+
66
+ def name_for(name)
67
+ symbol = name.to_s.downcase.to_sym
68
+ symbol = :tracknumber if symbol == :track_number
69
+ return symbol if NAMES.include?(symbol)
70
+
71
+ raise ArgumentError, "Unknown string #{name.inspect}; expected one of #{NAMES.join(", ")}"
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SndFile
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SndFile
4
+ # Lets libsndfile read and write an IO-like object, such as a StringIO,
5
+ # through sf_open_virtual. The object needs #read or #write, and #seek and
6
+ # #pos for formats that seek, which most do when writing.
7
+ #
8
+ # Exceptions from the IO must not unwind through libsndfile, so they are
9
+ # kept in #error, and SoundFile raises them again.
10
+ class VirtualIO
11
+ # For each SF_VIRTUAL_IO callback: the method that implements it and the
12
+ # value it returns to libsndfile when the IO raises.
13
+ CALLBACKS = {
14
+ get_filelen: [:io_length, -1],
15
+ seek: [:io_seek, -1],
16
+ read: [:io_read, 0],
17
+ write: [:io_write, 0],
18
+ tell: [:io_tell, -1]
19
+ }.freeze
20
+ private_constant :CALLBACKS
21
+
22
+ attr_reader :io, :struct, :error
23
+
24
+ def initialize(io)
25
+ @io = io
26
+ @struct = Native::VirtualIO.new
27
+ # The struct only holds C function pointers; keep the procs referenced
28
+ # so they aren't garbage collected while libsndfile may call them.
29
+ @callbacks = callbacks
30
+ @callbacks.each { |name, callback| @struct[name] = callback }
31
+ end
32
+
33
+ # Raises (and clears) the exception an IO callback caught, if any.
34
+ def raise_error!
35
+ return unless @error
36
+
37
+ error = @error
38
+ @error = nil
39
+ raise error
40
+ end
41
+
42
+ private
43
+
44
+ # The procs ignore the user_data pointer libsndfile passes last.
45
+ def callbacks
46
+ CALLBACKS.transform_values do |(method_name, failure_value)|
47
+ ->(*args) { guard(failure_value) { send(method_name, *args[0...-1]) } }
48
+ end
49
+ end
50
+
51
+ def guard(failure_value)
52
+ yield
53
+ rescue StandardError => e
54
+ @error ||= e
55
+ failure_value
56
+ end
57
+
58
+ def io_length
59
+ return io.size if io.respond_to?(:size)
60
+ return io.stat.size if io.respond_to?(:stat)
61
+
62
+ -1
63
+ end
64
+
65
+ def io_seek(offset, whence)
66
+ io.seek(offset, whence)
67
+ io.pos
68
+ end
69
+
70
+ def io_read(ptr, count)
71
+ data = io.read(count)
72
+ return 0 if data.nil? || data.empty?
73
+
74
+ ptr.put_bytes(0, data)
75
+ data.bytesize
76
+ end
77
+
78
+ def io_write(ptr, count)
79
+ io.write(ptr.get_bytes(0, count))
80
+ end
81
+
82
+ def io_tell
83
+ io.pos
84
+ end
85
+ end
86
+ end