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

#inspect ⇒ String

Returns a detailed inspection string for debugging.

Returns:

  • (String) —

    a formatted string with class name and state details



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

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.



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

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



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

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



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

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



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

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

#start ⇒ self

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
67
# 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 >/dev/null 2>&1", "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
  @thread.report_on_exception = false
  self
end

#stop ⇒ self

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



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

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

#to_s ⇒ String

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

Returns:

  • (String) —

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



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

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