rockbox_ffi 0.3.0 → 0.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 65435e6c7a7b4ef8e59f3bc088ce2c9eb978b34d49e134c3e42413e9790bb333
4
- data.tar.gz: 0d14838882902eaa6818a9726518f0c782848ad34e09b6afe6ca9ddbbf6cdd1a
3
+ metadata.gz: 98d2db65260fc33b5cfd9cf8963c2f96b782079c999ded173f64611a1f3d2554
4
+ data.tar.gz: 8b6fa4f9f221aad0367c913cc6ce494ea97ce62d3658309cc6ed8252003e28d1
5
5
  SHA512:
6
- metadata.gz: f4275075ebc6d35b7b495dfc3a081b97960f5dc81f8f279372acf5b852fc0723776f536f9f87602811499a23d1653de9a8c5b072144444fe15dd0cd4b4dc1066
7
- data.tar.gz: 5b4a35ca244aed1f570cf2d639a131eb8244645fe4ebe86513193345a95716107c72c67f7037f195e8048bff4f885c1d3c8af1fdd218ffabcf200860b841e363
6
+ metadata.gz: 97340abbfae011f7f3f09a0463046c4831eef6d429ea6d0f1ea0a814a8aac085a12309d46fe7e5ee670772aed59cf9045cbfe72606bdf34e0aab685c534aab04
7
+ data.tar.gz: b28b979145c98b851ab27cf731fb9929603a946a17435e656cb2f350af594bc8fefe525545c6bd2dafa3dd3a6236dcf998b672636f3d09ce16c95b94069d3d3d
data/README.md CHANGED
@@ -5,7 +5,8 @@
5
5
  ![FFI](https://img.shields.io/badge/FFI-fiddle-CC342D)
6
6
  ![License](https://img.shields.io/badge/license-GPL--2.0--or--later-blue)
7
7
 
8
- Ruby bindings for the Rockbox **DSP**, **metadata**, and **playback** engine,
8
+ Ruby bindings for the Rockbox **DSP**, **metadata**, **codecs**, and
9
+ **playback** engine,
9
10
  via [`fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) (Ruby stdlib)
10
11
  over the prebuilt `librockbox_ffi` shared library. No native extension is
11
12
  compiled — the gem `dlopen`s the shared library at load time.
@@ -53,28 +54,40 @@ RockboxFFI::Dsp.open(44_100) do |dsp|
53
54
  processed = dsp.process(samples) # Array<Integer>
54
55
  end
55
56
 
57
+ # --- codecs (decode a file to PCM, one chunk at a time) ---------------
58
+ RockboxFFI::Decoder.open("song.flac") do |dec|
59
+ dec.metadata[:title] # tags from the open file
60
+ dec.each_chunk do |samples, sample_rate| # Array<Integer>, Hz
61
+ # samples: interleaved-stereo signed 16-bit PCM
62
+ end
63
+ dec.finished # => [true, 0] (0 = clean)
64
+ end
65
+
56
66
  # --- playback (needs an output device) --------------------------------
57
67
  RockboxFFI::Player.open(volume: 0.8) do |player|
58
68
  player.set_replaygain(RockboxFFI::ReplayGainMode::TRACK, 0.0, true)
59
69
  player.set_crossfade(RockboxFFI::CrossfadeMode::ALWAYS)
60
- player.set_queue(["a.flac", "b.mp3", "c.opus"])
70
+ # Queue entries may be local files, http(s):// URLs to remote media,
71
+ # or live-radio / streaming URLs — mix and match freely.
72
+ player.set_queue(["a.flac", "https://example.com/b.mp3", "http://radio.example/stream"])
61
73
  player.play
62
74
  player.status # => {state: "playing", index: 0, ...}
63
75
  end
64
76
  ```
65
77
 
66
- `Dsp` and `Player` own native resources. Use the block form (`.open`) to close
67
- them automatically, or call `#close` yourself; a GC finalizer frees the handle
68
- as a backstop.
78
+ `Dsp`, `Decoder`, and `Player` own native resources. Use the block form
79
+ (`.open`) to close them automatically, or call `#close` yourself; a GC
80
+ finalizer frees the handle as a backstop.
69
81
 
70
82
  ## API
71
83
 
72
- | Namespace | Contents |
73
- | ------------------------ | ----------------------------------------------------------------- |
74
- | `RockboxFFI::Metadata` | `read(path) => Hash`, `probe(filename) => String \| nil` |
75
- | `RockboxFFI::Dsp` | EQ / tone / surround / compressor / ReplayGain, `process(samples)` |
76
- | `RockboxFFI::Player` | queue + transport + crossfade + ReplayGain, `status => Hash` |
77
- | `RockboxFFI::*Mode` | `DspReplayGainMode`, `ReplayGainMode`, `CrossfadeMode`, `MixMode` |
84
+ | Namespace | Contents |
85
+ | ---------------------- | ------------------------------------------------------------------------------------------ |
86
+ | `RockboxFFI::Metadata` | `read(path) => Hash`, `probe(filename) => String \| nil` |
87
+ | `RockboxFFI::Dsp` | EQ / tone / surround / compressor / ReplayGain, `process(samples)` |
88
+ | `RockboxFFI::Decoder` | codec engine: `metadata`, `each_chunk` / `next_chunk`, `seek_ms`, `elapsed_ms`, `finished` |
89
+ | `RockboxFFI::Player` | queue + transport + crossfade + ReplayGain, `status => Hash` |
90
+ | `RockboxFFI::*Mode` | `DspReplayGainMode`, `ReplayGainMode`, `CrossfadeMode`, `MixMode` |
78
91
 
79
92
  Rich values (metadata, player status) cross the FFI boundary as JSON and come
80
93
  back as Hashes with **symbol keys**. Sample buffers are plain `Array<Integer>`
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module RockboxFFI
6
+ # Decode a local audio file to interleaved-stereo S16 PCM, one chunk at a
7
+ # time, using the Rockbox codec engine.
8
+ #
9
+ # The codec state is process-wide, so only one Decoder may decode at a time
10
+ # and it must be used from one thread. Call #close when done, or use the block
11
+ # form Decoder.open(path) { |dec| ... }.
12
+ class Decoder
13
+ # Open a Decoder for +path+; if a block is given, close it automatically
14
+ # afterwards.
15
+ def self.open(path)
16
+ dec = new(path)
17
+ return dec unless block_given?
18
+
19
+ begin
20
+ yield dec
21
+ ensure
22
+ dec.close
23
+ end
24
+ end
25
+
26
+ def initialize(path)
27
+ @ptr = Lib.rb_decoder_open(path.to_s)
28
+ raise "could not open decoder for #{path.inspect}" if @ptr.null?
29
+
30
+ @finalizer = self.class.send(:finalizer, @ptr)
31
+ ObjectSpace.define_finalizer(self, @finalizer)
32
+ end
33
+
34
+ # -- lifecycle --------------------------------------------------------
35
+ def close
36
+ return if @ptr.nil?
37
+
38
+ ObjectSpace.undefine_finalizer(self)
39
+ Lib.rb_decoder_free(@ptr)
40
+ @ptr = nil
41
+ end
42
+
43
+ def closed?
44
+ @ptr.nil?
45
+ end
46
+
47
+ def self.finalizer(ptr)
48
+ proc { Lib.rb_decoder_free(ptr) }
49
+ end
50
+ private_class_method :finalizer
51
+
52
+ # -- metadata ---------------------------------------------------------
53
+ # Tags and stream properties (same shape as Metadata.read). Returns a Hash
54
+ # with symbol keys. Raises on failure.
55
+ def metadata
56
+ s = RockboxFFI.take_string(Lib.rb_decoder_metadata_json(@ptr))
57
+ raise "rb_decoder_metadata_json returned NULL" if s.nil?
58
+
59
+ JSON.parse(s, symbolize_names: true)
60
+ end
61
+
62
+ # -- decoding ---------------------------------------------------------
63
+ # Next buffer of decoded PCM as +[samples, sample_rate]+, where +samples+
64
+ # is an Array of interleaved stereo int16 Integers. Returns nil at end of
65
+ # track (or after a codec error — see #finished).
66
+ def next_chunk
67
+ out_len = "\0" * Fiddle::SIZEOF_VOIDP # size_t* out-param (i16 count)
68
+ out_rate = "\0" * Fiddle::SIZEOF_INT # uint32_t* out-param (Hz)
69
+ ptr = Lib.rb_decoder_next_chunk(@ptr, out_len, out_rate)
70
+
71
+ produced = out_len.unpack1(Fiddle::SIZEOF_VOIDP == 8 ? "Q" : "L")
72
+ return nil if ptr.null? || produced.zero?
73
+
74
+ rate = out_rate.unpack1("L")
75
+ begin
76
+ ptr.size = produced * 2
77
+ [ptr[0, produced * 2].unpack("s<*"), rate]
78
+ ensure
79
+ Lib.rb_buffer_free(ptr, produced)
80
+ end
81
+ end
82
+
83
+ # Yield each +[samples, sample_rate]+ chunk until end of track. Returns an
84
+ # Enumerator when no block is given.
85
+ def each_chunk
86
+ return enum_for(:each_chunk) unless block_given?
87
+
88
+ while (chunk = next_chunk)
89
+ yield chunk
90
+ end
91
+ end
92
+
93
+ # Request a seek to +ms+ milliseconds.
94
+ def seek_ms(ms)
95
+ Lib.rb_decoder_seek_ms(@ptr, Integer(ms))
96
+ end
97
+
98
+ # Playback position last reported by the codec, in milliseconds.
99
+ def elapsed_ms
100
+ Lib.rb_decoder_elapsed_ms(@ptr)
101
+ end
102
+
103
+ # +[done, code]+: +done+ is true once the track has ended; +code+ is the
104
+ # exit status (0 = clean end, negative = codec error), valid only when
105
+ # +done+.
106
+ def finished
107
+ out_code = "\0" * Fiddle::SIZEOF_INT # int32_t* out-param
108
+ r = Lib.rb_decoder_finished(@ptr, out_code)
109
+ done = r == true || r == 1
110
+ [done, out_code.unpack1("l")]
111
+ end
112
+ end
113
+ end
@@ -98,6 +98,15 @@ module RockboxFFI
98
98
  extern "void* rb_meta_read_json(void*)"
99
99
  extern "void* rb_meta_probe(void*)"
100
100
 
101
+ # ---- decoder ------------------------------------------------------
102
+ extern "void* rb_decoder_open(void*)"
103
+ extern "void rb_decoder_free(void*)"
104
+ extern "void* rb_decoder_metadata_json(void*)"
105
+ extern "int16_t* rb_decoder_next_chunk(void*, void*, void*)"
106
+ extern "void rb_decoder_seek_ms(void*, uint64_t)"
107
+ extern "uint64_t rb_decoder_elapsed_ms(void*)"
108
+ extern "bool rb_decoder_finished(void*, void*)"
109
+
101
110
  # ---- player -------------------------------------------------------
102
111
  extern "void* rb_player_new()"
103
112
  extern "void* rb_player_new_with_config(uint32_t, float, float, int32_t, float, bool, int32_t, uint32_t, uint32_t, uint32_t, uint32_t, int32_t)"
@@ -106,6 +115,8 @@ module RockboxFFI
106
115
  extern "void rb_player_set_queue_json(void*, void*)"
107
116
  extern "void rb_player_enqueue(void*, void*)"
108
117
  extern "void rb_player_insert_json(void*, void*, int32_t, size_t)"
118
+ extern "void rb_player_remove(void*, size_t)"
119
+ extern "void rb_player_clear_queue(void*)"
109
120
  extern "void* rb_player_queue_json(void*)"
110
121
  extern "void rb_player_play(void*)"
111
122
  extern "void rb_player_pause(void*)"
@@ -116,9 +127,11 @@ module RockboxFFI
116
127
  extern "void rb_player_skip_to(void*, size_t)"
117
128
  extern "void rb_player_seek_ms(void*, uint64_t)"
118
129
  extern "void rb_player_set_volume(void*, float)"
130
+ extern "void rb_player_set_balance(void*, int32_t)"
119
131
  extern "void rb_player_set_crossfade(void*, int32_t, uint32_t, uint32_t, uint32_t, uint32_t, int32_t)"
120
132
  extern "void rb_player_set_replaygain(void*, int32_t, float, bool)"
121
133
  extern "float rb_player_volume(void*)"
134
+ extern "int32_t rb_player_balance(void*)"
122
135
  extern "uint32_t rb_player_sample_rate(void*)"
123
136
  extern "void* rb_player_status_json(void*)"
124
137
  extern "void rb_player_set_shuffle(void*, bool)"
@@ -114,6 +114,22 @@ module RockboxFFI
114
114
  self
115
115
  end
116
116
 
117
+ # Remove the track at +index+ (0-based) from the queue. An out-of-range
118
+ # index is ignored. Removing a track before the current one keeps the
119
+ # current track playing; removing the currently-playing track hard-cuts to
120
+ # the track that slides into its place; removing the last remaining track
121
+ # stops playback.
122
+ def remove(index)
123
+ Lib.rb_player_remove(@ptr, Integer(index))
124
+ self
125
+ end
126
+
127
+ # Empty the queue and stop playback (also clears any saved resume state).
128
+ def clear_queue
129
+ Lib.rb_player_clear_queue(@ptr)
130
+ self
131
+ end
132
+
117
133
  # The current queue as an Array of String paths/URLs.
118
134
  def queue
119
135
  s = RockboxFFI.take_string(Lib.rb_player_queue_json(@ptr))
@@ -169,10 +185,21 @@ module RockboxFFI
169
185
  self
170
186
  end
171
187
 
188
+ # Stereo balance, -100 (full left) to +100 (full right); 0 = centre.
189
+ def set_balance(balance)
190
+ Lib.rb_player_set_balance(@ptr, Integer(balance))
191
+ self
192
+ end
193
+
172
194
  def volume
173
195
  Lib.rb_player_volume(@ptr)
174
196
  end
175
197
 
198
+ # Current stereo balance, -100 (full left) to +100 (full right).
199
+ def balance
200
+ Lib.rb_player_balance(@ptr)
201
+ end
202
+
176
203
  def sample_rate
177
204
  Lib.rb_player_sample_rate(@ptr)
178
205
  end
@@ -1,3 +1,3 @@
1
1
  module RockboxFFI
2
- VERSION = "0.3.0"
2
+ VERSION = "0.5.0"
3
3
  end
data/lib/rockbox_ffi.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  # Ruby bindings for the Rockbox DSP / metadata / playback engine.
4
4
  #
5
5
  # Thin Fiddle wrappers over the librockbox_ffi C ABI. See RockboxFFI::Metadata,
6
- # RockboxFFI::Dsp, and RockboxFFI::Player.
6
+ # RockboxFFI::Dsp, RockboxFFI::Player, and RockboxFFI::Decoder.
7
7
 
8
8
  require "rockbox_ffi/version"
9
9
  require "rockbox_ffi/ffi"
@@ -11,6 +11,7 @@ require "rockbox_ffi/enums"
11
11
  require "rockbox_ffi/metadata"
12
12
  require "rockbox_ffi/dsp"
13
13
  require "rockbox_ffi/player"
14
+ require "rockbox_ffi/decoder"
14
15
 
15
16
  require "json"
16
17
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rockbox_ffi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tsiry Sandratraina
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-12 00:00:00.000000000 Z
11
+ date: 2026-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: fiddle
@@ -37,6 +37,7 @@ files:
37
37
  - examples/play.rb
38
38
  - examples/smoke.rb
39
39
  - lib/rockbox_ffi.rb
40
+ - lib/rockbox_ffi/decoder.rb
40
41
  - lib/rockbox_ffi/dsp.rb
41
42
  - lib/rockbox_ffi/enums.rb
42
43
  - lib/rockbox_ffi/ffi.rb