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 =
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)


49
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
# File 'lib/sfml/audio/sound_stream.rb', line 49

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 Error, "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

#handleObject (readonly)

:nodoc:



172
173
174
# File 'lib/sfml/audio/sound_stream.rb', line 172

def handle
  @handle
end

Instance Method Details

#attenuationObject



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

def attenuation = C::Audio.sfSoundStream_getAttenuation(@handle)

#attenuation=(value) ⇒ Object



156
157
158
# File 'lib/sfml/audio/sound_stream.rb', line 156

def attenuation=(value)
  C::Audio.sfSoundStream_setAttenuation(@handle, value.to_f)
end

#channel_countObject



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

def channel_count = C::Audio.sfSoundStream_getChannelCount(@handle)

#looping=(value) ⇒ Object



116
117
118
119
# File 'lib/sfml/audio/sound_stream.rb', line 116

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)


112
113
114
# File 'lib/sfml/audio/sound_stream.rb', line 112

def looping?
  @looping == true
end

#min_distanceObject



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

def min_distance = C::Audio.sfSoundStream_getMinDistance(@handle)

#min_distance=(value) ⇒ Object



162
163
164
# File 'lib/sfml/audio/sound_stream.rb', line 162

def min_distance=(value)
  C::Audio.sfSoundStream_setMinDistance(@handle, value.to_f)
end

#on_get_dataObject

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)


86
87
88
# File 'lib/sfml/audio/sound_stream.rb', line 86

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.).



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

def on_seek(_time); end

#pauseObject



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

def pause = (C::Audio.sfSoundStream_pause(@handle); self)

#paused?Boolean

Returns:

  • (Boolean)


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

def paused?     = status == :paused

#pitchObject



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

def pitch = C::Audio.sfSoundStream_getPitch(@handle)

#pitch=(value) ⇒ Object



129
130
131
# File 'lib/sfml/audio/sound_stream.rb', line 129

def pitch=(value)
  C::Audio.sfSoundStream_setPitch(@handle, value.to_f)
end

#playObject

—- Public playback API ——————————————



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

def play  = (C::Audio.sfSoundStream_play(@handle); self)

#playing?Boolean

Returns:

  • (Boolean)


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

def playing?    = status == :playing

#playing_offsetObject



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

def playing_offset
  Time.from_native(C::Audio.sfSoundStream_getPlayingOffset(@handle))
end

#playing_offset=(value) ⇒ Object



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

def playing_offset=(value)
  t = value.is_a?(Time) ? value : Time.seconds(value.to_f)
  C::Audio.sfSoundStream_setPlayingOffset(@handle, t.to_native)
end

#positionObject



142
143
144
145
# File 'lib/sfml/audio/sound_stream.rb', line 142

def position
  v = C::Audio.sfSoundStream_getPosition(@handle)
  Vector3.new(v[:x], v[:y], v[:z])
end

#position=(value) ⇒ Object



147
148
149
150
151
152
# File 'lib/sfml/audio/sound_stream.rb', line 147

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



168
169
170
# File 'lib/sfml/audio/sound_stream.rb', line 168

def relative_to_listener=(value)
  C::Audio.sfSoundStream_setRelativeToListener(@handle, !!value)
end

#relative_to_listener?Boolean

Returns:

  • (Boolean)


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

def relative_to_listener? = C::Audio.sfSoundStream_isRelativeToListener(@handle)

#sample_rateObject



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

def sample_rate   = C::Audio.sfSoundStream_getSampleRate(@handle)

#statusObject



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

def status      = C::Audio::STATUSES[C::Audio.sfSoundStream_getStatus(@handle)]

#stopObject



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

def stop  = (C::Audio.sfSoundStream_stop(@handle); self)

#stopped?Boolean

Returns:

  • (Boolean)


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

def stopped?    = status == :stopped

#volumeObject



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

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

#volume=(value) ⇒ Object



123
124
125
# File 'lib/sfml/audio/sound_stream.rb', line 123

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