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, §ion); 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 }