strex: added `detectUrl()`
[iv.d.git] / openal_samples / openal_streaming.d
blobac98446a6504625ebf74b033710d67b6a5a9c633
1 // sample OpenAL streaming player
2 // based on the code by David Gow <david@ingeniumdigital.com>
3 // WTFPL
4 module openal_streaming is aliced;
6 import std.getopt;
8 import iv.audiostream;
9 import iv.openal;
10 import iv.pxclock;
11 import iv.vfs.io;
14 enum BufferSizeBytes = 960*2*2;
16 // the number of buffers we'll be rotating through
17 // ideally, all but one will be full
18 enum BufferCount = 3;
21 struct PlayTime {
22 string warning;
23 ulong warnStartFrm;
24 uint warnDurMsecs;
25 ulong framesDone; // with buffer precision
27 bool warnWasPainted;
28 bool newWarning;
30 void warn (string w, uint durms=1500) {
31 if (w.length > 0) {
32 warning = w;
33 warnStartFrm = framesDone;
34 warnDurMsecs = durms;
35 newWarning = true;
41 // returns number of *samples* (not frames!) queued, -1 on error, 0 on EOF
42 int fillBuffer (ref AudioStream ass, ALuint buffer) {
43 // let's have a buffer that is two opus frames long (and two channels)
44 short[BufferSizeBytes] buf = void; // no need to initialize it
46 immutable int numChannels = ass.channels;
48 // we only support stereo and mono
49 if (numChannels < 1 || numChannels > 2) {
50 stderr.writeln("File contained more channels than we support (", numChannels, ").");
51 return -1;
54 int samplesRead = 0;
55 // keep reading samples until we have them all
56 while (samplesRead < BufferSizeBytes) {
57 int ns = ass.readFrames(buf.ptr+samplesRead, (BufferSizeBytes-samplesRead)/numChannels);
58 if (ns < 0) { stderr.writeln("ERROR reading audio file!"); return -1; }
59 if (ns == 0) break;
60 samplesRead += ns*numChannels;
63 if (samplesRead > 0) {
64 ALenum format;
65 // try to use OpenAL Soft extension first
66 static if (AL_SOFT_buffer_samples) {
67 ALenum chantype;
68 static bssChecked = -1;
69 if (bssChecked < 0) {
70 if (alIsExtensionPresent("AL_SOFT_buffer_samples")) {
71 if (alGetProcAddress("alIsBufferFormatSupportedSOFT") !is null &&
72 alGetProcAddress("alBufferSamplesSOFT") !is null) bssChecked = 1; else bssChecked = 0;
73 //writeln("bssChecked=", bssChecked);
74 } else {
75 bssChecked = 0;
77 if (!bssChecked) writeln("OpenAL: no 'AL_SOFT_buffer_samples'");
79 if (bssChecked > 0) {
80 static bool warningDisplayed = false;
81 final switch (numChannels) {
82 case 1: format = AL_MONO16_SOFT; chantype = AL_MONO_SOFT; break;
83 case 2: format = AL_STEREO16_SOFT; chantype = AL_STEREO_SOFT; break;
85 if (alIsBufferFormatSupportedSOFT(format)) {
86 alBufferSamplesSOFT(buffer, ass.rate, format, samplesRead/numChannels, chantype, AL_SHORT_SOFT, buf.ptr);
87 return true;
89 if (!warningDisplayed) { warningDisplayed = true; stderr.writeln("fallback!"); }
92 // use normal OpenAL method
93 final switch (numChannels) {
94 case 1: format = AL_FORMAT_MONO16; break;
95 case 2: format = AL_FORMAT_STEREO16; break;
97 alBufferData(buffer, format, buf.ptr, samplesRead*2, ass.rate);
100 return samplesRead;
104 bool updateStream (ref AudioStream ass, ALuint source, ref PlayTime ptime) {
105 //bool someBufsAdded = false;
106 ALuint currentbuffer;
108 // how many buffers do we need to fill?
109 int numProcessedBuffers = 0;
110 alGetSourcei(source, AL_BUFFERS_PROCESSED, &numProcessedBuffers);
112 if (numProcessedBuffers > 0) {
113 // unqueue a finished buffer, fill it with new data, and re-add it to the end of the queue
114 while (numProcessedBuffers--) {
115 alSourceUnqueueBuffers(source, 1, &currentbuffer);
116 // add number of played samples to playtime
117 ALint bufsz;
118 alGetBufferi(currentbuffer, AL_SIZE, &bufsz);
119 ptime.framesDone += bufsz/2/ass.channels;
120 //writeln("buffer size: ", bufsz);
121 if (ass.fillBuffer(currentbuffer) <= 0) return false;
122 //someBufsAdded = true;
123 alSourceQueueBuffers(source, 1, &currentbuffer);
127 return true;
131 // AudioStream is required to get sampling rate and number of channels
132 uint getPositionMSec (ref AudioStream ass, ALuint source, in ref PlayTime ptime) {
133 ulong frames = ptime.framesDone;
134 int offset;
135 alGetSourcei(source, AL_SAMPLE_OFFSET, &offset); // in the current buffer
136 if (alGetError() == AL_NO_ERROR) {
137 // add processed buffers (assume that all buffers are of the same size)
138 int numProcessedBuffers = 0;
139 alGetSourcei(source, AL_BUFFERS_PROCESSED, &numProcessedBuffers);
140 if (alGetError() == AL_NO_ERROR) {
141 frames += numProcessedBuffers*(BufferSizeBytes/2/ass.channels);
142 frames += offset/ass.channels;
143 } else {
144 //assert(0);
146 } else {
147 //assert(0);
149 return cast(uint)(frames*1000/ass.rate);
153 // load an ogg opus file into the given AL buffer
154 void streamAudioFile (ALuint source, string filename) {
155 PlayTime ptime;
157 // open the file
158 writeln("opening '", filename, "'...");
159 auto ass = AudioStream.detect(VFile(filename));
160 scope(exit) ass.close();
162 // get the number of channels in the current link
163 immutable int numChannels = ass.channels;
164 // get the number of samples (per channel) in the current link
165 immutable long frameCount = ass.framesTotal;
167 uint nextProgressTime = 0;
168 int procBufs = -1;
171 void showTime () {
173 static if (AL_SOFT_source_latency) {
174 if (alIsExtensionPresent("AL_SOFT_source_latency")) {
175 ALint64SOFT[2] smpvals;
176 ALdouble[2] timevals;
177 alGetSourcei64vSOFT(source, AL_SAMPLE_OFFSET_LATENCY_SOFT, smpvals.ptr);
178 alGetSourcedvSOFT(source, AL_SEC_OFFSET_LATENCY_SOFT, timevals.ptr);
179 writeln("sample: ", smpvals[0]>>32, "; latency (ns): ", smpvals[1], "; seconds=", timevals[0], "; latency (msecs)=", timevals[1]*1000);
183 version(none) {
184 ALdouble blen;
185 alGetSourcedSOFT(source, AL_SEC_LENGTH_SOFT, &blen);
186 writeln("slen: ", blen);
189 // process warnings
190 if (ptime.newWarning && ptime.warning.length == 0) ptime.newWarning = false;
192 if (ptime.newWarning) {
193 import std.string : toStringz;
194 import core.stdc.stdio : stdout, fprintf;
195 if (ptime.warnWasPainted) stdout.fprintf("\e[A");
196 stdout.fprintf("\r%s\e[K\n", ptime.warning.toStringz);
197 ptime.warnWasPainted = true;
198 ptime.newWarning = false;
199 nextProgressTime = 0; // redraw time
202 uint time = cast(uint)(ptime.framesDone*1000/ass.rate);
203 uint total = cast(uint)(frameCount*1000/ass.rate);
204 uint xtime = getPositionMSec(ass, source, ptime);
206 if (ptime.warning.length > 0 && ptime.warnWasPainted) {
207 import core.stdc.stdio : stdout, fprintf;
208 uint etime = cast(uint)(ptime.warnStartFrm*1000/ass.rate)+ptime.warnDurMsecs;
209 if (etime <= time) {
210 stdout.fprintf("\e\r[A\e[K\n");
211 ptime.warning = null;
212 nextProgressTime = 0; // redraw time
216 if (time >= nextProgressTime) {
217 import core.stdc.stdio : stdout, fprintf, fflush;
218 if (procBufs >= 0) {
219 stdout.fprintf("\r%2u:%02u / %u:%02u (%u of %u) (%u : %u)\e[K", time/60/1000, time%60000/1000, total/60/1000, total%60000/1000, cast(uint)procBufs, BufferCount, time, xtime);
220 } else {
221 stdout.fprintf("\r%2u:%02u / %u:%02u (%u : %u)\e[K", time/60/1000, time%60000/1000, total/60/1000, total%60000/1000, time, xtime);
223 stdout.fflush();
224 nextProgressTime = time+500;
228 void doneTime () {
229 nextProgressTime = 0;
230 showTime();
231 import core.stdc.stdio : stdout, fprintf, fflush;
232 stdout.fprintf("\n");
233 stdout.fflush();
237 writeln(filename, ": ", numChannels, " channels, ", frameCount, " frames (", ass.timeTotal/1000, " seconds)");
239 ALuint[BufferCount] buffers; // no need to initialize it, but why not?
241 alGenBuffers(BufferCount, buffers.ptr);
243 foreach (ref buf; buffers) ass.fillBuffer(buf); //TODO: check for errors here too
245 alSourceQueueBuffers(source, BufferCount, buffers.ptr);
247 ulong stt = clockMicro();
249 alSourcePlay(source);
250 if (alGetError() != AL_NO_ERROR) throw new Exception("Could not play source!");
252 showTime();
253 pumploop: for (;;) {
254 import core.sys.posix.unistd : usleep;
255 //usleep(sleepTimeNS);
256 showTime();
257 // sleep until at least one buffer is empty
258 ulong ett = stt+(ass.rate*1000/(BufferSizeBytes/2/numChannels));
259 ulong ctt = clockMicro();
260 //writeln(" ", ctt, " ", ett, " " , ett-ctt);
261 if (ctt < ett && ett-ctt > 100) usleep(cast(uint)(ett-ctt)-100);
262 // statistics
263 alGetSourcei(source, AL_BUFFERS_PROCESSED, &procBufs);
264 // refill buffers
265 if (!ass.updateStream(source, ptime)) break pumploop;
266 stt = clockMicro();
267 // source can stop playing on buffer underflow
268 version(all) {
269 ALenum sourceState;
270 alGetSourcei(source, AL_SOURCE_STATE, &sourceState);
271 if (sourceState != AL_PLAYING && sourceState != AL_PAUSED) {
272 version(none) {
273 int numProcessedBuffers = 0;
274 alGetSourcei(source, AL_BUFFERS_PROCESSED, &numProcessedBuffers);
275 writeln(" npb=", numProcessedBuffers, " of ", BufferCount);
277 ptime.warn("Source not playing!", 600);
278 alSourcePlay(source);
282 // actually, "waiting" should go into time display too
283 doneTime();
285 // wait for source to finish playing
286 writeln("waiting source to finish playing...");
287 for (;;) {
288 ALenum sourceState;
289 alGetSourcei(source, AL_SOURCE_STATE, &sourceState);
290 if (sourceState != AL_PLAYING) break;
293 alSourceUnqueueBuffers(source, BufferCount, buffers.ptr);
295 // we have to delete the source here, as OpenAL soft seems to need the source gone before the buffers
296 // perhaps this is just timing
297 alDeleteSources(1, &source);
298 alDeleteBuffers(BufferCount, buffers.ptr);
302 void main (string[] args) {
303 import std.string : fromStringz;
305 ALuint testSource;
306 ALfloat listenerGain = 1.0f;
307 bool limiting = true;
309 ALCdevice* dev;
310 ALCcontext* ctx;
312 auto gof = getopt(args,
313 std.getopt.config.caseSensitive,
314 std.getopt.config.bundling,
315 "gain|g", &listenerGain,
316 "limit|l", &limiting,
319 if (args.length <= 1) throw new Exception("filename?!");
321 writeln("OpenAL device extensions: ", alcGetString(null, ALC_EXTENSIONS).fromStringz);
323 if (alcIsExtensionPresent(null, "ALC_ENUMERATE_ALL_EXT")) {
324 auto hwdevlist = alcGetString(null, ALC_ALL_DEVICES_SPECIFIER);
325 writeln("OpenAL hw devices:");
326 while (*hwdevlist) {
327 writeln(" ", hwdevlist.fromStringz);
328 while (*hwdevlist) ++hwdevlist;
329 ++hwdevlist;
333 if (alcIsExtensionPresent(null, "ALC_ENUMERATION_EXT")) {
334 auto devlist = alcGetString(null, ALC_DEVICE_SPECIFIER);
335 writeln("OpenAL renderers:");
336 while (*devlist) {
337 writeln(" ", devlist.fromStringz);
338 while (*devlist) ++devlist;
339 ++devlist;
343 writeln("OpenAL default renderer: ", alcGetString(null, ALC_DEFAULT_DEVICE_SPECIFIER).fromStringz);
345 // open the default device
346 dev = alcOpenDevice(null);
347 if (dev is null) throw new Exception("couldn't open OpenAL device");
348 scope(exit) alcCloseDevice(dev);
350 writeln("OpenAL renderer: ", alcGetString(dev, ALC_DEVICE_SPECIFIER).fromStringz);
351 writeln("OpenAL hw device: ", alcGetString(dev, ALC_ALL_DEVICES_SPECIFIER).fromStringz);
353 // we want an OpenAL context
355 immutable ALCint[$] attrs = [
356 ALC_STEREO_SOURCES, 1, // get at least one stereo source for music
357 ALC_MONO_SOURCES, 1, // this should be audio channels in our game engine
358 //ALC_FREQUENCY, 48000, // desired frequency; we don't really need this, let OpenAL choose the best
361 ctx = alcCreateContext(dev, attrs.ptr);
363 if (ctx is null) throw new Exception("couldn't create OpenAL context");
364 scope(exit) {
365 // just to show you how it's done
366 if (alcIsExtensionPresent(null, "ALC_EXT_thread_local_context")) alcSetThreadContext(null); else alcMakeContextCurrent(null);
367 alcDestroyContext(ctx);
370 if (!limiting && alcGetProcAddress(dev, "alcResetDeviceSOFT") !is null) {
371 immutable ALCint[$] attrs = [
372 ALC_OUTPUT_LIMITER_SOFT, ALC_FALSE,
375 if (!alcResetDeviceSOFT(dev, attrs.ptr)) stderr.writeln("WARNING: can't turn off OpenAL limiter");
378 // just to show you how it's done; see https://github.com/openalext/openalext/blob/master/ALC_EXT_thread_local_context.txt
379 if (alcIsExtensionPresent(null, "ALC_EXT_thread_local_context")) alcSetThreadContext(ctx); else alcMakeContextCurrent(ctx);
380 //alcMakeContextCurrent(ctx);
382 writeln("OpenAL vendor: ", alGetString(AL_VENDOR).fromStringz);
383 writeln("OpenAL version: ", alGetString(AL_VERSION).fromStringz);
384 writeln("OpenAL renderer: ", alGetString(AL_RENDERER).fromStringz);
385 writeln("OpenAL extensions: ", alGetString(AL_EXTENSIONS).fromStringz);
387 // get us a buffer and a source to attach it to
388 writeln("creating OpenAL source...");
389 alGenSources(1, &testSource);
391 // this turns off OpenAL spatial processing for the source,
392 // thus directly mapping stereo sound to the corresponding channels;
393 // but this works only for stereo samples, and we'd better do that
394 // after checking number of channels in input stream
395 static if (AL_SOFT_direct_channels) {
396 if (alIsExtensionPresent("AL_SOFT_direct_channels")) {
397 writeln("OpenAL: direct channels extension detected");
398 alSourcei(testSource, AL_DIRECT_CHANNELS_SOFT, AL_TRUE);
399 if (alGetError() != AL_NO_ERROR) stderr.writeln("WARNING: can't turn on direct channels");
403 writeln("setting OpenAL listener...");
404 // set position and gain for the listener
405 alListener3f(AL_POSITION, 0.0f, 0.0f, 0.0f);
406 //alListenerf(AL_GAIN, 1.0f);
407 // as listener gain is applied after source gain, and it not limited to 1.0, it is possible to do the following
408 writeln("listener gain: ", listenerGain);
409 alListenerf(AL_GAIN, listenerGain);
411 // ...and set source properties
412 writeln("setting OpenAL source properties...");
413 alSource3f(testSource, AL_POSITION, 0.0f, 0.0f, 0.0f);
416 //ALfloat maxGain;
417 //alGetSourcef(testSource, AL_MAX_GAIN, &maxGain);
418 //writeln("max gain: ", maxGain);
420 if (alIsExtensionPresent("AL_SOFT_gain_clamp_ex")) {
421 ALfloat gainLimit = 0.0f;
422 alGetFloatv(AL_GAIN_LIMIT_SOFT, &gainLimit);
423 writeln("gain limit: ", gainLimit);
426 alSourcef(testSource, AL_GAIN, 1.0f);
427 // MAX_GAIN is *user* limit, not library/hw; so you can do the following
428 // but somehow it doesn't work right on my system (or i misunderstood it's use case)
429 // it seems to slowly fall back to 1.0, and distort both volume and (sometimes) pitch
430 version(none) {
431 alSourcef(testSource, AL_MAX_GAIN, 2.0f);
432 alSourcef(testSource, AL_GAIN, 2.0f);
435 if (alGetError() != AL_NO_ERROR) throw new Exception("error initializing OpenAL");
437 writeln("streaming...");
438 streamAudioFile(testSource, args[1]);