unit UAudioPlayback_SoftMixer; interface {$IFDEF FPC} {$MODE Delphi} {$ENDIF} {$I switches.inc} uses Classes, SysUtils, sdl, UMusic; type TAudioPlayback_SoftMixer = class; TGenericPlaybackStream = 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 Engine: TAudioPlayback_SoftMixer; 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(Engine: TAudioPlayback_SoftMixer); 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: TGenericPlaybackStream; 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): TGenericPlaybackStream; 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; procedure MixBuffers(dst, src: PChar; size: Cardinal; volume: Integer); virtual; // 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(Engine: TAudioPlayback_SoftMixer); begin Self.Engine := Engine; 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: TGenericPlaybackStream; 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(); 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, BufSize); if (size > 0) then begin // mix stream-data with mixer-buffer // Note: use _volume (Application-Volume) instead of Volume to prevent recursive locking Engine.MixBuffers(Buffer, mixerBuffer, size, _volume * stream.Volume div 100); 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(); Reset(); end; destructor TGenericPlaybackStream.Destroy(); begin Close(); SDL_DestroyMutex(internalLock); inherited Destroy(); end; procedure TGenericPlaybackStream.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 TGenericPlaybackStream.Lock(); begin SDL_mutexP(internalLock); end; procedure TGenericPlaybackStream.Unlock(); begin SDL_mutexV(internalLock); end; class function TGenericPlaybackStream.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 TGenericPlaybackStream.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 TGenericPlaybackStream.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 TGenericPlaybackStream.Close(); begin Reset(); end; procedure TGenericPlaybackStream.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 TGenericPlaybackStream.Pause(); var mixer: TAudioMixerStream; begin status := ssPaused; mixer := Engine.GetMixer(); if (mixer <> nil) then mixer.RemoveStream(Self); end; procedure TGenericPlaybackStream.Stop(); var mixer: TAudioMixerStream; begin status := ssStopped; mixer := Engine.GetMixer(); if (mixer <> nil) then mixer.RemoveStream(Self); end; function TGenericPlaybackStream.IsLoaded(): boolean; begin result := assigned(DecodeStream); end; function TGenericPlaybackStream.GetLoop(): boolean; begin result := Loop; end; procedure TGenericPlaybackStream.SetLoop(Enabled: boolean); begin Loop := Enabled; end; function TGenericPlaybackStream.GetLength(): real; begin if assigned(DecodeStream) then result := DecodeStream.Length else result := -1; end; function TGenericPlaybackStream.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 TGenericPlaybackStream.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 TGenericPlaybackStream.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 TGenericPlaybackStream.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 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 := 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 TGenericPlaybackStream.GetPosition: real; begin if assigned(DecodeStream) then result := DecodeStream.Position else result := -1; end; procedure TGenericPlaybackStream.SetPosition(Time: real); begin if assigned(DecodeStream) then DecodeStream.Position := Time; end; function TGenericPlaybackStream.GetVolume(): integer; begin result := _volume; end; procedure TGenericPlaybackStream.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(Self); 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): TGenericPlaybackStream; var decodeStream: TAudioDecodeStream; playbackStream: TGenericPlaybackStream; 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 := TGenericPlaybackStream.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; procedure TAudioPlayback_SoftMixer.MixBuffers(dst, src: PChar; size: Cardinal; volume: Integer); var SampleIndex: Cardinal; SampleInt: Integer; SampleFlt: Single; begin // TODO: optimize this code, e.g. with assembler (MMX) SampleIndex := 0; case FormatInfo.Format of asfS16: begin while (SampleIndex < size) do begin // apply volume and sum with previous mixer value SampleInt := PSmallInt(@dst[SampleIndex])^ + PSmallInt(@src[SampleIndex])^ * volume div 100; // clip result if (SampleInt > High(SmallInt)) then SampleInt := High(SmallInt) else if (SampleInt < Low(SmallInt)) then SampleInt := Low(SmallInt); // assign result PSmallInt(@dst[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(@dst[SampleIndex])^ + PSingle(@src[SampleIndex])^ * volume/100; // clip result if (SampleFlt > 1.0) then SampleFlt := 1.0 else if (SampleFlt < -1.0) then SampleFlt := -1.0; // assign result PSingle(@dst[SampleIndex])^ := SampleFlt; // increase index by one sample Inc(SampleIndex, SizeOf(Single)); end; end; else begin Log.LogError('Incompatible format', 'TAudioMixerStream.MixAudio'); end; end; 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.