Memory Chunks and Memblockqs

A final piece of the puzzle in PulseAudio memory handling is memory chunks, and memblockqs. Memblockqs suffers from a bad naming, and it's actally a queue of memory chunks.

What is a memory chunk?

Memchunk is a very simple stack-allocated structure that references "part of" (chunk of) a memblock. The structure just contains 3 variables: the memblock of interest (which contains the data pointer), index within this block's data, and length of the part/chunk data within this block.

/* A memchunk describes a part of a memblock. In contrast to the memblock, a
 * memchunk is not allocated dynamically or reference counted, instead
 * it is usually stored on the stack and copied around */

struct pa_memchunk {
    pa_memblock *memblock;
    size_t index, length;
};

But why are memchunks created?

Remember the original role of PulseAudio? It mixes different streams and presents the result to the final ALSA driver. Maybe that's the reason? Sending different small chunks to the driver in sequence?

In the code base, a lot of memory chunks are created of memory blocks data and passed around.

An example is the "silence" memchunks, which are passed around a lot! They are normal memchunks, but with a repeatable sequence of data that is "silence". This repeatable sequence of silence data differs by the sample rate of the sink.

pa_memchunk* pa_silence_memchunk_get(pa_silence_cache *cache, pa_mempool *pool, pa_memchunk* ret, const pa_sample_spec *spec, size_t length) {
    pa_memblock *b;
    size_t l;

    pa_assert(cache);
    pa_assert(pa_sample_spec_valid(spec));

    if (!(b = cache->blocks[spec->format]))

        switch (spec->format) {
            case PA_SAMPLE_U8:
                cache->blocks[PA_SAMPLE_U8] = b = silence_memblock_new(pool, 0x80);
                break;
            case PA_SAMPLE_S16LE:
            case PA_SAMPLE_S16BE:
            case PA_SAMPLE_S32LE:
            case PA_SAMPLE_S32BE:
            case PA_SAMPLE_S24LE:
            case PA_SAMPLE_S24BE:
            case PA_SAMPLE_S24_32LE:
            case PA_SAMPLE_S24_32BE:
            case PA_SAMPLE_FLOAT32LE:
            case PA_SAMPLE_FLOAT32BE:
                cache->blocks[PA_SAMPLE_S16LE] = b = silence_memblock_new(pool, 0);
                cache->blocks[PA_SAMPLE_S16BE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_S32LE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_S32BE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_S24LE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_S24BE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_S24_32LE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_S24_32BE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_FLOAT32LE] = pa_memblock_ref(b);
                cache->blocks[PA_SAMPLE_FLOAT32BE] = pa_memblock_ref(b);
                break;
            case PA_SAMPLE_ALAW:
                cache->blocks[PA_SAMPLE_ALAW] = b = silence_memblock_new(pool, 0xd5);
                break;
            case PA_SAMPLE_ULAW:
                cache->blocks[PA_SAMPLE_ULAW] = b = silence_memblock_new(pool, 0xff);
                break;
            default:
                pa_assert_not_reached();
    }

    pa_assert(b);

    ret->memblock = pa_memblock_ref(b);

    l = pa_memblock_get_length(b);
    if (length > l || length == 0)
        length = l;

    ret->length = pa_frame_align(length, spec);
    ret->index = 0;

    return ret;
}

As can be seen above, silent memory chunks are created and cached to be passed around later any time, and it's indeed needed a lot.

Memblockq is just a queue of memory chunks! Yes, it's badly named, and this is acknowledged in the header file itself:

/* A memblockq is a queue of pa_memchunks (yepp, the name is not
 * perfect). It is similar to the ring buffers used by most other
 * audio software. In contrast to a ring buffer this memblockq data
 * type doesn't need to copy any data around, it just maintains
 * references to reference counted memory blocks. */

typedef struct pa_memblockq pa_memblockq;

Unfortunately the structure elements themselves are not well documented, but we will deduce their use by following around the code:

struct pa_memblockq {
    struct list_item *blocks, *blocks_tail;
    struct list_item *current_read, *current_write;
    unsigned n_blocks;
    size_t maxlength, tlength, base, prebuf, minreq, maxrewind;
    int64_t read_index, write_index;
    bool in_prebuf;
    pa_memchunk silence;
    pa_mcalign *mcalign;
    int64_t missing, requested;
    char *name;
    pa_sample_spec sample_spec;
};
  • blocks and blocks_tail: it's clear that these are the linked lists of memory chunks
  • n_blocks: number of memory chunks in this memblockq
  • current_read and current_write: Live indices that change while ringbuffer producing and consuming. Note that these are list pointers since the ringbuffer elements is list buffer node pointers.
  • read_index and write_index: Ringbuffer read and write integer index values. Note: These indices do not count list nodes, but count the actual bytes. So if I added a first chunk to the chunk queue of length 400 bytes, the write_index value will equal 400, not 1.
  • maxlength: Max length of the ringbuffer in bytes, not in number of nodes.
  • base: This is usually pa_frame_size(sample_spec). That is, the smallest possible "unit of audio" given the sample format chosen. This is why no memchunk will be allowed to be added to the ringbuffer if its index and length are not multiples of this base value!
  • prebuf, minreq, tlength, and maxlength: All four are common buffer properties across PulseAudio. Check the official pa_buffer_attr documentation for further details. They are also explained in more details below.
  • tlength: target length of the buffer. The server tries to assure that at least tlength bytes are always available in the per-stream server playback buffer. This value will default to something like 2s.
  • missing: This keyword is mentioned in important parts of the PulseAudio code base. If the write_index minus the read_index (amount of buffered audio) is bigger than the buffer target length tlength, then we have a missing value of zero. On the other hand, if the amount of buffered audio is smaller than tlength, then the missing value equals tlength - (write_index - read_index). That is, the "missing" amount of audio needed to reach the target length of cached audio in the buffer.
  • requested: This has a deep relation with missing, and seems to be only used for statistical book-keeping. I still did not figure out its actual use.
  • maxrewind:
  • prebuf and in_prebuf: pre-buffering. The server does not start playback before at least prebuf bytes are available in the buffer. That's why checking pa_memblockq_is_readable(), we see that the server does not start reading from buffer if the in_prebuf value is true and buffer length (write_index - read_index) is smaller than the requested prebuf size.
  • silence: What if there's an underrun? what should we play to the sink? Silence, of course! And this is the appropriate memchunk containing silence data given our desired sample rate!
  • mcalign:
  • name: Name of this memory chunk queue. Real-world examples include: "esound protocol connection input_memblockq", "esound protocol connection output memblockq", and "client record memblockq" (important!), "sound-file-stream memblockq", "pa_play_memchunk() queue", and "native protocol record stream memblockq".
  • sample_spec: The sample format(e.g. PA_SAMPLE_FLOAT32LE), the sample rate (e.g. 44100), and the number of audio channels (1 for montor, 2 for stereo, more for surround).

Each node of the ring buffer contains the following:

struct list_item {
    struct list_item *next, *prev;
    int64_t index;
    pa_memchunk chunk;
};

And just like above:

  • next and prev: next and previous memory chunks linked lists node
  • index: index of this node data in the buffer. Note: this is not the node index relative to other indices. Just like read_index and write_index in the memblockq structure above, this is the data index. So if we have two nodes in the ringbuffer, the first contains 400 bytes of data, and ours is the second. The first node index field will be zero, while the second node's index field will be 400.

results matching ""

    No results matching ""