After every agent conversation, I need to send an ...
# ask-ai
c
After every agent conversation, I need to send an mp3 track to user how to send it? is there any configuration i need to do in client also?
t
Thanks for reaching out, abilash. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
I'd love to understand a bit more about how you're managing agent conversations. Are you using LiveKit's APIs, SDKs, or maybe a particular integration? Also, when it comes to the mp3 track, would you prefer it to be sent automatically, or would the agent trigger it manually? I'm here to help, so feel free to share any details!
c
So lets say I said Hi, and then Agent will speak Hi there etc and once she stops speaking means when speaking goes to listening state then i need to play a local mp3 track
So lets say I said Hi, and then Agent will speak Hi there etc and once she stops speaking means when speaking goes to listening state then i need to play a local mp3 track from server to client
t
I'd be happy to help you play a local MP3 track from the server to the client when the agent switches from speaking to listening. Here's a friendly step-by-step guide you can follow: 1. Detect Agent State Transition: Keep an eye on the
agent_state_changed
event. This event is perfect for catching when the agent stops speaking and starts listening, which is exactly when you'd want to play your MP3. 2. Play MP3 Using AudioSource: You'll want to decode your MP3 into PCM audio frames (48kHz, mono). Using
rtc.AudioSource
and
rtc.LocalAudioTrack
, you can stream the audio directly into the room. Just make sure your MP3 is converted to the right format — tools like
pydub
or
ffmpeg
can help with this. 3. Publish and Unpublish the Track: When playing, publish the audio track to the room, and once it’s done, unpublish it to keep things running smoothly and free up resources. Here’s a friendly example to illustrate how you might do this:
Copy code
import asyncio
from livekit import rtc
from pydub import AudioSegment

async def play_mp3(file_path: str, room: rtc.Room):
    audio = AudioSegment.from_mp3(file_path)
    audio = audio.set_frame_rate(48000).set_channels(1)
    audio_bytes = audio.raw_data
    frame_length_ms = 20
    bytes_per_frame = 48000 * 2 * 1 * frame_length_ms // 1000

    source = rtc.AudioSource(48000, 1)
    track = rtc.LocalAudioTrack.create_audio_track("background-music", source)
    await room.local_participant.publish_track(track)

    for i in range(0, len(audio_bytes), bytes_per_frame):
        chunk = audio_bytes[i:i + bytes_per_frame]
        frame = rtc.AudioFrame(
            data=chunk,
            sample_rate=48000,
            num_channels=1,
            samples_per_channel=len(chunk) // 2
        )
        await source.capture_frame(frame)
        await asyncio.sleep(frame_length_ms / 1000)

    await room.local_participant.unpublish_track(track)
To tie it all together, you can hook this function up to your state listener so the MP3 plays right when the agent switches to listening:
Copy code
@agent_session.on("agent_state_changed")
async def on_agent_state_changed(ev):
    if ev.old_state == "speaking" and ev.new_state == "listening":
        asyncio.create_task(play_mp3("path/to/music.mp3", agent_session.room))
If you run into any questions or want to dive deeper into any part of this, just let me know — I’m here to help! Sources: Events and error handling | LiveKit Docs | examples/primitives/echo-agent.py
c
async def play_audio_file(room: Room, audio_file_path: str): try: # Get absolute path of the audio file if not os.path.isabs(audio_file_path): audio_file_path = os.path.join(os.path.dirname(file), audio_file_path) logging.info(f"Loading audio file from: {audio_file_path}") # Convert MP3 to WAV in memory if it's an MP3 file if audio_file_path.lower().endswith('.mp3'): audio = AudioSegment.from_mp3(audio_file_path) audio = audio.set_frame_rate(16000).set_channels(2).set_sample_width(2) audio_data = np.array(audio.get_array_of_samples(), dtype=np.int16) sample_rate = audio.frame_rate num_channels = audio.channels else: import soundfile as sf audio_data, sample_rate = sf.read(audio_file_path) num_channels = audio_data.shape[1] if audio_data.ndim > 1 else 1 audio_data = (audio_data * 32767).astype(np.int16) # Ensure stereo if num_channels is 1 if num_channels == 1: audio_data = np.repeat(audio_data[:, np.newaxis], 2, axis=1).flatten() num_channels = 2 # Create audio source and track source = AudioSource(sample_rate, num_channels) audio_track = LocalAudioTrack.create_audio_track("audio_track", source) # Use provided room options or default TrackPublishOptions track_options = TrackPublishOptions(source=TrackSource.SOURCE_MICROPHONE) # Publish track to room await room.local_participant.publish_track(audio_track, track_options) logging.info(f"Published audio file: {audio_file_path}") # Convert audio data to frames and capture chunk_size = (sample_rate // 50) * num_channels # 20ms chunks, adjusted for channels audio_frame = AudioFrame.create(sample_rate, num_channels, chunk_size // num_channels) audio_buffer = np.frombuffer(audio_frame.data, dtype=np.int16) for i in range(0, len(audio_data), chunk_size): chunk = audio_data[i:i + chunk_size] if len(chunk) < chunk_size: chunk = np.pad(chunk, (0, chunk_size - len(chunk)), mode='constant') np.copyto(audio_buffer, chunk) await source.capture_frame(audio_frame) await asyncio.sleep(0.02) # Wait for 20ms except Exception as e: logging.error(f"Error playing audio file: {e}") In this code what mistake i am doing
t
I'd love to help you get your audio playback working smoothly with LiveKit! Here are some common issues and friendly tips to improve your code: 1. Audio Format Mismatch: It’s important to make sure your audio data is converted to 48kHz, mono, and 16-bit PCM format. Right now, your code sets the frame rate to 16kHz and uses stereo, which might not play nicely with LiveKit’s requirements. 2. Frame Size and Timing: The chunk size should match 20ms of audio. For 48kHz mono audio, that means 960 samples per frame. You might want to adjust your chunk size calculation to reflect this. 3. Queue Overload: If your audio frame queue gets overloaded, try increasing the
queue_size_ms
parameter when creating the
AudioSource
. For example: `source = AudioSource(sample_rate, num_channels, queue_size_ms=2000)`This can help keep things running smoothly without dropping frames. 4. Final Chunk Padding: If the last chunk of audio data is smaller than expected, padding it with zeros can prevent timing hiccups during playback. 5. Playback Timing: Adding a little delay between sending frames helps maintain proper timing. For instance: `await asyncio.sleep(0.02)`This delay should match the frame duration (like 20ms for 48kHz audio). 6. Error Handling: Including detailed logging is a great way to catch and understand any issues. For example: `logging.error(f"Error playing audio file: {e}")`You might also want to log audio data size, frame size, and playback progress to keep track. 7. Resource Cleanup: Don’t forget to unpublish the audio track and close the audio source after playback to free up resources. For example:
await room.local_participant.unpublish_track(audio_track)
Tackling these points should really boost the reliability and compatibility of your audio playback. If you have any questions or want to dive deeper into any of these, just let me know—I’m here to help! Sources: Resolving &amp;#34;InvalidState - failed to capture frame&amp;#34; errors in AudioSource | agent_extensions/agent_extensions/utils/wav_player.py | livekit-rtc/livekit/rtc/track.py | Getting Help | livekit/livekit_composite | DeepWiki | Community &amp; Support | livekit/livekit_composite | DeepWiki
c
I am giving RoomOutput options 16KHz only and
t
Thanks for reaching out! Just to help you along, LiveKit works best with audio that's in 48kHz, mono, and 16-bit PCM format to ensure everything runs smoothly. If your audio is currently at 16kHz, no worries—you'll just need to resample it to 48kHz before sharing it in the room. Tools like
pydub
or
ffmpeg
can make this pretty straightforward. Here’s a friendly example of how you can resample your audio to 48kHz using `pydub`:
Copy code
from pydub import AudioSegment

audio = AudioSegment.from_file("path/to/audio.mp3")
audio = audio.set_frame_rate(48000).set_channels(1).set_sample_width(2)  # Convert to 48kHz, mono, 16-bit PCM
Once your audio is set up like this, you’ll be all set to publish it to the room. If you have any questions or need a hand with the next steps, just let me know—I’m here to help!
c
No i need output for client in 16Khz so i set RoomOutputoptions 16Khz
t
Thanks so much for reaching out! I wanted to share that LiveKit currently doesn’t support 16kHz audio output for clients using
RoomOutputOptions
. To keep things smooth and consistent, the system standardizes audio output at 24kHz, which is the supported sample rate for RTC streaming and client playback. If your project needs 16kHz output, no worries—you can resample the audio externally once it’s received on the client side. I’m here to help if you’d like some guidance on how to set that up, just let me know! Sources: livekit-plugins/livekit-plugins-clova/livekit/plugins/clova/constants.py | agents/src/voice/agent_session.ts | Speech &amp; Audio Processing | livekit/livekit_composite | DeepWiki
c
2025-09-17 175756,906 - INFO root - Published audio file: /Users/abilashs/development/agent-starter-python/src/sample.mp3 {"room": "cheeko", "pid": 14528, "job_id": "simulated-job-85b0aa3b542a"} 2025-09-17 175801,646 - ERROR root - Error playing audio file: bad argument type for built-in operation {"room": "cheeko", "pid": 14528, "job_id": "simulated-job-85b0aa3b542a"} I am getting error like this for async def play_audio_file(room: Room, audio_file_path: str): try: # Get absolute path of the audio file if not os.path.isabs(audio_file_path): audio_file_path = os.path.join(os.path.dirname(file), audio_file_path) logging.info(f"Loading audio file from: {audio_file_path}") # Convert MP3 to WAV in memory if it's an MP3 file if audio_file_path.lower().endswith('.mp3'): audio = AudioSegment.from_mp3(audio_file_path) audio = audio.set_frame_rate(24000).set_channels(2).set_sample_width(2) audio_data = np.array(audio.get_array_of_samples(), dtype=np.int16) sample_rate = audio.frame_rate num_channels = audio.channels else: import soundfile as sf audio_data, sample_rate = sf.read(audio_file_path) num_channels = audio_data.shape[1] if audio_data.ndim > 1 else 1 audio_data = (audio_data * 32767).astype(np.int16) # Ensure stereo if num_channels is 1 if num_channels == 1: audio_data = np.repeat(audio_data[:, np.newaxis], 2, axis=1).flatten() num_channels = 2 # Create audio source and track source = AudioSource(sample_rate, num_channels, queue_size_ms=2000) audio_track = LocalAudioTrack.create_audio_track("audio_track", source) # Use provided room options or default TrackPublishOptions track_options = TrackPublishOptions(source=TrackSource.SOURCE_MICROPHONE) # Publish track to room await room.local_participant.publish_track(audio_track, track_options) logging.info(f"Published audio file: {audio_file_path}") # Convert audio data to frames and capture chunk_size = (sample_rate // 50) * num_channels # 20ms chunks, adjusted for channels audio_frame = AudioFrame.create(sample_rate, num_channels, chunk_size // num_channels) audio_buffer = np.frombuffer(audio_frame.data, dtype=np.int16) for i in range(0, len(audio_data), chunk_size): chunk = audio_data[i:i + chunk_size] if len(chunk) < chunk_size: chunk = np.pad(chunk, (0, chunk_size - len(chunk)), mode='constant') np.copyto(audio_buffer, chunk) await source.capture_frame(audio_frame) await asyncio.sleep(0.02) # Wait for 20ms await room.local_participant.unpublish_track(audio_track) logging.info(f"Unpublished audio file: {audio_file_path}") except Exception as e: logging.error(f"Error playing audio file: {e}") this code what is the missing part in this what could be the error
t
I'd love to help you get to the bottom of this! Could you please share a bit more about the exact line of code where you're seeing the error? Also, if you could let me know the data types of the variables involved, that would be super helpful in figuring out what's going on. We're in this together! Sources: stream_audio.py | livekit-rtc/livekit/rtc/_proto/audio_frame_pb2.pyi | Camera &amp; microphone | LiveKit Docs | livekit-plugins/livekit-plugins-resemble/livekit/plugins/resemble/py.typed
c
await room.local_participant.unpublish_track(audio_track)
t
Could you please confirm the type of the
audio_track
variable you're passing to
unpublish_track
? It should be a valid string representing the track SID. Also, are you explicitly passing the
stop_on_unpublish
parameter? If so, I'd love to know what value you're using. I'm here to help you get this sorted out! Sources: livekit-protocol/livekit/protocol/py.typed | packages/livekit-rtc/src/participant.ts
c
audio_track = LocalAudioTrack.create_audio_track("audio_track", source)
t
It looks like the issue comes from the
unpublish_track
method expecting a track SID (which is a string identifier for the track) instead of the
LocalAudioTrack
object itself. No worries though! To fix this, you just need to grab the track SID from your
audio_track
object before calling
unpublish_track
. Here’s a simple way to do that:
Copy code
# Retrieve the track SID from the published track
track_sid = audio_track.sid

# Use the track SID to unpublish the track
await room.local_participant.unpublish_track(track_sid)
If you find that
audio_track.sid
isn’t accessible right away, it might be because the track hasn’t been published to the room yet—remember, the SID gets assigned during publishing. Feel free to reach out if you want me to walk you through this or if you have any other questions!
c
In client if it needs to hear all the tracks do we need to do add any additional settings?
t
By default, LiveKit clients automatically subscribe to all published tracks in a room, so you don’t need to change any settings to hear everything. This is managed by the
autoSubscribe
option, which is turned on by default when you connect to a room. If you’re not hearing all the tracks, here are a few things to check: 1. Make sure
autoSubscribe
isn’t set to
false
when connecting to the room. 2. Confirm that the other participants have properly published their remote tracks. 3. Check if your browser allows audio playback. Some browsers block autoplay unless you interact with the page first. In that case, try calling
Room.startAudio()
inside a user gesture handler to get the audio going. If you have any questions or need a hand with this, please don’t hesitate to reach out. I’m here to help! Sources: Rooms, participants, and tracks | LiveKit Docs | Subscribing to tracks | LiveKit Docs | README.md