|
| 1 | +const std = @import("std"); |
| 2 | +const c = @cImport({ |
| 3 | + @cInclude("opusfile.h"); |
| 4 | + @cInclude("opusenc.h"); |
| 5 | +}); |
| 6 | + |
| 7 | +const Opus = @This(); |
| 8 | + |
| 9 | +channels: u8, |
| 10 | +sample_rate: u24, |
| 11 | +samples: []align(alignment) f32, |
| 12 | + |
| 13 | +/// The length of a @Vector(len, f32) used for SIMD audio buffers. |
| 14 | +pub const simd_vector_length = std.simd.suggestVectorLength(f32) orelse 1; |
| 15 | + |
| 16 | +pub const alignment = simd_vector_length * @sizeOf(f32); |
| 17 | + |
| 18 | +pub const DecodeError = error{ |
| 19 | + OutOfMemory, |
| 20 | + InvalidData, |
| 21 | + Internal, |
| 22 | + Reading, |
| 23 | + Seeking, |
| 24 | + Unknown, |
| 25 | +}; |
| 26 | + |
| 27 | +pub fn decodeStream( |
| 28 | + allocator: std.mem.Allocator, |
| 29 | + stream: std.io.StreamSource, |
| 30 | +) (DecodeError || std.io.StreamSource.ReadError)!Opus { |
| 31 | + var decoder = Decoder{ .allocator = allocator, .stream = stream }; |
| 32 | + var err: c_int = 0; |
| 33 | + const opus_file = c.op_open_callbacks( |
| 34 | + &decoder, |
| 35 | + &c.OpusFileCallbacks{ |
| 36 | + .read = Decoder.readCallback, |
| 37 | + .seek = Decoder.seekCallback, |
| 38 | + .tell = Decoder.tellCallback, |
| 39 | + .close = Decoder.closeCallback, |
| 40 | + }, |
| 41 | + null, |
| 42 | + 0, |
| 43 | + &err, |
| 44 | + ); |
| 45 | + switch (err) { |
| 46 | + 0 => {}, |
| 47 | + // An underlying read operation failed. This may signal a truncation attack from an <https:> source. |
| 48 | + c.OP_EREAD => return error.Reading, |
| 49 | + // An internal memory allocation failed. |
| 50 | + c.OP_EFAULT => return error.OutOfMemory, |
| 51 | + // An unseekable stream encountered a new link that used a feature that is not implemented, such as an unsupported channel family. |
| 52 | + c.OP_EIMPL => return error.Internal, |
| 53 | + // The stream was only partially open. |
| 54 | + c.OP_EINVAL => return error.InvalidData, |
| 55 | + // An unseekable stream encountered a new link that did not have any logical Opus streams in it. |
| 56 | + c.OP_ENOTFORMAT => return error.InvalidData, |
| 57 | + // An unseekable stream encountered a new link with a required header packet that was not properly formatted, contained illegal values, or was missing altogether. |
| 58 | + c.OP_EBADHEADER => return error.InvalidData, |
| 59 | + // An unseekable stream encountered a new link with an ID header that contained an unrecognized version number. |
| 60 | + c.OP_EVERSION => return error.InvalidData, |
| 61 | + // We failed to find data we had seen before. |
| 62 | + c.OP_EBADLINK => return error.Seeking, |
| 63 | + // An unseekable stream encountered a new link with a starting timestamp that failed basic validity checks. |
| 64 | + c.OP_EBADTIMESTAMP => return error.InvalidData, |
| 65 | + else => return error.Unknown, |
| 66 | + } |
| 67 | + defer c.op_free(opus_file); |
| 68 | + |
| 69 | + const header = c.op_head(opus_file, 0); |
| 70 | + const channels: u8 = @intCast(header.*.channel_count); |
| 71 | + const sample_rate: u24 = @intCast(header.*.input_sample_rate); |
| 72 | + const total_samples: usize = @intCast(c.op_pcm_total(opus_file, -1)); |
| 73 | + var samples = try allocator.alignedAlloc(f32, alignment, total_samples * channels); |
| 74 | + errdefer allocator.free(samples); |
| 75 | + |
| 76 | + var i: usize = 0; |
| 77 | + while (i < samples.len) { |
| 78 | + const read = c.op_read_float(opus_file, samples[i..].ptr, @intCast(samples.len - i), null); |
| 79 | + if (read == 0) break else if (read < 0) return error.InvalidData; |
| 80 | + i += @intCast(read * channels); |
| 81 | + } |
| 82 | + |
| 83 | + return .{ |
| 84 | + .channels = channels, |
| 85 | + .sample_rate = sample_rate, |
| 86 | + .samples = samples, |
| 87 | + }; |
| 88 | +} |
| 89 | + |
| 90 | +const Decoder = struct { |
| 91 | + allocator: std.mem.Allocator, |
| 92 | + stream: std.io.StreamSource, |
| 93 | + samples: []f32 = &.{}, |
| 94 | + sample_index: usize = 0, |
| 95 | + |
| 96 | + fn readCallback(decoder_opaque: ?*anyopaque, ptr: [*c]u8, nbytes: c_int) callconv(.C) c_int { |
| 97 | + const decoder: *Decoder = @ptrCast(@alignCast(decoder_opaque)); |
| 98 | + const read = decoder.stream.read(ptr[0..@intCast(nbytes)]) catch return -1; |
| 99 | + return @intCast(read); |
| 100 | + } |
| 101 | + |
| 102 | + fn seekCallback(decoder_opaque: ?*anyopaque, offset: i64, whence: c_int) callconv(.C) c_int { |
| 103 | + const decoder: *Decoder = @ptrCast(@alignCast(decoder_opaque)); |
| 104 | + switch (whence) { |
| 105 | + c.SEEK_SET => decoder.stream.seekTo(@intCast(offset)) catch return -1, |
| 106 | + c.SEEK_CUR => decoder.stream.seekBy(offset) catch return -1, |
| 107 | + c.SEEK_END => decoder.stream.seekTo(decoder.stream.getEndPos() catch return -1) catch return -1, |
| 108 | + else => unreachable, |
| 109 | + } |
| 110 | + return 0; |
| 111 | + } |
| 112 | + |
| 113 | + fn tellCallback(decoder_opaque: ?*anyopaque) callconv(.C) i64 { |
| 114 | + const decoder: *Decoder = @ptrCast(@alignCast(decoder_opaque)); |
| 115 | + const pos = decoder.stream.getPos() catch unreachable; |
| 116 | + return @intCast(pos); |
| 117 | + } |
| 118 | + |
| 119 | + fn closeCallback(decoder_opaque: ?*anyopaque) callconv(.C) c_int { |
| 120 | + _ = decoder_opaque; |
| 121 | + return 0; |
| 122 | + } |
| 123 | +}; |
| 124 | + |
| 125 | +pub const Comments = struct { |
| 126 | + opus_comments: *c.OggOpusComments, |
| 127 | + |
| 128 | + pub fn init() error{OutOfMemory}!Comments { |
| 129 | + const comments = c.ope_comments_create() orelse return error.OutOfMemory; |
| 130 | + return .{ .opus_comments = comments }; |
| 131 | + } |
| 132 | + |
| 133 | + pub fn deinit(comments: Comments) void { |
| 134 | + c.ope_comments_destroy(comments.opus_comments); |
| 135 | + } |
| 136 | + |
| 137 | + pub fn addString(comments: Comments, tag: [*:0]const u8, value: [*:0]const u8) error{OutOfMemory}!void { |
| 138 | + const err = c.ope_comments_add(comments.opus_comments, tag, value); |
| 139 | + if (err != c.OPE_OK) return error.OutOfMemory; |
| 140 | + } |
| 141 | + |
| 142 | + pub const PictureType = enum(u5) { |
| 143 | + other = 0, |
| 144 | + /// PNG Only |
| 145 | + icon_32x32 = 1, |
| 146 | + icon_other = 2, |
| 147 | + cover_front = 3, |
| 148 | + cover_back = 4, |
| 149 | + leaflet_page = 5, |
| 150 | + /// (e.g. label side of CD) |
| 151 | + media = 6, |
| 152 | + /// Lead performer/soloist |
| 153 | + lead_artist = 7, |
| 154 | + /// Artist/Performer |
| 155 | + artist = 8, |
| 156 | + conductor = 9, |
| 157 | + /// Band/Orchestra |
| 158 | + band = 10, |
| 159 | + composer = 11, |
| 160 | + lyricist = 12, |
| 161 | + recording_location = 13, |
| 162 | + during_recording = 14, |
| 163 | + during_performance = 15, |
| 164 | + video_screen_capture = 16, |
| 165 | + a_bright_colored_fish = 17, |
| 166 | + illustration = 18, |
| 167 | + artist_logotype = 19, |
| 168 | + /// Publisher/Studio logoType |
| 169 | + publisher_logotype = 20, |
| 170 | + }; |
| 171 | + |
| 172 | + pub fn addPicture( |
| 173 | + comments: Comments, |
| 174 | + image: []const u8, |
| 175 | + picture_type: PictureType, |
| 176 | + description: [*:0]const u8, |
| 177 | + ) error{OutOfMemory}!void { |
| 178 | + const err = c.ope_comments_add_picture_from_memory( |
| 179 | + comments.opus_comments, |
| 180 | + image.ptr, |
| 181 | + image.len, |
| 182 | + @intFromEnum(picture_type), |
| 183 | + description, |
| 184 | + ); |
| 185 | + if (err != c.OPE_OK) return error.OutOfMemory; |
| 186 | + } |
| 187 | +}; |
| 188 | + |
| 189 | +pub const EncodeError = error{ |
| 190 | + OutOfMemory, |
| 191 | + InvalidPicture, |
| 192 | + InvalidIcon, |
| 193 | + Writing, |
| 194 | + Internal, |
| 195 | + Unknown, |
| 196 | +}; |
| 197 | + |
| 198 | +pub const ChannelMapping = enum(u1) { |
| 199 | + mono_stereo = 0, |
| 200 | + surround = 1, |
| 201 | +}; |
| 202 | + |
| 203 | +pub fn encodeStream( |
| 204 | + stream: std.io.StreamSource, |
| 205 | + comments: Comments, |
| 206 | + sample_rate: u24, |
| 207 | + channels: u24, |
| 208 | + channel_mapping: ChannelMapping, |
| 209 | + samples: []const f32, |
| 210 | +) (EncodeError || std.io.StreamSource.ReadError)!void { |
| 211 | + var encoder = Encoder{ .stream = stream }; |
| 212 | + var err: c_int = 0; |
| 213 | + const ope_encoder = c.ope_encoder_create_callbacks( |
| 214 | + &c.OpusEncCallbacks{ |
| 215 | + .write = Encoder.writeCallback, |
| 216 | + .close = Encoder.closeCallback, |
| 217 | + }, |
| 218 | + &encoder, |
| 219 | + comments.opus_comments, |
| 220 | + sample_rate, |
| 221 | + channels, |
| 222 | + @intFromEnum(channel_mapping), |
| 223 | + &err, |
| 224 | + ); |
| 225 | + try checkEncoderErr(err); |
| 226 | + defer c.ope_encoder_destroy(ope_encoder); |
| 227 | + |
| 228 | + try checkEncoderErr(c.ope_encoder_flush_header(ope_encoder)); |
| 229 | + try checkEncoderErr(c.ope_encoder_write_float(ope_encoder, samples.ptr, @intCast(samples.len / channels))); |
| 230 | + try checkEncoderErr(c.ope_encoder_drain(ope_encoder)); |
| 231 | +} |
| 232 | + |
| 233 | +const Encoder = struct { |
| 234 | + stream: std.io.StreamSource, |
| 235 | + |
| 236 | + fn writeCallback(encoder_opaque: ?*anyopaque, ptr: [*c]const u8, len: i32) callconv(.C) c_int { |
| 237 | + const encoder: *Encoder = @ptrCast(@alignCast(encoder_opaque)); |
| 238 | + _ = encoder.stream.write(ptr[0..@intCast(len)]) catch return 1; |
| 239 | + return 0; |
| 240 | + } |
| 241 | + |
| 242 | + fn closeCallback(encoder_opaque: ?*anyopaque) callconv(.C) c_int { |
| 243 | + _ = encoder_opaque; |
| 244 | + return 0; |
| 245 | + } |
| 246 | +}; |
| 247 | + |
| 248 | +fn checkEncoderErr(err: c_int) EncodeError!void { |
| 249 | + return switch (err) { |
| 250 | + c.OPE_OK => {}, |
| 251 | + c.OPE_BAD_ARG => unreachable, |
| 252 | + c.OPE_INTERNAL_ERROR => error.Internal, |
| 253 | + c.OPE_UNIMPLEMENTED => error.Internal, |
| 254 | + c.OPE_ALLOC_FAIL => error.OutOfMemory, |
| 255 | + c.OPE_CANNOT_OPEN => unreachable, |
| 256 | + c.OPE_TOO_LATE => unreachable, |
| 257 | + c.OPE_INVALID_PICTURE => error.InvalidPicture, |
| 258 | + c.OPE_INVALID_ICON => error.InvalidIcon, |
| 259 | + c.OPE_WRITE_FAIL => error.Writing, |
| 260 | + c.OPE_CLOSE_FAIL => unreachable, |
| 261 | + else => error.Unknown, |
| 262 | + }; |
| 263 | +} |
0 commit comments