DEADSOFTWARE

Sound: Initial OpenAL driver impl
[d2df-sdl.git] / src / engine / e_sound_al.inc
1 (* Copyright (C) Doom 2D: Forever Developers
2 *
3 * This program is free software: you can redistribute it and/or modify
4 * it under the terms of the GNU General Public License as published by
5 * the Free Software Foundation, either version 3 of the License, or
6 * (at your option) any later version.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
12 *
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 *)
16 interface
18 uses
19 AL,
20 {$IFDEF USE_MEMPOOL}mempool,{$ENDIF}
21 e_soundfile,
22 e_log,
23 SysUtils;
25 type
26 TSoundRec = record
27 Fmt: TSoundFormat;
28 Data: Pointer;
29 DataLen: Integer;
30 alBuffer: ALuint;
31 isMusic: Boolean;
32 Loops: Boolean;
33 nRefs: Integer;
34 end;
36 TBasicSound = class{$IFDEF USE_MEMPOOL}(TPoolObject){$ENDIF}
37 private
38 FSource: Integer;
39 FOldGain: ALfloat;
40 FMuted: Boolean;
42 function InvalidSource(): Boolean; inline;
44 protected
45 FID: DWORD;
46 FMusic: Boolean;
47 FPosition: DWORD;
49 function RawPlay(Pan: Single; Volume: Single; aPos: DWORD): Boolean;
51 public
52 constructor Create();
53 destructor Destroy(); override;
54 procedure SetID(ID: DWORD);
55 procedure FreeSound();
56 function IsPlaying(): Boolean;
57 procedure Stop();
58 function IsPaused(): Boolean;
59 procedure Pause(Enable: Boolean);
60 function GetVolume(): Single;
61 procedure SetVolume(Volume: Single);
62 function GetPan(): Single;
63 procedure SetPan(Pan: Single);
64 function IsMuted(): Boolean;
65 procedure Mute(Enable: Boolean);
66 function GetPosition(): DWORD;
67 procedure SetPosition(aPos: DWORD);
68 procedure SetPriority(priority: Integer);
69 end;
71 const
72 NO_SOUND_ID = DWORD(-1);
74 function e_InitSoundSystem(NoOutput: Boolean = False): Boolean;
76 function e_LoadSound(FileName: string; var ID: DWORD; isMusic: Boolean; ForceNoLoop: Boolean = False): Boolean;
77 function e_LoadSoundMem(pData: Pointer; Length: Integer; var ID: DWORD; isMusic: Boolean; ForceNoLoop: Boolean = False): Boolean;
79 function e_PlaySound(ID: DWORD): Integer;
80 function e_PlaySoundPan(ID: DWORD; Pan: Single): Integer;
81 function e_PlaySoundVolume(ID: DWORD; Volume: Single): Integer;
82 function e_PlaySoundPanVolume(ID: DWORD; Pan, Volume: Single): Integer;
84 procedure e_ModifyChannelsVolumes(SoundMod: Single; setMode: Boolean);
85 procedure e_MuteChannels(Enable: Boolean);
86 procedure e_StopChannels();
88 procedure e_DeleteSound(ID: DWORD);
89 procedure e_RemoveAllSounds();
90 procedure e_ReleaseSoundSystem();
91 procedure e_SoundUpdate();
93 var
94 e_SoundsArray: array of TSoundRec = nil;
95 e_ZeroPosition: array [0..2] of ALfloat;
96 e_ALError: ALenum = 0;
98 implementation
100 uses
101 g_window, g_options, utils;
103 const
104 NUM_SOURCES = 250;
105 MUSIC_SOURCE = 0;
107 var
108 alDevice: PALCdevice = nil;
109 alContext: PALCcontext = nil;
110 // sources for one-shot sounds
111 // TBasicSounds have a built in source that lives and dies with them
112 alSources: array of ALuint;
113 alOwners: array of TBasicSound;
114 SoundMuted: Boolean = False;
116 function CheckALError(): Boolean;
117 begin
118 e_ALError := alGetError();
119 Result := e_ALError <> AL_NO_ERROR;
120 end;
122 function GetALError(): string;
123 begin
124 Result := '';
125 case e_ALError of
126 AL_NO_ERROR: Result := '';
127 AL_INVALID_NAME: Result := 'AL_INVALID_NAME';
128 AL_INVALID_ENUM: Result := 'AL_INVALID_ENUM';
129 AL_INVALID_VALUE: Result := 'AL_INVALID_VALUE';
130 AL_INVALID_OPERATION: Result := 'AL_INVALID_OPERATION';
131 AL_OUT_OF_MEMORY: Result := 'AL_OUT_OF_MEMORY';
132 else Result := Format('unknown error %x', [e_ALError]);
133 end;
134 end;
136 function e_InitSoundSystem(NoOutput: Boolean = False): Boolean;
137 var
138 alExt, alRend, alVendor, alVer: string;
139 DevName: string = '';
140 alAttrs: array [0..4] of ALCint = (
141 ALC_STEREO_SOURCES, 1,
142 ALC_MONO_SOURCES, NUM_SOURCES,
144 );
145 begin
146 Result := False;
148 DevName := alcGetString(nil, ALC_DEVICE_SPECIFIER);
149 e_LogWritefln('AL: available devices: %s', [DevName]);
151 // TODO: open a dummy device when NoOutput is true or something
152 DevName := alcGetString(nil, ALC_DEFAULT_DEVICE_SPECIFIER);
153 e_LogWritefln('AL: trying to open device %s', [DevName]);
155 alDevice := alcOpenDevice(PChar(DevName));
156 if alDevice = nil then
157 begin
158 e_LogWritefln('AL: ERROR: could not open device %s: %s', [DevName, GetALError()]);
159 exit;
160 end;
162 alContext := alcCreateContext(alDevice, alAttrs);
163 if alContext = nil then
164 begin
165 e_LogWritefln('AL: ERROR: could not create context: %s', [GetALError()]);
166 alcCloseDevice(alDevice);
167 alDevice := nil;
168 exit;
169 end;
171 alcMakeContextCurrent(alContext);
173 alVendor := alGetString(AL_VENDOR);
174 alRend := alGetString(AL_RENDERER);
175 alVer := alGetString(AL_VERSION);
176 alExt := alGetString(AL_EXTENSIONS);
178 e_LogWriteln('AL INFO:');
179 e_LogWriteln(' Version: ' + alVer);
180 e_LogWriteln(' Vendor: ' + alVendor);
181 e_LogWriteln(' Renderer: ' + alRend);
182 e_LogWriteln(' Device: ' + DevName);
183 e_LogWriteln(' Extensions:');
184 e_LogWriteln(' ' + alExt);
186 SetLength(alSources, NUM_SOURCES + 1); // 0 is the music source
187 SetLength(alOwners, NUM_SOURCES + 1); // to avoid destructive operations on sources
188 ZeroMemory(@alSources[0], sizeof(alSources[0]) * Length(alSources));
189 ZeroMemory(@alOwners[0], sizeof(alOwners[0]) * Length(alOwners));
191 alGetError(); // reset the goddamn error state
192 alGenSources(1, @alSources[0]); // generate the music source
193 if CheckALError() then
194 e_LogWriteln('AL: ERROR: alGenSources() for music failed: ' + GetALError());
196 Result := True;
197 end;
199 function FindESound(): DWORD;
200 var
201 i: Integer;
203 begin
204 if e_SoundsArray <> nil then
205 for i := 0 to High(e_SoundsArray) do
206 if e_SoundsArray[i].alBuffer = 0 then
207 begin
208 Result := i;
209 Exit;
210 end;
212 if e_SoundsArray = nil then
213 begin
214 SetLength(e_SoundsArray, 16);
215 Result := 0;
216 end
217 else
218 begin
219 Result := High(e_SoundsArray) + 1;
220 SetLength(e_SoundsArray, Length(e_SoundsArray) + 16);
221 end;
222 end;
224 function GetALSoundFormat(Fmt: TSoundFormat): ALenum; inline;
225 begin
226 if Fmt.Channels = 2 then
227 begin
228 if Fmt.SampleBits = 16 then
229 Result := AL_FORMAT_STEREO16
230 else
231 Result := AL_FORMAT_STEREO8;
232 end
233 else
234 begin
235 if Fmt.SampleBits = 16 then
236 Result := AL_FORMAT_MONO16
237 else
238 Result := AL_FORMAT_MONO8;
239 end;
240 end;
242 function GetALSourceState(S: ALuint): ALint; inline;
243 begin
244 alGetSourcei(S, AL_SOURCE_STATE, Result);
245 end;
247 function e_LoadSound(FileName: String; var ID: DWORD; isMusic: Boolean; ForceNoLoop: Boolean = False): Boolean;
248 var
249 find_id: DWORD;
250 Loader: TSoundLoader;
251 begin
252 ID := NO_SOUND_ID;
253 Result := False;
255 find_id := FindESound();
257 e_SoundsArray[find_id].Data := nil;
258 e_SoundsArray[find_id].DataLen := 0;
259 e_SoundsArray[find_id].isMusic := isMusic;
260 e_SoundsArray[find_id].Loops := isMusic and not ForceNoLoop;
261 e_SoundsArray[find_id].nRefs := 0;
263 Loader := e_GetSoundLoader(FileName);
264 if Loader = nil then
265 begin
266 e_LogWritefln('Could not find loader for sound `%s`', [FileName]);
267 exit;
268 end;
270 e_SoundsArray[find_id].Data := Loader.Load(FileName, e_SoundsArray[find_id].DataLen, e_SoundsArray[find_id].Fmt);
271 if e_SoundsArray[find_id].Data = nil then
272 begin
273 e_LogWritefln('Could not load sound `%s`', [FileName]);
274 exit;
275 end;
277 alGetError(); // reset error state, god damn it
279 alGenBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
280 if CheckALError() then
281 begin
282 e_LogWritefln('Could not create AL buffer for `%s`: %s', [FileName, GetALError()]);
283 Loader.Free(e_SoundsArray[find_id].Data);
284 exit;
285 end;
287 alBufferData(
288 e_SoundsArray[find_id].alBuffer,
289 GetALSoundFormat(e_SoundsArray[find_id].Fmt),
290 e_SoundsArray[find_id].Data,
291 e_SoundsArray[find_id].DataLen,
292 e_SoundsArray[find_id].Fmt.SampleRate
293 );
295 // don't need this anymore
296 Loader.Free(e_SoundsArray[find_id].Data);
297 e_SoundsArray[find_id].Data := nil;
298 e_SoundsArray[find_id].DataLen := 0;
300 if CheckALError() then
301 begin
302 e_LogWriteln('AL: what the fuck: ' + GetALError());
303 alDeleteBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
304 exit;
305 end;
307 ID := find_id;
308 Result := True;
309 end;
311 function e_LoadSoundMem(pData: Pointer; Length: Integer; var ID: DWORD; isMusic: Boolean; ForceNoLoop: Boolean = False): Boolean;
312 var
313 find_id: DWORD;
314 Loader: TSoundLoader;
315 begin
316 ID := NO_SOUND_ID;
317 Result := False;
319 find_id := FindESound();
321 e_SoundsArray[find_id].Data := nil;
322 e_SoundsArray[find_id].DataLen := 0;
323 e_SoundsArray[find_id].isMusic := isMusic;
324 e_SoundsArray[find_id].Loops := isMusic and not ForceNoLoop;
325 e_SoundsArray[find_id].nRefs := 0;
327 Loader := e_GetSoundLoader(pData, Length);
328 if Loader = nil then
329 begin
330 e_LogWritefln('Could not find loader for sound `%p`', [pData]);
331 exit;
332 end;
334 e_SoundsArray[find_id].Data := Loader.Load(pData, Length, e_SoundsArray[find_id].DataLen, e_SoundsArray[find_id].Fmt);
335 if e_SoundsArray[find_id].Data = nil then
336 begin
337 e_LogWritefln('Could not load sound `%p`', [pData]);
338 exit;
339 end;
341 alGetError(); // reset error state, god damn it
343 alGenBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
344 if CheckALError() then
345 begin
346 e_LogWritefln('Could not create AL buffer for `%p`: %s', [pData, GetALError()]);
347 Loader.Free(e_SoundsArray[find_id].Data);
348 exit;
349 end;
351 alBufferData(
352 e_SoundsArray[find_id].alBuffer,
353 GetALSoundFormat(e_SoundsArray[find_id].Fmt),
354 e_SoundsArray[find_id].Data,
355 e_SoundsArray[find_id].DataLen,
356 e_SoundsArray[find_id].Fmt.SampleRate
357 );
359 // don't need this anymore
360 Loader.Free(e_SoundsArray[find_id].Data);
361 e_SoundsArray[find_id].Data := nil;
362 e_SoundsArray[find_id].DataLen := 0;
364 if CheckALError() then
365 begin
366 e_LogWriteln('AL: what the fuck: ' + GetALError());
367 alDeleteBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
368 exit;
369 end;
371 ID := find_id;
372 Result := True;
373 end;
375 function FindSourceForSound(ID: DWORD): Integer;
376 var
377 S: Integer;
378 begin
379 Result := -1;
380 if ID > High(e_SoundsArray) then
381 exit;
383 if e_SoundsArray[ID].isMusic then
384 begin
385 // last source is for music
386 Result := MUSIC_SOURCE;
387 exit;
388 end;
390 for S := 1 to High(alSources) do
391 if alSources[S] = 0 then
392 begin
393 alOwners[S] := nil; // TBasicSounds will set this if needed
394 Result := S;
395 break;
396 end;
398 if Result = -1 then Exit; // no voices left
400 alGetError(); // reset error state
401 alGenSources(1, @alSources[Result]);
402 if CheckALError() then
403 begin
404 e_LogWriteln('AL: FindSourceForSound(): alGenSources() failed: ' + GetALError());
405 Result := -1;
406 end;
407 end;
409 procedure AssignSound(ID: DWORD; Src: ALuint); inline;
410 begin
411 alGetError(); // reset error state
412 alSourcei(Src, AL_BUFFER, e_SoundsArray[ID].alBuffer);
413 alSourcei(Src, AL_SOURCE_RELATIVE, AL_TRUE);
414 if (e_SoundsArray[ID].Loops) then
415 alSourcei(Src, AL_LOOPING, AL_TRUE)
416 else
417 alSourcei(Src, AL_LOOPING, AL_FALSE);
418 end;
420 function e_PlaySound(ID: DWORD): Integer;
421 begin
422 Result := FindSourceForSound(ID);
423 if Result >= 0 then
424 begin
425 AssignSound(ID, alSources[Result]);
426 alSourcef(alSources[Result], AL_GAIN, 1);
427 alSourcefv(alSources[Result], AL_POSITION, e_ZeroPosition);
428 alSourcePlay(alSources[Result]);
429 end;
430 end;
432 function e_PlaySoundPan(ID: DWORD; Pan: Single): Integer;
433 var
434 Pos: array [0..2] of ALfloat;
435 begin
436 Result := FindSourceForSound(ID);
437 if Result >= 0 then
438 begin
439 Pos[0] := Pan;
440 AssignSound(ID, alSources[Result]);
441 alSourcef(alSources[Result], AL_GAIN, 1);
442 alSourcefv(alSources[Result], AL_POSITION, Pos);
443 alSourcePlay(alSources[Result]);
444 end;
445 end;
447 function e_PlaySoundVolume(ID: DWORD; Volume: Single): Integer;
448 begin
449 Result := FindSourceForSound(ID);
450 if Result >= 0 then
451 begin
452 AssignSound(ID, alSources[Result]);
453 alSourcef(alSources[Result], AL_GAIN, Volume);
454 alSourcefv(alSources[Result], AL_POSITION, e_ZeroPosition);
455 alSourcePlay(alSources[Result]);
456 end;
457 end;
459 function e_PlaySoundPanVolume(ID: DWORD; Pan, Volume: Single): Integer;
460 var
461 Pos: array [0..2] of ALfloat;
462 begin
463 Result := FindSourceForSound(ID);
464 if Result >= 0 then
465 begin
466 Pos[0] := Pan;
467 AssignSound(ID, alSources[Result]);
468 alSourcefv(alSources[Result], AL_POSITION, Pos);
469 alSourcef(alSources[Result], AL_GAIN, Volume);
470 alSourcePlay(alSources[Result]);
471 end;
472 end;
474 procedure e_DeleteSound(ID: DWORD);
475 begin
476 if ID > High(e_SoundsArray) then
477 exit;
478 if (e_SoundsArray[ID].alBuffer <> 0) then
479 begin
480 alDeleteBuffers(1, Addr(e_SoundsArray[ID].alBuffer));
481 e_SoundsArray[ID].alBuffer := 0;
482 end;
483 if (e_SoundsArray[ID].Data <> nil) then
484 begin
485 e_SoundsArray[ID].Fmt.Loader.Free(e_SoundsArray[ID].Data);
486 e_SoundsArray[ID].Data := nil;
487 e_SoundsArray[ID].DataLen := 0;
488 end;
489 end;
491 procedure e_ModifyChannelsVolumes(SoundMod: Single; setMode: Boolean);
492 var
493 S: Integer;
494 V: ALfloat;
495 begin
496 // TODO: replace manual volume calculations everywhere with
497 // alListenerf(AL_GAIN) or something
498 if setMode then
499 begin
500 for S := 1 to High(alSources) do
501 if alSources[S] <> 0 then
502 alSourcef(alSources[S], AL_GAIN, SoundMod)
503 end
504 else
505 begin
506 for S := 1 to High(alSources) do
507 if alSources[S] <> 0 then
508 begin
509 alGetSourcef(alSources[S], AL_GAIN, V);
510 alSourcef(alSources[S], AL_GAIN, V * SoundMod);
511 end;
512 end;
513 end;
515 procedure e_MuteChannels(Enable: Boolean);
516 begin
517 if Enable = SoundMuted then
518 Exit;
520 SoundMuted := Enable;
521 end;
523 procedure e_StopChannels();
524 var
525 S: Integer;
526 begin
527 alGetError(); // reset error state
528 for S := Low(alSources) to High(alSources) do
529 if (alSources[S] <> 0) and (GetALSourceState(alSources[S]) = AL_PLAYING) then
530 begin
531 alSourceStop(alSources[S]);
532 alDeleteSources(1, @alSources[S]);
533 alSources[S] := 0;
534 end;
535 end;
537 procedure e_RemoveAllSounds();
538 var
539 i: Integer;
540 begin
541 for i := 0 to High(e_SoundsArray) do
542 if e_SoundsArray[i].alBuffer <> 0 then
543 e_DeleteSound(i);
544 SetLength(e_SoundsArray, 0);
545 e_SoundsArray := nil;
546 end;
548 procedure e_ReleaseSoundSystem();
549 begin
550 e_RemoveAllSounds();
552 alcMakeContextCurrent(nil);
553 alcDestroyContext(alContext);
554 alcCloseDevice(alDevice);
556 alDevice := nil;
557 alContext := nil;
558 end;
560 procedure e_SoundUpdate();
561 var
562 S: Integer;
563 begin
564 alGetError(); // reset error state
565 // clear out all stopped sources
566 for S := 1 to High(alSources) do
567 if (alSources[S] <> 0) and (GetALSourceState(alSources[S]) = AL_STOPPED) then
568 begin
569 alDeleteSources(1, @alSources[S]);
570 alSources[S] := 0;
571 alOwners[S] := nil;
572 end;
573 end;
575 { TBasicSound: }
577 constructor TBasicSound.Create();
578 begin
579 FID := NO_SOUND_ID;
580 FMusic := False;
581 FSource := -1;
582 FPosition := 0;
583 FMuted := False;
584 FOldGain := 1;
585 end;
587 destructor TBasicSound.Destroy();
588 begin
589 FreeSound();
590 inherited;
591 end;
593 function TBasicSound.InvalidSource(): Boolean; inline;
594 begin
595 Result := (FSource < 0) or (alSources[FSource] = 0) or (alOwners[FSource] <> self);
596 end;
598 procedure TBasicSound.FreeSound();
599 begin
600 if FID = NO_SOUND_ID then
601 Exit;
603 Stop();
604 FID := NO_SOUND_ID;
605 FMusic := False;
606 FPosition := 0;
607 end;
609 function TBasicSound.RawPlay(Pan: Single; Volume: Single; aPos: DWORD): Boolean;
610 begin
611 Result := False;
612 if FID = NO_SOUND_ID then Exit;
614 if e_SoundsArray[FID].nRefs >= gMaxSimSounds then
615 begin
616 Result := True;
617 Exit;
618 end;
620 FSource := e_PlaySoundPanVolume(FID, Pan, Volume);
621 if FSource >= 0 then
622 begin
623 alOwners[FSource] := self;
624 Result := True;
625 end;
626 end;
628 procedure TBasicSound.SetID(ID: DWORD);
629 begin
630 FreeSound();
632 if ID > High(e_SoundsArray) then
633 exit;
635 FID := ID;
636 FMusic := e_SoundsArray[ID].isMusic;
637 end;
639 function TBasicSound.IsPlaying(): Boolean;
640 begin
641 Result := False;
642 if InvalidSource() then
643 Exit;
644 Result := GetALSourceState(alSources[FSource]) = AL_PLAYING;
645 end;
647 procedure TBasicSound.Stop();
648 begin
649 if InvalidSource() then
650 Exit;
651 GetPosition();
652 alSourceStop(alSources[FSource]);
653 end;
655 function TBasicSound.IsPaused(): Boolean;
656 begin
657 Result := False;
658 if InvalidSource() then
659 Exit;
660 Result := GetALSourceState(alSources[FSource]) = AL_PAUSED;
661 end;
663 procedure TBasicSound.Pause(Enable: Boolean);
664 begin
665 if InvalidSource() then
666 Exit;
667 if Enable then
668 alSourcePause(alSources[FSource])
669 else
670 alSourcePlay(alSources[FSource]);
671 end;
673 function TBasicSound.GetVolume(): Single;
674 begin
675 Result := 0.0;
676 if InvalidSource() then
677 Exit;
678 alGetSourcef(alSources[FSource], AL_GAIN, Result);
679 end;
681 procedure TBasicSound.SetVolume(Volume: Single);
682 begin
683 if InvalidSource() then
684 Exit;
685 alSourcef(alSources[FSource], AL_GAIN, Volume);
686 end;
688 function TBasicSound.GetPan(): Single;
689 var
690 Pos: array [0..2] of ALfloat;
691 begin
692 Result := 0.0;
693 if InvalidSource() then
694 Exit;
695 alGetSourcefv(alSources[FSource], AL_POSITION, Pos);
696 Result := Pos[0];
697 end;
699 procedure TBasicSound.SetPan(Pan: Single);
700 var
701 Pos: array [0..2] of ALfloat;
702 begin
703 if InvalidSource() then
704 Exit;
705 Pos[0] := Pan;
706 alSourcefv(alSources[FSource], AL_POSITION, Pos);
707 end;
709 function TBasicSound.IsMuted(): Boolean;
710 begin
711 if InvalidSource() then
712 Result := False
713 else
714 Result := FMuted;
715 end;
717 procedure TBasicSound.Mute(Enable: Boolean);
718 begin
719 if InvalidSource() then
720 Exit;
721 if Enable then
722 begin
723 FOldGain := GetVolume();
724 FMuted := True;
725 SetVolume(0);
726 end
727 else if FMuted then
728 begin
729 FMuted := False;
730 SetVolume(FOldGain);
731 end;
732 end;
734 function TBasicSound.GetPosition(): DWORD;
735 var
736 Bytes: ALint;
737 begin
738 Result := 0;
739 if InvalidSource() then
740 Exit;
741 alGetSourcei(alSources[FSource], AL_BYTE_OFFSET, Bytes);
742 FPosition := Bytes;
743 Result := FPosition;
744 end;
746 procedure TBasicSound.SetPosition(aPos: DWORD);
747 begin
748 FPosition := aPos;
749 if InvalidSource() then
750 Exit;
751 alSourcei(alSources[FSource], AL_BYTE_OFFSET, aPos);
752 end;
754 procedure TBasicSound.SetPriority(priority: Integer);
755 begin
756 end;
758 end.