如何无损地连接ogg vorbis文件?

7

我将尝试将多个ogg vorbis文件合并为一个。

理论上,只需要执行以下操作即可:

cat 1.ogg 2.ogg > combined.ogg

但这样做也有不足之处:
  • 不是所有的播放器都支持像这样创建的文件(如gstreamer)
  • 那些支持的播放器也不能平滑地连接它们,而是会产生难看的瞬间暂停
  • 似乎无法进行跳转

我不想失去音质,所以我可以将它们重新编码为类似flac这样的无损格式,但这会让文件大小急剧增加。

目前似乎没有这样的工具。例如,oggCat会重新编码音频,从而导致轻微的质量损失,而ffmpeg的concat demuxer并不适用于所有输入文件。我在这个超级用户问题中寻找了一个工具,但当我发现没有时,就自己写了一个。

因此,我尝试使用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流的地方会插入几乎听不见的点击声。
如何正确地实现?
这是否有可能实现?

一个相关的问题在这里 - https://superuser.com/questions/367584/concatenating-ogg-video-files-from-the-command-line - Pradyumna
@josch,在连接的文件中不会发生相同的“点击”吗?波形应该是相同的... - ivan_pozdeev
以下是与此问题相关的初始讨论:https://github.com/villermen/runescape-cache-tools/issues/8 - ivan_pozdeev
@VitalyZdanevich 因为我尝试的所有播放器都会拒绝在第一个文件之后进行搜索。 - josch
@VitalyZdanevich,有多个原因:为什么ogg解码器会读取标记为最后一页的ogg页面之外的任何内容?如果下一页的时间戳再次从零开始,ogg解码器将如何处理?显然,如果您只是连接两个ogg文件而不在单个页面中更改至少一些内容或更正时间戳,则无法进行寻求。此外,ogg页面序列号将完全失效。这一切难道不是显而易见的吗? - josch
显示剩余2条评论
1个回答

2

这是一个有趣的问题... :)

如果两个流之间可以承受几毫秒的沉默/偏差,只需在两个流之间插入几个静音数据包 (我需要检查规范以获取每个数据包中确切的位模式,但如果您可以访问解码器的源代码,这应该不难弄清楚)。

如果无法承受沉默/偏差,则可能需要重新编码,因为唯一的其他选择是调整压缩数据以更改波形的连接部分的斜率...

编辑

另一个选项是在连接文件的 PCM 数据点应用平滑算法。这并不容易做到,但其思想是你希望文件之间的波形是“平滑”的。这就是我的全部建议了...

编辑 2

为了明确起见,本问题的示例代码几乎可以完美地工作,假设源文件使用相同的参数。唯一缺失的是防止接缝处听得见的方法。我建议添加几个静音数据包来处理它,但对于那些无法承担此成本的人,可以(猜测)考虑将接缝周围两个数据包的 floors 的乘数减一,以使接缝不太明显。


谢谢,但我不能保持沉默,因为这样不会像原始的那样保持“无损连接”。 - josch
几个静默数据包如何修复寻址问题? - Vitaly Zdanevich
它实际上不会解决寻找任意位置的问题,但它会给解码器一些空白空间,在第一个文件的结尾和第二个文件的开头之间分散能量。这相当于在它们之间重新编码文件并加入下行斜坡和上行斜坡。请注意上面的第二次编辑。 - ioctlLR

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接