From 55595c6403ac90100bb9e3ea26ed7cef05d4c4c3 Mon Sep 17 00:00:00 2001 From: tobigun Date: Fri, 7 Mar 2008 20:23:53 +0000 Subject: GetPCMData added for SDL/Portaudio git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/trunk@935 b956fd51-792f-4845-bead-9b4dfca2ff2c --- Game/Code/Classes/UAudioPlayback_SoftMixer.pas | 1793 ++++++++++++------------ 1 file changed, 899 insertions(+), 894 deletions(-) (limited to 'Game/Code') diff --git a/Game/Code/Classes/UAudioPlayback_SoftMixer.pas b/Game/Code/Classes/UAudioPlayback_SoftMixer.pas index 29983778..75fddf95 100644 --- a/Game/Code/Classes/UAudioPlayback_SoftMixer.pas +++ b/Game/Code/Classes/UAudioPlayback_SoftMixer.pas @@ -1,894 +1,899 @@ -unit UAudioPlayback_SoftMixer; - -interface - -{$IFDEF FPC} - {$MODE Delphi} -{$ENDIF} - -{$I switches.inc} - - -uses - Classes, - SysUtils, - sdl, - UMusic; - -type - TAudioPlayback_SoftMixer = class; - - TSoftMixerPlaybackStream = class(TAudioPlaybackStream) - private - Engine: TAudioPlayback_SoftMixer; - - DecodeStream: TAudioDecodeStream; - SampleBuffer : PChar; - SampleBufferCount: integer; // number of available bytes in SampleBuffer - SampleBufferPos : cardinal; - BytesAvail: integer; - cvt: TSDL_AudioCVT; - - Status: TStreamStatus; - Loop: boolean; - _volume: integer; - - InternalLock: PSDL_Mutex; - - procedure Reset(); - - class function ConvertAudioFormatToSDL(fmt: TAudioSampleFormat): UInt16; - function InitFormatConversion(): boolean; - - procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} - procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} - public - constructor Create(Engine: TAudioPlayback_SoftMixer); - destructor Destroy(); override; - - function SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; - - procedure Play(); override; - procedure Pause(); override; - procedure Stop(); override; - procedure Close(); override; - function GetLoop(): boolean; override; - procedure SetLoop(Enabled: boolean); override; - function GetLength(): real; override; - function GetStatus(): TStreamStatus; override; - - function IsLoaded(): boolean; - - function GetVolume(): integer; override; - procedure SetVolume(Volume: integer); override; - - // functions delegated to the decode stream - function GetPosition: real; - procedure SetPosition(Time: real); - function ReadData(Buffer: PChar; BufSize: integer): integer; - - function GetPCMData(var data: TPCMData): Cardinal; - procedure GetFFTData(var data: TFFTData); - end; - - TAudioMixerStream = class - private - activeStreams: TList; - mixerBuffer: PChar; - internalLock: PSDL_Mutex; - - _volume: integer; - - procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} - procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} - - function GetVolume(): integer; - procedure SetVolume(volume: integer); - public - constructor Create(); - destructor Destroy(); override; - procedure AddStream(stream: TAudioPlaybackStream); - procedure RemoveStream(stream: TAudioPlaybackStream); - function ReadData(Buffer: PChar; BufSize: integer): integer; - - property Volume: integer READ GetVolume WRITE SetVolume; - end; - - TAudioPlayback_SoftMixer = class( TInterfacedObject, IAudioPlayback ) - private - MusicStream: TSoftMixerPlaybackStream; - MixerStream: TAudioMixerStream; - protected - FormatInfo: TAudioFormatInfo; - - function InitializeAudioPlaybackEngine(): boolean; virtual; abstract; - function StartAudioPlaybackEngine(): boolean; virtual; abstract; - procedure StopAudioPlaybackEngine(); virtual; abstract; - procedure AudioCallback(buffer: PChar; size: integer); {$IFDEF HasInline}inline;{$ENDIF} - public - function GetName: String; virtual; abstract; - - function InitializePlayback(): boolean; - destructor Destroy; override; - - function Load(const Filename: String): TSoftMixerPlaybackStream; - - procedure SetVolume(Volume: integer); - procedure SetMusicVolume(Volume: integer); - procedure SetLoop(Enabled: boolean); - function Open(const Filename: string): boolean; // true if succeed - procedure Rewind; - procedure SetPosition(Time: real); - procedure Play; - procedure Pause; - - procedure Stop; - procedure Close; - function Finished: boolean; - function Length: real; - function GetPosition: real; - - // Equalizer - procedure GetFFTData(var data: TFFTData); - - // Interface for Visualizer - function GetPCMData(var data: TPCMData): Cardinal; - - function GetMixer(): TAudioMixerStream; {$IFDEF HasInline}inline;{$ENDIF} - function GetAudioFormatInfo(): TAudioFormatInfo; - - // Sounds - function OpenSound(const Filename: String): TAudioPlaybackStream; - procedure PlaySound(stream: TAudioPlaybackStream); - procedure StopSound(stream: TAudioPlaybackStream); - end; - -implementation - -uses - Math, - //samplerate, - UFFT, - ULog, - UIni, - UMain; - -{ TAudioMixerStream } - -constructor TAudioMixerStream.Create(); -begin - activeStreams := TList.Create; - internalLock := SDL_CreateMutex(); - _volume := 100; -end; - -destructor TAudioMixerStream.Destroy(); -begin - if assigned(mixerBuffer) then - Freemem(mixerBuffer); - activeStreams.Free; - SDL_DestroyMutex(internalLock); -end; - -procedure TAudioMixerStream.Lock(); -begin - SDL_mutexP(internalLock); -end; - -procedure TAudioMixerStream.Unlock(); -begin - SDL_mutexV(internalLock); -end; - -function TAudioMixerStream.GetVolume(): integer; -begin - Lock(); - result := _volume; - Unlock(); -end; - -procedure TAudioMixerStream.SetVolume(volume: integer); -begin - Lock(); - _volume := 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: PChar; BufSize: integer): integer; -var - i: integer; - size: integer; - stream: TSoftMixerPlaybackStream; - appVolume: single; - needsPacking: boolean; -begin - result := BufSize; - - // zero target-buffer (silence) - FillChar(Buffer^, BufSize, 0); - - // resize mixer-buffer if necessary - ReallocMem(mixerBuffer, BufSize); - if not assigned(mixerBuffer) then - Exit; - - Lock(); - - // calc application volume - // use _volume instead of Volume to prevent recursive locking - appVolume := _volume / 100 * SDL_MIX_MAXVOLUME; - - 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 := TSoftMixerPlaybackStream(activeStreams[i]); - // fetch data from current stream - size := stream.ReadData(mixerBuffer, BufSize); - if (size > 0) then - begin - SDL_MixAudio(PUInt8(Buffer), PUInt8(mixerBuffer), size, - Trunc(appVolume * stream.Volume / 100)); - end; - end; - - // remove nil-pointers from list - if (needsPacking) then - begin - activeStreams.Pack(); - end; - - Unlock(); -end; - - -{ TSoftMixerPlaybackStream } - -constructor TSoftMixerPlaybackStream.Create(Engine: TAudioPlayback_SoftMixer); -begin - inherited Create(); - Self.Engine := Engine; - internalLock := SDL_CreateMutex(); - Reset(); -end; - -destructor TSoftMixerPlaybackStream.Destroy(); -begin - Close(); - SDL_DestroyMutex(internalLock); - inherited Destroy(); -end; - -procedure TSoftMixerPlaybackStream.Reset(); -begin - Stop(); - Loop := false; - // TODO: use DecodeStream.Unref() instead of Free(); - FreeAndNil(DecodeStream); - FreeMem(SampleBuffer); - SampleBuffer := nil; - SampleBufferPos := 0; - BytesAvail := 0; - _volume := 0; -end; - -procedure TSoftMixerPlaybackStream.Lock(); -begin - SDL_mutexP(internalLock); -end; - -procedure TSoftMixerPlaybackStream.Unlock(); -begin - SDL_mutexV(internalLock); -end; - -class function TSoftMixerPlaybackStream.ConvertAudioFormatToSDL(fmt: TAudioSampleFormat): UInt16; -begin - case fmt of - asfU8: Result := AUDIO_U8; - asfS8: Result := AUDIO_S8; - asfU16LSB: Result := AUDIO_U16LSB; - asfS16LSB: Result := AUDIO_S16LSB; - asfU16MSB: Result := AUDIO_U16MSB; - asfS16MSB: Result := AUDIO_S16MSB; - asfU16: Result := AUDIO_U16; - asfS16: Result := AUDIO_S16; - else Result := 0; - end; -end; - -function TSoftMixerPlaybackStream.InitFormatConversion(): boolean; -var - err: integer; - srcFormat: UInt16; - dstFormat: UInt16; - srcFormatInfo: TAudioFormatInfo; - dstFormatInfo: TAudioFormatInfo; -begin - Result := false; - - srcFormatInfo := DecodeStream.GetAudioFormatInfo(); - dstFormatInfo := Engine.GetAudioFormatInfo(); - - srcFormat := ConvertAudioFormatToSDL(srcFormatInfo.Format); - dstFormat := ConvertAudioFormatToSDL(dstFormatInfo.Format); - - if ((srcFormat = 0) or (dstFormat = 0)) then - begin - Log.LogError('Audio-format not supported by SDL', 'TSoftMixerPlaybackStream.InitFormatConversion'); - Exit; - end; - - if (SDL_BuildAudioCVT(@cvt, - srcFormat, srcFormatInfo.Channels, srcFormatInfo.SampleRate, - dstFormat, dstFormatInfo.Channels, dstFormatInfo.SampleRate) = -1) then - begin - Log.LogError(SDL_GetError(), 'TSoftMixerPlaybackStream.InitFormatConversion'); - Exit; - end; - - Result := true; -end; - -function TSoftMixerPlaybackStream.SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; -begin - result := false; - - Reset(); - - if not assigned(decodeStream) then - Exit; - Self.DecodeStream := decodeStream; - if not InitFormatConversion() then - Exit; - - _volume := 100; - - result := true; -end; - -procedure TSoftMixerPlaybackStream.Close(); -begin - Reset(); -end; - -procedure TSoftMixerPlaybackStream.Play(); -var - mixer: TAudioMixerStream; -begin - if (status <> ssPaused) then - begin - // rewind - if assigned(DecodeStream) then - DecodeStream.Position := 0; - end; - status := ssPlaying; - - mixer := Engine.GetMixer(); - if (mixer <> nil) then - mixer.AddStream(Self); -end; - -procedure TSoftMixerPlaybackStream.Pause(); -var - mixer: TAudioMixerStream; -begin - status := ssPaused; - - mixer := Engine.GetMixer(); - if (mixer <> nil) then - mixer.RemoveStream(Self); -end; - -procedure TSoftMixerPlaybackStream.Stop(); -var - mixer: TAudioMixerStream; -begin - status := ssStopped; - - mixer := Engine.GetMixer(); - if (mixer <> nil) then - mixer.RemoveStream(Self); -end; - -function TSoftMixerPlaybackStream.IsLoaded(): boolean; -begin - result := assigned(DecodeStream); -end; - -function TSoftMixerPlaybackStream.GetLoop(): boolean; -begin - result := Loop; -end; - -procedure TSoftMixerPlaybackStream.SetLoop(Enabled: boolean); -begin - Loop := Enabled; -end; - -function TSoftMixerPlaybackStream.GetLength(): real; -begin - if assigned(DecodeStream) then - result := DecodeStream.Length - else - result := -1; -end; - -function TSoftMixerPlaybackStream.GetStatus(): TStreamStatus; -begin - result := status; -end; - -{* - * Note: 44.1kHz to 48kHz conversion or vice versa is not supported - * by SDL at the moment. No conversion takes place in this cases. - * This is because SDL just converts differences in powers of 2. - * So the result might not be that accurate. Although this is not - * audible in most cases it needs synchronization with the video - * or the lyrics timer. - * Using libsamplerate might give better results. - *} -function TSoftMixerPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer; -var - decodeBufSize: integer; - sampleBufSize: integer; - nBytesDecoded: integer; - frameSize: integer; - remFrameBytes: integer; - copyCnt: integer; - BytesNeeded: integer; -begin - Result := -1; - - BytesNeeded := BufSize; - - // copy remaining data from the last call to the result-buffer - if (BytesAvail > 0) then - begin - copyCnt := Min(BufSize, BytesAvail); - Move(SampleBuffer[SampleBufferPos], Buffer[0], copyCnt); - Dec(BytesAvail, copyCnt); - Dec(BytesNeeded, copyCnt); - if (BytesNeeded = 0) then - begin - // Result-Buffer is full -> no need to decode more data. - // The sample-buffer might even contain some data for the next call - Inc(SampleBufferPos, copyCnt); - Result := BufSize; - Exit; - end; - end; - - if not assigned(DecodeStream) then - Exit; - - // calc number of bytes to decode - decodeBufSize := Ceil(BufSize / cvt.len_ratio); - // assure that the decode-size is a multiple of the frame size - frameSize := DecodeStream.GetAudioFormatInfo().FrameSize; - remFrameBytes := decodeBufSize mod frameSize; - if (remFrameBytes > 0) then - decodeBufSize := decodeBufSize + (frameSize - remFrameBytes); - - Lock(); - try - // calc buffer size - sampleBufSize := decodeBufSize * cvt.len_mult; - - // resize buffer if necessary. - // The required buffer-size will be smaller than the result-buffer - // in most cases (if the decoded signal is mono or has a lesser bitrate). - // If the output-rate is 44.1kHz and the decode-rate is 48kHz or 96kHz it - // will be ~1.09 or ~2.18 times bigger. Those extra memory consumption - // should be reasonable. If not we should call TDecodeStream.ReadData() - // multiple times. - // Note: we do not decrease the buffer by the count of bytes used from - // the previous call of this function (bytesAvail). Otherwise the - // buffer will be reallocated each time this function is called just to - // add or remove a few bytes from the buffer. - // By not doing this the buffer's size should be rather stable and it - // will not be reallocated/resized at all if the BufSize params does not - // change in consecutive calls. - ReallocMem(SampleBuffer, sampleBufSize); - if not assigned(SampleBuffer) then - Exit; - - // decode data - nBytesDecoded := DecodeStream.ReadData(SampleBuffer, decodeBufSize); - if (nBytesDecoded = -1) then - Exit; - - // end-of-file reached -> stop playback - if (DecodeStream.EOF) then - Stop(); - - // resample decoded data - cvt.buf := PUint8(SampleBuffer); - cvt.len := nBytesDecoded; - if (SDL_ConvertAudio(@cvt) = -1) then - Exit; - - SampleBufferCount := cvt.len_cvt; - finally - Unlock(); - end; - - BytesAvail := SampleBufferCount; - SampleBufferPos := 0; - - // copy data to result buffer - copyCnt := Min(BytesNeeded, BytesAvail); - Move(SampleBuffer[0], Buffer[BufSize - BytesNeeded], copyCnt); - Dec(BytesAvail, copyCnt); - Dec(BytesNeeded, copyCnt); - Inc(SampleBufferPos, copyCnt); - - Result := BufSize - BytesNeeded; -end; - -(* TODO: libsamplerate support -function TSoftMixerPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer; -var - convState: PSRC_STATE; - convData: SRC_DATA; - error: integer; -begin - // Note: needs mono->stereo conversion, multi-channel->stereo, etc. - // maybe we should use SDL for the channel-conversion stuff - // and use libsamplerate afterwards for the frequency-conversion - - //convState := src_new(SRC_SINC_MEDIUM_QUALITY, 2, @error); - //src_short_to_float_array(input, output, len); - convData. - if (src_process(convState, @convData) <> 0) then - begin - Log.LogError(src_strerror(src_error(convState)), 'TSoftMixerPlaybackStream.ReadData'); - Exit; - end; - src_float_to_short_array(); - //src_delete(convState); -end; -*) - -function TSoftMixerPlaybackStream.GetPCMData(var data: TPCMData): Cardinal; -var size: 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); - - Lock(); - Result := Min(SizeOf(data), SampleBufferCount); - if (Result > 0) then - begin - Move(SampleBuffer[0], data[0], Result); - end; - Unlock(); -*) -end; - -procedure TSoftMixerPlaybackStream.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 := Engine.GetAudioFormatInfo(); - - DataIn := AllocMem(FFTSize * SizeOf(Single)); - if (DataIn = nil) then - Exit; - - Lock(); - // TODO: We just use the first Frames frames, the others are ignored. - // This is OK for the equalizer display but not if we want to use - // this function for voice-analysis someday (I don't think we want). - 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; - Unlock(); - - 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 - // TODO: this might need some work - data[i] := Sqrt(data[i]) / 100; - end; -end; - -function TSoftMixerPlaybackStream.GetPosition: real; -begin - if assigned(DecodeStream) then - result := DecodeStream.Position - else - result := -1; -end; - -procedure TSoftMixerPlaybackStream.SetPosition(Time: real); -begin - if assigned(DecodeStream) then - DecodeStream.Position := Time; -end; - -function TSoftMixerPlaybackStream.GetVolume(): integer; -begin - result := _volume; -end; - -procedure TSoftMixerPlaybackStream.SetVolume(volume: integer); -begin - // clamp volume - if (volume > 100) then - _volume := 100 - else if (volume < 0) then - _volume := 0 - else - _volume := volume; -end; - - -{ TAudioPlayback_SoftMixer } - -function TAudioPlayback_SoftMixer.InitializePlayback: boolean; -begin - result := false; - - //Log.LogStatus('InitializePlayback', 'UAudioPlayback_SoftMixer'); - - if(not InitializeAudioPlaybackEngine()) then - Exit; - - MixerStream := TAudioMixerStream.Create; - - if(not StartAudioPlaybackEngine()) then - Exit; - - result := true; -end; - -destructor TAudioPlayback_SoftMixer.Destroy; -begin - StopAudioPlaybackEngine(); - - FreeAndNil(MusicStream); - FreeAndNil(MixerStream); - FreeAndNil(FormatInfo); - - inherited Destroy(); -end; - -procedure TAudioPlayback_SoftMixer.AudioCallback(buffer: PChar; 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.Load(const Filename: String): TSoftMixerPlaybackStream; -var - decodeStream: TAudioDecodeStream; - playbackStream: TSoftMixerPlaybackStream; -begin - Result := nil; - - decodeStream := AudioDecoder.Open(Filename); - if not assigned(decodeStream) then - begin - Log.LogStatus('LoadSoundFromFile: Sound not found "' + Filename + '"', 'UAudioPlayback_SoftMixer'); - Exit; - end; - - playbackStream := TSoftMixerPlaybackStream.Create(Self); - if (not playbackStream.SetDecodeStream(decodeStream)) then - Exit; - - result := playbackStream; -end; - -procedure TAudioPlayback_SoftMixer.SetVolume(Volume: integer); -begin - // sets volume only for this application - MixerStream.Volume := Volume; -end; - -procedure TAudioPlayback_SoftMixer.SetMusicVolume(Volume: Integer); -begin - if assigned(MusicStream) then - MusicStream.Volume := Volume; -end; - -procedure TAudioPlayback_SoftMixer.SetLoop(Enabled: boolean); -begin - if assigned(MusicStream) then - MusicStream.SetLoop(Enabled); -end; - -function TAudioPlayback_SoftMixer.Open(const Filename: string): boolean; -var - decodeStream: TAudioDecodeStream; -begin - Result := false; - - // free old MusicStream - MusicStream.Free(); - // and load new one - MusicStream := Load(Filename); - if not assigned(MusicStream) then - Exit; - - //Set Max Volume - SetMusicVolume(100); - - Result := true; -end; - -procedure TAudioPlayback_SoftMixer.Rewind; -begin - SetPosition(0); -end; - -procedure TAudioPlayback_SoftMixer.SetPosition(Time: real); -begin - if assigned(MusicStream) then - MusicStream.SetPosition(Time); -end; - -function TAudioPlayback_SoftMixer.GetPosition: real; -begin - if assigned(MusicStream) then - Result := MusicStream.GetPosition() - else - Result := -1; -end; - -function TAudioPlayback_SoftMixer.Length: real; -begin - if assigned(MusicStream) then - Result := MusicStream.GetLength() - else - Result := -1; -end; - -procedure TAudioPlayback_SoftMixer.Play; -begin - if assigned(MusicStream) then - MusicStream.Play(); -end; - -procedure TAudioPlayback_SoftMixer.Pause; -begin - if assigned(MusicStream) then - MusicStream.Pause(); -end; - -procedure TAudioPlayback_SoftMixer.Stop; -begin - if assigned(MusicStream) then - MusicStream.Stop(); -end; - -procedure TAudioPlayback_SoftMixer.Close; -begin - if assigned(MusicStream) then - begin - MusicStream.Close(); - end; -end; - -function TAudioPlayback_SoftMixer.Finished: boolean; -begin - if assigned(MusicStream) then - Result := (MusicStream.GetStatus() = ssStopped) - else - Result := true; -end; - -//Equalizer -procedure TAudioPlayback_SoftMixer.GetFFTData(var data: TFFTData); -begin - if assigned(MusicStream) then - MusicStream.GetFFTData(data); -end; - -// Interface for Visualizer -function TAudioPlayback_SoftMixer.GetPCMData(var data: TPCMData): Cardinal; -begin - if assigned(MusicStream) then - Result := MusicStream.GetPCMData(data) - else - Result := 0; -end; - -function TAudioPlayback_SoftMixer.OpenSound(const Filename: String): TAudioPlaybackStream; -begin - Result := Load(Filename); -end; - -procedure TAudioPlayback_SoftMixer.PlaySound(stream: TAudioPlaybackStream); -begin - if assigned(stream) then - stream.Play(); -end; - -procedure TAudioPlayback_SoftMixer.StopSound(stream: TAudioPlaybackStream); -begin - if assigned(stream) then - stream.Stop(); -end; - - -end. +unit UAudioPlayback_SoftMixer; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + + +uses + Classes, + SysUtils, + sdl, + UMusic; + +type + TAudioPlayback_SoftMixer = class; + + TSoftMixerPlaybackStream = class(TAudioPlaybackStream) + private + Engine: TAudioPlayback_SoftMixer; + + DecodeStream: TAudioDecodeStream; + SampleBuffer : PChar; + SampleBufferCount: integer; // number of available bytes in SampleBuffer + SampleBufferPos : cardinal; + BytesAvail: integer; + cvt: TSDL_AudioCVT; + + Status: TStreamStatus; + Loop: boolean; + _volume: integer; + + InternalLock: PSDL_Mutex; + + procedure Reset(); + + class function ConvertAudioFormatToSDL(fmt: TAudioSampleFormat): UInt16; + function InitFormatConversion(): boolean; + + procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} + procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} + public + constructor Create(Engine: TAudioPlayback_SoftMixer); + destructor Destroy(); override; + + function SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; + + procedure Play(); override; + procedure Pause(); override; + procedure Stop(); override; + procedure Close(); override; + function GetLoop(): boolean; override; + procedure SetLoop(Enabled: boolean); override; + function GetLength(): real; override; + function GetStatus(): TStreamStatus; override; + + function IsLoaded(): boolean; + + function GetVolume(): integer; override; + procedure SetVolume(Volume: integer); override; + + // functions delegated to the decode stream + function GetPosition: real; + procedure SetPosition(Time: real); + function ReadData(Buffer: PChar; BufSize: integer): integer; + + function GetPCMData(var data: TPCMData): Cardinal; + procedure GetFFTData(var data: TFFTData); + end; + + TAudioMixerStream = class + private + activeStreams: TList; + mixerBuffer: PChar; + internalLock: PSDL_Mutex; + + _volume: integer; + + procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} + procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} + + function GetVolume(): integer; + procedure SetVolume(volume: integer); + public + constructor Create(); + destructor Destroy(); override; + procedure AddStream(stream: TAudioPlaybackStream); + procedure RemoveStream(stream: TAudioPlaybackStream); + function ReadData(Buffer: PChar; BufSize: integer): integer; + + property Volume: integer READ GetVolume WRITE SetVolume; + end; + + TAudioPlayback_SoftMixer = class( TInterfacedObject, IAudioPlayback ) + private + MusicStream: TSoftMixerPlaybackStream; + MixerStream: TAudioMixerStream; + protected + FormatInfo: TAudioFormatInfo; + + function InitializeAudioPlaybackEngine(): boolean; virtual; abstract; + function StartAudioPlaybackEngine(): boolean; virtual; abstract; + procedure StopAudioPlaybackEngine(); virtual; abstract; + procedure AudioCallback(buffer: PChar; size: integer); {$IFDEF HasInline}inline;{$ENDIF} + public + function GetName: String; virtual; abstract; + + function InitializePlayback(): boolean; + destructor Destroy; override; + + function Load(const Filename: String): TSoftMixerPlaybackStream; + + procedure SetVolume(Volume: integer); + procedure SetMusicVolume(Volume: integer); + procedure SetLoop(Enabled: boolean); + function Open(const Filename: string): boolean; // true if succeed + procedure Rewind; + procedure SetPosition(Time: real); + procedure Play; + procedure Pause; + + procedure Stop; + procedure Close; + function Finished: boolean; + function Length: real; + function GetPosition: real; + + // Equalizer + procedure GetFFTData(var data: TFFTData); + + // Interface for Visualizer + function GetPCMData(var data: TPCMData): Cardinal; + + function GetMixer(): TAudioMixerStream; {$IFDEF HasInline}inline;{$ENDIF} + function GetAudioFormatInfo(): TAudioFormatInfo; + + // Sounds + function OpenSound(const Filename: String): TAudioPlaybackStream; + procedure PlaySound(stream: TAudioPlaybackStream); + procedure StopSound(stream: TAudioPlaybackStream); + end; + +implementation + +uses + Math, + //samplerate, + UFFT, + ULog, + UIni, + UMain; + +{ TAudioMixerStream } + +constructor TAudioMixerStream.Create(); +begin + activeStreams := TList.Create; + internalLock := SDL_CreateMutex(); + _volume := 100; +end; + +destructor TAudioMixerStream.Destroy(); +begin + if assigned(mixerBuffer) then + Freemem(mixerBuffer); + activeStreams.Free; + SDL_DestroyMutex(internalLock); +end; + +procedure TAudioMixerStream.Lock(); +begin + SDL_mutexP(internalLock); +end; + +procedure TAudioMixerStream.Unlock(); +begin + SDL_mutexV(internalLock); +end; + +function TAudioMixerStream.GetVolume(): integer; +begin + Lock(); + result := _volume; + Unlock(); +end; + +procedure TAudioMixerStream.SetVolume(volume: integer); +begin + Lock(); + _volume := 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: PChar; BufSize: integer): integer; +var + i: integer; + size: integer; + stream: TSoftMixerPlaybackStream; + appVolume: single; + needsPacking: boolean; +begin + result := BufSize; + + // zero target-buffer (silence) + FillChar(Buffer^, BufSize, 0); + + // resize mixer-buffer if necessary + ReallocMem(mixerBuffer, BufSize); + if not assigned(mixerBuffer) then + Exit; + + Lock(); + + // calc application volume + // use _volume instead of Volume to prevent recursive locking + appVolume := _volume / 100 * SDL_MIX_MAXVOLUME; + + 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 := TSoftMixerPlaybackStream(activeStreams[i]); + // fetch data from current stream + size := stream.ReadData(mixerBuffer, BufSize); + if (size > 0) then + begin + SDL_MixAudio(PUInt8(Buffer), PUInt8(mixerBuffer), size, + Trunc(appVolume * stream.Volume / 100)); + end; + end; + + // remove nil-pointers from list + if (needsPacking) then + begin + activeStreams.Pack(); + end; + + Unlock(); +end; + + +{ TSoftMixerPlaybackStream } + +constructor TSoftMixerPlaybackStream.Create(Engine: TAudioPlayback_SoftMixer); +begin + inherited Create(); + Self.Engine := Engine; + internalLock := SDL_CreateMutex(); + Reset(); +end; + +destructor TSoftMixerPlaybackStream.Destroy(); +begin + Close(); + SDL_DestroyMutex(internalLock); + inherited Destroy(); +end; + +procedure TSoftMixerPlaybackStream.Reset(); +begin + Stop(); + Loop := false; + // TODO: use DecodeStream.Unref() instead of Free(); + FreeAndNil(DecodeStream); + FreeMem(SampleBuffer); + SampleBuffer := nil; + SampleBufferPos := 0; + BytesAvail := 0; + _volume := 0; +end; + +procedure TSoftMixerPlaybackStream.Lock(); +begin + SDL_mutexP(internalLock); +end; + +procedure TSoftMixerPlaybackStream.Unlock(); +begin + SDL_mutexV(internalLock); +end; + +class function TSoftMixerPlaybackStream.ConvertAudioFormatToSDL(fmt: TAudioSampleFormat): UInt16; +begin + case fmt of + asfU8: Result := AUDIO_U8; + asfS8: Result := AUDIO_S8; + asfU16LSB: Result := AUDIO_U16LSB; + asfS16LSB: Result := AUDIO_S16LSB; + asfU16MSB: Result := AUDIO_U16MSB; + asfS16MSB: Result := AUDIO_S16MSB; + asfU16: Result := AUDIO_U16; + asfS16: Result := AUDIO_S16; + else Result := 0; + end; +end; + +function TSoftMixerPlaybackStream.InitFormatConversion(): boolean; +var + err: integer; + srcFormat: UInt16; + dstFormat: UInt16; + srcFormatInfo: TAudioFormatInfo; + dstFormatInfo: TAudioFormatInfo; +begin + Result := false; + + srcFormatInfo := DecodeStream.GetAudioFormatInfo(); + dstFormatInfo := Engine.GetAudioFormatInfo(); + + srcFormat := ConvertAudioFormatToSDL(srcFormatInfo.Format); + dstFormat := ConvertAudioFormatToSDL(dstFormatInfo.Format); + + if ((srcFormat = 0) or (dstFormat = 0)) then + begin + Log.LogError('Audio-format not supported by SDL', 'TSoftMixerPlaybackStream.InitFormatConversion'); + Exit; + end; + + if (SDL_BuildAudioCVT(@cvt, + srcFormat, srcFormatInfo.Channels, srcFormatInfo.SampleRate, + dstFormat, dstFormatInfo.Channels, dstFormatInfo.SampleRate) = -1) then + begin + Log.LogError(SDL_GetError(), 'TSoftMixerPlaybackStream.InitFormatConversion'); + Exit; + end; + + Result := true; +end; + +function TSoftMixerPlaybackStream.SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; +begin + result := false; + + Reset(); + + if not assigned(decodeStream) then + Exit; + Self.DecodeStream := decodeStream; + if not InitFormatConversion() then + Exit; + + _volume := 100; + + result := true; +end; + +procedure TSoftMixerPlaybackStream.Close(); +begin + Reset(); +end; + +procedure TSoftMixerPlaybackStream.Play(); +var + mixer: TAudioMixerStream; +begin + if (status <> ssPaused) then + begin + // rewind + if assigned(DecodeStream) then + DecodeStream.Position := 0; + end; + status := ssPlaying; + + mixer := Engine.GetMixer(); + if (mixer <> nil) then + mixer.AddStream(Self); +end; + +procedure TSoftMixerPlaybackStream.Pause(); +var + mixer: TAudioMixerStream; +begin + status := ssPaused; + + mixer := Engine.GetMixer(); + if (mixer <> nil) then + mixer.RemoveStream(Self); +end; + +procedure TSoftMixerPlaybackStream.Stop(); +var + mixer: TAudioMixerStream; +begin + status := ssStopped; + + mixer := Engine.GetMixer(); + if (mixer <> nil) then + mixer.RemoveStream(Self); +end; + +function TSoftMixerPlaybackStream.IsLoaded(): boolean; +begin + result := assigned(DecodeStream); +end; + +function TSoftMixerPlaybackStream.GetLoop(): boolean; +begin + result := Loop; +end; + +procedure TSoftMixerPlaybackStream.SetLoop(Enabled: boolean); +begin + Loop := Enabled; +end; + +function TSoftMixerPlaybackStream.GetLength(): real; +begin + if assigned(DecodeStream) then + result := DecodeStream.Length + else + result := -1; +end; + +function TSoftMixerPlaybackStream.GetStatus(): TStreamStatus; +begin + result := status; +end; + +{* + * Note: 44.1kHz to 48kHz conversion or vice versa is not supported + * by SDL at the moment. No conversion takes place in this cases. + * This is because SDL just converts differences in powers of 2. + * So the result might not be that accurate. Although this is not + * audible in most cases it needs synchronization with the video + * or the lyrics timer. + * Using libsamplerate might give better results. + *} +function TSoftMixerPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer; +var + decodeBufSize: integer; + sampleBufSize: integer; + nBytesDecoded: integer; + frameSize: integer; + remFrameBytes: integer; + copyCnt: integer; + BytesNeeded: integer; +begin + Result := -1; + + BytesNeeded := BufSize; + + // copy remaining data from the last call to the result-buffer + if (BytesAvail > 0) then + begin + copyCnt := Min(BufSize, BytesAvail); + Move(SampleBuffer[SampleBufferPos], Buffer[0], copyCnt); + Dec(BytesAvail, copyCnt); + Dec(BytesNeeded, copyCnt); + if (BytesNeeded = 0) then + begin + // Result-Buffer is full -> no need to decode more data. + // The sample-buffer might even contain some data for the next call + Inc(SampleBufferPos, copyCnt); + Result := BufSize; + Exit; + end; + end; + + if not assigned(DecodeStream) then + Exit; + + // calc number of bytes to decode + decodeBufSize := Ceil(BufSize / cvt.len_ratio); + // assure that the decode-size is a multiple of the frame size + frameSize := DecodeStream.GetAudioFormatInfo().FrameSize; + remFrameBytes := decodeBufSize mod frameSize; + if (remFrameBytes > 0) then + decodeBufSize := decodeBufSize + (frameSize - remFrameBytes); + + Lock(); + try + // calc buffer size + sampleBufSize := decodeBufSize * cvt.len_mult; + + // resize buffer if necessary. + // The required buffer-size will be smaller than the result-buffer + // in most cases (if the decoded signal is mono or has a lesser bitrate). + // If the output-rate is 44.1kHz and the decode-rate is 48kHz or 96kHz it + // will be ~1.09 or ~2.18 times bigger. Those extra memory consumption + // should be reasonable. If not we should call TDecodeStream.ReadData() + // multiple times. + // Note: we do not decrease the buffer by the count of bytes used from + // the previous call of this function (bytesAvail). Otherwise the + // buffer will be reallocated each time this function is called just to + // add or remove a few bytes from the buffer. + // By not doing this the buffer's size should be rather stable and it + // will not be reallocated/resized at all if the BufSize params does not + // change in consecutive calls. + ReallocMem(SampleBuffer, sampleBufSize); + if not assigned(SampleBuffer) then + Exit; + + // decode data + nBytesDecoded := DecodeStream.ReadData(SampleBuffer, decodeBufSize); + if (nBytesDecoded = -1) then + Exit; + + // end-of-file reached -> stop playback + if (DecodeStream.EOF) then + Stop(); + + // resample decoded data + cvt.buf := PUint8(SampleBuffer); + cvt.len := nBytesDecoded; + if (SDL_ConvertAudio(@cvt) = -1) then + Exit; + + SampleBufferCount := cvt.len_cvt; + finally + Unlock(); + end; + + BytesAvail := SampleBufferCount; + SampleBufferPos := 0; + + // copy data to result buffer + copyCnt := Min(BytesNeeded, BytesAvail); + Move(SampleBuffer[0], Buffer[BufSize - BytesNeeded], copyCnt); + Dec(BytesAvail, copyCnt); + Dec(BytesNeeded, copyCnt); + Inc(SampleBufferPos, copyCnt); + + Result := BufSize - BytesNeeded; +end; + +(* TODO: libsamplerate support +function TSoftMixerPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer; +var + convState: PSRC_STATE; + convData: SRC_DATA; + error: integer; +begin + // Note: needs mono->stereo conversion, multi-channel->stereo, etc. + // maybe we should use SDL for the channel-conversion stuff + // and use libsamplerate afterwards for the frequency-conversion + + //convState := src_new(SRC_SINC_MEDIUM_QUALITY, 2, @error); + //src_short_to_float_array(input, output, len); + convData. + if (src_process(convState, @convData) <> 0) then + begin + Log.LogError(src_strerror(src_error(convState)), 'TSoftMixerPlaybackStream.ReadData'); + Exit; + end; + src_float_to_short_array(); + //src_delete(convState); +end; +*) + +function TSoftMixerPlaybackStream.GetPCMData(var data: TPCMData): Cardinal; +var + nBytes: 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. + + Lock(); + nBytes := Min(SizeOf(data), SampleBufferCount); + if (nBytes > 0) then + begin + Move(SampleBuffer[0], data, nBytes); + end; + Unlock(); + + Result := nBytes div SizeOf(TPCMStereoSample); +end; + +procedure TSoftMixerPlaybackStream.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 := Engine.GetAudioFormatInfo(); + + DataIn := AllocMem(FFTSize * SizeOf(Single)); + if (DataIn = nil) then + Exit; + + Lock(); + // TODO: We just use the first Frames frames, the others are ignored. + // This is OK for the equalizer display but not if we want to use + // this function for voice-analysis someday (I don't think we want). + 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; + Unlock(); + + 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 + // TODO: this might need some work + data[i] := Sqrt(data[i]) / 100; + end; +end; + +function TSoftMixerPlaybackStream.GetPosition: real; +begin + if assigned(DecodeStream) then + result := DecodeStream.Position + else + result := -1; +end; + +procedure TSoftMixerPlaybackStream.SetPosition(Time: real); +begin + if assigned(DecodeStream) then + DecodeStream.Position := Time; +end; + +function TSoftMixerPlaybackStream.GetVolume(): integer; +begin + result := _volume; +end; + +procedure TSoftMixerPlaybackStream.SetVolume(volume: integer); +begin + // clamp volume + if (volume > 100) then + _volume := 100 + else if (volume < 0) then + _volume := 0 + else + _volume := volume; +end; + + +{ TAudioPlayback_SoftMixer } + +function TAudioPlayback_SoftMixer.InitializePlayback: boolean; +begin + result := false; + + //Log.LogStatus('InitializePlayback', 'UAudioPlayback_SoftMixer'); + + if(not InitializeAudioPlaybackEngine()) then + Exit; + + MixerStream := TAudioMixerStream.Create; + + if(not StartAudioPlaybackEngine()) then + Exit; + + result := true; +end; + +destructor TAudioPlayback_SoftMixer.Destroy; +begin + StopAudioPlaybackEngine(); + + FreeAndNil(MusicStream); + FreeAndNil(MixerStream); + FreeAndNil(FormatInfo); + + inherited Destroy(); +end; + +procedure TAudioPlayback_SoftMixer.AudioCallback(buffer: PChar; 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.Load(const Filename: String): TSoftMixerPlaybackStream; +var + decodeStream: TAudioDecodeStream; + playbackStream: TSoftMixerPlaybackStream; +begin + Result := nil; + + decodeStream := AudioDecoder.Open(Filename); + if not assigned(decodeStream) then + begin + Log.LogStatus('LoadSoundFromFile: Sound not found "' + Filename + '"', 'UAudioPlayback_SoftMixer'); + Exit; + end; + + playbackStream := TSoftMixerPlaybackStream.Create(Self); + if (not playbackStream.SetDecodeStream(decodeStream)) then + Exit; + + result := playbackStream; +end; + +procedure TAudioPlayback_SoftMixer.SetVolume(Volume: integer); +begin + // sets volume only for this application + MixerStream.Volume := Volume; +end; + +procedure TAudioPlayback_SoftMixer.SetMusicVolume(Volume: Integer); +begin + if assigned(MusicStream) then + MusicStream.Volume := Volume; +end; + +procedure TAudioPlayback_SoftMixer.SetLoop(Enabled: boolean); +begin + if assigned(MusicStream) then + MusicStream.SetLoop(Enabled); +end; + +function TAudioPlayback_SoftMixer.Open(const Filename: string): boolean; +var + decodeStream: TAudioDecodeStream; +begin + Result := false; + + // free old MusicStream + MusicStream.Free(); + // and load new one + MusicStream := Load(Filename); + if not assigned(MusicStream) then + Exit; + + //Set Max Volume + SetMusicVolume(100); + + Result := true; +end; + +procedure TAudioPlayback_SoftMixer.Rewind; +begin + SetPosition(0); +end; + +procedure TAudioPlayback_SoftMixer.SetPosition(Time: real); +begin + if assigned(MusicStream) then + MusicStream.SetPosition(Time); +end; + +function TAudioPlayback_SoftMixer.GetPosition: real; +begin + if assigned(MusicStream) then + Result := MusicStream.GetPosition() + else + Result := -1; +end; + +function TAudioPlayback_SoftMixer.Length: real; +begin + if assigned(MusicStream) then + Result := MusicStream.GetLength() + else + Result := -1; +end; + +procedure TAudioPlayback_SoftMixer.Play; +begin + if assigned(MusicStream) then + MusicStream.Play(); +end; + +procedure TAudioPlayback_SoftMixer.Pause; +begin + if assigned(MusicStream) then + MusicStream.Pause(); +end; + +procedure TAudioPlayback_SoftMixer.Stop; +begin + if assigned(MusicStream) then + MusicStream.Stop(); +end; + +procedure TAudioPlayback_SoftMixer.Close; +begin + if assigned(MusicStream) then + begin + MusicStream.Close(); + end; +end; + +function TAudioPlayback_SoftMixer.Finished: boolean; +begin + if assigned(MusicStream) then + Result := (MusicStream.GetStatus() = ssStopped) + else + Result := true; +end; + +//Equalizer +procedure TAudioPlayback_SoftMixer.GetFFTData(var data: TFFTData); +begin + if assigned(MusicStream) then + MusicStream.GetFFTData(data); +end; + +// Interface for Visualizer +function TAudioPlayback_SoftMixer.GetPCMData(var data: TPCMData): Cardinal; +begin + if assigned(MusicStream) then + Result := MusicStream.GetPCMData(data) + else + Result := 0; +end; + +function TAudioPlayback_SoftMixer.OpenSound(const Filename: String): TAudioPlaybackStream; +begin + Result := Load(Filename); +end; + +procedure TAudioPlayback_SoftMixer.PlaySound(stream: TAudioPlaybackStream); +begin + if assigned(stream) then + stream.Play(); +end; + +procedure TAudioPlayback_SoftMixer.StopSound(stream: TAudioPlaybackStream); +begin + if assigned(stream) then + stream.Stop(); +end; + + +end. -- cgit v1.2.3