1 module dcad.types; 2 3 import std.json, 4 std.stdio, 5 std.bitmanip, 6 std.outbuffer; 7 8 ubyte[][] rawReadFramesFromFile(File f) { 9 ubyte[][] frames; 10 11 while (true) { 12 ubyte[] frame = rawReadFrameFromFile(f); 13 if (frame.length == 0) break; 14 frames ~= frame; 15 } 16 17 return frames; 18 } 19 20 ubyte[] rawReadFrameFromFile(File f) { 21 ubyte[] data; 22 23 auto frameSize = f.rawRead(new ubyte[2]); 24 if (frameSize.length == 0) { 25 return data; 26 } 27 28 short size = frameSize.read!(short, Endian.littleEndian); 29 if (size == 0) { 30 return data; 31 } 32 33 return f.rawRead(new ubyte[size]); 34 } 35 36 struct Frame { 37 ubyte[] data; 38 39 this(ubyte[] data) { 40 this.data = data; 41 } 42 43 bool read(File f) { 44 this.data = rawReadFrameFromFile(f); 45 46 if (this.data.length == 0) { 47 return false; 48 } 49 50 return true; 51 } 52 53 void write(OutBuffer buffer) { 54 buffer.write(nativeToLittleEndian(cast(short)this.data.length)); 55 buffer.write(this.data); 56 } 57 58 void write(File file) { 59 file.rawWrite(nativeToLittleEndian(cast(short)this.data.length)); 60 file.rawWrite(this.data); 61 } 62 } 63 64 class DCAFile { 65 DCAFileMeta meta; 66 Frame[] frames; 67 68 this() {} 69 70 this(File f) { 71 char[3] magicHeader; 72 f.rawRead(magicHeader); 73 74 if (cast(string)magicHeader == "DCA") { 75 assert(false, "standard DCA files are not supported yet"); 76 } else { 77 f.seek(0); 78 this.readOpusDataFile(f); 79 } 80 } 81 82 this(ubyte[][] rawFrames) { 83 foreach (frame; rawFrames) { 84 this.frames ~= Frame(frame); 85 } 86 } 87 88 OutBuffer toOutBuffer() { 89 OutBuffer buffer = new OutBuffer; 90 91 foreach (frame; this.frames) { 92 frame.write(buffer); 93 } 94 95 return buffer; 96 } 97 98 void save(string path) { 99 File f = File(path, "w"); 100 f.rawWrite(this.toOutBuffer().toBytes); 101 f.close(); 102 } 103 104 /** 105 Creates a new DCAFile without trying to read magic bytes. This is useful 106 for file objects that do not support streaming. 107 */ 108 static DCAFile fromRawDCA(File f) { 109 DCAFile dca = new DCAFile; 110 dca.readOpusDataFile(f); 111 return dca; 112 } 113 114 private void readOpusDataFile(File f) { 115 while (true) { 116 Frame frame; 117 118 if (!frame.read(f)) { 119 break; 120 } 121 122 this.frames ~= frame; 123 } 124 } 125 } 126 127 class DCAFileMeta { 128 DCAMetadata* dca; 129 SongInfoMetadata* info; 130 OriginMetadata* origin; 131 OpusMetadata* opus; 132 JSONValue extra; 133 134 this(JSONValue baseObj) { 135 this.dca = new DCAMetadata(baseObj["dca"]); 136 this.info = new SongInfoMetadata(baseObj["info"]); 137 this.origin = new OriginMetadata(baseObj["origin"]); 138 this.opus = new OpusMetadata(baseObj["opus"]); 139 this.extra = baseObj["extra"]; 140 } 141 } 142 143 struct DCAMetadata { 144 ushort v; 145 DCAToolMetadata* tool; 146 147 this(JSONValue obj) { 148 this.v = cast(ushort)obj["version"].integer; 149 this.tool = new DCAToolMetadata(obj["tool"]); 150 } 151 } 152 153 struct DCAToolMetadata { 154 string name; 155 string v; 156 string url; 157 string author; 158 159 this(JSONValue obj) { 160 this.name = obj["name"].str; 161 this.v = obj["version"].str; 162 this.url = obj["url"].str; 163 this.author = obj["author"].str; 164 } 165 } 166 167 struct SongInfoMetadata { 168 string title; 169 string artist; 170 string album; 171 string genre; 172 string comments; 173 string cover; 174 175 this(JSONValue obj) { 176 this.title = obj["title"].str; 177 this.artist = obj["artist"].str; 178 this.album = obj["album"].str; 179 this.genre = obj["genre"].str; 180 this.comments = obj["comments"].str; 181 if (!obj["cover"].isNull) { 182 this.cover = obj["cover"].str; 183 } 184 } 185 } 186 187 struct OriginMetadata { 188 string source; 189 uint bitrate; 190 ushort channels; 191 string encoding; 192 string url; 193 194 this(JSONValue obj) { 195 this.source = obj["source"].str; 196 this.bitrate = cast(uint)obj["abr"].integer; 197 this.channels = cast(ushort)obj["channels"].integer; 198 this.encoding = obj["encoding"].str; 199 this.url = obj["url"].str; 200 } 201 } 202 203 struct OpusMetadata { 204 string mode; 205 uint bitrate; 206 uint sampleRate; 207 uint frameSize; 208 ushort channels; 209 210 this(JSONValue obj) { 211 this.mode = obj["mode"].str; 212 this.bitrate = cast(uint)obj["abr"].integer; 213 this.sampleRate = cast(uint)obj["sample_rate"].integer; 214 this.frameSize = cast(uint)obj["frame_size"].integer; 215 this.channels = cast(ushort)obj["channels"].integer; 216 } 217 } 218 219 unittest { 220 auto obj = parseJSON(`{ "dca": { "version": 1, "tool": { "name": "dca-encoder", "version": "1.0.0", "url": "https://github.com/bwmarrin/dca/", "author": "bwmarrin" } }, "opus": { "mode": "voip", "sample_rate": 48000, "frame_size": 960, "abr": 64000, "vbr": true, "channels": 2 }, "info": { "title": "Out of Control", "artist": "Nothing's Carved in Stone", "album": "Revolt", "genre": "jrock", "comments": "Second Opening for the anime Psycho Pass", "cover": null }, "origin": { "source": "file", "abr": 192000, "channels": 2, "encoding": "MP3/MPEG-2L3", "url": "https://www.dropbox.com/s/bwc73zb44o3tj3m/Out%20of%20Control.mp3?dl=0" }, "extra": {}}`); 221 222 auto file = new DCAFileMeta(obj); 223 assert(file.dca.v == 1); 224 assert(file.dca.tool.author == "bwmarrin"); 225 assert(file.opus.mode == "voip"); 226 assert(file.info.genre == "jrock"); 227 assert(file.origin.source == "file"); 228 229 auto dca = new DCAFile(File("test/airhorn_default.dca", "r")); 230 }