/* SPDX-License-Identifier: GPL-2.0 */ /** \file wma_afh.c The audio format handler for WMA files. */ #include #include #include "para.h" #include "error.h" #include "afh.h" #include "portable_io.h" #include "string.h" #include "wma.h" #include "fd.h" #define FOR_EACH_FRAME(_f, _buf, _size, _ps) for (_f = (_buf); \ _f + (_ps) < (_buf) + (_size); \ _f += (_ps)) /* * Must be called on a frame boundary, e.g. start + header_len. * \return Frame count, superframe count via *num_superframes. */ static int count_frames(const char *buf, int buf_size, uint32_t packet_size, int *num_superframes) { int fc = 0, sfc = 0; /* frame count, superframe count */ const uint8_t *p; FOR_EACH_FRAME(p, (uint8_t *)buf, buf_size, packet_size) { fc += p[WMA_FRAME_SKIP] & 0x0f; sfc++; } PARA_INFO_LOG("%d frames, %d superframes\n", fc, sfc); *num_superframes = sfc; return fc; } /* * put_utf8() and get_str16() below are based on macros in libavutil/common.h * of the mplayer source code, copyright (c) 2006 Michael Niedermayer * . */ /* * Convert a 32-bit Unicode character to its UTF-8 encoded form. * * Writes up to 4 bytes for values in the valid UTF-8 range and up to 7 bytes * in the general case, depending on the length of the converted Unicode * character. * * \param result Where the converted UTF-8 bytes are written. */ static int put_utf8(uint32_t val, char *result) { char *out = result; int bytes, shift; uint32_t in = val; if (in < 0x80) { *out++ = in; return 1; } bytes = DIV_ROUND_UP(wma_log2(in), 5); shift = (bytes - 1) * 6; *out++ = (256 - (256 >> bytes)) | (in >> shift); while (shift >= 6) { shift -= 6; *out++ = 0x80 | ((in >> shift) & 0x3f); } return out - result; } static char *get_str16(const char *in, int len) { const char *p = in; int out_size = 0, out_len = 0; char *out = NULL; len /= 2; while (len--) { uint32_t x; if (out_len + 7 + 1 >= out_size) { out_size = 2 * out_size + 50; out = para_realloc(out, out_size); } x = read_u16(p); p += 2; out_len += put_utf8(x, out + out_len); if (x == 0) return out; } if (out) out[out_len] = '\0'; return out; } static const uint8_t content_description_header[] = { 0x33, 0x26, 0xb2, 0x75, 0x8E, 0x66, 0xCF, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c }; static const uint8_t extended_content_header[] = { 0x40, 0xA4, 0xD0, 0xD2, 0x07, 0xE3, 0xD2, 0x11, 0x97, 0xF0, 0x00, 0xA0, 0xC9, 0x5E, 0xA8, 0x50 }; static const uint8_t year_tag_header[] = { /* WM/Year */ 0x57, 0x00, 0x4d, 0x00, 0x2f, 0x00, 0x59, 0x00, 0x65, 0x00, 0x61, 0x00, 0x72, 0x00 }; static const uint8_t album_tag_header[] = { /* WM/AlbumTitle */ 0x57, 0x00, 0x4d, 0x00, 0x2f, 0x00, 0x41, 0x00, 0x6c, 0x00, 0x62, 0x00, 0x75, 0x00, 0x6d, 0x00, 0x54, 0x00, 0x69, 0x00, 0x74, 0x00, 0x6c, 0x00, 0x65, 0x00 }; static void read_asf_tags(const char *buf, int buf_size, struct taginfo *ti) { const char *p, *end = buf + buf_size, *q; uint16_t len1, len2, len3, len4; p = search_pattern(content_description_header, sizeof(content_description_header), buf, buf_size); if (!p || p + 34 >= end) { PARA_NOTICE_LOG("content description header not found\n"); goto next; } p += 24; len1 = read_u16(p); p += 2; len2 = read_u16(p); p += 2; len3 = read_u16(p); p += 2; len4 = read_u16(p); p += 2; /* ignore length of the rating information */ p += 2; if (p + len1 >= end) goto next; ti->title = get_str16(p, len1); p += len1; if (p + len2 >= end) goto next; ti->artist = get_str16(p, len2); p += len2 + len3; if (p + len4 >= end) goto next; ti->comment = get_str16(p, len4); next: p = search_pattern(extended_content_header, sizeof(extended_content_header), buf, buf_size); if (!p) { PARA_NOTICE_LOG("extended content header not found\n"); return; } q = search_pattern(year_tag_header, sizeof(year_tag_header), p, end - p); if (q) { const char *r = q + sizeof(year_tag_header) + 6; if (r < end) ti->year = get_str16(r, end - r); } q = search_pattern(album_tag_header, sizeof(album_tag_header), p, end - p); if (q) { const char *r = q + sizeof(album_tag_header) + 6; if (r < end) ti->album = get_str16(r, end - r); } } static void set_chunk_tv(int frames_per_chunk, int frequency, struct timeval *result) { uint64_t x = (uint64_t)frames_per_chunk * 2048 * 1000 * 1000 / frequency; result->tv_sec = x / 1000 / 1000; result->tv_usec = x % (1000 * 1000); PARA_INFO_LOG("chunk time: %lums\n", tv2ms(result)); } /* Must be called on a frame boundary. */ static int wma_make_chunk_table(char *buf, size_t buf_size, uint32_t packet_size, struct afh_info *afhi) { const uint8_t *f, *start = (uint8_t *)buf; int j, frames_per_chunk; size_t ct_size = 250; int ret, count = 0, num_frames, num_superframes; afhi->chunk_table = arr_alloc(ct_size, sizeof(uint32_t)); afhi->chunk_table[0] = 0; afhi->chunk_table[1] = afhi->header_len; num_frames = count_frames(buf, buf_size, packet_size, &num_superframes); ret = -E_NO_WMA; if (num_frames == 0 || num_superframes == 0) goto fail; afhi->seconds_total = num_frames * 2048 /* FIXME */ / afhi->frequency; frames_per_chunk = num_frames / num_superframes / 2; PARA_INFO_LOG("%d frames per chunk\n", frames_per_chunk); j = 1; FOR_EACH_FRAME(f, start, buf_size, packet_size) { count += f[WMA_FRAME_SKIP] & 0x0f; while (count > j * frames_per_chunk) { j++; if (j >= ct_size) { ct_size *= 2; afhi->chunk_table = para_realloc( afhi->chunk_table, ct_size * sizeof(uint32_t)); } afhi->chunk_table[j] = f - start + afhi->header_len + packet_size; } } afhi->chunks_total = j; set_max_chunk_size(afhi); set_chunk_tv(frames_per_chunk, afhi->frequency, &afhi->chunk_tv); return 1; fail: free(afhi->chunk_table); return ret; } static int wma_get_file_info(char *map, size_t numbytes, struct afh_info *afhi) { int ret; struct asf_header_info ahi; ret = read_asf_header(map, numbytes, &ahi); if (ret < 0) return ret; if (ret == 0) return -E_NO_WMA; afhi->bitrate = ahi.bit_rate / 1000; if (ahi.sample_rate == 0) return -E_NO_WMA; afhi->frequency = ahi.sample_rate; afhi->channels = ahi.channels; afhi->header_len = ahi.header_len; afhi->techinfo = make_message("%s%s%s%s%s", ahi.use_exp_vlc? "exp vlc" : "", (ahi.use_bit_reservoir && ahi.use_exp_vlc)? ", " : "", ahi.use_bit_reservoir? "bit reservoir" : "", (ahi.use_variable_block_len && (ahi.use_exp_vlc || ahi.use_bit_reservoir)? ", " : ""), ahi.use_variable_block_len? "vbl" : "" ); wma_make_chunk_table(map + ahi.header_len, numbytes - ahi.header_len, ahi.packet_size, afhi); read_asf_tags(map, ahi.header_len, &afhi->tags); return 0; } struct asf_object { char *ptr; uint64_t size; }; struct tag_object_nums { int content_descr_obj_num; int extended_content_descr_obj_num; }; struct afs_top_level_header_object { uint64_t size; uint32_t num_objects; uint8_t reserved1, reserved2; struct asf_object *objects; }; #define CHECK_HEADER(_p, _h) (memcmp((_p), (_h), sizeof((_h))) == 0) static int read_asf_objects(const char *src, size_t size, uint32_t num_objects, struct asf_object *objs, struct tag_object_nums *ton) { int i; const char *p; for (i = 0, p = src; i < num_objects; p += objs[i++].size) { if (p + 24 > src + size) return -E_NO_WMA; objs[i].ptr = (char *)p; objs[i].size = read_u64(p + 16); if (p + objs[i].size > src + size) return -E_NO_WMA; if (CHECK_HEADER(p, content_description_header)) ton->content_descr_obj_num = i; else if (CHECK_HEADER(p, extended_content_header)) ton->extended_content_descr_obj_num = i; } return 1; } static const uint8_t top_level_header_object_guid[] = { 0x30, 0x26, 0xb2, 0x75, 0x8e, 0x66, 0xcf, 0x11, 0xa6, 0xd9, 0x00, 0xaa, 0x00, 0x62, 0xce, 0x6c }; static int convert_utf8_to_utf16(char *src, char **dst) { iconv_t cd; size_t sz, inbytes, outbytes, inbytesleft, outbytesleft; char *inbuf, *outbuf; int ret; if (!src || !*src) { *dst = zalloc(2); return 0; } /* * Without specifying LE (little endian), iconv includes a byte order * mark (e.g. 0xFFFE) at the beginning. */ cd = iconv_open("UTF-16LE", "UTF-8"); if (cd == (iconv_t)-1) { *dst = NULL; return -ERRNO_TO_PARA_ERROR(errno); } inbuf = src; /* even though src is in UTF-8, strlen() should DTRT */ inbytes = inbytesleft = strlen(src); outbytes = outbytesleft = 4 * inbytes + 2; /* hope that's enough */ *dst = outbuf = alloc(outbytes); sz = iconv(cd, &inbuf, &inbytesleft, &outbuf, &outbytesleft); if (sz == (size_t)-1) { ret = -ERRNO_TO_PARA_ERROR(errno); free(*dst); *dst = NULL; goto out; } assert(outbytes >= outbytesleft); assert(outbytes - outbytesleft < INT_MAX - 2); ret = outbytes - outbytesleft; outbuf = para_realloc(*dst, ret + 2); outbuf[ret] = outbuf[ret + 1] = '\0'; ret += 2; *dst = outbuf; PARA_INFO_LOG("converted %s to %d UTF-16 bytes\n", src, ret); out: if (iconv_close(cd) < 0) PARA_WARNING_LOG("iconv_close: %s\n", strerror(errno)); return ret; } /* The content description object contains artist, title, comment. */ static int make_cdo(struct taginfo *tags, const struct asf_object *cdo, struct asf_object *result) { const char *cr, *rating; /* orig data */ uint16_t orig_cr_bytes, orig_rating_bytes; /* pointers to new UTF-16 tags */ char *artist = NULL, *title = NULL, *comment = NULL; /* number of bytes in UTF-16 for the new tags */ int artist_bytes, title_bytes, comment_bytes, ret; char *p; __nonstring char null[2] = "\0\0"; result->ptr = NULL; result->size = 0; ret = convert_utf8_to_utf16(tags->artist, &artist); if (ret < 0) return ret; assert(artist); artist_bytes = ret; ret = convert_utf8_to_utf16(tags->title, &title); if (ret < 0) goto out; assert(title); title_bytes = ret; ret = convert_utf8_to_utf16(tags->comment, &comment); if (ret < 0) goto out; assert(comment); comment_bytes = ret; if (cdo) { uint16_t orig_title_bytes, orig_artist_bytes, orig_comment_bytes; /* * Sizes of the five fields (stored as 16-bit numbers) are * located after the header (16 bytes) and the cdo size (8 * bytes). */ orig_title_bytes = read_u16(cdo->ptr + 24); orig_artist_bytes = read_u16(cdo->ptr + 26); orig_cr_bytes = read_u16(cdo->ptr + 28); orig_comment_bytes = read_u16(cdo->ptr + 30); orig_rating_bytes = read_u16(cdo->ptr + 32); cr = cdo->ptr + 34 + orig_title_bytes + orig_artist_bytes; rating = cr + orig_cr_bytes + orig_comment_bytes; } else { orig_cr_bytes = 2; orig_rating_bytes = 2; cr = null; rating = null; } /* compute size of result cdo */ result->size = 16 + 8 + 5 * 2 + title_bytes + artist_bytes + orig_cr_bytes + comment_bytes + orig_rating_bytes; PARA_DEBUG_LOG("cdo is %zu bytes\n", (size_t)result->size); p = result->ptr = alloc(result->size); memcpy(p, content_description_header, 16); p += 16; write_u64(p, result->size); p += 8; write_u16(p, title_bytes); p += 2; write_u16(p, artist_bytes); p += 2; write_u16(p, orig_cr_bytes); p += 2; write_u16(p, comment_bytes); p += 2; write_u16(p, orig_rating_bytes); p += 2; memcpy(p, title, title_bytes); p += title_bytes; memcpy(p, artist, artist_bytes); p += artist_bytes; memcpy(p, cr, orig_cr_bytes); p += orig_cr_bytes; memcpy(p, comment, comment_bytes); p += comment_bytes; memcpy(p, rating, orig_rating_bytes); p += orig_rating_bytes; assert(p - result->ptr == result->size); ret = 1; out: free(artist); free(title); free(comment); return ret; } /* The extended content description object contains album and year. */ static int make_ecdo(struct taginfo *tags, struct asf_object *result) { int ret; char *p, *album = NULL, *year = NULL; __nonstring char null[2] = "\0\0"; int album_bytes, year_bytes; result->ptr = NULL; result->size = 0; ret = convert_utf8_to_utf16(tags->album, &album); if (ret < 0) return ret; assert(album); album_bytes = ret; ret = convert_utf8_to_utf16(tags->year, &year); if (ret < 0) goto out; assert(year); year_bytes = ret; result->size = 16 + 8 + 2; /* GUID, size, count */ /* name_length + name + null + data type + val length + val */ result->size += 2 + sizeof(album_tag_header) + 2 + 2 + 2 + album_bytes; result->size += 2 + sizeof(year_tag_header) + 2 + 2 + 2 + year_bytes; p = result->ptr = alloc(result->size); memcpy(p, extended_content_header, 16); p += 16; write_u64(p, result->size); p += 8; write_u16(p, 2); /* count */ p += 2; /* album */ write_u16(p, sizeof(album_tag_header) + 2); p += 2; memcpy(p, album_tag_header, sizeof(album_tag_header)); p += sizeof(album_tag_header); memcpy(p, null, 2); p += 2; write_u16(p, 0); /* data type (UTF-16) */ p += 2; write_u16(p, album_bytes); p += 2; memcpy(p, album, album_bytes); p += album_bytes; /* year */ write_u16(p, sizeof(year_tag_header)); p += 2; memcpy(p, year_tag_header, sizeof(year_tag_header)); p += sizeof(year_tag_header); memcpy(p, null, 2); p += 2; write_u16(p, 0); /* data type (UTF-16) */ p += 2; write_u16(p, year_bytes); p += 2; memcpy(p, year, year_bytes); p += year_bytes; assert(p - result->ptr == result->size); ret = 1; out: free(album); free(year); return ret; } static int write_output_file(int fd, const char *map, size_t mapsize, struct afs_top_level_header_object *top, struct tag_object_nums *ton, struct asf_object *cdo, struct asf_object *ecdo) { int i, ret; uint64_t sz; /* of the new header object */ uint32_t num_objects; char tmp[8]; sz = 16 + 8 + 4 + 1 + 1; /* top-level header object */ for (i = 0; i < top->num_objects; i++) { if (i == ton->content_descr_obj_num) continue; if (i == ton->extended_content_descr_obj_num) continue; sz += top->objects[i].size; } sz += cdo->size; sz += ecdo->size; num_objects = top->num_objects; if (ton->content_descr_obj_num < 0) num_objects++; if (ton->extended_content_descr_obj_num < 0) num_objects++; ret = xwrite(fd, top_level_header_object_guid, 16); if (ret < 0) goto out; write_u64(tmp, sz); ret = xwrite(fd, tmp, 8); if (ret < 0) goto out; write_u32(tmp, num_objects); ret = xwrite(fd, tmp, 4); if (ret < 0) goto out; write_u8(tmp, top->reserved1); ret = xwrite(fd, tmp, 1); if (ret < 0) goto out; write_u8(tmp, top->reserved2); ret = xwrite(fd, tmp, 1); if (ret < 0) goto out; /* * Write cto and ecto as objects 0 and 1 if they did not exist in the * original file. */ if (ton->content_descr_obj_num < 0) { ret = xwrite(fd, cdo->ptr, cdo->size); if (ret < 0) goto out; } if (ton->extended_content_descr_obj_num < 0) { ret = xwrite(fd, ecdo->ptr, ecdo->size); if (ret < 0) goto out; } for (i = 0; i < top->num_objects; i++) { char *buf = top->objects[i].ptr; sz = top->objects[i].size; if (i == ton->content_descr_obj_num) { buf = cdo->ptr; sz = cdo->size; } else if (i == ton->extended_content_descr_obj_num) { buf = ecdo->ptr; sz = ecdo->size; } ret = xwrite(fd, buf, sz); if (ret < 0) goto out; } ret = xwrite(fd, map + top->size, mapsize - top->size); out: return ret; } static int wma_rewrite_tags(const char *map, size_t mapsize, struct taginfo *tags, int fd) { struct afs_top_level_header_object top; struct tag_object_nums ton = {-1, -1}; const char *p = map; /* (extended) content description object */ struct asf_object cdo = {.ptr = NULL}, ecdo = {.ptr = NULL}; int ret; /* guid + size + num_objects + 2 * reserved */ if (mapsize < 16 + 8 + 4 + 1 + 1) return -E_NO_WMA; if (memcmp(map, top_level_header_object_guid, 16)) return -E_NO_WMA; p += 16; top.size = read_u64(p); PARA_INFO_LOG("header_size: %lu\n", (long unsigned)top.size); if (top.size >= mapsize) return -E_NO_WMA; p += 8; top.num_objects = read_u32(p); PARA_NOTICE_LOG("%u header objects\n", top.num_objects); if (top.num_objects > top.size / 24) return -E_NO_WMA; p += 4; top.reserved1 = read_u8(p); p++; top.reserved2 = read_u8(p); if (top.reserved2 != 2) return -E_NO_WMA; p++; /* objects start at p */ top.objects = arr_alloc(top.num_objects, sizeof(struct asf_object)); ret = read_asf_objects(p, top.size - (p - map), top.num_objects, top.objects, &ton); if (ret < 0) goto out; ret = make_cdo(tags, ton.content_descr_obj_num >= 0? top.objects + ton.content_descr_obj_num : NULL, &cdo); if (ret < 0) goto out; ret = make_ecdo(tags, &ecdo); if (ret < 0) goto out; ret = write_output_file(fd, map, mapsize, &top, &ton, &cdo, &ecdo); out: free(cdo.ptr); free(ecdo.ptr); free(top.objects); return ret; } static const char * const wma_suffixes[] = {"wma", NULL}; /** * The audio format handler for Windows Media Audio. * * Only WMA version 2 is supported. This audio format handler does not depend * on any third party libraries and is therefore always compiled in. */ const struct audio_format_handler wma_afh = { .get_file_info = wma_get_file_info, .suffixes = wma_suffixes, .rewrite_tags = wma_rewrite_tags, };