Как объединить файлы ogg vorbis без потерь?

Я пытаюсь объединить несколько файлов ogg vorbis в один.

Я знаю, что теоретически должно быть достаточно:

cat 1.ogg 2.ogg > combined.ogg

Но у этого есть недостатки:

  • не все плееры поддерживают файлы, созданные таким образом (gstreamer не поддерживает)
  • игроки, которые это делают, не соединяют их гладко, а создают уродливые паузы в доли секунды
  • поиск кажется невозможным

Я не хочу терять качество, поэтому я мог бы перекодировать их в формат без потерь, такой как flac, но это привело бы к увеличению размера файла.

Кажется, нет никакого инструмента, который делает это. Например, oggCat будет перекодировать звук, что приведет к небольшой потере качества, а ffmpeg concat демультиплексор не будет работать для всех входных файлов. Я открыл этот вопрос суперпользователя, чтобы найти инструмент, но написал свой, когда понял, что его нет.

Поэтому я попытался использовать libogg и libvorbis для ручного объединения ogg-пакетов из входных файлов в ogg-страницы выходного файла. Предполагается, что все входные файлы ogg были закодированы с использованием одних и тех же параметров.

Я придумал следующий код:

#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;
}

Это почти работает, но вставляет едва слышимые щелчки в точках, где соединяются потоки vorbis.

Как это сделать правильно?

Можно ли это сделать вообще?


person josch    schedule 16.01.2015    source источник
comment
Связанный с этим вопрос здесь - superuser.com/questions/367584/   -  person Pradyumna    schedule 16.01.2015
comment
@josch, разве в объединенном файле не происходит такой же щелчок? Форма волны должна быть одинаковой для него...   -  person ivan_pozdeev    schedule 06.12.2016
comment
Для контекста, вот первоначальное обсуждение, которое привело к этому вопросу: github.com/villermen /runescape-cache-tools/issues/8   -  person ivan_pozdeev    schedule 06.12.2016
comment
Как вы думаете, почему поиск прервался?   -  person Vitaly Zdanevich    schedule 11.04.2019
comment
@VitalyZdanevich, потому что все плееры, которые я пробовал, отказываются искать дальше первого файла.   -  person josch    schedule 11.04.2019
comment
@josch Я имею в виду, знаешь ли ты, в чем внутренняя причина неработающего поиска? У меня такая же проблема, но я конкатенирую Ogg на стороне клиента (в браузере с JS).   -  person Vitaly Zdanevich    schedule 11.04.2019
comment
@VitalyZdanevich, есть несколько причин: зачем декодеру ogg читать что-либо, кроме страницы ogg, которая была отмечена как последняя? Как поведет себя декодер ogg, если отметка времени следующей страницы снова начнется с нуля? Очевидно, что поиск не может работать, если вы просто соедините два файла ogg, не изменив хотя бы что-то на отдельных страницах или исправив временные метки. Кроме того, порядковый номер страницы ogg будет полностью отключен. Разве все это не очевидно?   -  person josch    schedule 11.04.2019


Ответы (1)


Вот это весело... :)

Если вы можете позволить себе несколько мс тишины/перекоса между файлами, просто поместите пару молчаливых пакетов между двумя потоками (мне нужно будет проверить спецификацию на предмет точного битового шаблона в каждом пакете, но это не должно быть сложно чтобы выяснить, есть ли у вас доступ к исходному коду декодера).

Если вы не можете позволить себе тишину/перекос, вам, вероятно, придется перекодировать, так как единственный другой вариант — перевернуть сжатые данные, чтобы изменить наклон соединяющих частей сигнала...

изменить

Другой вариант — применить алгоритм сглаживания к данным PCM в точке объединения файлов. Это не просто сделать, но идея в том, что вы хотите, чтобы сигнал был «гладким» между файлами. Это все, что у меня есть...

изменить 2

Просто для ясности: пример кода вопроса будет работать почти идеально, если исходные файлы используют одни и те же параметры. Единственное, чего ему не хватает, так это способа сделать так, чтобы швы не были слышны. Мое предложение поместить пару тихих пакетов справится с этим, но для тех, кто не может себе этого позволить, можно (как чистое предположение) рассмотреть возможность уменьшения множителя на этажах двух пакетов вокруг стыка на единицу, чтобы сделать шов менее заметен.

person ioctlLR    schedule 16.01.2015
comment
спасибо, но я не могу позволить себе молчание, потому что это не сохранит конкатенацию без потерь по сравнению с оригиналом. - person josch; 16.01.2015
comment
Как пара тихих пакетов исправит поиск? - person Vitaly Zdanevich; 11.04.2019
comment
На самом деле это не будет адресовать поиск произвольной позиции, но это даст декодеру некоторое пустое пространство, в котором можно распределить энергию между концом первого файла и началом второго. Это то же самое, что перекодировать файлы с переходом между ними вниз и вверх. Однако обратите внимание на второе редактирование выше. - person ioctlLR; 14.04.2019