How to merge ogg vorbis files without loss?

I am trying to merge several vorbis ogg files into one.

I know that theoretically this should be enough:

cat 1.ogg 2.ogg > combined.ogg

But this has disadvantages:

  • not all players support files created this way (gstreamer does not)
  • players who do this do not smoothly concatenate them, but create ugly divided seconds of pause
  • search seems impossible

I do not want to lose quality, so I could recode them to a lossless format, for example flac, but this would allow the file size to explode.

There seems to be no tool that does this. For example, oggCat will transcode audio and, consequently, lead to a slight loss in quality, and ffmpeg concat demuxer will not work for all input files.I opened this superuser question to find the tool, but wrote my own when I realized that it was not there.

So, I tried to use libogg and libvorbis to manually concatenate ogg packages from input files to ogg pages of the output file. It is assumed that all ogg input files have been encoded using the same parameters.

I came up with the following code:

#include <ogg/ogg.h>
#include <vorbis/codec.h>
#include <stdio.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <time.h>

int read_page(int fd, ogg_sync_state *state, ogg_page *page)
{
    int ret;
    ssize_t bytes;

    while(ogg_sync_pageout(state, page) != 1) {
        char *buffer = ogg_sync_buffer(state, 4096);
        if (buffer == NULL) {
            fprintf(stderr, "ogg_sync_buffer failed\n");
            return -1;
        }
        bytes = read(fd, buffer, 4096);
        if (bytes == 0) {
            return -1;
        }
        ret = ogg_sync_wrote(state, bytes);
        if (ret != 0) {
            fprintf(stderr, "ogg_sync_wrote failed\n");
            return -1;
        }
    }
    return 0;
}

int main(int argc, char *argv[])
{
    int ret;
    ogg_sync_state state;
    ogg_page page;
    int serial;
    ogg_stream_state sstate;
    bool found_bos;
    ogg_packet packet;
    int fd;
    int i;
    vorbis_info info;
    vorbis_comment comment;
    int vorbis_header_read;
    ssize_t bytes;
    ogg_stream_state out_stream;
    ogg_page out_page;

    if (argc < 2) {
        fprintf(stderr, "usage: %s file.ogg\n", argv[0]);
        return 1;
    }

    srand(time(NULL));
    ogg_stream_init(&out_stream, rand());

    // go through all input files
    for (i = 1; i < argc; i++) {
        vorbis_header_read = 0;
        found_bos = false;

        fd = open(argv[i], O_RDONLY);
        if (fd < 0) {
            fprintf(stderr, "cannot open %s\n", argv[1]);
            return 1;
        }

        ret = ogg_sync_init(&state);
        if (ret != 0) {
            fprintf(stderr, "ogg_sync_init failed\n");
            return 1;
        }

        vorbis_info_init(&info);
        vorbis_comment_init(&comment);

        // go through all ogg pages
        while (read_page(fd, &state, &page) == 0) {
            serial = ogg_page_serialno(&page);

            if (ogg_page_bos(&page)) {
                if (found_bos) {
                    fprintf(stderr, "cannot handle more than one stream\n");
                    return 1;
                }
                ret = ogg_stream_init(&sstate, serial);
                if (ret != 0) {
                    fprintf(stderr, "ogg_stream_init failed\n");
                    return 1;
                }
                found_bos = true;
            }

            if (!found_bos) {
                fprintf(stderr, "cannot continue without bos\n");
                return 1;
            }

            ret = ogg_stream_pagein(&sstate, &page);
            if (ret != 0) {
                fprintf(stderr, "ogg_stream_pagein failed\n");
                return 1;
            }

            // if this is the last page, then only write it if we are in the
            // last file
            if (ogg_page_eos(&page) && i != argc - 1) {
                continue;
            }

            // go through all (hopefully vorbis) packets
            while((ret = ogg_stream_packetout(&sstate, &packet)) != 0) {
                if (ret != 1) {
                    fprintf(stderr, "ogg_stream_packetout failed\n");
                    return 1;
                }

                // test if this stream is vorbis
                if (vorbis_header_read == 0) {
                    ret = vorbis_synthesis_idheader(&packet);
                    if (ret == 0) {
                        fprintf(stderr, "stream is not vorbis\n");
                        return 1;
                    }
                }

                // read exactly three vorbis headers
                if (vorbis_header_read < 3) {
                    ret = vorbis_synthesis_headerin(&info, &comment, &packet);
                    if (ret != 0) {
                        fprintf(stderr, "vorbis_synthesis_headerin failed\n");
                        return 1;
                    }
                    // if this is the first file, copy the header packet to the
                    // output
                    if (i == 1) {
                        ret = ogg_stream_packetin(&out_stream, &packet);
                        if (ret != 0) {
                            fprintf(stderr, "ogg_stream_packetin failed\n");
                            return 1;
                        }
                    }
                    vorbis_header_read++;
                    continue;
                }

                // if this is the first file, write a page to the output
                if (vorbis_header_read == 3 && i == 1) {
                    while ((ret = ogg_stream_flush(&out_stream, &out_page)) != 0) {
                        bytes = write(STDOUT_FILENO, out_page.header, out_page.header_len);
                        if (bytes != out_page.header_len) {
                            fprintf(stderr, "write failed\n");
                            return 1;
                        }
                        bytes = write(STDOUT_FILENO, out_page.body, out_page.body_len);
                        if (bytes != out_page.body_len) {
                            fprintf(stderr, "write failed\n");
                            return 1;
                        }
                    }
                    vorbis_header_read++;
                }

                ogg_stream_packetin(&out_stream, &packet);
                do {
                    ret = ogg_stream_pageout(&out_stream, &out_page);
                    if (ret == 0) break;
                    bytes = write(STDOUT_FILENO, out_page.header, out_page.header_len);
                    if (bytes != out_page.header_len) {
                        fprintf(stderr, "write failed\n");
                        return 1;
                    }
                    bytes = write(STDOUT_FILENO, out_page.body, out_page.body_len);
                    if (bytes != out_page.body_len) {
                        fprintf(stderr, "write failed\n");
                        return 1;
                    }
                } while (!ogg_page_eos(&out_page));

            }
        }

        vorbis_info_clear(&info);
        vorbis_comment_clear(&comment);

        ret = ogg_sync_clear(&state);
        if (ret != 0) {
            fprintf(stderr, "ogg_sync_clear failed\n");
            return 1;
        }

        ret = ogg_stream_clear(&sstate);
        if (ret != 0) {
            fprintf(stderr, "ogg_stream_clear failed\n");
            return 1;
        }

        close(fd);
    }

    ogg_stream_clear(&out_stream);

    return 0;
}

It almost works, but inserts barely audible click sounds at the points where the vorbis flows connect.

How to do it right?

Can this be done at all?

+5
1

... :)

/ , ( , , ).

/, , , , - , ...

- PCM . , , , "" . , ...

2

, , , . , , - . , , , ( ) , .

+2

All Articles