/* SPDX-License-Identifier: GPL-2.0 */ /** \file opus_afh.c Audio format handler for ogg/opus files. */ #include #include "para.h" #include "afh.h" #include "error.h" #include "portable_io.h" #include "string.h" #include "opus_common.h" #include "ogg_afh_common.h" static const char * const opus_suffixes[] = {"opus", NULL}; #define OPUS_COMMENT_HEADER "OpusTags" static bool copy_if_tag_type(const char *tag, int taglen, const char *type, char **p) { char *q = key_value_copy(tag, taglen, type); if (!q) return false; free(*p); *p = q; return true; } static int opus_get_comments(char *comments, int length, struct taginfo *tags) { char *p = comments, *end = comments + length; int i; uint32_t val, ntags; /* min size of a opus header is 16 bytes */ if (length < 16) return -E_OPUS_COMMENT; if (memcmp(p, OPUS_COMMENT_HEADER, strlen(OPUS_COMMENT_HEADER)) != 0) return -E_OPUS_COMMENT; p += 8; val = read_u32(p); p += 4; if (p + val > end) return -E_OPUS_COMMENT; tags->comment = safe_strdup(p, val); p += val; ntags = read_u32(p); p += 4; if (p + ntags * 4 > end) return -E_OPUS_COMMENT; PARA_INFO_LOG("found %u tag(s)\n", ntags); for (i = 0; i < ntags; i++, p += val) { char *tag; if (p + 4 > end) return -E_OPUS_COMMENT; val = read_u32(p); p += 4; if (p + val > end) return -E_OPUS_COMMENT; if (copy_if_tag_type(p, val, "author", &tags->artist)) continue; if (copy_if_tag_type(p, val, "artist", &tags->artist)) continue; if (copy_if_tag_type(p, val, "title", &tags->title)) continue; if (copy_if_tag_type(p, val, "album", &tags->album)) continue; if (copy_if_tag_type(p, val, "year", &tags->year)) continue; if (copy_if_tag_type(p, val, "comment", &tags->comment)) continue; tag = safe_strdup(p, val); PARA_NOTICE_LOG("unrecognized tag: %s\n", tag); free(tag); } return 1; } /* * Ogg/Opus has two mandatory header packets: * * 1. ID header (identifies the stream as Opus). Dedicated "BOS" ogg page. * 2. Comment header (metadata). May span multiple pages. * * See doc/draft-ietf-codec-oggopus.xml in the opus source tree for details. */ static int opus_packet_callback(ogg_packet *packet, int packet_num, __a_unused int serial, struct afh_info *afhi, void *private_data) { int ret; struct opus_header *oh = private_data; if (packet_num == 0) { ret = opus_parse_header((char *)packet->packet, packet->bytes, oh); if (ret < 0) return ret; afhi->channels = oh->channels; afhi->techinfo = make_message( "header version %d, input sample rate: %" PRIu32 "Hz", oh->version, oh->input_sample_rate); /* * The input sample rate is irrelevant for afhi->frequency as * we always decode to 48kHz. */ afhi->frequency = 48000; return 1; } if (packet_num == 1) { ret = opus_get_comments((char *)packet->packet, packet->bytes, &afhi->tags); if (ret < 0) return ret; return 0; /* header complete */ } /* never reached */ assert(0); } static int opus_get_file_info(char *map, size_t numbytes, struct afh_info *afhi) { int ret, ms; struct opus_header oh = {.version = 0}; struct oac_callback_info opus_callback_info = { .packet_callback = opus_packet_callback, .private_data = &oh, }; ret = oac_get_file_info(map, numbytes, afhi, &opus_callback_info); if (ret < 0) return ret; ret = (afhi->chunk_table[afhi->chunks_total] - afhi->chunk_table[0]) * 8; /* bits */ ms = tv2ms(&afhi->chunk_tv) * afhi->chunks_total; afhi->bitrate = ret / ms; return 1; } static size_t opus_make_meta_packet(struct taginfo *tags, char **result) { size_t sz; char *buf, *p; size_t comment_len = strlen(tags->comment), artist_len = strlen(tags->artist), title_len = strlen(tags->title), album_len = strlen(tags->album), year_len = strlen(tags->year); uint32_t comment_sz = comment_len, artist_sz = artist_len + strlen("artist="), title_sz = title_len + strlen("title="), album_sz = album_len + strlen("album="), year_sz = year_len + strlen("year="); uint32_t num_tags; sz = strlen(OPUS_COMMENT_HEADER) + 4 /* comment length (always present) */ + comment_sz + 4; /* number of tags */ num_tags = 0; if (artist_len) { num_tags++; sz += 4 + artist_sz; } if (title_len) { num_tags++; sz += 4 + title_sz; } if (album_len) { num_tags++; sz += 4 + album_sz; } if (year_len) { num_tags++; sz += 4 + year_sz; } PARA_DEBUG_LOG("meta packet size: %zu bytes\n", sz); /* terminating zero byte for the last sprintf() */ buf = p = alloc(sz + 1); memcpy(p, OPUS_COMMENT_HEADER, strlen(OPUS_COMMENT_HEADER)); p += strlen(OPUS_COMMENT_HEADER); write_u32(p, comment_sz); p += 4; strcpy(p, tags->comment); p += comment_sz; write_u32(p, num_tags); p += 4; if (artist_len) { write_u32(p, artist_sz); p += 4; sprintf(p, "artist=%s", tags->artist); p += artist_sz; } if (title_len) { write_u32(p, title_sz); p += 4; sprintf(p, "title=%s", tags->title); p += title_sz; } if (album_len) { write_u32(p, album_sz); p += 4; sprintf(p, "album=%s", tags->album); p += album_sz; } if (year_len) { write_u32(p, year_sz); p += 4; sprintf(p, "year=%s", tags->year); p += year_sz; } assert(p == buf + sz); *result = buf; return sz; } static int opus_rewrite_tags(const char *map, size_t mapsize, struct taginfo *tags, int output_fd) { char *meta_packet; size_t meta_sz; int ret; meta_sz = opus_make_meta_packet(tags, &meta_packet); ret = oac_rewrite_tags(map, mapsize, output_fd, meta_packet, meta_sz); free(meta_packet); return ret; } /* * See doc/draft-ietf-codec-oggopus.xml in the opus source tree for details * about the format of the comment header. */ static int opus_get_header_callback(ogg_packet *packet, int packet_num, int serial, __a_unused struct afh_info *afhi, void *private_data) { struct oac_custom_header *h = private_data; int ret; static unsigned char dummy_tags[] = { /* a minimal comment header */ 'O', 'p', 'u', 's', 'T', 'a', 'g', 's', 0x06, 0x00, 0x00, 0x00, /* vendor string length */ 'd', 'u', 'm', 'm', 'y', '\0', /* vendor string */ 0x00, 0x00, 0x00, 0x00, /* user comment list length */ }; ogg_packet replacement; if (packet_num == 0) { oac_custom_header_init(serial, h); ret = oac_custom_header_append(packet, h); if (ret < 0) return ret; oac_custom_header_flush(h); return 1; } assert(packet_num == 1); PARA_INFO_LOG("replacing metadata packet\n"); replacement = *packet; replacement.packet = dummy_tags; replacement.bytes = sizeof(dummy_tags); ret = oac_custom_header_append(&replacement, h); if (ret < 0) return ret; oac_custom_header_flush(h); return 0; } static void opus_get_header(void *map, size_t mapsize, char **buf, size_t *len) { int ret; struct oac_custom_header *h = oac_custom_header_new(); struct oac_callback_info cb = { .packet_callback = opus_get_header_callback, .private_data = h, }; ret = oac_get_file_info(map, mapsize, NULL, &cb); *len = oac_custom_header_get(buf, h); if (ret < 0) { PARA_ERROR_LOG("could not create custom header: %s\n", para_strerror(-ret)); free(*buf); *buf = NULL; *len = 0; } else PARA_INFO_LOG("created %zu byte ogg/opus header\n", *len); } /** * The audio format handler for ogg/opus. * * The opus codec was standardized by the Internet Engineering Task Force and * is described in RFC 6716 (2012). The audio format handler depends on the ogg * and the opus libraries. */ const struct audio_format_handler opus_afh = { .get_file_info = opus_get_file_info, .get_header = opus_get_header, .suffixes = opus_suffixes, .rewrite_tags = opus_rewrite_tags, };