ruby-sfml 3.0.0.0 → 3.0.0.1

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: 8688630fa5286e1b5c28ed9c5cda8aa635a197fe74dc422859253f45f11f111a
4
- data.tar.gz: bcb83c0e0667fcae607fa3d3b0ca32527d25dec96fd8726ae697b1b5340c68f9
3
+ metadata.gz: 7f6e44279278680f06163024e2dd2f1eecbc8e8624960d589ec74e591849c9b3
4
+ data.tar.gz: 23fc48fc06aa0f7e6807b681c2601784de0159d9429055617e87d1e7977d20fa
5
5
  SHA512:
6
- metadata.gz: d2a0d250ee6f2a7ca62d7da0305f3345bed8170be02f7700074ae1dbb76a24379687c13e64488ab6cbea5eed78821aa9554ecac6501a76ca16eb3febcbe8ca47
7
- data.tar.gz: f9b7399e3e2b6168e217292573ef54a15a6217c4675f21f3718a4786787590fda2900d6795b68cd422ca1a5859962aa75d51371305f3ca1584914d5d4e0d13e2
6
+ metadata.gz: d351b03bb583fb5bb7ddd469da17838d876d7f5c99cbdae5387c94dfbf933f7299362d70bc8360c71b1b5737c49622bb55795869f2bd49d1784aab910f16d32d
7
+ data.tar.gz: d5b91cdb38ee758ca2fc76daf52f2aba4c6172b7d6798d51783caa677a10684d394881dca6e01e704e8db9d1be372ef38035de43e4f49b3bab26a9066d5728f5
data/CHANGELOG.md CHANGED
@@ -8,6 +8,136 @@ ruby-sfml's own patch level.
8
8
 
9
9
  ## [Unreleased]
10
10
 
11
+ ## [3.0.0.1] — 2026-05-07
12
+
13
+ ### Added
14
+ - `Window#icon=` and `RenderWindow#icon=` — set the window's title-bar /
15
+ taskbar icon from any `SFML::Image`. Wraps `sfWindow_setIcon` /
16
+ `sfRenderWindow_setIcon`. New example
17
+ [21_window_icon](examples/21_window_icon/window_icon.rb) builds a
18
+ procedural 32×32 ruby-style icon to demo the API.
19
+ - `Image#save_to_memory(format)` — encode an image to a Ruby String of
20
+ bytes in the given format (`"png"`, `"jpg"`, `"bmp"`, `"tga"`),
21
+ without touching the disk. Useful for screenshots over the network,
22
+ data: URLs, or piping into other image-processing libraries. Wraps
23
+ `sfImage_saveToMemory` plus the `sfBuffer_*` helpers in
24
+ libcsfml-system.
25
+ - Shader array uniforms — `shader[:positions] = [[x, y], ...]` and
26
+ similar for vec3 / vec4 arrays; also accepts `Vector2` / `Vector3`
27
+ elements interchangeably. New explicit `Shader#set_float_array` for
28
+ `uniform float arr[N];` (which can't be inferred via `[]=` because
29
+ it'd collide with the vec3 case). Wraps the
30
+ `sfShader_set{Float,Vec2,Vec3,Vec4}UniformArray` family.
31
+ - `Window#minimum_size=` / `#maximum_size=` and the same on
32
+ `RenderWindow` — clamp how small or large the OS lets the user
33
+ drag the window. Accepts `[w, h]`, `Vector2`, or `nil` (clears the
34
+ limit). Wraps `sfWindow_setMinimumSize` / `setMaximumSize` and the
35
+ RenderWindow equivalents.
36
+ - `Listener.velocity` and `Listener.cone` — finish the 3D-audio
37
+ surface on the listener side. Velocity feeds the Doppler effect
38
+ for sources whose `doppler_factor` is non-zero; cone (a
39
+ `SoundCone`, or a Hash convertible to one) attenuates sources
40
+ outside a directional pickup pattern.
41
+ - 3D-audio polish for `Sound` and `Music` — now expose `velocity`,
42
+ `doppler_factor`, `direction`, and `cone` (via the new
43
+ `SFML::SoundCone` value class — `inner_angle`, `outer_angle`,
44
+ `outer_gain`). Cone setter accepts both a `SoundCone` and a
45
+ Hash. Plus `effect_processor=` for installing a real-time DSP
46
+ Ruby callable on the audio thread (`->(samples, channels) {
47
+ ... }`); pass `nil` to remove it. The DSP path is documented as
48
+ Ruby+GVL-limited and best for very light effects only. Wraps
49
+ the corresponding `sfSound_*` and `sfMusic_*` setters/getters
50
+ plus `setEffectProcessor`.
51
+ - `SFML::VertexBuffer` — GPU-resident vertex buffer (VBO). Same
52
+ shape as `VertexArray` but vertices live on the GPU, so a draw
53
+ call ships only an OpenGL handle instead of re-uploading every
54
+ frame. `new(vertices, primitive_type:, usage:)` (one of `:stream`
55
+ / `:dynamic` / `:static`), `update(vertices, offset:)` for
56
+ partial uploads, `draw_range_on(target, first, count)` to draw a
57
+ slice. `VertexBuffer.available?` reports whether the GPU
58
+ supports VBOs at all (fall back to `VertexArray` if it doesn't).
59
+ Wraps the `sfVertexBuffer_*` family plus the
60
+ `sfRender{Window,Texture}_drawVertexBuffer{,Range}` draw paths.
61
+ - `Window.from_handle` / `RenderWindow.from_handle` — wrap an
62
+ existing OS-level window (HWND, NSView*, X11 Window xid). The
63
+ outside framework owns the window's lifecycle; SFML just renders
64
+ into it. Pair with `#native_handle` to interop in the other
65
+ direction. Wraps `sfWindow_createFromHandle` /
66
+ `sfRenderWindow_createFromHandle` and the matching
67
+ `getNativeHandle` getters.
68
+ - `SFML::SoundStream` — procedural audio source. Subclass it and
69
+ override `#on_get_data` to return an Array of `Int16` PCM samples
70
+ (or `nil` to stop); optionally override `#on_seek(time)` to
71
+ support `playing_offset=`. Same playback / 3D-positional API as
72
+ `Sound` and `Music` (volume, pitch, looping, position,
73
+ attenuation, min_distance, relative_to_listener). Wraps the full
74
+ `sfSoundStream_*` family. Includes example
75
+ [23_sound_stream](examples/23_sound_stream/sound_stream.rb) — a
76
+ real-time sine synth with arrow-key pitch / volume control.
77
+ - `SFML::Network::SocketSelector` — multiplex many sockets onto one
78
+ blocking `wait`. `add` / `remove` / `clear` / `wait(timeout:)`
79
+ / `ready?(socket)`. Polymorphic across `TcpListener`, `TcpSocket`,
80
+ `UdpSocket`. The `wait` call releases the GVL, so other Ruby
81
+ threads keep running during the syscall. Wraps the
82
+ `sfSocketSelector_*` family.
83
+ - `SFML::Network::Ftp` — CSFML's FTP client wrapped as idiomatic Ruby:
84
+ `connect`, `login` / `login_anonymous`, `working_directory`,
85
+ `directory_listing`, `change_directory`, `parent_directory`,
86
+ `create_directory`, `delete_directory`, `rename_file`,
87
+ `delete_file`, `download`, `upload`, `send_command`, `keep_alive`,
88
+ `disconnect`. Each call returns a `Response` (or
89
+ `DirectoryResponse` / `ListingResponse`) with `#ok?`, `#status`,
90
+ `#status_symbol`, `#message`, plus `#directory` or `#names`
91
+ where applicable. Network calls release the GVL.
92
+ Same caveat as Http — Ruby stdlib `Net::FTP` is the better choice
93
+ in production.
94
+ - `SFML::Network::Http` — CSFML's HTTP/1.x client in idiomatic Ruby
95
+ form. `Http.new(host, port:)` plus `#send_request(method:, uri:,
96
+ fields:, body:, http_version:, timeout:)` returns an `Http::Response`
97
+ with `#status` (Integer) / `#status_symbol` (`:ok`, `:not_found`,
98
+ `:connection_failed`, …), `#body`, `#field(name)`, `#http_version`.
99
+ Marked `blocking: true` so the GVL is released during the network
100
+ round-trip and concurrent Ruby threads can run. Wraps
101
+ `sfHttp_*`, `sfHttpRequest_*`, `sfHttpResponse_*`. Note: for any
102
+ non-trivial use (TLS, redirects, JSON, retries), Ruby's stdlib
103
+ `Net::HTTP` is the better tool — this binding exists for parity
104
+ with CSFML, not because we recommend it.
105
+ - `SFML::Sensor` polling module — `available?(type)`, `enable(type)`,
106
+ `disable(type)`, `value(type)` for the six sensor types
107
+ (`:accelerometer`, `:gyroscope`, `:magnetometer`, `:gravity`,
108
+ `:user_acceleration`, `:orientation`). The `:sensor_changed` event
109
+ variant now decodes its `sensor:` and `value:` payloads. Wraps
110
+ `sfSensor_isAvailable` / `sfSensor_setEnabled` /
111
+ `sfSensor_getValue`.
112
+ - `SFML::Touch` polling module — `down?(finger)` and
113
+ `position(finger, relative_to: window)`. Touch event variants
114
+ (`:touch_began`, `:touch_moved`, `:touch_ended`) now decode their
115
+ `finger:` and `position:` payloads (previously fell through to
116
+ empty data). Wraps `sfTouch_isDown` / `sfTouch_getPosition` /
117
+ `sfTouch_getPositionRenderWindow`.
118
+ - `Sound#playing_offset` / `playing_offset=` and `Music#playing_offset`
119
+ / `playing_offset=` — read or seek the playback head as a
120
+ `SFML::Time`. Setter also accepts a Numeric (interpreted as
121
+ seconds). Wraps `sfSound_setPlayingOffset` /
122
+ `sfMusic_setPlayingOffset` and the matching getters.
123
+ - Stencil buffer support — new `SFML::StencilMode` value class with
124
+ symbolic comparisons (`:equal`, `:always`, etc.) and update
125
+ operations (`:replace`, `:keep`, etc.). Pass it via `stencil_mode:`
126
+ to `RenderTarget#draw` for two-pass mask/clip effects, and clear
127
+ the stencil with `target.clear(color, stencil: N)` or
128
+ `target.clear(stencil: N)`. Wraps `sfRenderWindow_clearStencil` /
129
+ `clearColorAndStencil` (and the RenderTexture twins) plus the
130
+ existing `stencil_mode` slot in `sfRenderStates`. New example
131
+ [22_stencil_mask](examples/22_stencil_mask/stencil_mask.rb)
132
+ demonstrates a cursor-following spotlight that clips an animated
133
+ rainbow background.
134
+
135
+ ### Fixed
136
+ - `at_exit` hook now writes the unhandled exception (message, class,
137
+ backtrace) to stderr before calling `exit!`. Previously `exit!`
138
+ short-circuited Ruby's terminal exception reporter, so an error in
139
+ `setup` or any other top-level user code looked like a silent exit.
140
+
11
141
  ## [3.0.0.0] — initial release
12
142
 
13
143
  First public cut. Targets **CSFML 3.0.0** (released March 2025) and
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Modern, idiomatic Ruby bindings for [SFML 3.x](https://www.sfml-dev.org/) via [CSFML](https://github.com/SFML/CSFML) and [Ruby FFI](https://github.com/ffi/ffi).
4
4
 
5
- > **Status:** the API surface is complete for SFML 3.0 — system, window, graphics, audio, network, plus the higher-level `Game` and `Assets` helpers. 287 RSpec examples, 20 runnable example folders. Some details (gem-build verification, RBS signatures, hosted docs) are still pending.
5
+ > **Status:** the API surface is complete for SFML 3.0 — system, window, graphics (incl. stencil buffer + VBOs), audio (incl. 3D positional + custom DSP + procedural streams), network (incl. HTTP / FTP / socket selector), input (keyboard, mouse, joystick, touch, sensors), plus the higher-level `Game` and `Assets` helpers. 387 RSpec examples, 23 runnable example folders. RBS signatures are the main remaining engineering item.
6
6
 
7
7
  ## Why
8
8
 
@@ -76,61 +76,47 @@ end
76
76
  | Area | Classes |
77
77
  | -------- | ------------------------------------------------------------ |
78
78
  | System | `Vector2`, `Vector3`, `Rect`, `Time`, `Clock` |
79
- | Window | `RenderWindow`, `Window` (bare, GL-only), `VideoMode`, `Event`, `Keyboard`, `Mouse`, `Joystick`, `Cursor`, `Clipboard` |
80
- | Graphics | `Color`, `Image`, `Texture`, `RenderTexture`, `Sprite`, `CircleShape`, `RectangleShape`, `ConvexShape`, `Vertex`, `VertexArray`, `Font`, `Text`, `View`, `BlendMode`, `RenderStates`, `Shader`, `Transform` |
81
- | Audio | `SoundBuffer`, `Sound`, `Music`, `Listener`, `SoundRecorder`, `SoundBufferRecorder` (3D positional audio supported on Sound and Music) |
79
+ | Window | `RenderWindow`, `Window` (bare, GL-only), `VideoMode`, `Event`, `Keyboard`, `Mouse`, `Joystick`, `Touch`, `Sensor`, `Cursor`, `Clipboard` |
80
+ | Graphics | `Color`, `Image`, `Texture`, `RenderTexture`, `Sprite`, `CircleShape`, `RectangleShape`, `ConvexShape`, `Vertex`, `VertexArray`, `VertexBuffer`, `Font`, `Text`, `View`, `BlendMode`, `StencilMode`, `RenderStates`, `Shader`, `Transform` |
81
+ | Audio | `SoundBuffer`, `Sound`, `Music`, `Listener`, `SoundCone`, `SoundStream`, `SoundRecorder`, `SoundBufferRecorder` (3D positional + cones + Doppler + custom DSP via `effect_processor=`) |
82
82
  | Helpers | `Assets` (search-path + cache), `Game` (lifecycle main loop) |
83
83
 
84
- **Network**: `IpAddress`, `TcpSocket`, `TcpListener`, `UdpSocket` for stream / datagram networking.
84
+ **Network**: `IpAddress`, `TcpSocket`, `TcpListener`, `UdpSocket`, `SocketSelector` for stream / datagram networking, plus the niche `Http` and `Ftp` clients (use Ruby's `Net::HTTP` / `Net::FTP` if you have the choice — these exist for parity with CSFML).
85
85
 
86
86
  ## What's intentionally *not* wrapped
87
87
 
88
- CSFML 3 has a few corners we deliberately don't expose. Each is either
89
- (a) niche enough not to justify the surface area, (b) better served by
90
- a Ruby standard library, or (c) requires patterns that don't translate
91
- cleanly to FFI.
92
-
93
- **Use Ruby stdlib instead**
94
- - `sf::Http` `Net::HTTP` is a better Ruby fit
95
- - `sf::Ftp` — `Net::FTP` likewise
96
- - `sf::SocketSelector` `IO.select` or [Async](https://github.com/socketry/async)
97
-
98
- **Callback-based APIs that fight FFI / the GVL**
99
- - Raw `sf::SoundRecorder` (per-buffer callbacks on the audio thread) —
100
- use `SFML::SoundBufferRecorder` for "record into memory, save on stop"
101
- - `sf::SoundStream` (custom audio source via inheritance) niche; if
102
- you need it, generate samples to a file and play via `Music`
103
-
104
- **Mobile / niche inputs** (SFML 3 itself treats these as experimental)
105
- - `sf::Touch`, `sf::Sensor` (accelerometer, gyro, etc.)
106
-
107
- **Advanced graphics features**
108
- - `sf::VertexBuffer` (static GPU vertex buffer) — `VertexArray` covers
109
- the common case; if you need static-mesh perf, open an issue
110
- - Geometry shaders — only vertex and fragment stages on `SFML::Shader`
111
- - `sf::Shader#setUniformArray` (bulk uniforms) — set elements one by one
112
- - Stencil buffer ops (`clearStencil`, custom `StencilMode`) — accept
113
- CSFML defaults
114
- - `sf::Image#saveToMemory` — only `Image#save(path)` is wrapped
115
-
116
- **Advanced audio features**
117
- - Sound / Music cones, velocity, Doppler factor, custom DSP via
118
- `setEffectProcessor` — basic 3D positional + attenuation is in;
119
- the rest is rarely used in 2D gamedev
120
- - `sf::Listener` cone — same reasoning
121
-
122
- **Embedding / integration corners**
123
- - `RenderWindow.createFromHandle` (embed in another framework's window)
124
- - Custom `sf::InputStream` for loading assets from non-file sources
125
- - Window icon, min/max size, native handle accessors on `SFML::Window`
88
+ A handful of CSFML 3 corners deliberately stay out:
89
+
90
+ - **Geometry shaders** CSFML doesn't expose them at all (only vertex
91
+ and fragment stages); nothing for us to wrap.
92
+ - **Raw `sf::SoundRecorder`** (per-buffer callbacks on the audio
93
+ thread) use `SFML::SoundBufferRecorder` for the common "record
94
+ into memory, save on stop" path. The raw callback variant fights
95
+ the GVL hard.
96
+ - **Custom `sf::InputStream`** for loading assets from non-file
97
+ sources — Ruby has `IO`, just read into memory and use the
98
+ byte-string constructors.
99
+
100
+ **An aside on `Http` / `Ftp` / `SocketSelector`** these *are*
101
+ wrapped (matches CSFML for parity), but for any non-trivial use
102
+ Ruby's stdlib `Net::HTTP` / `Net::FTP` and `IO.select` are better
103
+ tools.
104
+
105
+ **An aside on `SoundStream` and `effect_processor=`** — both *are*
106
+ wrapped, but their callbacks run on the SFML audio thread and
107
+ must reacquire the GVL each invocation. Fine for trivial DSP;
108
+ expect glitches for anything heavier.
126
109
 
127
110
  **Other Ruby bindings worth knowing about**
111
+
128
112
  - SFML 2.x is *not* covered. The previous-generation gem
129
113
  [rbSFML](https://github.com/Groogy/rbSFML) targets SFML 2; it's
130
114
  unmaintained and only works with Ruby ≤ 2.2.
115
+ - **RubySFML3** (Andy P., 2026) — a thin FFI wrapper that exposes
116
+ the CSFML C API directly. If you want raw bindings (no idiomatic
117
+ Ruby layer, no auto-cleanup), check that project instead.
131
118
 
132
- If anything in the list above is blocking you, **open an issue** —
133
- "niche" is just a default, not a closed door.
119
+ If anything missing here is blocking you, **open an issue**.
134
120
 
135
121
  ## Examples
136
122
 
@@ -164,6 +150,9 @@ bundle exec ruby examples/<NN_name>/<name>.rb
164
150
  | 18 | [draw_primitives](examples/18_draw_primitives/draw_primitives.rb) | Raw `draw_primitives` — line burst rebuilt every frame |
165
151
  | 19 | [udp_loopback](examples/19_udp_loopback/udp_loopback.rb) | UDP send/receive on localhost via `Network::UdpSocket` |
166
152
  | 20 | [bare_window](examples/20_bare_window/bare_window.rb) | `SFML::Window` (no 2D batcher) — events for raw-OpenGL apps |
153
+ | 21 | [window_icon](examples/21_window_icon/window_icon.rb) | Procedural 32×32 icon set as the window/taskbar icon |
154
+ | 22 | [stencil_mask](examples/22_stencil_mask/stencil_mask.rb) | Two-pass `StencilMode` masking — cursor spotlight clip |
155
+ | 23 | [sound_stream](examples/23_sound_stream/sound_stream.rb) | Real-time sine synth via `SFML::SoundStream` subclass |
167
156
 
168
157
  ## Idioms baked in
169
158
 
@@ -0,0 +1,41 @@
1
+ module SFML
2
+ module Audio
3
+ # Internal helpers shared between Sound, Music, and SoundStream.
4
+ # Not part of the public API.
5
+ module_function
6
+
7
+ # Build an FFI::Function that adapts CSFML's effect-processor
8
+ # signature (raw float buffers, in/out frame counts) to a Ruby
9
+ # callable taking `(samples, channels)` and returning samples.
10
+ #
11
+ # Returns the FFI::Function — callers must keep a strong Ruby
12
+ # reference to it for as long as it's installed (otherwise GC
13
+ # collects the closure and the audio thread crashes).
14
+ def _build_effect_processor(callable)
15
+ FFI::Function.new(
16
+ :void,
17
+ [:pointer, :pointer, :pointer, :pointer, :uint32, :pointer],
18
+ ) do |in_ptr, in_cnt_ptr, out_ptr, out_cnt_ptr, channels, _user|
19
+ in_count = in_cnt_ptr.read_uint32
20
+ out_count = out_cnt_ptr.read_uint32
21
+
22
+ written = 0
23
+ if in_count > 0
24
+ input = in_ptr.read_array_of_float(in_count * channels)
25
+ output =
26
+ begin
27
+ callable.call(input, channels) || []
28
+ rescue => e
29
+ warn "ruby-sfml effect_processor raised: #{e.class}: #{e.message}"
30
+ []
31
+ end
32
+
33
+ written = [output.length / channels, out_count].min
34
+ out_ptr.write_array_of_float(output.first(written * channels)) if written.positive?
35
+ end
36
+
37
+ out_cnt_ptr.write_uint32(written)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -51,5 +51,36 @@ module SFML
51
51
  vec = value.is_a?(Vector3) ? value : Vector3.new(*value)
52
52
  C::Audio.sfListener_setUpVector(vec.to_native_f)
53
53
  end
54
+
55
+ # Listener velocity in world units / second — used by the
56
+ # Doppler effect on sources whose `doppler_factor` is non-zero.
57
+ def velocity
58
+ Vector3.from_native(C::Audio.sfListener_getVelocity)
59
+ end
60
+
61
+ def velocity=(value)
62
+ vec = value.is_a?(Vector3) ? value : Vector3.new(*value)
63
+ C::Audio.sfListener_setVelocity(vec.to_native_f)
64
+ end
65
+
66
+ # Directional cone for the listener — same shape as the cone on
67
+ # individual sound sources (see SFML::SoundCone). Used for
68
+ # cone-shaped attenuation when the listener faces a particular
69
+ # direction (think headphones turning to follow a character's
70
+ # head).
71
+ def cone
72
+ SoundCone.from_native(C::Audio.sfListener_getCone)
73
+ end
74
+
75
+ def cone=(value)
76
+ cone =
77
+ case value
78
+ when SoundCone then value
79
+ when Hash then SoundCone.new(**value)
80
+ else
81
+ raise ArgumentError, "Listener.cone= expects SoundCone or Hash; got #{value.class}"
82
+ end
83
+ C::Audio.sfListener_setCone(cone.to_native)
84
+ end
54
85
  end
55
86
  end
@@ -29,6 +29,70 @@ module SFML
29
29
 
30
30
  def duration = Time.from_native(C::Audio.sfMusic_getDuration(@handle))
31
31
 
32
+ # Current playback head as a SFML::Time. Reads from the underlying
33
+ # OpenAL source — only meaningful while the music is playing or
34
+ # paused (not after #stop).
35
+ def playing_offset
36
+ Time.from_native(C::Audio.sfMusic_getPlayingOffset(@handle))
37
+ end
38
+
39
+ # Seek to `value` (a SFML::Time, or seconds as a Numeric). Works
40
+ # while the music is playing, paused, or stopped.
41
+ def playing_offset=(value)
42
+ t = value.is_a?(Time) ? value : Time.seconds(value.to_f)
43
+ C::Audio.sfMusic_setPlayingOffset(@handle, t.to_native)
44
+ end
45
+
46
+ # 3D velocity, Doppler factor, direction, cone — see Sound for
47
+ # the same methods on the simpler buffered source.
48
+ def velocity
49
+ v = C::Audio.sfMusic_getVelocity(@handle)
50
+ Vector3.new(v[:x], v[:y], v[:z])
51
+ end
52
+
53
+ def velocity=(value)
54
+ vec = value.is_a?(Vector3) ? value : Vector3.new(*value)
55
+ packed = C::System::Vector3f.new
56
+ packed[:x] = vec.x.to_f; packed[:y] = vec.y.to_f; packed[:z] = vec.z.to_f
57
+ C::Audio.sfMusic_setVelocity(@handle, packed)
58
+ end
59
+
60
+ def doppler_factor = C::Audio.sfMusic_getDopplerFactor(@handle)
61
+ def doppler_factor=(v) C::Audio.sfMusic_setDopplerFactor(@handle, v.to_f); end
62
+
63
+ def direction
64
+ v = C::Audio.sfMusic_getDirection(@handle)
65
+ Vector3.new(v[:x], v[:y], v[:z])
66
+ end
67
+
68
+ def direction=(value)
69
+ vec = value.is_a?(Vector3) ? value : Vector3.new(*value)
70
+ packed = C::System::Vector3f.new
71
+ packed[:x] = vec.x.to_f; packed[:y] = vec.y.to_f; packed[:z] = vec.z.to_f
72
+ C::Audio.sfMusic_setDirection(@handle, packed)
73
+ end
74
+
75
+ def cone
76
+ SoundCone.from_native(C::Audio.sfMusic_getCone(@handle))
77
+ end
78
+
79
+ def cone=(value)
80
+ cone =
81
+ case value
82
+ when SoundCone then value
83
+ when Hash then SoundCone.new(**value)
84
+ else
85
+ raise ArgumentError, "Music#cone= expects SoundCone or Hash; got #{value.class}"
86
+ end
87
+ C::Audio.sfMusic_setCone(@handle, cone.to_native)
88
+ end
89
+
90
+ # See Sound#effect_processor= — same audio-thread DSP callback.
91
+ def effect_processor=(callable)
92
+ @effect_cb = callable.nil? ? nil : Audio._build_effect_processor(callable)
93
+ C::Audio.sfMusic_setEffectProcessor(@handle, @effect_cb, nil)
94
+ end
95
+
32
96
  # Cached on the Ruby side; see Sound#looping? for the why.
33
97
  def looping?
34
98
  @looping
@@ -41,6 +41,88 @@ module SFML
41
41
  def paused? = status == :paused
42
42
  def stopped? = status == :stopped
43
43
 
44
+ # Current playback head as a SFML::Time. Reads from the underlying
45
+ # OpenAL source — only meaningful while the sound is playing or
46
+ # paused (not after #stop).
47
+ def playing_offset
48
+ Time.from_native(C::Audio.sfSound_getPlayingOffset(@handle))
49
+ end
50
+
51
+ # Seek to `value` (a SFML::Time, or seconds as a Numeric). Works
52
+ # while the sound is playing, paused, or stopped — calling #play
53
+ # afterwards resumes from the new offset.
54
+ def playing_offset=(value)
55
+ t = value.is_a?(Time) ? value : Time.seconds(value.to_f)
56
+ C::Audio.sfSound_setPlayingOffset(@handle, t.to_native)
57
+ end
58
+
59
+ # 3D velocity in world units / second — used by the Doppler
60
+ # effect to shift pitch as the source approaches or recedes from
61
+ # the listener.
62
+ def velocity
63
+ v = C::Audio.sfSound_getVelocity(@handle)
64
+ Vector3.new(v[:x], v[:y], v[:z])
65
+ end
66
+
67
+ def velocity=(value)
68
+ vec = value.is_a?(Vector3) ? value : Vector3.new(*value)
69
+ packed = C::System::Vector3f.new
70
+ packed[:x] = vec.x.to_f; packed[:y] = vec.y.to_f; packed[:z] = vec.z.to_f
71
+ C::Audio.sfSound_setVelocity(@handle, packed)
72
+ end
73
+
74
+ # Per-source Doppler scale. 1.0 is realistic; bump it up for an
75
+ # exaggerated Doppler shift, drop to 0 to disable per-source.
76
+ def doppler_factor = C::Audio.sfSound_getDopplerFactor(@handle)
77
+ def doppler_factor=(v) C::Audio.sfSound_setDopplerFactor(@handle, v.to_f); end
78
+
79
+ # The direction the sound's cone points. Used together with
80
+ # #cone= for directional attenuation.
81
+ def direction
82
+ v = C::Audio.sfSound_getDirection(@handle)
83
+ Vector3.new(v[:x], v[:y], v[:z])
84
+ end
85
+
86
+ def direction=(value)
87
+ vec = value.is_a?(Vector3) ? value : Vector3.new(*value)
88
+ packed = C::System::Vector3f.new
89
+ packed[:x] = vec.x.to_f; packed[:y] = vec.y.to_f; packed[:z] = vec.z.to_f
90
+ C::Audio.sfSound_setDirection(@handle, packed)
91
+ end
92
+
93
+ # Directional-attenuation cone — see SFML::SoundCone.
94
+ def cone
95
+ SoundCone.from_native(C::Audio.sfSound_getCone(@handle))
96
+ end
97
+
98
+ def cone=(value)
99
+ cone =
100
+ case value
101
+ when SoundCone then value
102
+ when Hash then SoundCone.new(**value)
103
+ else
104
+ raise ArgumentError, "Sound#cone= expects SoundCone or Hash; got #{value.class}"
105
+ end
106
+ C::Audio.sfSound_setCone(@handle, cone.to_native)
107
+ end
108
+
109
+ # Install a real-time DSP filter. The callable is invoked from the
110
+ # CSFML audio thread once per audio frame batch with:
111
+ # * `input` — Array<Float> of interleaved samples (signed [-1, 1])
112
+ # * `channels` — Integer channel count (e.g. 2 for stereo)
113
+ # and should return an Array<Float> of the same length (or shorter
114
+ # if the effect produces fewer frames). Pass `nil` to remove an
115
+ # installed processor.
116
+ #
117
+ # CAVEAT: the callback runs on a real-time audio thread and is
118
+ # called every few milliseconds. Ruby + GVL is rarely fast enough
119
+ # for non-trivial DSP — expect glitches for anything heavier than
120
+ # a constant-gain or simple IIR. For real DSP, do it offline.
121
+ def effect_processor=(callable)
122
+ @effect_cb = callable.nil? ? nil : Audio._build_effect_processor(callable)
123
+ C::Audio.sfSound_setEffectProcessor(@handle, @effect_cb, nil)
124
+ end
125
+
44
126
  def looping?
45
127
  @looping
46
128
  end
@@ -0,0 +1,56 @@
1
+ module SFML
2
+ # Directional-attenuation cone for a 3D sound source. Inside the
3
+ # `inner_angle` cone the sound plays at full volume; outside the
4
+ # `outer_angle` cone it's attenuated by `outer_gain`; between the
5
+ # two it's smoothly interpolated.
6
+ #
7
+ # Angles are in degrees, measured from the source's `direction`
8
+ # axis. `outer_gain` is a scalar in [0, 1] (0 = silent outside).
9
+ #
10
+ # sound.direction = [0, 0, -1]
11
+ # sound.cone = SFML::SoundCone.new(
12
+ # inner_angle: 30, outer_angle: 90, outer_gain: 0.2,
13
+ # )
14
+ class SoundCone
15
+ attr_reader :inner_angle, :outer_angle, :outer_gain
16
+
17
+ def initialize(inner_angle:, outer_angle:, outer_gain:)
18
+ @inner_angle = inner_angle.to_f
19
+ @outer_angle = outer_angle.to_f
20
+ @outer_gain = outer_gain.to_f
21
+ freeze
22
+ end
23
+
24
+ def ==(other)
25
+ other.is_a?(SoundCone) &&
26
+ inner_angle == other.inner_angle &&
27
+ outer_angle == other.outer_angle &&
28
+ outer_gain == other.outer_gain
29
+ end
30
+ alias eql? ==
31
+ def hash = [inner_angle, outer_angle, outer_gain].hash
32
+
33
+ def to_s
34
+ "SoundCone(inner=#{inner_angle}°, outer=#{outer_angle}°, outer_gain=#{outer_gain})"
35
+ end
36
+ alias inspect to_s
37
+
38
+ # @!visibility private
39
+ def to_native
40
+ s = C::Audio::SoundSourceCone.new
41
+ s[:inner_angle] = @inner_angle
42
+ s[:outer_angle] = @outer_angle
43
+ s[:outer_gain] = @outer_gain
44
+ s
45
+ end
46
+
47
+ # @!visibility private
48
+ def self.from_native(struct)
49
+ new(
50
+ inner_angle: struct[:inner_angle],
51
+ outer_angle: struct[:outer_angle],
52
+ outer_gain: struct[:outer_gain],
53
+ )
54
+ end
55
+ end
56
+ end