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 }