Detect Speaker Changes in Meeting Audio: Active Speaker Events

Detecting the active speaker in a live meeting involves parsing metadata from a real-time audio stream. Each audio packet, or frame, from the stream is tagged with an identifier for the person speaking at that moment. By tracking changes to this identifier across consecutive frames, an application can build a precise timeline of who spoke and when.

This capability is a fundamental building block for creating interactive AI voice agents. An agent that knows who is speaking can attribute comments correctly, wait for a natural pause in conversation before responding, or trigger different workflows based on the speaker. It is a core component of the voice infrastructure required to build agents that can participate in meetings intelligently, rather than just passively recording them.

This real-time approach differs from post-call analysis. Instead of inferring speakers from a finished audio file, you receive speaker identity directly from the meeting platform as the conversation happens. The audio itself is a single mixed stream containing all participants' voices, but each frame is tagged with the dominant speaker. This provides reliable, named attribution without the ambiguity of acoustic-based speaker clustering.

Let's walk through the binary frame format and a Python implementation for tracking these speaker turns from a live WebSocket stream.

How Real-Time Speaker Detection Works

When a MeetStream bot joins a meeting with live audio streaming enabled, it establishes a WebSocket connection to your server. Through this connection, it sends a continuous stream of binary messages. Each message is a self-contained audio frame that represents a small slice of the meeting's audio.

What matters is that this is a single, mixed audio stream. You are not receiving separate audio channels for each participant. Instead, the audio from all speakers is combined into one stream. To provide attribution, each frame includes metadata identifying the dominant speaker during that audio segment. If the conversation switches from one person to another, the speaker ID and name in subsequent frames will change to reflect the new speaker.

Flowchart showing a four-step process: WebSocket server receives binary frames, a frame parser decodes them, a change tracker detects speaker transitions, and application logic consumes the resulting events.
A pipeline for processing live meeting audio, from raw WebSocket frames to structured speaker turn events for your application.

Your application's task is to parse these binary frames, extract the speaker identifier from each one, and notice when it changes. This signals the end of one person's speaking turn and the beginning of another's. By buffering the audio associated with each speaker ID, you can reconstruct complete, speaker-separated utterances.

Understanding the MeetStream Audio Frame Format

The live audio stream sends binary WebSocket messages, where each message is a single frame with a specific byte layout. Using a binary format minimizes overhead and latency compared to alternatives like JSON with base64-encoded audio.

A layered diagram showing the structure of a binary audio frame. It has four parts: a message type byte, a length-prefixed speaker ID, a length-prefixed speaker name, and the raw PCM audio data.
The binary layout of a single audio frame sent over the WebSocket, containing speaker metadata and mixed audio data.

The structure is as follows:

Field Size Type Description
msg_type 1 byte uint8 Always 0x01 for audio frames.
sid_length 2 bytes uint16 LE Byte length of the speaker_id string.
speaker_id sid_length bytes UTF-8 Stable platform-assigned speaker identifier.
sname_length 2 bytes uint16 LE Byte length of the speaker_name string.
speaker_name sname_length bytes UTF-8 Participant display name from the meeting.
pcm_data remaining bytes int16 LE Raw audio samples, 48kHz mono, mixed.

A few things to keep in mind. The length fields are little-endian unsigned shorts, which corresponds to the <H format code in Python's struct module. The audio data is signed 16-bit PCM samples at a 48kHz sample rate. Most speech-to-text models expect 16kHz, so you will need to downsample the audio before transcription.

Implementing a Frame Parser in Python

The first step is to parse the binary format. The following Python code defines a simple data class for a parsed frame and a parser class that safely decodes the raw bytes. It validates the message type, correctly handles the variable-length string fields, and returns a structured object.

import struct
from dataclasses import dataclass
from typing import Optional

@dataclass
class AudioFrame:
    speaker_id: str
    speaker_name: str
    pcm_bytes: bytes
    sample_count: int
    sample_rate: int = 48000

class FrameParser:
    """
    Parses MeetStream binary audio frames from a mixed stream.
    Each frame is tagged with the dominant speaker.
    """
    AUDIO_MSG_TYPE = 0x01
    BYTES_PER_SAMPLE = 2  # int16

    def parse(self, raw: bytes) -> Optional[AudioFrame]:
        if not raw:
            return None

        offset = 0

        # Message type
        msg_type = raw[offset]
        offset += 1
        if msg_type != self.AUDIO_MSG_TYPE:
            return None

        # speaker_id
        if offset + 2 > len(raw):
            raise ValueError("Frame too short for sid_length")
        sid_length = struct.unpack_from('<H', raw, offset)[0]
        offset += 2
        if offset + sid_length > len(raw):
            raise ValueError(f"Frame too short for speaker_id of length {sid_length}")
        speaker_id = raw[offset:offset + sid_length].decode('utf-8')
        offset += sid_length

        # speaker_name
        if offset + 2 > len(raw):
            raise ValueError("Frame too short for sname_length")
        sname_length = struct.unpack_from('<H', raw, offset)[0]
        offset += 2
        if offset + sname_length > len(raw):
            raise ValueError(f"Frame too short for speaker_name of length {sname_length}")
        speaker_name = raw[offset:offset + sname_length].decode('utf-8')
        offset += sname_length

        # PCM audio data
        pcm_bytes = raw[offset:]
        sample_count = len(pcm_bytes) // self.BYTES_PER_SAMPLE

        return AudioFrame(
            speaker_id=speaker_id,
            speaker_name=speaker_name,
            pcm_bytes=pcm_bytes,
            sample_count=sample_count
        )

Tracking Speaker Changes

With the parser handling the binary decoding, the next layer tracks speaker turns. A turn begins when a new speaker_id appears and concludes when a different one is detected. The tracker accumulates audio for the current speaker and fires a callback with the completed turn data, including the buffered audio and timing information.

from typing import Callable, List

@dataclass
class SpeakerTurn:
    speaker_id: str
    speaker_name: str
    start_sample: int
    end_sample: int
    sample_rate: int
    pcm_bytes: bytes

    @property
    def start_seconds(self) -> float:
        return self.start_sample / self.sample_rate

    @property
    def end_seconds(self) -> float:
        return self.end_sample / self.sample_rate

class SpeakerChangeTracker:
    """
    Tracks speaker changes in a stream of AudioFrames.
    Fires on_turn_complete callback when a speaker turn ends.
    """
    def __init__(
        self,
        on_turn_complete: Callable[[SpeakerTurn], None],
        min_turn_samples: int = 2400  # 50ms at 48kHz
    ):
        self.on_turn_complete = on_turn_complete
        self.min_turn_samples = min_turn_samples
        self._current_speaker_id: Optional[str] = None
        self._current_speaker_name: Optional[str] = None
        self._turn_start_sample: int = 0
        self._turn_audio_buffer: List[bytes] = []
        self._total_samples: int = 0
        self._sample_rate: int = 48000

    def process(self, frame: AudioFrame) -> None:
        if self._current_speaker_id is None:
            # First frame, initialize
            self._current_speaker_id = frame.speaker_id
            self._current_speaker_name = frame.speaker_name
            self._turn_start_sample = 0

        if frame.speaker_id != self._current_speaker_id:
            # Speaker changed, flush the completed turn
            self._flush_turn()
            self._current_speaker_id = frame.speaker_id
            self._current_speaker_name = frame.speaker_name
            self._turn_start_sample = self._total_samples
            self._turn_audio_buffer = []

        self._turn_audio_buffer.append(frame.pcm_bytes)
        self._total_samples += frame.sample_count

    def flush(self) -> None:
        """Call at the end of the stream to emit the final turn."""
        if self._turn_audio_buffer:
            self._flush_turn()

    def _flush_turn(self) -> None:
        turn_samples = sum(len(b) // 2 for b in self._turn_audio_buffer)
        if turn_samples < self.min_turn_samples:
            return  # Skip short noise bursts

        turn = SpeakerTurn(
            speaker_id=self._current_speaker_id,
            speaker_name=self._current_speaker_name,
            start_sample=self._turn_start_sample,
            end_sample=self._total_samples,
            sample_rate=self._sample_rate,
            pcm_bytes=b''.join(self._turn_audio_buffer)
        )
        self.on_turn_complete(turn)

Connecting to the Live Audio Stream

To receive this audio stream, you must provide a publicly accessible WebSocket URL when you create a bot using the MeetStream API. Set the live_audio_required parameter in your request. MeetStream's infrastructure will then connect to your WebSocket server as a client and begin sending audio frames once the bot is in the meeting.

curl -X POST "https://api.meetstream.ai/api/v1/bots/create_bot" \
  -H "Authorization: Token <YOUR_API_KEY>" \
  -H "Content-Type: application/json" \
  -d '{
    "meeting_link": "<YOUR_MEETING_LINK>",
    "bot_name": "AudioBot",
    "live_audio_required": {
      "websocket_url": "wss://your-server.com/audio"
    }
  }'

Your server will need to accept this incoming WebSocket connection. The handler can then wire the parser and tracker together. As binary messages arrive, they are parsed into frames and passed to the tracker, which in turn emits completed speaker turns for your application to process.

How MeetStream Enables Real-Time Applications

Detecting speaker changes is one part of MeetStream's broader platform for building on live meetings. Our unified API provides the core voice infrastructure to deploy bots and AI agents into Zoom, Google Meet, and Microsoft Teams. The real-time audio stream is designed for applications that need to react to conversation dynamics as they happen.

This is useful for building sales coaching tools that provide live feedback, AI notetakers that attribute action items to the correct person, or voice-controlled agents that can interact with meeting participants. By providing platform-native speaker identities alongside the audio, MeetStream removes the guesswork from speaker attribution. You can focus on your application's logic instead of building complex audio processing pipelines.

See the full API reference at docs.meetstream.ai.

Frequently Asked Questions

What is active speaker detection?

Active speaker detection is the process of identifying who is speaking at any given moment in a multi-party conversation. In the context of the MeetStream API, this is achieved by analyzing metadata sent with each audio frame that tags the dominant speaker, rather than through acoustic analysis.

How is real-time speaker detection different from post-call diarization?

Real-time detection uses speaker identity provided by the meeting platform (e.g., Zoom, Google Meet) during the live call. Post-call speaker diarization is an offline process that analyzes a completed audio recording to cluster and label speakers based on voice characteristics, without knowing their actual identities.

How do you handle simultaneous speech?

When multiple people speak at once, the underlying audio is mixed together. The speaker metadata tag in each frame will identify the dominant (usually loudest) speaker for that short time slice. This may result in rapid alternations between speaker IDs, which your application can interpret as overlapping speech.

Can I get separate audio tracks for each participant?

The real-time WebSocket stream provides a single mixed audio track with dominant speaker metadata. For post-call analysis, MeetStream also offers per-participant audio recording. This provides fully isolated audio files for each speaker on Zoom, and separate speaker-attributed streams for Google Meet and Microsoft Teams.

What meeting platforms are supported?

The MeetStream API supports deploying bots to capture audio from Zoom, Google Meet, and Microsoft Teams through a single integration. This allows you to build applications that work consistently across the major meeting platforms.

You might also like