Class: SFML::SoundStream

Inherits:
Object
  • Object
show all
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

Instance Method Summary collapse

Constructor Details

#initialize(channel_count:, sample_rate:) ⇒ SoundStream

Returns a new instance of SoundStream.

Raises:

  • (ArgumentError)


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.

Returns:

  • (Boolean)


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.

Raises:

  • (NoMethodError)


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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


109
110
# File 'lib/sfml/audio/sound_stream.rb', line 109

def playing?    = 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 playing_offset
  Time.from_native(C::Audio.sfSoundStream_getPlayingOffset(@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 playing_offset=(value)
  t = value.is_a?(Time) ? value : Time.seconds(value.to_f)
  C::Audio.sfSoundStream_setPlayingOffset(@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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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

#volume ⇒ Object

Playback volume in [0, 100].



134
# File 'lib/sfml/audio/sound_stream.rb', line 134

def volume = C::Audio.sfSoundStream_getVolume(@handle)

#volume=(value) ⇒ Object

Set the volume.



137
138
139
# File 'lib/sfml/audio/sound_stream.rb', line 137

def volume=(value)
  C::Audio.sfSoundStream_setVolume(@handle, value.to_f)
end