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 +4 -4
- data/README.md +24 -11
- data/lib/rockbox_ffi/decoder.rb +113 -0
- data/lib/rockbox_ffi/ffi.rb +13 -0
- data/lib/rockbox_ffi/player.rb +27 -0
- data/lib/rockbox_ffi/version.rb +1 -1
- data/lib/rockbox_ffi.rb +2 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 98d2db65260fc33b5cfd9cf8963c2f96b782079c999ded173f64611a1f3d2554
|
|
4
|
+
data.tar.gz: 8b6fa4f9f221aad0367c913cc6ce494ea97ce62d3658309cc6ed8252003e28d1
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 97340abbfae011f7f3f09a0463046c4831eef6d429ea6d0f1ea0a814a8aac085a12309d46fe7e5ee670772aed59cf9045cbfe72606bdf34e0aab685c534aab04
|
|
7
|
+
data.tar.gz: b28b979145c98b851ab27cf731fb9929603a946a17435e656cb2f350af594bc8fefe525545c6bd2dafa3dd3a6236dcf998b672636f3d09ce16c95b94069d3d3d
|
data/README.md
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|

|
|
6
6
|

|
|
7
7
|
|
|
8
|
-
Ruby bindings for the Rockbox **DSP**, **metadata**,
|
|
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
|
-
|
|
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
|
|
67
|
-
them automatically, or call `#close` yourself; a GC
|
|
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
|
|
73
|
-
|
|
|
74
|
-
| `RockboxFFI::Metadata`
|
|
75
|
-
| `RockboxFFI::Dsp`
|
|
76
|
-
| `RockboxFFI::
|
|
77
|
-
| `RockboxFFI
|
|
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
|
data/lib/rockbox_ffi/ffi.rb
CHANGED
|
@@ -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)"
|
data/lib/rockbox_ffi/player.rb
CHANGED
|
@@ -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
|
data/lib/rockbox_ffi/version.rb
CHANGED
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::
|
|
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.
|
|
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-
|
|
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
|