rockbox_ffi 0.1.0-x86_64-linux
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/README.md +115 -0
- data/examples/play.rb +45 -0
- data/examples/smoke.rb +55 -0
- data/lib/rockbox_ffi/dsp.rb +164 -0
- data/lib/rockbox_ffi/enums.rb +44 -0
- data/lib/rockbox_ffi/ffi.rb +139 -0
- data/lib/rockbox_ffi/metadata.rb +29 -0
- data/lib/rockbox_ffi/player.rb +169 -0
- data/lib/rockbox_ffi/version.rb +5 -0
- data/lib/rockbox_ffi.rb +20 -0
- data/vendor/librockbox_ffi.so +0 -0
- metadata +72 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 5f33beb6b8aa564e0241a6e4cd568eade3679921426a20aa0e15e29fe1ea9c5a
|
|
4
|
+
data.tar.gz: 45b34e21a05614eeb43df032e8cafbf4cc878d78fb3478f350cf1184610b05f8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: da520edf71f48c5fd922f11a4dc005fb91df3ec21557de38b8d96032ba4b8d1105f189f22772d07379a56436b20b40b884e4c505e9b9a318a4ec4ec2df37e616
|
|
7
|
+
data.tar.gz: 2d51eb2bc2be6afa6af83c7e14d9f4eb28f52267be782031548b0f93bb4d4f1335437d39648da84d096aab1d735af0c8c4e4d56bdbbfc15fe7c6b830f8b2be6c
|
data/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# rockbox_ffi (Ruby)
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/rockbox_ffi)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+
|
|
8
|
+
Ruby bindings for the Rockbox **DSP**, **metadata**, and **playback** engine,
|
|
9
|
+
via [`fiddle`](https://docs.ruby-lang.org/en/master/Fiddle.html) (Ruby stdlib)
|
|
10
|
+
over the prebuilt `librockbox_ffi` shared library. No native extension is
|
|
11
|
+
compiled — the gem `dlopen`s the shared library at load time.
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Build the shared library once (from the repo root):
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
cargo build --release -p rockbox-ffi
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Then, from this directory:
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
cd bindings/ruby
|
|
25
|
+
bundle install # installs rake + minitest (dev only)
|
|
26
|
+
ruby -Ilib examples/smoke.rb # end-to-end check
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The library is located automatically by walking up to
|
|
30
|
+
`target/release/librockbox_ffi.{dylib,so}`. Override with the
|
|
31
|
+
`ROCKBOX_FFI_LIB` environment variable.
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
```ruby
|
|
36
|
+
require "rockbox_ffi"
|
|
37
|
+
|
|
38
|
+
# --- metadata ---------------------------------------------------------
|
|
39
|
+
meta = RockboxFFI::Metadata.read("song.flac")
|
|
40
|
+
puts "#{meta[:artist]} — #{meta[:title]} (#{meta[:duration_ms]} ms)"
|
|
41
|
+
RockboxFFI::Metadata.probe("track.opus") # => "Opus"
|
|
42
|
+
|
|
43
|
+
# --- DSP (interleaved stereo int16) -----------------------------------
|
|
44
|
+
RockboxFFI::Dsp.open(44_100) do |dsp|
|
|
45
|
+
dsp.eq_enable(true)
|
|
46
|
+
dsp.set_eq_band(0, 60, 0.7, 3.0)
|
|
47
|
+
dsp.set_replaygain(RockboxFFI::DspReplayGainMode::TRACK, true, 0.0)
|
|
48
|
+
dsp.set_replaygain_gains(track_gain_db: -6.02) # halves amplitude
|
|
49
|
+
processed = dsp.process(samples) # Array<Integer>
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# --- playback (needs an output device) --------------------------------
|
|
53
|
+
RockboxFFI::Player.open(volume: 0.8) do |player|
|
|
54
|
+
player.set_replaygain(RockboxFFI::ReplayGainMode::TRACK, 0.0, true)
|
|
55
|
+
player.set_crossfade(RockboxFFI::CrossfadeMode::ALWAYS)
|
|
56
|
+
player.set_queue(["a.flac", "b.mp3", "c.opus"])
|
|
57
|
+
player.play
|
|
58
|
+
player.status # => {state: "playing", index: 0, ...}
|
|
59
|
+
end
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`Dsp` and `Player` own native resources. Use the block form (`.open`) to close
|
|
63
|
+
them automatically, or call `#close` yourself; a GC finalizer frees the handle
|
|
64
|
+
as a backstop.
|
|
65
|
+
|
|
66
|
+
## API
|
|
67
|
+
|
|
68
|
+
| Namespace | Contents |
|
|
69
|
+
| ------------------------ | ----------------------------------------------------------------- |
|
|
70
|
+
| `RockboxFFI::Metadata` | `read(path) => Hash`, `probe(filename) => String \| nil` |
|
|
71
|
+
| `RockboxFFI::Dsp` | EQ / tone / surround / compressor / ReplayGain, `process(samples)` |
|
|
72
|
+
| `RockboxFFI::Player` | queue + transport + crossfade + ReplayGain, `status => Hash` |
|
|
73
|
+
| `RockboxFFI::*Mode` | `DspReplayGainMode`, `ReplayGainMode`, `CrossfadeMode`, `MixMode` |
|
|
74
|
+
|
|
75
|
+
Rich values (metadata, player status) cross the FFI boundary as JSON and come
|
|
76
|
+
back as Hashes with **symbol keys**. Sample buffers are plain `Array<Integer>`
|
|
77
|
+
of interleaved-stereo signed 16-bit samples.
|
|
78
|
+
|
|
79
|
+
### Two ReplayGain encodings
|
|
80
|
+
|
|
81
|
+
The DSP and player use *different* mode integers (a quirk of the C ABI):
|
|
82
|
+
|
|
83
|
+
- `Dsp#set_replaygain` → `DspReplayGainMode` (`TRACK=0, ALBUM=1, SHUFFLE=2, OFF=3`)
|
|
84
|
+
- `Player#set_replaygain` → `ReplayGainMode` (`OFF=0, TRACK=1, ALBUM=2`)
|
|
85
|
+
|
|
86
|
+
Use the named constants and you won't have to remember which is which.
|
|
87
|
+
|
|
88
|
+
## Examples
|
|
89
|
+
|
|
90
|
+
```sh
|
|
91
|
+
ruby -Ilib examples/smoke.rb # metadata + DSP + player checks
|
|
92
|
+
ruby -Ilib examples/play.rb [path] # play a file through the output device
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Interactive console
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
./bin/console # or: rake console
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Drops into IRB with `RockboxFFI` loaded and `FIXTURE` pointing at a sample
|
|
102
|
+
track:
|
|
103
|
+
|
|
104
|
+
```ruby
|
|
105
|
+
RockboxFFI::Metadata.read(FIXTURE)[:title] # => "Speak"
|
|
106
|
+
p = RockboxFFI::Player.new(volume: 0.6)
|
|
107
|
+
p.set_queue([FIXTURE]); p.play
|
|
108
|
+
p.status[:state] # => "playing"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Test
|
|
112
|
+
|
|
113
|
+
```sh
|
|
114
|
+
bundle exec rake test
|
|
115
|
+
```
|
data/examples/play.rb
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Play an audio file through the real output device.
|
|
4
|
+
#
|
|
5
|
+
# Run: ruby -Ilib examples/play.rb [path-to-audio]
|
|
6
|
+
|
|
7
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
8
|
+
require "rockbox_ffi"
|
|
9
|
+
|
|
10
|
+
REPO = File.expand_path("../../..", __dir__)
|
|
11
|
+
FIXTURE = File.join(REPO, "crates", "rocksky", "fixtures", "08 - Internet Money - Speak(Explicit).m4a")
|
|
12
|
+
|
|
13
|
+
file = ARGV[0] || FIXTURE
|
|
14
|
+
|
|
15
|
+
player = RockboxFFI::Player.new(volume: 0.8)
|
|
16
|
+
player.set_queue([file])
|
|
17
|
+
player.play
|
|
18
|
+
puts "▶ playing #{file}"
|
|
19
|
+
|
|
20
|
+
# Reinstall a SIGINT handler AFTER the player boots: the native audio engine
|
|
21
|
+
# installs its own signal handler while starting the output device, which
|
|
22
|
+
# otherwise swallows Ctrl-C. We exit! straight away instead of calling
|
|
23
|
+
# player.stop/close — those are blocking native calls that can deadlock
|
|
24
|
+
# against the engine thread. The OS reclaims the output device on exit.
|
|
25
|
+
trap("INT") do
|
|
26
|
+
puts "\nstopped"
|
|
27
|
+
exit!(130)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Poll status until playback finishes (state returns to "stopped").
|
|
31
|
+
loop do
|
|
32
|
+
st = player.status
|
|
33
|
+
pos = st[:position_ms] / 1000.0
|
|
34
|
+
dur = st[:duration_ms] / 1000.0
|
|
35
|
+
printf("\r[%s] %.1fs / %.1fs ", st[:state], pos, dur)
|
|
36
|
+
$stdout.flush
|
|
37
|
+
|
|
38
|
+
if st[:state] == "stopped" && st[:position_ms].positive?
|
|
39
|
+
puts "\n✔ done"
|
|
40
|
+
break
|
|
41
|
+
end
|
|
42
|
+
sleep 0.5
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
exit!(0)
|
data/examples/smoke.rb
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# End-to-end smoke test for the Ruby bindings.
|
|
4
|
+
#
|
|
5
|
+
# Run: ruby -Ilib examples/smoke.rb
|
|
6
|
+
|
|
7
|
+
$LOAD_PATH.unshift File.expand_path("../lib", __dir__)
|
|
8
|
+
require "rockbox_ffi"
|
|
9
|
+
|
|
10
|
+
REPO = File.expand_path("../../..", __dir__)
|
|
11
|
+
FIXTURE = File.join(REPO, "crates", "rocksky", "fixtures", "08 - Internet Money - Speak(Explicit).m4a")
|
|
12
|
+
|
|
13
|
+
puts "ABI version: #{RockboxFFI.abi_version}"
|
|
14
|
+
|
|
15
|
+
# -- metadata -----------------------------------------------------------
|
|
16
|
+
meta = RockboxFFI::Metadata.read(FIXTURE)
|
|
17
|
+
puts "codec=#{meta[:codec].inspect} title=#{meta[:title].inspect} " \
|
|
18
|
+
"artist=#{meta[:artist].inspect} duration_ms=#{meta[:duration_ms]} " \
|
|
19
|
+
"sample_rate=#{meta[:sample_rate]}"
|
|
20
|
+
raise "codec label should not be empty" if meta[:codec].to_s.empty?
|
|
21
|
+
|
|
22
|
+
probe = RockboxFFI::Metadata.probe("song.flac")
|
|
23
|
+
puts "probe('song.flac') = #{probe.inspect}"
|
|
24
|
+
raise "expected FLAC, got #{probe.inspect}" unless probe == "FLAC"
|
|
25
|
+
|
|
26
|
+
# -- DSP: -6.0206 dB track gain should HALVE amplitude ------------------
|
|
27
|
+
rate = 44_100
|
|
28
|
+
RockboxFFI::Dsp.open(rate) do |dsp|
|
|
29
|
+
dsp.set_replaygain(RockboxFFI::DspReplayGainMode::TRACK, false, 0.0)
|
|
30
|
+
dsp.set_replaygain_gains(track_gain_db: -6.0206) # x0.5
|
|
31
|
+
|
|
32
|
+
sine = RockboxFFI.sine_stereo(1000, 1.0, rate, 16_000)
|
|
33
|
+
out = dsp.process(sine)
|
|
34
|
+
peak = out.map(&:abs).max
|
|
35
|
+
puts "DSP peak after -6.02 dB track gain: #{peak} (expected ~8000)"
|
|
36
|
+
raise "peak #{peak} out of range" unless (7_600..8_400).cover?(peak)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# -- Player (construct only, no audible playback) -----------------------
|
|
40
|
+
RockboxFFI::Player.open(volume: 0.0) do |player|
|
|
41
|
+
sr = player.sample_rate
|
|
42
|
+
puts "Player sample_rate=#{sr}"
|
|
43
|
+
raise "sample_rate should be > 0" unless sr.positive?
|
|
44
|
+
|
|
45
|
+
player.set_volume(0.0)
|
|
46
|
+
player.set_queue([FIXTURE])
|
|
47
|
+
sleep 0.1 # the queue command is applied asynchronously by the engine thread
|
|
48
|
+
|
|
49
|
+
st = player.status
|
|
50
|
+
puts "Player status: state=#{st[:state].inspect} queue_len=#{st[:queue_len]}"
|
|
51
|
+
raise "expected stopped, got #{st[:state].inspect}" unless st[:state] == "stopped"
|
|
52
|
+
raise "expected queue_len 1, got #{st[:queue_len]}" unless st[:queue_len] == 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
puts "\nALL CHECKS PASSED ✔"
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockboxFFI
|
|
4
|
+
# The DSP pipeline: EQ, tone, surround, compressor, ReplayGain, resampler.
|
|
5
|
+
#
|
|
6
|
+
# The underlying dsp_config is a process-wide singleton, so only one Dsp may
|
|
7
|
+
# exist at a time and it must be used from one thread. Call #close when done,
|
|
8
|
+
# or use the block form Dsp.open(rate) { |dsp| ... }.
|
|
9
|
+
class Dsp
|
|
10
|
+
# Open a Dsp; if a block is given, close it automatically afterwards.
|
|
11
|
+
def self.open(sample_rate)
|
|
12
|
+
dsp = new(sample_rate)
|
|
13
|
+
return dsp unless block_given?
|
|
14
|
+
|
|
15
|
+
begin
|
|
16
|
+
yield dsp
|
|
17
|
+
ensure
|
|
18
|
+
dsp.close
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize(sample_rate)
|
|
23
|
+
@ptr = Lib.rb_dsp_new(Integer(sample_rate))
|
|
24
|
+
raise "rb_dsp_new returned NULL" if @ptr.null?
|
|
25
|
+
|
|
26
|
+
@finalizer = self.class.send(:finalizer, @ptr)
|
|
27
|
+
ObjectSpace.define_finalizer(self, @finalizer)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# -- lifecycle --------------------------------------------------------
|
|
31
|
+
def close
|
|
32
|
+
return if @ptr.nil?
|
|
33
|
+
|
|
34
|
+
ObjectSpace.undefine_finalizer(self)
|
|
35
|
+
Lib.rb_dsp_free(@ptr)
|
|
36
|
+
@ptr = nil
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def closed?
|
|
40
|
+
@ptr.nil?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.finalizer(ptr)
|
|
44
|
+
proc { Lib.rb_dsp_free(ptr) }
|
|
45
|
+
end
|
|
46
|
+
private_class_method :finalizer
|
|
47
|
+
|
|
48
|
+
# -- configuration ----------------------------------------------------
|
|
49
|
+
def set_input_frequency(hz)
|
|
50
|
+
Lib.rb_dsp_set_input_frequency(@ptr, Integer(hz))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def flush
|
|
54
|
+
Lib.rb_dsp_flush(@ptr)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def eq_enable(enable)
|
|
58
|
+
Lib.rb_dsp_eq_enable(@ptr, RockboxFFI.b(enable))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Configure one EQ band (0..=9). Band 0 low shelf, 9 high shelf.
|
|
62
|
+
def set_eq_band(band, cutoff_hz, q, gain_db)
|
|
63
|
+
Lib.rb_dsp_set_eq_band(@ptr, Integer(band), Integer(cutoff_hz), Float(q), Float(gain_db))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def set_eq_precut(db)
|
|
67
|
+
Lib.rb_dsp_set_eq_precut(@ptr, Float(db))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def set_tone(bass_db, treble_db)
|
|
71
|
+
Lib.rb_dsp_set_tone(@ptr, Integer(bass_db), Integer(treble_db))
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def set_tone_cutoffs(bass_hz, treble_hz)
|
|
75
|
+
Lib.rb_dsp_set_tone_cutoffs(@ptr, Integer(bass_hz), Integer(treble_hz))
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def set_surround(delay_ms, balance, fx1, fx2)
|
|
79
|
+
Lib.rb_dsp_set_surround(@ptr, Integer(delay_ms), Integer(balance), Integer(fx1), Integer(fx2))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def set_channel_config(mode)
|
|
83
|
+
Lib.rb_dsp_set_channel_config(@ptr, Integer(mode))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def set_stereo_width(percent)
|
|
87
|
+
Lib.rb_dsp_set_stereo_width(@ptr, Integer(percent))
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def set_compressor(threshold, makeup_gain, ratio, knee, release_time, attack_time)
|
|
91
|
+
Lib.rb_dsp_set_compressor(
|
|
92
|
+
@ptr, Integer(threshold), Integer(makeup_gain), Integer(ratio),
|
|
93
|
+
Integer(knee), Integer(release_time), Integer(attack_time)
|
|
94
|
+
)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# mode: see DspReplayGainMode (TRACK=0, ALBUM=1, SHUFFLE=2, OFF=3).
|
|
98
|
+
def set_replaygain(mode, noclip, preamp_db)
|
|
99
|
+
Lib.rb_dsp_set_replaygain(@ptr, Integer(mode), RockboxFFI.b(noclip), Float(preamp_db))
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# Per-track gains in plain dB / peaks as linear amplitude (1.0 = full
|
|
103
|
+
# scale). nil for any absent tag (mapped to the ABI's NaN sentinel).
|
|
104
|
+
def set_replaygain_gains(track_gain_db: nil, album_gain_db: nil, track_peak: nil, album_peak: nil)
|
|
105
|
+
Lib.rb_dsp_set_replaygain_gains(
|
|
106
|
+
@ptr, opt(track_gain_db), opt(album_gain_db), opt(track_peak), opt(album_peak)
|
|
107
|
+
)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Native Q7.24 linear factors (the raw_* fields from Metadata.read),
|
|
111
|
+
# 0 = not tagged.
|
|
112
|
+
def set_replaygain_gains_raw(track_gain, album_gain, track_peak, album_peak)
|
|
113
|
+
Lib.rb_dsp_set_replaygain_gains_raw(
|
|
114
|
+
@ptr, Integer(track_gain), Integer(album_gain), Integer(track_peak), Integer(album_peak)
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# -- processing -------------------------------------------------------
|
|
119
|
+
# Run interleaved stereo S16 +samples+ (an Array of Integers) through the
|
|
120
|
+
# pipeline. Returns a new Array of processed samples (length may differ
|
|
121
|
+
# from the input — the resampler buffers).
|
|
122
|
+
def process(samples)
|
|
123
|
+
raise ArgumentError, "input must be interleaved stereo (even length)" if samples.length.odd?
|
|
124
|
+
|
|
125
|
+
input = samples.pack("s<*") # int16 little-endian
|
|
126
|
+
out_len = "\0" * Fiddle::SIZEOF_VOIDP # size_t* out-param (written in place)
|
|
127
|
+
out_ptr = Lib.rb_dsp_process(@ptr, input, samples.length, out_len)
|
|
128
|
+
|
|
129
|
+
produced = out_len.unpack1(Fiddle::SIZEOF_VOIDP == 8 ? "Q" : "L")
|
|
130
|
+
return [] if out_ptr.null? || produced.zero?
|
|
131
|
+
|
|
132
|
+
begin
|
|
133
|
+
out_ptr.size = produced * 2
|
|
134
|
+
out_ptr[0, produced * 2].unpack("s<*")
|
|
135
|
+
ensure
|
|
136
|
+
Lib.rb_buffer_free(out_ptr, produced)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private
|
|
141
|
+
|
|
142
|
+
NAN = Float::NAN
|
|
143
|
+
private_constant :NAN
|
|
144
|
+
|
|
145
|
+
# nil -> NaN, the ABI's "tag absent" sentinel.
|
|
146
|
+
def opt(value)
|
|
147
|
+
value.nil? ? NAN : Float(value)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Generate +seconds+ of a sine as interleaved stereo int16 (an Array).
|
|
152
|
+
def self.sine_stereo(freq_hz, seconds, rate, amplitude = 16_000)
|
|
153
|
+
n = (seconds * rate).to_i
|
|
154
|
+
buf = Array.new(n * 2)
|
|
155
|
+
(0...n).each do |i|
|
|
156
|
+
s = (Math.sin(i * 2.0 * Math::PI * freq_hz / rate) * amplitude).to_i
|
|
157
|
+
s = -32_768 if s < -32_768
|
|
158
|
+
s = 32_767 if s > 32_767
|
|
159
|
+
buf[i * 2] = s
|
|
160
|
+
buf[(i * 2) + 1] = s
|
|
161
|
+
end
|
|
162
|
+
buf
|
|
163
|
+
end
|
|
164
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RockboxFFI
|
|
4
|
+
# Note the two *different* ReplayGain encodings in the C ABI.
|
|
5
|
+
|
|
6
|
+
# Values for Dsp#set_replaygain (native Rockbox).
|
|
7
|
+
module DspReplayGainMode
|
|
8
|
+
TRACK = 0
|
|
9
|
+
ALBUM = 1
|
|
10
|
+
SHUFFLE = 2
|
|
11
|
+
OFF = 3
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Values for Player#set_replaygain.
|
|
15
|
+
module ReplayGainMode
|
|
16
|
+
OFF = 0
|
|
17
|
+
TRACK = 1
|
|
18
|
+
ALBUM = 2
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
module CrossfadeMode
|
|
22
|
+
OFF = 0
|
|
23
|
+
AUTO_SKIP = 1
|
|
24
|
+
MANUAL_SKIP = 2
|
|
25
|
+
SHUFFLE = 3
|
|
26
|
+
SHUFFLE_OR_MANUAL = 4
|
|
27
|
+
ALWAYS = 5
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
module MixMode
|
|
31
|
+
CROSSFADE = 0
|
|
32
|
+
MIX = 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module ChannelConfig
|
|
36
|
+
STEREO = 0
|
|
37
|
+
MONO = 1
|
|
38
|
+
CUSTOM = 2
|
|
39
|
+
MONO_LEFT = 3
|
|
40
|
+
MONO_RIGHT = 4
|
|
41
|
+
KARAOKE = 5
|
|
42
|
+
SWAP = 6
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Fiddle (Ruby stdlib) loader for librockbox_ffi.
|
|
4
|
+
#
|
|
5
|
+
# Declares exactly the functions we call and dlopens the prebuilt shared
|
|
6
|
+
# library — no C is compiled here. The library is located via the
|
|
7
|
+
# ROCKBOX_FFI_LIB env var, then by walking up to the repo's target/release
|
|
8
|
+
# directory. Mirrors include/rockbox_ffi.h — keep in sync with the C ABI.
|
|
9
|
+
|
|
10
|
+
require "fiddle"
|
|
11
|
+
require "fiddle/import"
|
|
12
|
+
|
|
13
|
+
module RockboxFFI
|
|
14
|
+
# Locate the prebuilt shared library. Precedence:
|
|
15
|
+
# 1. ROCKBOX_FFI_LIB env var (explicit override)
|
|
16
|
+
# 2. the platform binary bundled in the gem's vendor/ dir (published gems)
|
|
17
|
+
# 3. target/release, by walking up from here (repo checkout / dev)
|
|
18
|
+
def self.library_path
|
|
19
|
+
names = %w[librockbox_ffi.dylib librockbox_ffi.so rockbox_ffi.dll]
|
|
20
|
+
tried = []
|
|
21
|
+
|
|
22
|
+
if (env = ENV["ROCKBOX_FFI_LIB"])
|
|
23
|
+
tried << env
|
|
24
|
+
return env if File.exist?(env)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Bundled binary (platform gems ship one under <gem_root>/vendor/).
|
|
28
|
+
vendor = File.expand_path("../../vendor", __dir__)
|
|
29
|
+
names.each do |name|
|
|
30
|
+
cand = File.join(vendor, name)
|
|
31
|
+
tried << cand
|
|
32
|
+
return cand if File.exist?(cand)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Walk up from this file looking for target/release (repo checkout).
|
|
36
|
+
dir = File.expand_path(__dir__)
|
|
37
|
+
loop do
|
|
38
|
+
rel = File.join(dir, "target", "release")
|
|
39
|
+
if File.directory?(rel)
|
|
40
|
+
names.each do |name|
|
|
41
|
+
cand = File.join(rel, name)
|
|
42
|
+
tried << cand
|
|
43
|
+
return cand if File.exist?(cand)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
parent = File.dirname(dir)
|
|
47
|
+
break if parent == dir
|
|
48
|
+
|
|
49
|
+
dir = parent
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
raise LoadError, "could not locate librockbox_ffi shared library. Set " \
|
|
53
|
+
"ROCKBOX_FFI_LIB or run `cargo build --release -p " \
|
|
54
|
+
"rockbox-ffi`. Tried:\n #{tried.join("\n ")}"
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Thin Fiddle wrappers over the C ABI. Every function is a module method
|
|
58
|
+
# (e.g. `Lib.rb_dsp_new`). Opaque handles and char*/int16* returns come back
|
|
59
|
+
# as Fiddle::Pointer; scalars come back as Integer/Float.
|
|
60
|
+
module Lib
|
|
61
|
+
extend Fiddle::Importer
|
|
62
|
+
dlload RockboxFFI.library_path
|
|
63
|
+
|
|
64
|
+
# Fixed-width C types → Fiddle's native aliases (64-bit LP64 assumed).
|
|
65
|
+
typealias "uint32_t", "unsigned int"
|
|
66
|
+
typealias "int32_t", "int"
|
|
67
|
+
typealias "int16_t", "short"
|
|
68
|
+
typealias "int64_t", "long long"
|
|
69
|
+
typealias "uint64_t", "unsigned long long"
|
|
70
|
+
typealias "size_t", "unsigned long"
|
|
71
|
+
typealias "bool", "int" # passed as 0/1 in a full register on SysV/AAPCS
|
|
72
|
+
|
|
73
|
+
extern "uint32_t rb_ffi_abi_version()"
|
|
74
|
+
|
|
75
|
+
extern "void rb_string_free(void*)"
|
|
76
|
+
extern "void rb_buffer_free(void*, size_t)"
|
|
77
|
+
|
|
78
|
+
# ---- DSP ----------------------------------------------------------
|
|
79
|
+
extern "void* rb_dsp_new(uint32_t)"
|
|
80
|
+
extern "void rb_dsp_free(void*)"
|
|
81
|
+
extern "void rb_dsp_set_input_frequency(void*, uint32_t)"
|
|
82
|
+
extern "void rb_dsp_flush(void*)"
|
|
83
|
+
extern "void rb_dsp_eq_enable(void*, bool)"
|
|
84
|
+
extern "void rb_dsp_set_tone(void*, int32_t, int32_t)"
|
|
85
|
+
extern "void rb_dsp_set_tone_cutoffs(void*, int32_t, int32_t)"
|
|
86
|
+
extern "void rb_dsp_set_surround(void*, int32_t, int32_t, int32_t, int32_t)"
|
|
87
|
+
extern "void rb_dsp_set_channel_config(void*, int32_t)"
|
|
88
|
+
extern "void rb_dsp_set_stereo_width(void*, int32_t)"
|
|
89
|
+
extern "void rb_dsp_set_compressor(void*, int32_t, int32_t, int32_t, int32_t, int32_t, int32_t)"
|
|
90
|
+
extern "void rb_dsp_set_replaygain(void*, int32_t, bool, float)"
|
|
91
|
+
extern "void rb_dsp_set_replaygain_gains(void*, float, float, float, float)"
|
|
92
|
+
extern "void rb_dsp_set_replaygain_gains_raw(void*, int64_t, int64_t, int64_t, int64_t)"
|
|
93
|
+
extern "void rb_dsp_set_eq_band(void*, size_t, int32_t, float, float)"
|
|
94
|
+
extern "void rb_dsp_set_eq_precut(void*, float)"
|
|
95
|
+
extern "void* rb_dsp_process(void*, void*, size_t, void*)"
|
|
96
|
+
|
|
97
|
+
# ---- metadata -----------------------------------------------------
|
|
98
|
+
extern "void* rb_meta_read_json(void*)"
|
|
99
|
+
extern "void* rb_meta_probe(void*)"
|
|
100
|
+
|
|
101
|
+
# ---- player -------------------------------------------------------
|
|
102
|
+
extern "void* rb_player_new()"
|
|
103
|
+
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)"
|
|
104
|
+
extern "void rb_player_free(void*)"
|
|
105
|
+
extern "void rb_player_set_queue_json(void*, void*)"
|
|
106
|
+
extern "void rb_player_enqueue(void*, void*)"
|
|
107
|
+
extern "void rb_player_play(void*)"
|
|
108
|
+
extern "void rb_player_pause(void*)"
|
|
109
|
+
extern "void rb_player_toggle(void*)"
|
|
110
|
+
extern "void rb_player_stop(void*)"
|
|
111
|
+
extern "void rb_player_next(void*)"
|
|
112
|
+
extern "void rb_player_previous(void*)"
|
|
113
|
+
extern "void rb_player_skip_to(void*, size_t)"
|
|
114
|
+
extern "void rb_player_seek_ms(void*, uint64_t)"
|
|
115
|
+
extern "void rb_player_set_volume(void*, float)"
|
|
116
|
+
extern "void rb_player_set_crossfade(void*, int32_t, uint32_t, uint32_t, uint32_t, uint32_t, int32_t)"
|
|
117
|
+
extern "void rb_player_set_replaygain(void*, int32_t, float, bool)"
|
|
118
|
+
extern "float rb_player_volume(void*)"
|
|
119
|
+
extern "uint32_t rb_player_sample_rate(void*)"
|
|
120
|
+
extern "void* rb_player_status_json(void*)"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# true/false → 1/0 for the ABI's `bool` (declared as int above).
|
|
124
|
+
def self.b(flag)
|
|
125
|
+
flag ? 1 : 0
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Copy a heap C string returned by the ABI into a String, then free it.
|
|
129
|
+
# Returns nil for a NULL pointer (the ABI's error/absent signal).
|
|
130
|
+
def self.take_string(ptr)
|
|
131
|
+
return nil if ptr.null?
|
|
132
|
+
|
|
133
|
+
begin
|
|
134
|
+
ptr.to_s.force_encoding("UTF-8")
|
|
135
|
+
ensure
|
|
136
|
+
Lib.rb_string_free(ptr)
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RockboxFFI
|
|
6
|
+
# Audio file metadata parsing.
|
|
7
|
+
module Metadata
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
# Parse the metadata of the audio file at +path+.
|
|
11
|
+
#
|
|
12
|
+
# Returns a Hash (symbol keys) with tag strings, +:duration_ms+,
|
|
13
|
+
# +:bitrate+, +:sample_rate+, a nested +:replaygain+ Hash (including the
|
|
14
|
+
# raw Q7.24 +raw_*+ fields), and optional +:album_art+ / +:cuesheet+
|
|
15
|
+
# locations. Raises on failure.
|
|
16
|
+
def read(path)
|
|
17
|
+
s = RockboxFFI.take_string(Lib.rb_meta_read_json(path.to_s))
|
|
18
|
+
raise "could not read metadata from #{path.inspect}" if s.nil?
|
|
19
|
+
|
|
20
|
+
JSON.parse(s, symbolize_names: true)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Guess the codec label (e.g. "FLAC") from a filename's extension without
|
|
24
|
+
# opening the file. Returns nil for an unknown extension.
|
|
25
|
+
def probe(filename)
|
|
26
|
+
RockboxFFI.take_string(Lib.rb_meta_probe(filename.to_s))
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
module RockboxFFI
|
|
6
|
+
# Queue-based player with native ReplayGain and Rockbox crossfade.
|
|
7
|
+
#
|
|
8
|
+
# A Player owns a live audio output device and a background engine thread —
|
|
9
|
+
# construct it only where an output device exists. Call #close when done,
|
|
10
|
+
# or use the block form Player.open(...) { |p| ... }.
|
|
11
|
+
#
|
|
12
|
+
# ReplayGain +mode+ here uses the *player* values: 0 off, 1 track, 2 album
|
|
13
|
+
# (see ReplayGainMode) — distinct from the DSP encoding.
|
|
14
|
+
class Player
|
|
15
|
+
DEFAULT_CONFIG = {
|
|
16
|
+
sample_rate: 0, # 0 => device default
|
|
17
|
+
buffer_seconds: 4.0,
|
|
18
|
+
volume: 1.0,
|
|
19
|
+
replaygain_mode: ReplayGainMode::OFF,
|
|
20
|
+
replaygain_preamp_db: 0.0,
|
|
21
|
+
replaygain_prevent_clipping: true,
|
|
22
|
+
crossfade_mode: CrossfadeMode::OFF,
|
|
23
|
+
fade_out_delay_ms: 0,
|
|
24
|
+
fade_out_duration_ms: 2000,
|
|
25
|
+
fade_in_delay_ms: 0,
|
|
26
|
+
fade_in_duration_ms: 2000,
|
|
27
|
+
mix_mode: MixMode::CROSSFADE
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Open a Player; if a block is given, close it automatically afterwards.
|
|
31
|
+
def self.open(**opts)
|
|
32
|
+
player = new(**opts)
|
|
33
|
+
return player unless block_given?
|
|
34
|
+
|
|
35
|
+
begin
|
|
36
|
+
yield player
|
|
37
|
+
ensure
|
|
38
|
+
player.close
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Player on the default device with Rockbox default settings.
|
|
43
|
+
def self.default
|
|
44
|
+
player = allocate
|
|
45
|
+
player.send(:init_ptr, Lib.rb_player_new)
|
|
46
|
+
player
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Create a player with configuration overrides (see DEFAULT_CONFIG keys).
|
|
50
|
+
# sample_rate: 0 means the device default.
|
|
51
|
+
def initialize(**opts)
|
|
52
|
+
c = DEFAULT_CONFIG.merge(opts)
|
|
53
|
+
ptr = Lib.rb_player_new_with_config(
|
|
54
|
+
Integer(c[:sample_rate]), Float(c[:buffer_seconds]), Float(c[:volume]),
|
|
55
|
+
Integer(c[:replaygain_mode]), Float(c[:replaygain_preamp_db]),
|
|
56
|
+
RockboxFFI.b(c[:replaygain_prevent_clipping]), Integer(c[:crossfade_mode]),
|
|
57
|
+
Integer(c[:fade_out_delay_ms]), Integer(c[:fade_out_duration_ms]),
|
|
58
|
+
Integer(c[:fade_in_delay_ms]), Integer(c[:fade_in_duration_ms]),
|
|
59
|
+
Integer(c[:mix_mode])
|
|
60
|
+
)
|
|
61
|
+
init_ptr(ptr)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# -- lifecycle --------------------------------------------------------
|
|
65
|
+
def close
|
|
66
|
+
return if @ptr.nil?
|
|
67
|
+
|
|
68
|
+
ObjectSpace.undefine_finalizer(self)
|
|
69
|
+
Lib.rb_player_free(@ptr)
|
|
70
|
+
@ptr = nil
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def closed?
|
|
74
|
+
@ptr.nil?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.finalizer(ptr)
|
|
78
|
+
proc { Lib.rb_player_free(ptr) }
|
|
79
|
+
end
|
|
80
|
+
private_class_method :finalizer
|
|
81
|
+
|
|
82
|
+
# -- queue ------------------------------------------------------------
|
|
83
|
+
def set_queue(paths)
|
|
84
|
+
Lib.rb_player_set_queue_json(@ptr, JSON.generate(Array(paths).map(&:to_s)))
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def enqueue(path)
|
|
88
|
+
Lib.rb_player_enqueue(@ptr, path.to_s)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# -- transport --------------------------------------------------------
|
|
92
|
+
def play
|
|
93
|
+
Lib.rb_player_play(@ptr)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def pause
|
|
97
|
+
Lib.rb_player_pause(@ptr)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def toggle
|
|
101
|
+
Lib.rb_player_toggle(@ptr)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def stop
|
|
105
|
+
Lib.rb_player_stop(@ptr)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def next
|
|
109
|
+
Lib.rb_player_next(@ptr)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def previous
|
|
113
|
+
Lib.rb_player_previous(@ptr)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def skip_to(index)
|
|
117
|
+
Lib.rb_player_skip_to(@ptr, Integer(index))
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def seek_ms(ms)
|
|
121
|
+
Lib.rb_player_seek_ms(@ptr, Integer(ms))
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# -- settings ---------------------------------------------------------
|
|
125
|
+
def set_volume(vol)
|
|
126
|
+
Lib.rb_player_set_volume(@ptr, Float(vol))
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def volume
|
|
130
|
+
Lib.rb_player_volume(@ptr)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def sample_rate
|
|
134
|
+
Lib.rb_player_sample_rate(@ptr)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def set_crossfade(mode, fade_out_delay_ms: 0, fade_out_duration_ms: 2000,
|
|
138
|
+
fade_in_delay_ms: 0, fade_in_duration_ms: 2000,
|
|
139
|
+
mix_mode: MixMode::CROSSFADE)
|
|
140
|
+
Lib.rb_player_set_crossfade(
|
|
141
|
+
@ptr, Integer(mode), Integer(fade_out_delay_ms), Integer(fade_out_duration_ms),
|
|
142
|
+
Integer(fade_in_delay_ms), Integer(fade_in_duration_ms), Integer(mix_mode)
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# mode: see ReplayGainMode (OFF=0, TRACK=1, ALBUM=2).
|
|
147
|
+
def set_replaygain(mode, preamp_db, prevent_clipping)
|
|
148
|
+
Lib.rb_player_set_replaygain(@ptr, Integer(mode), Float(preamp_db), RockboxFFI.b(prevent_clipping))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# -- status -----------------------------------------------------------
|
|
152
|
+
# A snapshot of the player's status as a Hash with symbol keys.
|
|
153
|
+
def status
|
|
154
|
+
s = RockboxFFI.take_string(Lib.rb_player_status_json(@ptr))
|
|
155
|
+
raise "rb_player_status_json returned NULL" if s.nil?
|
|
156
|
+
|
|
157
|
+
JSON.parse(s, symbolize_names: true)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private
|
|
161
|
+
|
|
162
|
+
def init_ptr(ptr)
|
|
163
|
+
raise "failed to create Player (no output device?)" if ptr.nil? || ptr.null?
|
|
164
|
+
|
|
165
|
+
@ptr = ptr
|
|
166
|
+
ObjectSpace.define_finalizer(self, self.class.send(:finalizer, ptr))
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
data/lib/rockbox_ffi.rb
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Ruby bindings for the Rockbox DSP / metadata / playback engine.
|
|
4
|
+
#
|
|
5
|
+
# Thin Fiddle wrappers over the librockbox_ffi C ABI. See RockboxFFI::Metadata,
|
|
6
|
+
# RockboxFFI::Dsp, and RockboxFFI::Player.
|
|
7
|
+
|
|
8
|
+
require "rockbox_ffi/version"
|
|
9
|
+
require "rockbox_ffi/ffi"
|
|
10
|
+
require "rockbox_ffi/enums"
|
|
11
|
+
require "rockbox_ffi/metadata"
|
|
12
|
+
require "rockbox_ffi/dsp"
|
|
13
|
+
require "rockbox_ffi/player"
|
|
14
|
+
|
|
15
|
+
module RockboxFFI
|
|
16
|
+
# ABI major version of the loaded library (bumped on breaking changes).
|
|
17
|
+
def self.abi_version
|
|
18
|
+
Lib.rb_ffi_abi_version
|
|
19
|
+
end
|
|
20
|
+
end
|
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: rockbox_ffi
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: x86_64-linux
|
|
6
|
+
authors:
|
|
7
|
+
- Tsiry Sandratraina
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-07-09 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: fiddle
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.0'
|
|
27
|
+
description: 'Fiddle-based bindings over the librockbox_ffi C ABI: audio metadata
|
|
28
|
+
parsing, the Rockbox DSP pipeline (EQ / tone / surround / compressor / ReplayGain),
|
|
29
|
+
and a queue-based player.'
|
|
30
|
+
email:
|
|
31
|
+
- tsiry.sndr@rocksky.app
|
|
32
|
+
executables: []
|
|
33
|
+
extensions: []
|
|
34
|
+
extra_rdoc_files: []
|
|
35
|
+
files:
|
|
36
|
+
- README.md
|
|
37
|
+
- examples/play.rb
|
|
38
|
+
- examples/smoke.rb
|
|
39
|
+
- lib/rockbox_ffi.rb
|
|
40
|
+
- lib/rockbox_ffi/dsp.rb
|
|
41
|
+
- lib/rockbox_ffi/enums.rb
|
|
42
|
+
- lib/rockbox_ffi/ffi.rb
|
|
43
|
+
- lib/rockbox_ffi/metadata.rb
|
|
44
|
+
- lib/rockbox_ffi/player.rb
|
|
45
|
+
- lib/rockbox_ffi/version.rb
|
|
46
|
+
- vendor/librockbox_ffi.so
|
|
47
|
+
homepage: https://github.com/tsirysndr/rockboxd
|
|
48
|
+
licenses:
|
|
49
|
+
- GPL-2.0-or-later
|
|
50
|
+
metadata:
|
|
51
|
+
homepage_uri: https://github.com/tsirysndr/rockboxd
|
|
52
|
+
source_code_uri: https://github.com/tsirysndr/rockboxd/tree/master/bindings/ruby
|
|
53
|
+
post_install_message:
|
|
54
|
+
rdoc_options: []
|
|
55
|
+
require_paths:
|
|
56
|
+
- lib
|
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '2.6'
|
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
63
|
+
requirements:
|
|
64
|
+
- - ">="
|
|
65
|
+
- !ruby/object:Gem::Version
|
|
66
|
+
version: '0'
|
|
67
|
+
requirements: []
|
|
68
|
+
rubygems_version: 3.5.22
|
|
69
|
+
signing_key:
|
|
70
|
+
specification_version: 4
|
|
71
|
+
summary: Ruby bindings for the Rockbox DSP / metadata / playback engine
|
|
72
|
+
test_files: []
|