Class: SFML::SoundStream
- Inherits:
-
Object
- Object
- SFML::SoundStream
- Defined in:
- lib/sfml/audio/sound_stream.rb
Overview
Procedural audio source. Subclass it and override #on_get_data
to fill each chunk on demand — CSFML invokes the callback from
its audio thread whenever the sound queue runs low.
#on_get_data returns either:
* an Array (or anything responding to `#to_a`) of Int16 PCM
samples — they're packed into a buffer and handed to CSFML;
* `nil` to stop the stream.
Override #on_seek(time) to support playing_offset=. Default
is a no-op (the next callback continues from wherever the
subclass internal state happens to be).
class SineWave < SFML::SoundStream
def initialize(freq:, sample_rate: 44_100)
super(channel_count: 1, sample_rate: sample_rate)
@freq, @sample_rate = freq, sample_rate
@phase = 0.0
end
def on_get_data
n = @sample_rate / 10 # ~100ms chunks
step = 2 * Math::PI * @freq / @sample_rate
Array.new(n) do
s = (Math.sin(@phase) * 30_000).to_i
@phase = (@phase + step) % (2 * Math::PI)
s
end
end
def on_seek(time)
@phase = 0.0
end
end
CAVEATS
- The callback runs on the SFML audio thread; doing heavy work there will glitch the audio. Generate samples and return.
- Ruby threads still need the GVL — your callback acquires it each invocation. Long Ruby work on the main thread can starve the audio thread and produce dropouts.
- Always keep a reference to the SoundStream object (assign to a variable, store in an instance var). If the Ruby object is GC'd while CSFML is still calling callbacks, the process crashes.
Constant Summary collapse
- DEFAULT_CHUNK_FRAMES =
Per-callback PCM-buffer size for the sample queue.
4096
Instance Attribute Summary collapse
-
#handle ⇒ Object
readonly
:nodoc:.
Instance Method Summary collapse
-
#attenuation ⇒ Object
Falloff sharpness with distance — see
Sound#attenuation. -
#attenuation=(value) ⇒ Object
Set the attenuation.
-
#channel_count ⇒ Object
Channel count this stream produces (1 = mono, 2 = stereo).
-
#channel_map ⇒ Object
The channel layout the stream is producing, as an Array of
sfSoundChannelenum values (1 = Mono, 2 = FrontLeft, 3 = FrontRight, etc — see SoundBuffer::DEFAULT_CHANNEL_MAPS). -
#cone ⇒ Object
Directional-attenuation cone.
-
#cone=(value) ⇒ Object
Set the cone.
-
#direction ⇒ Object
The direction vector for the cone.
-
#direction=(value) ⇒ Object
Set the direction — accepts Vector3 or
[x, y, z]. -
#directional_attenuation_factor ⇒ Object
Multiplier on the Listener's directional attenuation cone.
-
#directional_attenuation_factor=(value) ⇒ Object
Set the directional-attenuation factor.
-
#doppler_factor ⇒ Object
Per-source Doppler scale.
-
#doppler_factor=(value) ⇒ Object
Set the Doppler factor.
-
#effect_processor=(callable) ⇒ Object
Install a real-time DSP filter (same contract as Sound / Music — see Sound#effect_processor=).
-
#initialize(channel_count:, sample_rate:) ⇒ SoundStream
constructor
A new instance of SoundStream.
-
#looping=(value) ⇒ Object
Toggle looping playback.
-
#looping? ⇒ Boolean
Cached on the Ruby side; some OpenAL backends (notably the headless null sink we get on Linux CI) don't reliably read the loop flag back through CSFML once it's been set.
-
#max_distance ⇒ Object
Distance beyond which the source is fully attenuated.
-
#max_distance=(value) ⇒ Object
Set the max-distance.
-
#max_gain ⇒ Object
Upper gain bound.
-
#max_gain=(value) ⇒ Object
Set the max-gain cap.
-
#min_distance ⇒ Object
Distance below which volume is not attenuated.
-
#min_distance=(value) ⇒ Object
Set the min-distance.
-
#min_gain ⇒ Object
Lower gain bound.
-
#min_gain=(value) ⇒ Object
Set the min-gain floor.
-
#on_get_data ⇒ Object
Return an Array of Int16 PCM samples (interleaved if multi-channel), or
nilto stop the stream. -
#on_seek(_time) ⇒ Object
Called when the user changes the playing offset.
-
#pan ⇒ Object
Stereo pan in [-1.0, 1.0].
-
#pan=(value) ⇒ Object
Set the stereo pan.
-
#pause ⇒ Object
Pause streaming.
-
#paused? ⇒ Boolean
status == :paused. -
#pitch ⇒ Object
Pitch multiplier.
-
#pitch=(value) ⇒ Object
Set the pitch.
-
#play ⇒ Object
Start streaming.
-
#playing? ⇒ Boolean
status == :playing. -
#playing_offset ⇒ Object
Current playback head as
SFML::Time. -
#playing_offset=(value) ⇒ Object
Seek to
value(SFML::Timeor numeric seconds). -
#position ⇒ Object
3D position as a Vector3.
-
#position=(value) ⇒ Object
Set the 3D position — accepts Vector3 or
[x, y, z]. -
#relative_to_listener=(value) ⇒ Object
Toggle listener-relative positioning.
-
#relative_to_listener? ⇒ Boolean
trueif#positionis interpreted relative to the listener. -
#sample_rate ⇒ Object
Sample rate in Hz.
-
#spatialization_enabled=(value) ⇒ Object
Toggle 3D spatialisation.
-
#spatialization_enabled? ⇒ Boolean
trueif 3D positional / Doppler / cone math is applied. -
#status ⇒ Object
Playback state —
:stopped,:paused, or:playing. -
#stop ⇒ Object
Stop streaming and reset internal state.
-
#stopped? ⇒ Boolean
status == :stopped. -
#velocity ⇒ Object
3D velocity in world units / second.
-
#velocity=(value) ⇒ Object
Set the velocity — accepts Vector3 or
[x, y, z]. -
#volume ⇒ Object
Playback volume in [0, 100].
-
#volume=(value) ⇒ Object
Set the volume.
Constructor Details
#initialize(channel_count:, sample_rate:) ⇒ SoundStream
Returns a new instance of SoundStream.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 |
# File 'lib/sfml/audio/sound_stream.rb', line 50 def initialize(channel_count:, sample_rate:) raise ArgumentError, "channel_count must be >= 1" if channel_count < 1 raise ArgumentError, "sample_rate must be > 0" if sample_rate < 1 # Hold strong refs so neither GC nor reload disposes the # callbacks while CSFML is still calling them. @get_data_cb = FFI::Function.new(:bool, [:pointer, :pointer]) do |chunk_ptr, _user| _on_get_data_callback(chunk_ptr) end @seek_cb = FFI::Function.new(:void, [C::System::Time.by_value, :pointer]) do |time, _user| on_seek(Time.from_native(time)) nil end ptr = C::Audio.sfSoundStream_create( @get_data_cb, @seek_cb, Integer(channel_count), Integer(sample_rate), nil, 0, nil, ) raise AudioError, "sfSoundStream_create returned NULL" if ptr.null? @handle = FFI::AutoPointer.new(ptr, C::Audio.method(:sfSoundStream_destroy)) # We re-use a single MemoryPointer for the sample buffer, # growing it on demand. CSFML reads from the pointer between # callbacks, then asks for the next chunk — by the time we # overwrite, CSFML is done with the previous data. @sample_buffer = nil @sample_buffer_capacity = 0 end |
Instance Attribute Details
#handle ⇒ Object (readonly)
:nodoc:
322 323 324 |
# File 'lib/sfml/audio/sound_stream.rb', line 322 def handle @handle end |
Instance Method Details
#attenuation ⇒ Object
Falloff sharpness with distance — see Sound#attenuation.
176 |
# File 'lib/sfml/audio/sound_stream.rb', line 176 def attenuation = C::Audio.sfSoundStream_getAttenuation(@handle) |
#attenuation=(value) ⇒ Object
Set the attenuation.
179 180 181 |
# File 'lib/sfml/audio/sound_stream.rb', line 179 def attenuation=(value) C::Audio.sfSoundStream_setAttenuation(@handle, value.to_f) end |
#channel_count ⇒ Object
Channel count this stream produces (1 = mono, 2 = stereo).
116 117 |
# File 'lib/sfml/audio/sound_stream.rb', line 116 def channel_count = C::Audio.sfSoundStream_getChannelCount(@handle) # Sample rate in Hz. |
#channel_map ⇒ Object
The channel layout the stream is producing, as an Array of
sfSoundChannel enum values (1 = Mono, 2 = FrontLeft,
3 = FrontRight, etc — see SoundBuffer::DEFAULT_CHANNEL_MAPS).
314 315 316 317 318 319 320 |
# File 'lib/sfml/audio/sound_stream.rb', line 314 def channel_map count_buf = FFI::MemoryPointer.new(:size_t) ptr = C::Audio.sfSoundStream_getChannelMap(@handle, count_buf) n = count_buf.read(:size_t) return [] if ptr.null? || n.zero? ptr.read_array_of_int32(n) end |
#cone ⇒ Object
Directional-attenuation cone.
256 257 258 |
# File 'lib/sfml/audio/sound_stream.rb', line 256 def cone SoundCone.from_native(C::Audio.sfSoundStream_getCone(@handle)) end |
#cone=(value) ⇒ Object
Set the cone. Accepts a SoundCone or Hash.
261 262 263 264 265 266 267 268 269 270 |
# File 'lib/sfml/audio/sound_stream.rb', line 261 def cone=(value) cone = case value when SoundCone then value when Hash then SoundCone.new(**value) else raise ArgumentError, "SoundStream#cone= expects SoundCone or Hash; got #{value.class}" end C::Audio.sfSoundStream_setCone(@handle, cone.to_native) end |
#direction ⇒ Object
The direction vector for the cone.
242 243 244 245 |
# File 'lib/sfml/audio/sound_stream.rb', line 242 def direction v = C::Audio.sfSoundStream_getDirection(@handle) Vector3.new(v[:x], v[:y], v[:z]) end |
#direction=(value) ⇒ Object
Set the direction — accepts Vector3 or [x, y, z].
248 249 250 251 252 253 |
# File 'lib/sfml/audio/sound_stream.rb', line 248 def direction=(value) vec = value.is_a?(Vector3) ? value : Vector3.new(*value) packed = C::System::Vector3f.new packed[:x] = vec.x.to_f; packed[:y] = vec.y.to_f; packed[:z] = vec.z.to_f C::Audio.sfSoundStream_setDirection(@handle, packed) end |
#directional_attenuation_factor ⇒ Object
Multiplier on the Listener's directional attenuation cone.
295 296 297 |
# File 'lib/sfml/audio/sound_stream.rb', line 295 def directional_attenuation_factor C::Audio.sfSoundStream_getDirectionalAttenuationFactor(@handle) end |
#directional_attenuation_factor=(value) ⇒ Object
Set the directional-attenuation factor.
300 301 302 |
# File 'lib/sfml/audio/sound_stream.rb', line 300 def directional_attenuation_factor=(value) C::Audio.sfSoundStream_setDirectionalAttenuationFactor(@handle, value.to_f) end |
#doppler_factor ⇒ Object
Per-source Doppler scale.
287 |
# File 'lib/sfml/audio/sound_stream.rb', line 287 def doppler_factor = C::Audio.sfSoundStream_getDopplerFactor(@handle) |
#doppler_factor=(value) ⇒ Object
Set the Doppler factor.
290 291 292 |
# File 'lib/sfml/audio/sound_stream.rb', line 290 def doppler_factor=(value) C::Audio.sfSoundStream_setDopplerFactor(@handle, value.to_f) end |
#effect_processor=(callable) ⇒ Object
Install a real-time DSP filter (same contract as Sound /
Music — see Sound#effect_processor=). Pass nil to remove.
306 307 308 309 |
# File 'lib/sfml/audio/sound_stream.rb', line 306 def effect_processor=(callable) @effect_cb = callable.nil? ? nil : Audio._build_effect_processor(callable) C::Audio.sfSoundStream_setEffectProcessor(@handle, @effect_cb, nil) end |
#looping=(value) ⇒ Object
Toggle looping playback.
128 129 130 131 |
# File 'lib/sfml/audio/sound_stream.rb', line 128 def looping=(value) @looping = !!value C::Audio.sfSoundStream_setLooping(@handle, @looping) end |
#looping? ⇒ Boolean
Cached on the Ruby side; some OpenAL backends (notably the headless null sink we get on Linux CI) don't reliably read the loop flag back through CSFML once it's been set.
123 124 125 |
# File 'lib/sfml/audio/sound_stream.rb', line 123 def looping? @looping == true end |
#max_distance ⇒ Object
Distance beyond which the source is fully attenuated.
210 |
# File 'lib/sfml/audio/sound_stream.rb', line 210 def max_distance = C::Audio.sfSoundStream_getMaxDistance(@handle) |
#max_distance=(value) ⇒ Object
Set the max-distance.
213 214 215 |
# File 'lib/sfml/audio/sound_stream.rb', line 213 def max_distance=(value) C::Audio.sfSoundStream_setMaxDistance(@handle, value.to_f) end |
#max_gain ⇒ Object
Upper gain bound.
226 |
# File 'lib/sfml/audio/sound_stream.rb', line 226 def max_gain = C::Audio.sfSoundStream_getMaxGain(@handle) |
#max_gain=(value) ⇒ Object
Set the max-gain cap.
229 230 231 |
# File 'lib/sfml/audio/sound_stream.rb', line 229 def max_gain=(value) C::Audio.sfSoundStream_setMaxGain(@handle, value.to_f) end |
#min_distance ⇒ Object
Distance below which volume is not attenuated.
184 |
# File 'lib/sfml/audio/sound_stream.rb', line 184 def min_distance = C::Audio.sfSoundStream_getMinDistance(@handle) |
#min_distance=(value) ⇒ Object
Set the min-distance.
187 188 189 |
# File 'lib/sfml/audio/sound_stream.rb', line 187 def min_distance=(value) C::Audio.sfSoundStream_setMinDistance(@handle, value.to_f) end |
#min_gain ⇒ Object
Lower gain bound.
218 |
# File 'lib/sfml/audio/sound_stream.rb', line 218 def min_gain = C::Audio.sfSoundStream_getMinGain(@handle) |
#min_gain=(value) ⇒ Object
Set the min-gain floor.
221 222 223 |
# File 'lib/sfml/audio/sound_stream.rb', line 221 def min_gain=(value) C::Audio.sfSoundStream_setMinGain(@handle, value.to_f) end |
#on_get_data ⇒ Object
Return an Array of Int16 PCM samples (interleaved if multi-channel),
or nil to stop the stream. Default raises so subclasses must
implement it.
87 88 89 |
# File 'lib/sfml/audio/sound_stream.rb', line 87 def on_get_data raise NoMethodError, "#{self.class} must override #on_get_data" end |
#on_seek(_time) ⇒ Object
Called when the user changes the playing offset. Default is a no-op — override if your stream tracks position internally (counters, file offsets, etc.).
94 |
# File 'lib/sfml/audio/sound_stream.rb', line 94 def on_seek(_time); end |
#pan ⇒ Object
Stereo pan in [-1.0, 1.0].
202 |
# File 'lib/sfml/audio/sound_stream.rb', line 202 def pan = C::Audio.sfSoundStream_getPan(@handle) |
#pan=(value) ⇒ Object
Set the stereo pan.
205 206 207 |
# File 'lib/sfml/audio/sound_stream.rb', line 205 def pan=(value) C::Audio.sfSoundStream_setPan(@handle, value.to_f) end |
#pause ⇒ Object
Pause streaming. #play resumes from here.
102 103 |
# File 'lib/sfml/audio/sound_stream.rb', line 102 def pause = (C::Audio.sfSoundStream_pause(@handle); self) # Stop streaming and reset internal state. |
#paused? ⇒ Boolean
status == :paused.
111 112 |
# File 'lib/sfml/audio/sound_stream.rb', line 111 def paused? = status == :paused # `status == :stopped`. |
#pitch ⇒ Object
Pitch multiplier. 1.0 = normal.
142 |
# File 'lib/sfml/audio/sound_stream.rb', line 142 def pitch = C::Audio.sfSoundStream_getPitch(@handle) |
#pitch=(value) ⇒ Object
Set the pitch.
145 146 147 |
# File 'lib/sfml/audio/sound_stream.rb', line 145 def pitch=(value) C::Audio.sfSoundStream_setPitch(@handle, value.to_f) end |
#play ⇒ Object
Start streaming. Triggers the audio thread to start polling
#on_get_data for chunks. Returns self.
100 101 |
# File 'lib/sfml/audio/sound_stream.rb', line 100 def play = (C::Audio.sfSoundStream_play(@handle); self) # Pause streaming. `#play` resumes from here. |
#playing? ⇒ Boolean
status == :playing.
109 110 |
# File 'lib/sfml/audio/sound_stream.rb', line 109 def = status == :playing # `status == :paused`. |
#playing_offset ⇒ Object
Current playback head as SFML::Time.
150 151 152 |
# File 'lib/sfml/audio/sound_stream.rb', line 150 def Time.from_native(C::Audio.(@handle)) end |
#playing_offset=(value) ⇒ Object
Seek to value (SFML::Time or numeric seconds). Triggers
#on_seek on the subclass.
156 157 158 159 |
# File 'lib/sfml/audio/sound_stream.rb', line 156 def (value) t = value.is_a?(Time) ? value : Time.seconds(value.to_f) C::Audio.(@handle, t.to_native) end |
#position ⇒ Object
3D position as a Vector3.
162 163 164 165 |
# File 'lib/sfml/audio/sound_stream.rb', line 162 def position v = C::Audio.sfSoundStream_getPosition(@handle) Vector3.new(v[:x], v[:y], v[:z]) end |
#position=(value) ⇒ Object
Set the 3D position — accepts Vector3 or [x, y, z].
168 169 170 171 172 173 |
# File 'lib/sfml/audio/sound_stream.rb', line 168 def position=(value) v = value.is_a?(Vector3) ? value : Vector3.new(*value) packed = C::System::Vector3f.new packed[:x] = v.x.to_f; packed[:y] = v.y.to_f; packed[:z] = v.z.to_f C::Audio.sfSoundStream_setPosition(@handle, packed) end |
#relative_to_listener=(value) ⇒ Object
Toggle listener-relative positioning.
195 196 197 |
# File 'lib/sfml/audio/sound_stream.rb', line 195 def relative_to_listener=(value) C::Audio.sfSoundStream_setRelativeToListener(@handle, !!value) end |
#relative_to_listener? ⇒ Boolean
true if #position is interpreted relative to the listener.
192 |
# File 'lib/sfml/audio/sound_stream.rb', line 192 def relative_to_listener? = C::Audio.sfSoundStream_isRelativeToListener(@handle) |
#sample_rate ⇒ Object
Sample rate in Hz.
118 |
# File 'lib/sfml/audio/sound_stream.rb', line 118 def sample_rate = C::Audio.sfSoundStream_getSampleRate(@handle) |
#spatialization_enabled=(value) ⇒ Object
Toggle 3D spatialisation.
237 238 239 |
# File 'lib/sfml/audio/sound_stream.rb', line 237 def spatialization_enabled=(value) C::Audio.sfSoundStream_setSpatializationEnabled(@handle, !!value) end |
#spatialization_enabled? ⇒ Boolean
true if 3D positional / Doppler / cone math is applied.
234 |
# File 'lib/sfml/audio/sound_stream.rb', line 234 def spatialization_enabled? = C::Audio.sfSoundStream_isSpatializationEnabled(@handle) |
#status ⇒ Object
Playback state — :stopped, :paused, or :playing.
107 108 |
# File 'lib/sfml/audio/sound_stream.rb', line 107 def status = C::Audio::STATUSES[C::Audio.sfSoundStream_getStatus(@handle)] # `status == :playing`. |
#stop ⇒ Object
Stop streaming and reset internal state.
104 |
# File 'lib/sfml/audio/sound_stream.rb', line 104 def stop = (C::Audio.sfSoundStream_stop(@handle); self) |
#stopped? ⇒ Boolean
status == :stopped.
113 |
# File 'lib/sfml/audio/sound_stream.rb', line 113 def stopped? = status == :stopped |
#velocity ⇒ Object
3D velocity in world units / second.
273 274 275 276 |
# File 'lib/sfml/audio/sound_stream.rb', line 273 def velocity v = C::Audio.sfSoundStream_getVelocity(@handle) Vector3.new(v[:x], v[:y], v[:z]) end |
#velocity=(value) ⇒ Object
Set the velocity — accepts Vector3 or [x, y, z].
279 280 281 282 283 284 |
# File 'lib/sfml/audio/sound_stream.rb', line 279 def velocity=(value) vec = value.is_a?(Vector3) ? value : Vector3.new(*value) packed = C::System::Vector3f.new packed[:x] = vec.x.to_f; packed[:y] = vec.y.to_f; packed[:z] = vec.z.to_f C::Audio.sfSoundStream_setVelocity(@handle, packed) end |