Class: OllamaChat::Utils::AudioPlayer

Inherits:
Object
  • Object
show all
Defined in:
lib/ollama_chat/utils/audio_player.rb

Overview

A robust audio player that streams raw PCM data to ffplay.

This class solves the issue of ffplay hanging when data arrives late by injecting silence chunks into the pipe. It uses a background thread and a Queue to decouple audio fetching from playback, ensuring smooth continuous audio even if the TTS backend is slow.

Examples:

Basic usage

player = AudioPlayer.new.start
player.push(audio_data)
player.stop

Instance Method Summary collapse

Constructor Details

#initialize(command: nil, frequency: nil, bits: nil, pause: nil) ⇒ AudioPlayer

Creates a new AudioPlayer instance.

Parameters:

  • command (Array<String>) (defaults to: nil)

    the command to execute for playback

  • frequency (Integer) (defaults to: nil)

    the sample rate of the audio (defaults to 24_000)

  • bits (Integer) (defaults to: nil)

    the bit depth of the audio (defaults to 16)

  • pause (Float) (defaults to: nil)

    the duration of silence to inject when the queue is empty (defaults to 0.01)



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/ollama_chat/utils/audio_player.rb', line 21

def initialize(command: nil, frequency: nil, bits: nil, pause: nil)
  config = OC::OLLAMA::CHAT::AUDIO_PLAYER_CONFIG? or
    raise 'env var %s is required' % (
      OC::OLLAMA::CHAT::AUDIO_PLAYER_CONFIG!.env_var_name.inspect
    )
  command   ||= config.command
  frequency ||= config.frequency
  bits      ||= config.bits
  pause     ||= config.pause
  @audio_queue   = Queue.new
  @playing       = false
  @command       = command
  @frequency     = frequency.to_f
  @bits          = bits
  @pause         = pause.to_f
  @silence_chunk = silent_bytes(@pause)
end

Instance Method Details

#inspectString

Returns a detailed inspection string for debugging.

Returns:

  • (String)

    a formatted string with class name and state details



134
135
136
# File 'lib/ollama_chat/utils/audio_player.rb', line 134

def inspect
  "#<#{self.class}: #{to_s}>"
end

#play {|player| ... } ⇒ Object

Plays audio using a block-based iterator pattern.

This method simplifies lifecycle management by automatically starting the player before yielding and stopping it afterwards, ensuring the playback thread is properly cleaned up.

Examples:

audio_player.play do |ap|
  ap << audio_chunk_1
  ap << audio_chunk_2
end

Yields:

  • (player)

    Yields the AudioPlayer instance to the block.

Raises:

  • (ArgumentError)

    if no block is given.



82
83
84
85
86
87
# File 'lib/ollama_chat/utils/audio_player.rb', line 82

def play(&block)
  block or raise ArgumentError, 'require &block argument'
  start
  block.(self)
  stop
end

#playing?Boolean

Checks if the player is currently active or has pending audio.

The player is considered "playing" if the @playing flag is true or if there are still audio chunks waiting in the queue.

Returns:

  • (Boolean)

    true if the player is active or queue is non-empty



95
96
97
# File 'lib/ollama_chat/utils/audio_player.rb', line 95

def playing?
  @playing || @audio_queue.present?
end

#push(chunk) ⇒ self Also known as: <<

Pushes an audio chunk to the playback queue.

Parameters:

  • chunk (String)

    the raw PCM audio bytes to play

Returns:

  • (self)

    returns the player instance for chaining



115
116
117
118
# File 'lib/ollama_chat/utils/audio_player.rb', line 115

def push(chunk)
  @audio_queue.push(chunk)
  start
end

#silent_bytes(duration) ⇒ String (private)

Generates a silence chunk of PCM null bytes for the given duration.

Parameters:

  • duration (Float)

    the duration of silence in seconds

Returns:

  • (String)

    a binary string of zero bytes representing silence



144
145
146
# File 'lib/ollama_chat/utils/audio_player.rb', line 144

def silent_bytes(duration)
  "\x00" * ((@frequency * (@bits / 8) * duration)).ceil
end

#startself

Starts the background playback thread.

This method launches a thread that opens a pipe to ffplay and begins consuming audio chunks from the internal queue. If the queue is empty, it writes silence to keep the stream alive.

Returns:

  • (self)

    returns the player instance for chaining



46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# File 'lib/ollama_chat/utils/audio_player.rb', line 46

def start
  @playing and return self
  @playing = true
  @thread = Thread.new do
    IO.popen(@command, "w") do |io|
      io.binmode
      io.sync = true

      while playing?
        chunk = @audio_queue.shift(true) rescue nil
        if chunk
          io.write(chunk)
        else
          io.write(@silence_chunk)
          sleep @pause
        end
      end
    end
  end
  self
end

#stopself

Stops the playback and waits for the background thread to finish.

This sets the playing flag to false and joins the thread, ensuring all resources are cleaned up properly.

Returns:

  • (self)

    returns the player instance



105
106
107
108
109
# File 'lib/ollama_chat/utils/audio_player.rb', line 105

def stop
  @playing = false
  @thread&.join
  self
end

#to_sString

Returns a string representation of the player's current state.

Returns:

  • (String)

    a summary including playing status, pause duration, and silence chunk size



127
128
129
# File 'lib/ollama_chat/utils/audio_player.rb', line 127

def to_s
  "playing=#{playing?} pause=#@pause #{@silence_chunk.bytesize} B"
end