{* UltraStar Deluxe - Karaoke Game * * UltraStar Deluxe is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * 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 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; see the file COPYING. If not, write to * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, * Boston, MA 02110-1301, USA. * * $URL$ * $Id$ *} unit UAudioPlayback_SoftMixer; interface {$IFDEF FPC} {$MODE Delphi} {$ENDIF} {$I switches.inc} uses Classes, sdl, SysUtils, URingBuffer, UMusic, UAudioPlaybackBase; type TAudioPlayback_SoftMixer = class; TGenericPlaybackStream = class(TAudioPlaybackStream) private Engine: TAudioPlayback_SoftMixer; LastReadSize: integer; // size of data returned by the last ReadData() call LastReadTime: Cardinal; // time of the last ReadData() call SampleBuffer: PByteArray; SampleBufferSize: integer; SampleBufferCount: integer; // number of available bytes in SampleBuffer SampleBufferPos: integer; SourceBuffer: PByteArray; SourceBufferSize: integer; SourceBufferCount: integer; // number of available bytes in SourceBuffer Converter: TAudioConverter; Status: TStreamStatus; InternalLock: PSDL_Mutex; SoundEffects: TList; fVolume: single; FadeInStartTime, FadeInTime: cardinal; FadeInStartVolume, FadeInTargetVolume: single; NeedsRewind: boolean; procedure Reset(); procedure ApplySoundEffects(Buffer: PByteArray; BufferSize: integer); function InitFormatConversion(): boolean; procedure FlushBuffers(); procedure LockSampleBuffer(); {$IFDEF HasInline}inline;{$ENDIF} procedure UnlockSampleBuffer(); {$IFDEF HasInline}inline;{$ENDIF} protected function GetLatency(): double; override; function GetStatus(): TStreamStatus; override; function GetVolume(): single; override; procedure SetVolume(Volume: single); override; function GetLength(): real; override; function GetLoop(): boolean; override; procedure SetLoop(Enabled: boolean); override; function GetPosition: real; override; procedure SetPosition(Time: real); override; function GetRemainingBufferSize(): integer; public constructor Create(Engine: TAudioPlayback_SoftMixer); destructor Destroy(); override; function Open(SourceStream: TAudioSourceStream): boolean; override; procedure Close(); override; procedure Play(); override; procedure Pause(); override; procedure Stop(); override; procedure FadeIn(Time: real; TargetVolume: single); override; function GetAudioFormatInfo(): TAudioFormatInfo; override; function ReadData(Buffer: PByteArray; BufferSize: integer): integer; function GetPCMData(var Data: TPCMData): cardinal; override; procedure GetFFTData(var Data: TFFTData); override; procedure AddSoundEffect(Effect: TSoundEffect); override; procedure RemoveSoundEffect(Effect: TSoundEffect); override; end; TAudioMixerStream = class private Engine: TAudioPlayback_SoftMixer; ActiveStreams: TList; MixerBuffer: PByteArray; InternalLock: PSDL_Mutex; AppVolume: single; procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} function GetVolume(): single; procedure SetVolume(Volume: single); public constructor Create(Engine: TAudioPlayback_SoftMixer); destructor Destroy(); override; procedure AddStream(Stream: TAudioPlaybackStream); procedure RemoveStream(Stream: TAudioPlaybackStream); function ReadData(Buffer: PByteArray; BufferSize: integer): integer; property Volume: single read GetVolume write SetVolume; end; TAudioPlayback_SoftMixer = class(TAudioPlaybackBase) private MixerStream: TAudioMixerStream; protected FormatInfo: TAudioFormatInfo; function InitializeAudioPlaybackEngine(): boolean; virtual; abstract; function StartAudioPlaybackEngine(): boolean; virtual; abstract; procedure StopAudioPlaybackEngine(); virtual; abstract; function FinalizeAudioPlaybackEngine(): boolean; virtual; abstract; procedure AudioCallback(Buffer: PByteArray; Size: integer); {$IFDEF HasInline}inline;{$ENDIF} function CreatePlaybackStream(): TAudioPlaybackStream; override; public function GetName: string; override; abstract; function InitializePlayback(): boolean; override; function FinalizePlayback: boolean; override; procedure SetAppVolume(Volume: single); override; function CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; override; function GetMixer(): TAudioMixerStream; {$IFDEF HasInline}inline;{$ENDIF} function GetAudioFormatInfo(): TAudioFormatInfo; procedure MixBuffers(DstBuffer, SrcBuffer: PByteArray; Size: cardinal; Volume: single); virtual; end; type TGenericVoiceStream = class(TAudioVoiceStream) private VoiceBuffer: TRingBuffer; BufferLock: PSDL_Mutex; PlaybackStream: TGenericPlaybackStream; Engine: TAudioPlayback_SoftMixer; public constructor Create(Engine: TAudioPlayback_SoftMixer); function Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; override; procedure Close(); override; procedure WriteData(Buffer: PByteArray; BufferSize: integer); override; function ReadData(Buffer: PByteArray; BufferSize: integer): integer; override; function IsEOF(): boolean; override; function IsError(): boolean; override; end; const SOURCE_BUFFER_FRAMES = 4096; const MAX_VOICE_DELAY = 0.500; // 20ms implementation uses Math, ULog, UIni, UFFT, UAudioConverter, UMain; { TAudioMixerStream } constructor TAudioMixerStream.Create(Engine: TAudioPlayback_SoftMixer); begin inherited Create(); Self.Engine := Engine; ActiveStreams := TList.Create; InternalLock := SDL_CreateMutex(); AppVolume := 1.0; end; destructor TAudioMixerStream.Destroy(); begin if assigned(MixerBuffer) then Freemem(MixerBuffer); ActiveStreams.Free; SDL_DestroyMutex(InternalLock); inherited; end; procedure TAudioMixerStream.Lock(); begin SDL_mutexP(InternalLock); end; procedure TAudioMixerStream.Unlock(); begin SDL_mutexV(InternalLock); end; function TAudioMixerStream.GetVolume(): single; begin Lock(); Result := AppVolume; Unlock(); end; procedure TAudioMixerStream.SetVolume(Volume: single); begin Lock(); AppVolume := Volume; Unlock(); end; procedure TAudioMixerStream.AddStream(Stream: TAudioPlaybackStream); begin if not assigned(Stream) then Exit; Lock(); // check if stream is already in list to avoid duplicates if (ActiveStreams.IndexOf(Pointer(Stream)) = -1) then ActiveStreams.Add(Pointer(Stream)); Unlock(); end; (* * Sets the entry of stream in the ActiveStreams-List to nil * but does not remove it from the list (Count is not changed!). * Otherwise iterations over the elements might fail due to a * changed Count-property. * Call ActiveStreams.Pack() to remove the nil-pointers * or check for nil-pointers when accessing ActiveStreams. *) procedure TAudioMixerStream.RemoveStream(Stream: TAudioPlaybackStream); var Index: integer; begin Lock(); Index := activeStreams.IndexOf(Pointer(Stream)); if (Index <> -1) then begin // remove entry but do not decrease count-property ActiveStreams[Index] := nil; end; Unlock(); end; function TAudioMixerStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer; var i: integer; Size: integer; Stream: TGenericPlaybackStream; NeedsPacking: boolean; begin Result := BufferSize; // zero target-buffer (silence) FillChar(Buffer^, BufferSize, 0); // resize mixer-buffer if necessary ReallocMem(MixerBuffer, BufferSize); if not assigned(MixerBuffer) then Exit; Lock(); NeedsPacking := false; // mix streams to one stream for i := 0 to ActiveStreams.Count-1 do begin if (ActiveStreams[i] = nil) then begin NeedsPacking := true; continue; end; Stream := TGenericPlaybackStream(ActiveStreams[i]); // fetch data from current stream Size := Stream.ReadData(MixerBuffer, BufferSize); if (Size > 0) then begin // mix stream-data with mixer-buffer // Note: use Self.appVolume instead of Self.Volume to prevent recursive locking Engine.MixBuffers(Buffer, MixerBuffer, Size, AppVolume * Stream.Volume); end; end; // remove nil-pointers from list if (NeedsPacking) then begin ActiveStreams.Pack(); end; Unlock(); end; { TGenericPlaybackStream } constructor TGenericPlaybackStream.Create(Engine: TAudioPlayback_SoftMixer); begin inherited Create(); Self.Engine := Engine; InternalLock := SDL_CreateMutex(); SoundEffects := TList.Create; Status := ssStopped; Reset(); end; destructor TGenericPlaybackStream.Destroy(); begin Close(); SDL_DestroyMutex(InternalLock); FreeAndNil(SoundEffects); inherited; end; procedure TGenericPlaybackStream.Reset(); begin SourceStream := nil; FreeAndNil(Converter); FreeMem(SampleBuffer); SampleBuffer := nil; SampleBufferPos := 0; SampleBufferSize := 0; SampleBufferCount := 0; FreeMem(SourceBuffer); SourceBuffer := nil; SourceBufferSize := 0; SourceBufferCount := 0; NeedsRewind := false; fVolume := 0; SoundEffects.Clear; FadeInTime := 0; LastReadSize := 0; end; function TGenericPlaybackStream.Open(SourceStream: TAudioSourceStream): boolean; begin Result := false; Close(); if not assigned(SourceStream) then Exit; Self.SourceStream := SourceStream; if not InitFormatConversion() then begin // reset decode-stream so it will not be freed on destruction Self.SourceStream := nil; Exit; end; SourceBufferSize := SOURCE_BUFFER_FRAMES * SourceStream.GetAudioFormatInfo().FrameSize; GetMem(SourceBuffer, SourceBufferSize); fVolume := 1.0; Result := true; end; procedure TGenericPlaybackStream.Close(); begin // stop audio-callback on this stream Stop(); // Note: PerformOnClose must be called before SourceStream is invalidated PerformOnClose(); // and free data Reset(); end; procedure TGenericPlaybackStream.LockSampleBuffer(); begin SDL_mutexP(InternalLock); end; procedure TGenericPlaybackStream.UnlockSampleBuffer(); begin SDL_mutexV(InternalLock); end; function TGenericPlaybackStream.InitFormatConversion(): boolean; var SrcFormatInfo: TAudioFormatInfo; DstFormatInfo: TAudioFormatInfo; begin Result := false; SrcFormatInfo := SourceStream.GetAudioFormatInfo(); DstFormatInfo := GetAudioFormatInfo(); // TODO: selection should not be done here, use a factory (TAudioConverterFactory) instead {$IF Defined(UseFFmpegResample)} Converter := TAudioConverter_FFmpeg.Create(); {$ELSEIF Defined(UseSRCResample)} Converter := TAudioConverter_SRC.Create(); {$ELSE} Converter := TAudioConverter_SDL.Create(); {$IFEND} Result := Converter.Init(SrcFormatInfo, DstFormatInfo); end; procedure TGenericPlaybackStream.Play(); var Mixer: TAudioMixerStream; begin // only paused streams are not flushed if (Status = ssPaused) then NeedsRewind := false; // rewind if necessary. Cases that require no rewind are: // - stream was created and never played // - stream was paused and is resumed now // - stream was stopped and set to a new position already if (NeedsRewind) then SetPosition(0); // update status Status := ssPlaying; NeedsRewind := true; // add this stream to the mixer Mixer := Engine.GetMixer(); if (Mixer <> nil) then Mixer.AddStream(Self); end; procedure TGenericPlaybackStream.FadeIn(Time: real; TargetVolume: single); begin FadeInTime := Trunc(Time * 1000); FadeInStartTime := SDL_GetTicks(); FadeInStartVolume := fVolume; FadeInTargetVolume := TargetVolume; Play(); end; procedure TGenericPlaybackStream.Pause(); var Mixer: TAudioMixerStream; begin if (Status <> ssPlaying) then Exit; Status := ssPaused; Mixer := Engine.GetMixer(); if (Mixer <> nil) then Mixer.RemoveStream(Self); end; procedure TGenericPlaybackStream.Stop(); var Mixer: TAudioMixerStream; begin if (Status = ssStopped) then Exit; Status := ssStopped; // stop fading FadeInTime := 0; LastReadSize := 0; Mixer := Engine.GetMixer(); if (Mixer <> nil) then Mixer.RemoveStream(Self); end; function TGenericPlaybackStream.GetLoop(): boolean; begin if assigned(SourceStream) then Result := SourceStream.Loop else Result := false; end; procedure TGenericPlaybackStream.SetLoop(Enabled: boolean); begin if assigned(SourceStream) then SourceStream.Loop := Enabled; end; function TGenericPlaybackStream.GetLength(): real; begin if assigned(SourceStream) then Result := SourceStream.Length else Result := -1; end; function TGenericPlaybackStream.GetLatency(): double; begin Result := Engine.GetLatency(); end; function TGenericPlaybackStream.GetStatus(): TStreamStatus; begin Result := Status; end; function TGenericPlaybackStream.GetAudioFormatInfo(): TAudioFormatInfo; begin Result := Engine.GetAudioFormatInfo(); end; procedure TGenericPlaybackStream.FlushBuffers(); begin SampleBufferCount := 0; SampleBufferPos := 0; SourceBufferCount := 0; LastReadSize := 0; end; procedure TGenericPlaybackStream.ApplySoundEffects(Buffer: PByteArray; BufferSize: integer); var i: integer; begin for i := 0 to SoundEffects.Count-1 do begin if (SoundEffects[i] <> nil) then begin TSoundEffect(SoundEffects[i]).Callback(Buffer, BufferSize); end; end; end; function TGenericPlaybackStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer; var ConversionInputCount: integer; ConversionOutputSize: integer; // max. number of converted data (= buffer size) ConversionOutputCount: integer; // actual number of converted data SourceSize: integer; NeededSampleBufferSize: integer; BytesNeeded: integer; SourceFormatInfo, OutputFormatInfo: TAudioFormatInfo; SourceFrameSize, OutputFrameSize: integer; SkipOutputCount: integer; // number of output-data bytes to skip SkipSourceCount: integer; // number of source-data bytes to skip FillCount: integer; // number of bytes to fill with padding data CopyCount: integer; PadFrame: PByteArray; begin Result := -1; LastReadSize := 0; // sanity check for the source-stream if (not assigned(SourceStream)) then Exit; SkipOutputCount := 0; SkipSourceCount := 0; FillCount := 0; SourceFormatInfo := SourceStream.GetAudioFormatInfo(); SourceFrameSize := SourceFormatInfo.FrameSize; OutputFormatInfo := GetAudioFormatInfo(); OutputFrameSize := OutputFormatInfo.FrameSize; // synchronize (adjust buffer size) BytesNeeded := Synchronize(BufferSize, OutputFormatInfo); if (BytesNeeded > BufferSize) then begin SkipOutputCount := BytesNeeded - BufferSize; BytesNeeded := BufferSize; end else if (BytesNeeded < BufferSize) then begin FillCount := BufferSize - BytesNeeded; end; // lock access to sample-buffer LockSampleBuffer(); try // skip sample-buffer data SampleBufferPos := SampleBufferPos + SkipOutputCount; // size of available bytes in SampleBuffer after skipping SampleBufferCount := SampleBufferCount - SampleBufferPos; // update byte skip-count SkipOutputCount := -SampleBufferCount; // now that we skipped all buffered data from the last pass, we have to skip // data directly after fetching it from the source-stream. if (SkipOutputCount > 0) then begin SampleBufferCount := 0; // convert skip-count to source-format units and resize to a multiple of // the source frame-size. SkipSourceCount := Round((SkipOutputCount * OutputFormatInfo.GetRatio(SourceFormatInfo)) / SourceFrameSize) * SourceFrameSize; SkipOutputCount := 0; end; // copy data to front of buffer if ((SampleBufferCount > 0) and (SampleBufferPos > 0)) then Move(SampleBuffer[SampleBufferPos], SampleBuffer[0], SampleBufferCount); SampleBufferPos := 0; // resize buffer to a reasonable size if (BufferSize > SampleBufferCount) then begin // Note: use BufferSize instead of BytesNeeded to minimize the need for resizing SampleBufferSize := BufferSize; ReallocMem(SampleBuffer, SampleBufferSize); if (not assigned(SampleBuffer)) then Exit; end; // fill sample-buffer (fetch and convert one block of source data per loop) while (SampleBufferCount < BytesNeeded) do begin // move remaining source data from the previous pass to front of buffer if (SourceBufferCount > 0) then begin Move(SourceBuffer[SourceBufferSize-SourceBufferCount], SourceBuffer[0], SourceBufferCount); end; SourceSize := SourceStream.ReadData( @SourceBuffer[SourceBufferCount], SourceBufferSize-SourceBufferCount); // break on error (-1) or if no data is available (0), e.g. while seeking if (SourceSize <= 0) then begin // if we do not have data -> exit if (SourceBufferCount = 0) then begin FlushBuffers(); Exit; end; // if we have some data, stop retrieving data from the source stream // and use the data we have so far Break; end; SourceBufferCount := SourceBufferCount + SourceSize; // end-of-file reached -> stop playback if (SourceStream.EOF) then begin if (Loop) then SourceStream.Position := 0 else Stop(); end; if (SkipSourceCount > 0) then begin // skip data and update source buffer count SourceBufferCount := SourceBufferCount - SkipSourceCount; SkipSourceCount := -SourceBufferCount; // continue with next pass if we skipped all data if (SourceBufferCount <= 0) then begin SourceBufferCount := 0; Continue; end; end; // calc buffer size (might be bigger than actual resampled byte count) ConversionOutputSize := Converter.GetOutputBufferSize(SourceBufferCount); NeededSampleBufferSize := SampleBufferCount + ConversionOutputSize; // resize buffer if necessary if (SampleBufferSize < NeededSampleBufferSize) then begin SampleBufferSize := NeededSampleBufferSize; ReallocMem(SampleBuffer, SampleBufferSize); if (not assigned(SampleBuffer)) then begin FlushBuffers(); Exit; end; end; // resample source data (Note: ConversionInputCount might be adjusted by Convert()) ConversionInputCount := SourceBufferCount; ConversionOutputCount := Converter.Convert( SourceBuffer, @SampleBuffer[SampleBufferCount], ConversionInputCount); if (ConversionOutputCount = -1) then begin FlushBuffers(); Exit; end; // adjust sample- and source-buffer count by the number of converted bytes SampleBufferCount := SampleBufferCount + ConversionOutputCount; SourceBufferCount := SourceBufferCount - ConversionInputCount; end; // apply effects ApplySoundEffects(SampleBuffer, SampleBufferCount); // copy data to result buffer CopyCount := Min(BytesNeeded, SampleBufferCount); Move(SampleBuffer[0], Buffer[BufferSize - BytesNeeded], CopyCount); Dec(BytesNeeded, CopyCount); SampleBufferPos := CopyCount; // release buffer lock finally UnlockSampleBuffer(); end; // pad the buffer with the last frame if we are to fast if (FillCount > 0) then begin if (CopyCount >= OutputFrameSize) then PadFrame := @Buffer[CopyCount-OutputFrameSize] else PadFrame := nil; FillBufferWithFrame(@Buffer[CopyCount], FillCount, PadFrame, OutputFrameSize); end; // BytesNeeded now contains the number of remaining bytes we were not able to fetch LastReadTime := SDL_GetTicks; LastReadSize := BufferSize - BytesNeeded; Result := LastReadSize; end; function TGenericPlaybackStream.GetPCMData(var Data: TPCMData): cardinal; var ByteCount: integer; begin Result := 0; // just SInt16 stereo support for now if ((Engine.GetAudioFormatInfo().Format <> asfS16) or (Engine.GetAudioFormatInfo().Channels <> 2)) then begin Exit; end; // zero memory FillChar(Data, SizeOf(Data), 0); // TODO: At the moment just the first samples of the SampleBuffer // are returned, even if there is newer data in the upper samples. LockSampleBuffer(); ByteCount := Min(SizeOf(Data), SampleBufferCount); if (ByteCount > 0) then begin Move(SampleBuffer[0], Data, ByteCount); end; UnlockSampleBuffer(); Result := ByteCount div SizeOf(TPCMStereoSample); end; procedure TGenericPlaybackStream.GetFFTData(var Data: TFFTData); var i: integer; Frames: integer; DataIn: PSingleArray; AudioFormat: TAudioFormatInfo; begin // only works with SInt16 and Float values at the moment AudioFormat := GetAudioFormatInfo(); DataIn := AllocMem(FFTSize * SizeOf(single)); if (DataIn = nil) then Exit; LockSampleBuffer(); // TODO: We just use the first Frames frames, the others are ignored. Frames := Min(FFTSize, SampleBufferCount div AudioFormat.FrameSize); // use only first channel and convert data to float-values case AudioFormat.Format of asfS16: begin for i := 0 to Frames-1 do DataIn[i] := PSmallInt(@SampleBuffer[i*AudioFormat.FrameSize])^ / -Low(SmallInt); end; asfFloat: begin for i := 0 to Frames-1 do DataIn[i] := PSingle(@SampleBuffer[i*AudioFormat.FrameSize])^; end; end; UnlockSampleBuffer(); WindowFunc(fwfHanning, FFTSize, DataIn); PowerSpectrum(FFTSize, DataIn, @Data); FreeMem(DataIn); // resize data to a 0..1 range for i := 0 to High(TFFTData) do begin Data[i] := Sqrt(Data[i]) / 100; end; end; procedure TGenericPlaybackStream.AddSoundEffect(Effect: TSoundEffect); begin if (not assigned(Effect)) then Exit; LockSampleBuffer(); // check if effect is already in list to avoid duplicates if (SoundEffects.IndexOf(Pointer(Effect)) = -1) then SoundEffects.Add(Pointer(Effect)); UnlockSampleBuffer(); end; procedure TGenericPlaybackStream.RemoveSoundEffect(Effect: TSoundEffect); begin LockSampleBuffer(); SoundEffects.Remove(Effect); UnlockSampleBuffer(); end; {** * Returns the approximate number of bytes left in the audio engines buffer queue. *} function TGenericPlaybackStream.GetRemainingBufferSize(): integer; var TimeDiff: double; begin if (LastReadSize <= 0) then begin Result := 0; end else begin TimeDiff := (SDL_GetTicks() - LastReadTime) / 1000; // we gave the data-sink LastReadSize bytes at the last call to ReadData(). // Calculate how much of this should be left in the data-sink Result := LastReadSize - Trunc(TimeDiff * Engine.FormatInfo.BytesPerSec); if (Result < 0) then Result := 0; end; end; function TGenericPlaybackStream.GetPosition: real; var BufferedTime: double; begin if assigned(SourceStream) then begin LockSampleBuffer(); // the duration of source stream data that is buffered in this stream. // (this is the data retrieved from the source but has not been resampled) BufferedTime := SourceBufferCount / SourceStream.GetAudioFormatInfo().BytesPerSec; // the duration of data that is buffered in this stream. // (this is the already resampled data that has not yet been passed to the audio engine) BufferedTime := BufferedTime + (SampleBufferCount - SampleBufferPos) / Engine.FormatInfo.BytesPerSec; // Now consider the samples left in the engine's (e.g. SDL) buffer. // Otherwise the result calculated so far will not change until the callback // is called the next time. // For example, if the buffer has a size of 2048 frames we would not be // able to return a new new position for approx. 40ms (at 44.1kHz) which // would be very bad for synching. BufferedTime := BufferedTime + GetRemainingBufferSize() / Engine.FormatInfo.BytesPerSec; // use the timestamp of the source as reference and subtract the time of // the data that is still buffered and not yet output. Result := SourceStream.Position - BufferedTime; UnlockSampleBuffer(); end else begin Result := -1; end; end; procedure TGenericPlaybackStream.SetPosition(Time: real); begin if assigned(SourceStream) then begin LockSampleBuffer(); SourceStream.Position := Time; if (Status = ssStopped) then NeedsRewind := false; // do not use outdated data FlushBuffers(); AvgSyncDiff := -1; UnlockSampleBuffer(); end; end; function TGenericPlaybackStream.GetVolume(): single; var FadeAmount: single; begin LockSampleBuffer(); // adjust volume if fading is enabled if (FadeInTime > 0) then begin FadeAmount := (SDL_GetTicks() - FadeInStartTime) / FadeInTime; // check if fade-target is reached if (FadeAmount >= 1) then begin // target reached -> stop fading FadeInTime := 0; fVolume := FadeInTargetVolume; end else begin // fading in progress fVolume := FadeAmount*FadeInTargetVolume + (1-FadeAmount)*FadeInStartVolume; end; end; // return current volume Result := fVolume; UnlockSampleBuffer(); end; procedure TGenericPlaybackStream.SetVolume(Volume: single); begin LockSampleBuffer(); // stop fading FadeInTime := 0; // clamp volume if (Volume > 1.0) then fVolume := 1.0 else if (Volume < 0) then fVolume := 0 else fVolume := Volume; UnlockSampleBuffer(); end; { TGenericVoiceStream } constructor TGenericVoiceStream.Create(Engine: TAudioPlayback_SoftMixer); begin inherited Create(); Self.Engine := Engine; end; function TGenericVoiceStream.Open(ChannelMap: integer; FormatInfo: TAudioFormatInfo): boolean; var BufferSize: integer; begin Result := false; Close(); if (not inherited Open(ChannelMap, FormatInfo)) then Exit; // Note: // - use Self.FormatInfo instead of FormatInfo as the latter one might have a // channel size of 2. // - the buffer-size must be a multiple of the FrameSize BufferSize := (Ceil(MAX_VOICE_DELAY * Self.FormatInfo.BytesPerSec) div Self.FormatInfo.FrameSize) * Self.FormatInfo.FrameSize; VoiceBuffer := TRingBuffer.Create(BufferSize); BufferLock := SDL_CreateMutex(); // create a matching playback stream for the voice-stream PlaybackStream := TGenericPlaybackStream.Create(Engine); // link voice- and playback-stream if (not PlaybackStream.Open(Self)) then begin PlaybackStream.Free; Exit; end; // start voice passthrough PlaybackStream.Play(); Result := true; end; procedure TGenericVoiceStream.Close(); begin // stop and free the playback stream FreeAndNil(PlaybackStream); // free data FreeAndNil(VoiceBuffer); if (BufferLock <> nil) then SDL_DestroyMutex(BufferLock); inherited Close(); end; procedure TGenericVoiceStream.WriteData(Buffer: PByteArray; BufferSize: integer); begin // lock access to buffer SDL_mutexP(BufferLock); try if (VoiceBuffer = nil) then Exit; VoiceBuffer.Write(Buffer, BufferSize); finally SDL_mutexV(BufferLock); end; end; function TGenericVoiceStream.ReadData(Buffer: PByteArray; BufferSize: integer): integer; begin Result := -1; // lock access to buffer SDL_mutexP(BufferLock); try if (VoiceBuffer = nil) then Exit; Result := VoiceBuffer.Read(Buffer, BufferSize); finally SDL_mutexV(BufferLock); end; end; function TGenericVoiceStream.IsEOF(): boolean; begin SDL_mutexP(BufferLock); Result := (VoiceBuffer = nil); SDL_mutexV(BufferLock); end; function TGenericVoiceStream.IsError(): boolean; begin Result := false; end; { TAudioPlayback_SoftMixer } function TAudioPlayback_SoftMixer.InitializePlayback: boolean; begin Result := false; //Log.LogStatus('InitializePlayback', 'UAudioPlayback_SoftMixer'); if (not InitializeAudioPlaybackEngine()) then Exit; MixerStream := TAudioMixerStream.Create(Self); if (not StartAudioPlaybackEngine()) then Exit; Result := true; end; function TAudioPlayback_SoftMixer.FinalizePlayback: boolean; begin Close; StopAudioPlaybackEngine(); FreeAndNil(MixerStream); FreeAndNil(FormatInfo); FinalizeAudioPlaybackEngine(); inherited FinalizePlayback; Result := true; end; procedure TAudioPlayback_SoftMixer.AudioCallback(Buffer: PByteArray; Size: integer); begin MixerStream.ReadData(Buffer, Size); end; function TAudioPlayback_SoftMixer.GetMixer(): TAudioMixerStream; begin Result := MixerStream; end; function TAudioPlayback_SoftMixer.GetAudioFormatInfo(): TAudioFormatInfo; begin Result := FormatInfo; end; function TAudioPlayback_SoftMixer.CreatePlaybackStream(): TAudioPlaybackStream; begin Result := TGenericPlaybackStream.Create(Self); end; function TAudioPlayback_SoftMixer.CreateVoiceStream(ChannelMap: integer; FormatInfo: TAudioFormatInfo): TAudioVoiceStream; var VoiceStream: TGenericVoiceStream; begin Result := nil; // create a voice stream VoiceStream := TGenericVoiceStream.Create(Self); if (not VoiceStream.Open(ChannelMap, FormatInfo)) then begin VoiceStream.Free; Exit; end; Result := VoiceStream; end; procedure TAudioPlayback_SoftMixer.SetAppVolume(Volume: single); begin // sets volume only for this application MixerStream.Volume := Volume; end; procedure TAudioPlayback_SoftMixer.MixBuffers(DstBuffer, SrcBuffer: PByteArray; Size: cardinal; Volume: single); var SampleIndex: cardinal; SampleInt: integer; SampleFlt: single; begin SampleIndex := 0; case FormatInfo.Format of asfS16: begin while (SampleIndex < Size) do begin // apply volume and sum with previous mixer value SampleInt := PSmallInt(@DstBuffer[SampleIndex])^ + Round(PSmallInt(@SrcBuffer[SampleIndex])^ * Volume); // clip result if (SampleInt > High(SmallInt)) then SampleInt := High(SmallInt) else if (SampleInt < Low(SmallInt)) then SampleInt := Low(SmallInt); // assign result PSmallInt(@DstBuffer[SampleIndex])^ := SampleInt; // increase index by one sample Inc(SampleIndex, SizeOf(SmallInt)); end; end; asfFloat: begin while (SampleIndex < Size) do begin // apply volume and sum with previous mixer value SampleFlt := PSingle(@DstBuffer[SampleIndex])^ + PSingle(@SrcBuffer[SampleIndex])^ * Volume; // clip result if (SampleFlt > 1.0) then SampleFlt := 1.0 else if (SampleFlt < -1.0) then SampleFlt := -1.0; // assign result PSingle(@DstBuffer[SampleIndex])^ := SampleFlt; // increase index by one sample Inc(SampleIndex, SizeOf(single)); end; end; else begin Log.LogError('Incompatible format', 'TAudioMixerStream.MixAudio'); end; end; end; end.