1 /*
2     Copyright © 2020, Luna Nielsen
3     Distributed under the 2-Clause BSD License, see LICENSE file.
4     
5     Authors: Luna Nielsen
6 */
7 module engine.audio.astream.ogg;
8 import engine.audio.astream;
9 import vorbisfile;
10 import std.string;
11 import engine.core.log;
12 import std.exception;
13 import std.typecons;
14 
15 /**
16     An Ogg Vorbis audio stream
17 */
18 class OggStream : AudioStream {
19 private:
20     string fname;
21     OggVorbis_File file;
22     int section;
23     int word;
24     int signed;
25 
26     int bitrate_;
27 
28 
29     void verifyError(int error) {
30         switch(error) {
31             case OV_EREAD: throw new Exception("A read from media returned an error");
32             case OV_ENOTVORBIS: throw new Exception("File is not a valid ogg vorbis file");
33             case OV_EVERSION: throw new Exception("Vorbis version mismatch");
34             case OV_EBADHEADER: throw new Exception("Bad OGG Vorbis header");
35             case OV_EFAULT: throw new Exception("OGG Vorbis bug or stack corruption");
36             default: break;
37         }
38     }
39 
40 public:
41 
42     /**
43         Deallocates on deconstruction
44     */
45     ~this() {
46         ov_clear(&file);
47     }
48 
49     /**
50         Opens the OGG Vorbis stream file
51     */
52     this(string file, bool bit16) {
53         this.fname = file;
54 
55         // Open the file and verify it opened correctly
56         int error = ov_fopen(file.toStringz, &this.file);
57         this.verifyError(error);
58 
59         // Get info about file
60         auto info = ov_info(&this.file, -1);
61         enforce(info.channels <= 2, "Too many channels in OGG Vorbis file");
62 
63         this.bitrate_ = info.rate;
64 
65         // Set info about this file
66         this.channels = info.channels;
67         if (this.channels == 1) {
68             this.format = bit16 ? Format.Mono16 : Format.Mono8;
69         } else {
70             this.format = bit16 ? Format.Stereo16 : Format.Stereo8;
71         }
72         word = format == Format.Stereo8 || format == Format.Mono8 ? 1 : 2;
73         signed = format == Format.Mono16 || format == Format.Stereo16 ? 1 : 0;
74     }
75 
76     ptrdiff_t iReadSamples(ref ubyte[] toArray, size_t readLength) {
77 
78         // Read a verify the success of the read
79         ptrdiff_t readAmount = cast(ptrdiff_t)ov_read(&file, cast(byte*)toArray.ptr, cast(int)readLength, 0, word, signed, &section);
80         assert(readAmount >= 0, "An error occured trying to read from the ogg vorbis stream");
81         return readAmount;
82     }
83 
84 override:
85     ptrdiff_t readSamples(ref ubyte[] toArray) {
86         ubyte[] tmpBuf = new ubyte[4096];
87         ptrdiff_t buffOffset;
88         ptrdiff_t buffLength;
89         do {
90             buffLength = iReadSamples(tmpBuf, toArray.length-buffOffset);
91             toArray[buffOffset..buffOffset+buffLength] = tmpBuf[0..buffLength];
92             buffOffset += buffLength; 
93         } while(buffOffset < toArray.length && buffLength > 0);
94         return buffOffset;
95     }
96 
97     /**
98         Gets whether the file can be seeked
99     */
100     bool canSeek() {
101         return cast(bool)ov_seekable(&file);
102     }
103 
104     /**
105         Seek to a PCM location in the stream
106     */
107     void seek(size_t location) {
108         ov_pcm_seek(&file, location);
109     }
110 
111     /**
112         Get the position in the stream
113     */
114     size_t tell() {
115         return ov_pcm_tell(&file);
116     }
117 
118     /**
119         Gets the bitrate
120     */
121     size_t bitrate() {
122         return bitrate_;
123     }
124 
125     /**
126         Gets info about the OGG audio
127 
128         Only music usually uses this
129     */
130     AudioInfo getInfo() {
131         
132         // Inline function to get the ogg comments as a D string array
133         string[] getOggInfo() {
134             string[] fields;
135 
136             // Iterate over every comment
137             foreach(i; 0..file.vc.comments) {
138                 immutable(int) commentLength = file.vc.comment_lengths[i];
139                 string comment = cast(string)file.vc.user_comments[i][0..commentLength];
140                 fields ~= comment;
141             }
142             return fields;
143         }
144 
145         // Parse ogg info as a array of key and value
146         string[2] parseOggInfo(string info) {
147             auto idx = info.indexOf("=");
148             enforce(idx >= 0, "Invalid info");
149             return [info[0..idx], info[idx+1..$]];
150         }
151 
152         // Inline function to parse the ogg info
153         string[string] parseOggInfos() {
154             string[string] infos;
155 
156             string[] fields = getOggInfo();
157             foreach(i, field; fields) {
158                 try {
159                     string[2] info = parseOggInfo(field);
160                     infos[info[0]] = info[1];
161                 } catch (Exception ex) {
162                     AppLog.warn("Ogg Subsystem", "Failed the parse comment field %s: %s! Got data %s", i, ex.msg, field);
163                 }
164             }
165 
166             return infos;
167         }
168 
169         AudioInfo info;
170         string[string] kv = parseOggInfos();
171         info.file = this.fname;
172         if ("ARTIST" in kv) info.artist = kv["ARTIST"];
173         if ("TITLE" in kv) info.title = kv["TITLE"];
174         if ("ALBUM" in kv) info.album = kv["ALBUM"];
175         if ("PERFOMER" in kv) info.performer = kv["PERFOMER"];
176         if ("DATE" in kv) info.date = kv["DATE"];
177         return info;
178     }
179 }