index d5701298b933e05c529d9ec15835d0b33f94cc66..d3ee161d8c3e099aaa24ae52793b2078a93343be 100644 (file)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * the Free Software Foundation, version 3 of the License ONLY.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
type
TSoundRec = record
- Fmt: TSoundFormat;
- Data: Pointer;
- DataLen: Integer;
+ Loader: TSoundLoader;
alBuffer: ALuint;
isMusic: Boolean;
Loops: Boolean;
procedure e_SoundUpdate();
var
+ e_SoundFormat: TSoundFormat; // desired sound format
e_SoundsArray: array of TSoundRec = nil;
- e_ZeroPosition: array [0..2] of ALfloat;
+ e_ZeroPosition: array [0..2] of ALfloat = (0, 0, 0);
e_ALError: ALenum = 0;
+ e_SoundFont: string = '';
+ e_MusicLerp: Boolean = True;
implementation
uses
- g_window, g_options, utils;
+ g_options, utils;
const
- NUM_SOURCES = 250;
+ NUM_SOURCES = 255; // + 1 stereo
+ NUM_STREAM_BUFFERS = 8;
+ STREAM_BUFSIZE = 8192;
MUSIC_SOURCE = 0;
var
+ SoundMuted: Boolean = False;
+ CurStream: DWORD = NO_SOUND_ID;
alDevice: PALCdevice = nil;
alContext: PALCcontext = nil;
- // sources for one-shot sounds
- // TBasicSounds have a built in source that lives and dies with them
- alSources: array of ALuint;
- alOwners: array of TBasicSound;
- SoundMuted: Boolean = False;
+ // sources for everything
+ alSources: array [0..NUM_SOURCES] of ALuint;
+ // last TBasicSound that has played on each source
+ alOwners: array [0..NUM_SOURCES] of TBasicSound;
+ // buffers for the music stream
+ alStreamBufs: array [0..NUM_STREAM_BUFFERS-1] of ALuint;
+ alStreamData: array [0..STREAM_BUFSIZE-1] of Byte;
+ alStreamAvail: Integer = NUM_STREAM_BUFFERS;
+
+{$IFNDEF OPENAL_SINGLETHREADED}
+var
+ StreamThread: TThreadID = NilThreadId;
+ StreamThreadRunning: Boolean = False;
+ StreamLock: TRTLCriticalSection;
+ StreamBufTime: Integer = 10; // time to sleep between buffer checks
+
+procedure UpdateStreamSource(Src: Integer); forward;
+
+function StreamThreadProc(Param: Pointer): PtrInt;
+begin
+ while StreamThreadRunning do
+ begin
+ EnterCriticalSection(StreamLock);
+ UpdateStreamSource(MUSIC_SOURCE);
+ LeaveCriticalSection(StreamLock);
+ Sleep(StreamBufTime);
+ end;
+ Result := 0;
+end;
+{$ENDIF}
function CheckALError(): Boolean;
begin
function e_InitSoundSystem(NoOutput: Boolean = False): Boolean;
var
alExt, alRend, alVendor, alVer: string;
- DevName: string = '';
- alAttrs: array [0..4] of ALCint = (
+ WantDev: string = '';
+ WantAttrs: array [0..4] of ALCint = (
ALC_STEREO_SOURCES, 1,
ALC_MONO_SOURCES, NUM_SOURCES,
0
begin
Result := False;
- DevName := alcGetString(nil, ALC_DEVICE_SPECIFIER);
- e_LogWritefln('AL: available devices: %s', [DevName]);
+ WantDev := alcGetString(nil, ALC_DEVICE_SPECIFIER);
+ e_LogWritefln('AL: available devices: %s', [WantDev]);
// TODO: open a dummy device when NoOutput is true or something
- DevName := alcGetString(nil, ALC_DEFAULT_DEVICE_SPECIFIER);
- e_LogWritefln('AL: trying to open device %s', [DevName]);
+ WantDev := alcGetString(nil, ALC_DEFAULT_DEVICE_SPECIFIER);
+ e_LogWritefln('AL: trying to open device %s', [WantDev]);
- alDevice := alcOpenDevice(PChar(DevName));
+ alDevice := alcOpenDevice(PChar(WantDev));
if alDevice = nil then
begin
- e_LogWritefln('AL: ERROR: could not open device %s: %s', [DevName, GetALError()]);
+ e_LogWritefln('AL: ERROR: could not open device %s: %s', [WantDev, GetALError()]);
exit;
end;
- alContext := alcCreateContext(alDevice, alAttrs);
+ alContext := alcCreateContext(alDevice, WantAttrs);
if alContext = nil then
begin
e_LogWritefln('AL: ERROR: could not create context: %s', [GetALError()]);
alcMakeContextCurrent(alContext);
+ // TODO: actually parse these from alc attributes or something
+ e_SoundFormat.SampleRate := 48000;
+ e_SoundFormat.SampleBits := 16;
+ e_SoundFormat.Channels := 2;
+
alVendor := alGetString(AL_VENDOR);
alRend := alGetString(AL_RENDERER);
alVer := alGetString(AL_VERSION);
alExt := alGetString(AL_EXTENSIONS);
e_LogWriteln('AL INFO:');
- e_LogWriteln(' Version: ' + alVer);
- e_LogWriteln(' Vendor: ' + alVendor);
- e_LogWriteln(' Renderer: ' + alRend);
- e_LogWriteln(' Device: ' + DevName);
+ e_LogWriteln(' Version: ' + alVer);
+ e_LogWriteln(' Vendor: ' + alVendor);
+ e_LogWriteln(' Renderer: ' + alRend);
+ e_LogWriteln(' Device: ' + WantDev);
+ e_LogWriteln(' Sample rate: ' + IntToStr(e_SoundFormat.SampleRate));
e_LogWriteln(' Extensions:');
e_LogWriteln(' ' + alExt);
- SetLength(alSources, NUM_SOURCES + 1); // 0 is the music source
- SetLength(alOwners, NUM_SOURCES + 1); // to avoid destructive operations on sources
- ZeroMemory(@alSources[0], sizeof(alSources[0]) * Length(alSources));
- ZeroMemory(@alOwners[0], sizeof(alOwners[0]) * Length(alOwners));
+ ZeroMemory(@alSources[0], sizeof(alSources));
+ ZeroMemory(@alOwners[0], sizeof(alOwners));
+ ZeroMemory(@alStreamBufs[0], sizeof(alStreamBufs));
+ ZeroMemory(@alStreamData[0], sizeof(alStreamData));
+ CurStream := NO_SOUND_ID;
alGetError(); // reset the goddamn error state
alGenSources(1, @alSources[0]); // generate the music source
if CheckALError() then
e_LogWriteln('AL: ERROR: alGenSources() for music failed: ' + GetALError());
+ alStreamAvail := 0;
+ alGenBuffers(NUM_STREAM_BUFFERS, @alStreamBufs[0]); // generate buffers for the music stream
+ if CheckALError() then
+ e_LogWriteln('AL: ERROR: alGenSources() for music failed: ' + GetALError())
+ else
+ alStreamAvail := NUM_STREAM_BUFFERS;
+
+ {$IFNDEF OPENAL_SINGLETHREADED}
+ InitCriticalSection(StreamLock);
+ StreamThreadRunning := True;
+ StreamThread := BeginThread(Addr(StreamThreadProc));
+ {$ENDIF}
+
Result := True;
end;
begin
if e_SoundsArray <> nil then
for i := 0 to High(e_SoundsArray) do
- if e_SoundsArray[i].alBuffer = 0 then
+ if (e_SoundsArray[i].alBuffer = 0) and (e_SoundsArray[i].Loader = nil) then
begin
Result := i;
Exit;
alGetSourcei(S, AL_SOURCE_STATE, Result);
end;
+function LoadEntireSound(var Snd: TSoundRec; Loader: TSoundLoader): Boolean;
+var
+ Frame: Pointer;
+ Data: Pointer;
+ Rx: LongWord;
+ DataLen, OldLen: LongWord;
+const
+ CHUNK_SIZE = 65536 * 2 * 2;
+begin
+ Result := False;
+
+ Frame := GetMem(CHUNK_SIZE);
+ if Frame = nil then exit;
+
+ Data := nil;
+ DataLen := 0;
+
+ repeat
+ Rx := Loader.FillBuffer(Frame, CHUNK_SIZE);
+ if Rx = 0 then break;
+
+ OldLen := DataLen;
+ DataLen := DataLen + Rx;
+ Data := ReAllocMem(Data, DataLen);
+ if Data = nil then begin FreeMem(Frame); exit; end;
+
+ Move(Frame^, (Data + OldLen)^, Rx);
+ until Loader.Finished();
+
+ FreeMem(Frame);
+
+ alGenBuffers(1, Addr(Snd.alBuffer));
+ if CheckALError() then
+ begin
+ e_LogWritefln('AL: Could not create AL buffer: %s', [GetALError()]);
+ FreeMem(Data);
+ exit;
+ end;
+
+ alBufferData(
+ Snd.alBuffer,
+ GetALSoundFormat(Loader.Format),
+ Data,
+ DataLen,
+ Loader.Format.SampleRate
+ );
+
+ FreeMem(Data);
+
+ if CheckALError() then
+ begin
+ e_LogWriteln('AL: Could not fill AL buffer: ' + GetALError());
+ alDeleteBuffers(1, Addr(Snd.alBuffer));
+ Snd.alBuffer := 0;
+ exit;
+ end;
+
+ Result := True;
+end;
+
function e_LoadSound(FileName: String; var ID: DWORD; isMusic: Boolean; ForceNoLoop: Boolean = False): Boolean;
var
find_id: DWORD;
find_id := FindESound();
- e_SoundsArray[find_id].Data := nil;
- e_SoundsArray[find_id].DataLen := 0;
+ e_SoundsArray[find_id].Loader := nil;
e_SoundsArray[find_id].isMusic := isMusic;
e_SoundsArray[find_id].Loops := isMusic and not ForceNoLoop;
e_SoundsArray[find_id].nRefs := 0;
exit;
end;
- e_SoundsArray[find_id].Data := Loader.Load(FileName, e_SoundsArray[find_id].DataLen, e_SoundsArray[find_id].Fmt);
- if e_SoundsArray[find_id].Data = nil then
+ if not Loader.Load(FileName, e_SoundsArray[find_id].Loops) then
begin
e_LogWritefln('Could not load sound `%s`', [FileName]);
exit;
alGetError(); // reset error state, god damn it
- alGenBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
- if CheckALError() then
+ if not isMusic then
begin
- e_LogWritefln('Could not create AL buffer for `%s`: %s', [FileName, GetALError()]);
- Loader.Free(e_SoundsArray[find_id].Data);
- exit;
- end;
-
- alBufferData(
- e_SoundsArray[find_id].alBuffer,
- GetALSoundFormat(e_SoundsArray[find_id].Fmt),
- e_SoundsArray[find_id].Data,
- e_SoundsArray[find_id].DataLen,
- e_SoundsArray[find_id].Fmt.SampleRate
- );
-
- // don't need this anymore
- Loader.Free(e_SoundsArray[find_id].Data);
- e_SoundsArray[find_id].Data := nil;
- e_SoundsArray[find_id].DataLen := 0;
-
- if CheckALError() then
+ if not LoadEntireSound(e_SoundsArray[find_id], Loader) then
+ e_LogWritefln('AL: Could not buffer sound effect `%s`', [FileName]);
+ // don't need this anymore
+ Loader.Free();
+ Loader := nil;
+ end
+ else
begin
- e_LogWriteln('AL: what the fuck: ' + GetALError());
- alDeleteBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
- exit;
+ e_SoundsArray[find_id].alBuffer := 0;
+ e_SoundsArray[find_id].Loader := Loader;
end;
ID := find_id;
find_id := FindESound();
- e_SoundsArray[find_id].Data := nil;
- e_SoundsArray[find_id].DataLen := 0;
+ e_SoundsArray[find_id].Loader := nil;
e_SoundsArray[find_id].isMusic := isMusic;
e_SoundsArray[find_id].Loops := isMusic and not ForceNoLoop;
e_SoundsArray[find_id].nRefs := 0;
- Loader := e_GetSoundLoader(pData, Length);
+ Loader := e_GetSoundLoader(pData, LongWord(Length));
if Loader = nil then
begin
e_LogWritefln('Could not find loader for sound `%p`', [pData]);
exit;
end;
- e_SoundsArray[find_id].Data := Loader.Load(pData, Length, e_SoundsArray[find_id].DataLen, e_SoundsArray[find_id].Fmt);
- if e_SoundsArray[find_id].Data = nil then
+ if not Loader.Load(pData, LongWord(Length), e_SoundsArray[find_id].Loops) then
begin
e_LogWritefln('Could not load sound `%p`', [pData]);
exit;
alGetError(); // reset error state, god damn it
- alGenBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
- if CheckALError() then
+ if not isMusic then
begin
- e_LogWritefln('Could not create AL buffer for `%p`: %s', [pData, GetALError()]);
- Loader.Free(e_SoundsArray[find_id].Data);
- exit;
- end;
-
- alBufferData(
- e_SoundsArray[find_id].alBuffer,
- GetALSoundFormat(e_SoundsArray[find_id].Fmt),
- e_SoundsArray[find_id].Data,
- e_SoundsArray[find_id].DataLen,
- e_SoundsArray[find_id].Fmt.SampleRate
- );
-
- // don't need this anymore
- Loader.Free(e_SoundsArray[find_id].Data);
- e_SoundsArray[find_id].Data := nil;
- e_SoundsArray[find_id].DataLen := 0;
-
- if CheckALError() then
+ if not LoadEntireSound(e_SoundsArray[find_id], Loader) then
+ e_LogWritefln('AL: Could not buffer sound effect `%p`', [pData]);
+ // don't need this anymore
+ Loader.Free();
+ Loader := nil;
+ end
+ else
begin
- e_LogWriteln('AL: what the fuck: ' + GetALError());
- alDeleteBuffers(1, Addr(e_SoundsArray[find_id].alBuffer));
- exit;
+ e_SoundsArray[find_id].alBuffer := 0;
+ e_SoundsArray[find_id].Loader := Loader;
end;
+ // the calling side won't free this, the loader will get a copy, so fuck it
+ FreeMem(pData);
ID := find_id;
Result := True;
end;
if ID > High(e_SoundsArray) then
exit;
- if e_SoundsArray[ID].isMusic then
+ if e_SoundsArray[ID].Loader <> nil then
begin
- // last source is for music
+ // first source is for streaming sounds
+ // it always exists
+ alOwners[MUSIC_SOURCE] := nil;
Result := MUSIC_SOURCE;
exit;
end;
end;
procedure AssignSound(ID: DWORD; Src: ALuint); inline;
+var
+ S: ALint;
begin
alGetError(); // reset error state
- alSourcei(Src, AL_BUFFER, e_SoundsArray[ID].alBuffer);
- alSourcei(Src, AL_SOURCE_RELATIVE, AL_TRUE);
- if (e_SoundsArray[ID].Loops) then
- alSourcei(Src, AL_LOOPING, AL_TRUE)
+
+ if e_SoundsArray[ID].Loader <> nil then
+ begin
+ // this is a stream
+ {$IFNDEF OPENAL_SINGLETHREADED}
+ // lock the stream so the stream thread doesn't shit itself
+ EnterCriticalSection(StreamLock);
+ // number of stereo samples / samplerate =
+ // time until buffer runs out
+ StreamBufTime :=
+ (STREAM_BUFSIZE div (2 * e_SoundsArray[ID].Loader.Format.SampleBits div 8)) div
+ (e_SoundsArray[ID].Loader.Format.SampleRate div 1000) - 1;
+ if StreamBufTime < 1 then StreamBufTime := 1;
+ {$ENDIF}
+ // reset position
+ e_SoundsArray[ID].Loader.Restart();
+ if CurStream <> ID then // changing streams
+ begin
+ alSourceStop(Src); // this should mark all buffers as processed
+ alGetSourcei(Src, AL_BUFFERS_PROCESSED, S);
+ // unqueue all buffers
+ if S > 0 then
+ begin
+ alSourceUnqueueBuffers(Src, S, @alStreamBufs[alStreamAvail]);
+ alStreamAvail := NUM_STREAM_BUFFERS;
+ end;
+ end;
+ // this shit is playing now
+ CurStream := ID;
+ {$IFNDEF OPENAL_SINGLETHREADED}
+ // unlock the stream
+ LeaveCriticalSection(StreamLock);
+ {$ENDIF}
+ end
else
- alSourcei(Src, AL_LOOPING, AL_FALSE);
+ begin
+ // this is a full chunk, assign local buffer
+ alSourcei(Src, AL_BUFFER, e_SoundsArray[ID].alBuffer);
+ // these can loop
+ if (e_SoundsArray[ID].Loops) then
+ alSourcei(Src, AL_LOOPING, AL_TRUE)
+ else
+ alSourcei(Src, AL_LOOPING, AL_FALSE);
+ end;
+
+ alSourcei(Src, AL_SOURCE_RELATIVE, AL_TRUE);
end;
function e_PlaySound(ID: DWORD): Integer;
if Result >= 0 then
begin
Pos[0] := Pan;
+ Pos[1] := 0;
+ Pos[2] := 0;
AssignSound(ID, alSources[Result]);
alSourcef(alSources[Result], AL_GAIN, 1);
alSourcefv(alSources[Result], AL_POSITION, Pos);
if Result >= 0 then
begin
Pos[0] := Pan;
+ Pos[1] := 0;
+ Pos[2] := 0;
AssignSound(ID, alSources[Result]);
alSourcefv(alSources[Result], AL_POSITION, Pos);
alSourcef(alSources[Result], AL_GAIN, Volume);
alDeleteBuffers(1, Addr(e_SoundsArray[ID].alBuffer));
e_SoundsArray[ID].alBuffer := 0;
end;
- if (e_SoundsArray[ID].Data <> nil) then
+ if (e_SoundsArray[ID].Loader <> nil) then
begin
- e_SoundsArray[ID].Fmt.Loader.Free(e_SoundsArray[ID].Data);
- e_SoundsArray[ID].Data := nil;
- e_SoundsArray[ID].DataLen := 0;
+ e_SoundsArray[ID].Loader.Free();
+ e_SoundsArray[ID].Loader := nil;
+ if ID = CurStream then
+ CurStream := NO_SOUND_ID;
end;
end;
e_DeleteSound(i);
SetLength(e_SoundsArray, 0);
e_SoundsArray := nil;
+ CurStream := NO_SOUND_ID;
end;
procedure e_ReleaseSoundSystem();
begin
+ {$IFNDEF OPENAL_SINGLETHREADED}
+ if StreamThread <> NilThreadId then
+ begin
+ StreamThreadRunning := False;
+ WaitForThreadTerminate(StreamThread, 66666);
+ StreamThread := NilThreadId;
+ DoneCriticalSection(StreamLock);
+ end;
+ {$ENDIF}
+
e_RemoveAllSounds();
alcMakeContextCurrent(nil);
alContext := nil;
end;
+procedure UpdateStreamSource(Src: Integer);
+var
+ OutLen: LongWord;
+ Buf: ALuint;
+ S: Integer;
+begin
+ if alSources[Src] = 0 then Exit;
+
+ alGetError(); // reset error state
+
+ alGetSourcei(alSources[Src], AL_BUFFERS_PROCESSED, S);
+ // unqueue processed buffers
+ if S > 0 then
+ begin
+ alSourceUnqueueBuffers(alSources[Src], S, @alStreamBufs[alStreamAvail]);
+ alStreamAvail := alStreamAvail + S;
+ end;
+
+ alGetError(); // reset error state
+
+ if (alStreamAvail > 0) and (CurStream <> NO_SOUND_ID) then
+ begin
+ // some buffers have freed up, advance stream playback
+ OutLen := e_SoundsArray[CurStream].Loader.FillBuffer(@alStreamData[0], STREAM_BUFSIZE);
+ if OutLen = 0 then Exit; // ran out of stream
+ Buf := alStreamBufs[alStreamAvail-1];
+ Dec(alStreamAvail);
+ // upload
+ alBufferData(
+ Buf,
+ GetALSoundFormat(e_SoundsArray[CurStream].Loader.Format),
+ @alStreamData[0],
+ OutLen,
+ e_SoundsArray[CurStream].Loader.Format.SampleRate
+ );
+ // attach
+ alSourceQueueBuffers(alSources[Src], 1, @Buf);
+ // restart if needed
+ S := GetALSourceState(alSources[Src]);
+ if (S = AL_STOPPED) or (S = AL_INITIAL) then
+ alSourcePlay(alSources[Src]);
+ end;
+end;
+
procedure e_SoundUpdate();
var
S: Integer;
begin
alGetError(); // reset error state
+
// clear out all stopped sources
for S := 1 to High(alSources) do
if (alSources[S] <> 0) and (GetALSourceState(alSources[S]) = AL_STOPPED) then
alSources[S] := 0;
alOwners[S] := nil;
end;
+
+ {$IFDEF OPENAL_SINGLETHREADED}
+ // update the stream sources
+ UpdateStreamSource(MUSIC_SOURCE);
+ {$ENDIF}
end;
{ TBasicSound: }
procedure TBasicSound.Stop();
begin
+ if FID = CurStream then
+ CurStream := NO_SOUND_ID;
if InvalidSource() then
Exit;
GetPosition();
function TBasicSound.GetPan(): Single;
var
- Pos: array [0..2] of ALfloat;
+ Pos: array [0..2] of ALfloat = (0, 0, 0);
begin
Result := 0.0;
if InvalidSource() then
if InvalidSource() then
Exit;
Pos[0] := Pan;
+ Pos[1] := 0;
+ Pos[2] := 0;
alSourcefv(alSources[FSource], AL_POSITION, Pos);
end;