From e5bdcfc8e584d2867d0ad7995d18bd30c7874d1b Mon Sep 17 00:00:00 2001 From: tobigun Date: Fri, 28 Dec 2007 11:46:04 +0000 Subject: New class-structure seperates decoders/input and playback. The files aren't used in usdx until they are stable so they will not conflict with the old structure. The older files (UAudio_Bass etc.) will be replaced then. git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/trunk@752 b956fd51-792f-4845-bead-9b4dfca2ff2c --- Game/Code/Classes/UAudioDecoder_FFMpeg.pas | 600 ++++++++++++++++++++++ Game/Code/Classes/UAudioInput_Bass.pas | 286 +++++++++++ Game/Code/Classes/UAudioInput_Portaudio.pas | 420 ++++++++++++++++ Game/Code/Classes/UAudioPlayback_Bass.pas | 460 +++++++++++++++++ Game/Code/Classes/UAudioPlayback_Portaudio.pas | 664 +++++++++++++++++++++++++ 5 files changed, 2430 insertions(+) create mode 100644 Game/Code/Classes/UAudioDecoder_FFMpeg.pas create mode 100644 Game/Code/Classes/UAudioInput_Bass.pas create mode 100644 Game/Code/Classes/UAudioInput_Portaudio.pas create mode 100644 Game/Code/Classes/UAudioPlayback_Bass.pas create mode 100644 Game/Code/Classes/UAudioPlayback_Portaudio.pas (limited to 'Game') diff --git a/Game/Code/Classes/UAudioDecoder_FFMpeg.pas b/Game/Code/Classes/UAudioDecoder_FFMpeg.pas new file mode 100644 index 00000000..54055454 --- /dev/null +++ b/Game/Code/Classes/UAudioDecoder_FFMpeg.pas @@ -0,0 +1,600 @@ +unit UAudioDecoder_FFMpeg; + +(******************************************************************************* + +This unit is primarily based upon - + http://www.dranger.com/ffmpeg/ffmpegtutorial_all.html + + and tutorial03.c + + http://www.inb.uni-luebeck.de/~boehme/using_libavcodec.html + +*******************************************************************************) + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + + +uses Classes, + {$IFDEF win32} + windows, + {$ENDIF} + SysUtils, + SDL, + avcodec, // FFMpeg Audio file decoding + avformat, + avutil, + ULog, + UMusic; + +implementation + +uses + {$IFDEF LAZARUS} + lclintf, + {$ifndef win32} + libc, + {$endif} + {$ENDIF} + UIni, + UMain, + UThemes; + + +type + PPacketQueue = ^TPacketQueue; + TPacketQueue = class + private + firstPkt, + lastPkt : PAVPacketList; + nbPackets : integer; + size : integer; + mutex : PSDL_Mutex; + cond : PSDL_Cond; + quit : boolean; + + public + constructor Create(); + destructor Destroy(); override; + + function Put(pkt : PAVPacket): integer; + function Get(var pkt: TAVPacket; block: boolean): integer; + end; + +var + EOSPacket: TAVPacket; + +type + PAudioBuffer = ^TAudioBuffer; + TAudioBuffer = array[0 .. (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3 div 2)-1] of byte; + +type + TFFMpegDecodeStream = class(TAudioDecodeStream) + private + status: TStreamStatus; + + EOS_Flag: boolean; // end-of-stream flag + + parseThread: PSDL_Thread; + packetQueue: TPacketQueue; + + // FFMpeg internal data + pFormatCtx : PAVFormatContext; + pCodecCtx : PAVCodecContext; + pCodec : PAVCodec; + ffmpegStreamID : Integer; + ffmpegStream : PAVStream; + + // "static" vars for DecodeFrame + pkt : TAVPacket; + audio_pkt_data : PChar; + audio_pkt_size : integer; + + // "static" vars for AudioCallback + audio_buf_index : cardinal; + audio_buf_size : cardinal; + audio_buf : TAudioBuffer; + + function DecodeFrame(var buffer: TAudioBuffer; bufSize: integer): integer; + public + constructor Create(pFormatCtx: PAVFormatContext; + pCodecCtx: PAVCodecContext; pCodec: PAVCodec; + ffmpegStreamID : Integer; ffmpegStream: PAVStream); + destructor Destroy(); override; + + procedure Close(); override; + + function GetLength(): real; override; + function GetChannelCount(): cardinal; override; + function GetSampleRate(): cardinal; override; + function GetPosition: real; override; + procedure SetPosition(Time: real); override; + function IsEOS(): boolean; override; + + function ReadData(Buffer: PChar; BufSize: integer): integer; override; + end; + +type + TAudioDecoder_FFMpeg = class( TInterfacedObject, IAudioDecoder ) + private + class function FindAudioStreamID(pFormatCtx : PAVFormatContext): integer; + public + function GetName: String; + + function InitializeDecoder(): boolean; + function Open(const Filename: string): TAudioDecodeStream; + end; + +function ParseAudio(streamPtr: Pointer): integer; cdecl; forward; + +var + singleton_AudioDecoderFFMpeg : IAudioDecoder; + + +{ TFFMpegDecodeStream } + +constructor TFFMpegDecodeStream.Create(pFormatCtx: PAVFormatContext; + pCodecCtx: PAVCodecContext; pCodec: PAVCodec; + ffmpegStreamID : Integer; ffmpegStream: PAVStream); +begin + inherited Create(); + + status := sStopped; + packetQueue := TPacketQueue.Create(); + + audio_pkt_data := nil; + audio_pkt_size := 0; + + audio_buf_index := 0; + audio_buf_size := 0; + + FillChar(pkt, sizeof(TAVPacket), #0); + + Self.pFormatCtx := pFormatCtx; + Self.pCodecCtx := pCodecCtx; + Self.pCodec := pCodec; + Self.ffmpegStreamID := ffmpegStreamID; + Self.ffmpegStream := ffmpegStream; + + EOS_Flag := false; + + parseThread := SDL_CreateThread(@ParseAudio, Self); +end; + +destructor TFFMpegDecodeStream.Destroy(); +begin + packetQueue.Free(); + //SDL_WaitThread(parseThread, nil); + inherited; +end; + +procedure TFFMpegDecodeStream.Close(); +begin + // Close the codec + avcodec_close(pCodecCtx); + + // Close the video file + av_close_input_file(pFormatCtx); + + // TODO: abort thread +end; + +function TFFMpegDecodeStream.GetLength(): real; +begin + result := pFormatCtx^.duration / AV_TIME_BASE; +end; + +function TFFMpegDecodeStream.GetChannelCount(): cardinal; +begin + result := pCodecCtx^.channels; +end; + +function TFFMpegDecodeStream.GetSampleRate(): cardinal; +begin + result := pCodecCtx^.sample_rate; +end; + +function TFFMpegDecodeStream.IsEOS(): boolean; +begin + result := EOS_Flag; +end; + + +function ParseAudio(streamPtr: Pointer): integer; cdecl; +var + packet: TAVPacket; + stream: TFFMpegDecodeStream; +begin + stream := TFFMpegDecodeStream(streamPtr); + + while (av_read_frame(stream.pFormatCtx, packet) >= 0) do + begin + //writeln( 'ffmpeg - av_read_frame' ); + + if (packet.stream_index = stream.ffmpegStreamID) then + begin + //writeln( 'packet_queue_put' ); + stream.packetQueue.put(@packet); + end + else + begin + av_free_packet(packet); + end; + end; + + //Writeln('Done: ' + inttostr(stream.packetQueue.nbPackets)); + + // signal end-of-stream + stream.packetQueue.put(@EOSPacket); + + result := 0; +end; + +function TFFMpegDecodeStream.DecodeFrame(var buffer: TAudioBuffer; bufSize: integer): integer; +var + len1, + data_size: integer; +begin + result := -1; + + if (EOS_Flag) then + exit; + + while true do + begin + while (audio_pkt_size > 0) do + begin + //writeln( 'got audio packet' ); + data_size := bufSize; + + // TODO: should be avcodec_decode_audio2 but this wont link on my ubuntu box. + len1 := avcodec_decode_audio(pCodecCtx, @buffer, + data_size, audio_pkt_data, audio_pkt_size); + + //writeln('avcodec_decode_audio : ' + inttostr( len1 )); + + if(len1 < 0) then + begin + // if error, skip frame + //writeln( 'Skip audio frame' ); + audio_pkt_size := 0; + break; + end; + + Inc(audio_pkt_data, len1); + Dec(audio_pkt_size, len1); + + if (data_size <= 0) then + begin + // No data yet, get more frames + continue; + end; + + // We have data, return it and come back for more later + result := data_size; + exit; + end; + + if (pkt.data <> nil) then + begin + av_free_packet(pkt); + end; + + if (packetQueue.quit) then + exit; + + if (packetQueue.Get(pkt, true) < 0) then + exit; + + audio_pkt_data := PChar(pkt.data); + audio_pkt_size := pkt.size; + + // check for end-of-stream + if (audio_pkt_data = PChar(EOSPacket.data)) then + begin + // end-of-stream reached -> set EOS-flag + EOS_Flag := true; + // note: buffer is not (even partially) filled -> no data to return + exit; + end; + + //writeln( 'Audio Packet Size - ' + inttostr(audio_pkt_size) ); + end; +end; + +function TFFMpegDecodeStream.ReadData(Buffer : PChar; BufSize: integer): integer; +var + outStream : TFFMpegDecodeStream; + len1, + audio_size : integer; + pSrc : Pointer; + len : integer; +begin + len := BufSize; + + // end-of-stream reached + if (EOS_Flag) then + exit; + + while (len > 0) do begin + if (audio_buf_index >= audio_buf_size) then + begin + // We have already sent all our data; get more + audio_size := DecodeFrame(audio_buf, sizeof(TAudioBuffer)); + //writeln('audio_decode_frame : '+ inttostr(audio_size)); + + if(audio_size < 0) then + begin + // If error, output silence + audio_buf_size := 1024; + FillChar(audio_buf, audio_buf_size, #0); + //writeln( 'Silence' ); + end + else + begin + audio_buf_size := audio_size; + end; + audio_buf_index := 0; + end; + + len1 := audio_buf_size - audio_buf_index; + if (len1 > len) then + len1 := len; + + pSrc := PChar(@audio_buf) + audio_buf_index; + {$ifdef WIN32} + CopyMemory(Buffer, pSrc , len1); + {$else} + memcpy(Buffer, pSrc , len1); + {$endif} + + Dec(len, len1); + Inc(PChar(Buffer), len1); + Inc(audio_buf_index, len1); + end; + + result := BufSize; +end; + +function TFFMpegDecodeStream.GetPosition(): real; +var + bytes: integer; +begin + Result := 0; +end; + +procedure TFFMpegDecodeStream.SetPosition(Time: real); +var + bytes: integer; +begin +end; + + +{ TAudioDecoder_FFMpeg } + +function TAudioDecoder_FFMpeg.GetName: String; +begin + result := 'FFMpeg_Decoder'; +end; + +function TAudioDecoder_FFMpeg.InitializeDecoder: boolean; +begin + //Log.LogStatus('InitializeDecoder', 'UAudioDecoder_FFMpeg'); + + av_register_all(); + + // init end-of-stream package + av_init_packet(EOSPacket); + EOSPacket.data := Pointer(PChar('EOS')); + + result := true; +end; + +class function TAudioDecoder_FFMpeg.FindAudioStreamID(pFormatCtx : PAVFormatContext): integer; +var + i : integer; + streamID: integer; + stream : PAVStream; +begin + // Find the first audio stream + streamID := -1; + + for i := 0 to pFormatCtx^.nb_streams-1 do + begin + //Log.LogStatus('aFormatCtx.streams[i] : ' + inttostr(i), 'UAudio_FFMpeg'); + stream := pFormatCtx^.streams[i]; + + if ( stream.codec^.codec_type = CODEC_TYPE_AUDIO ) then + begin + //Log.LogStatus('Found Audio Stream', 'UAudio_FFMpeg'); + streamID := i; + break; + end; + end; + + result := streamID; +end; + +function TAudioDecoder_FFMpeg.Open(const Filename: string): TAudioDecodeStream; +var + pFormatCtx : PAVFormatContext; + pCodecCtx : PAVCodecContext; + pCodec : PAVCodec; + ffmpegStreamID : Integer; + ffmpegStream : PAVStream; + wanted_spec, + csIndex : integer; + stream : TFFMpegDecodeStream; +begin + result := nil; + + if (not FileExists(Filename)) then + begin + Log.LogStatus('LoadSoundFromFile: Sound not found "' + Filename + '"', 'UAudio_FFMpeg'); + exit; + end; + + // Open audio file + if (av_open_input_file(pFormatCtx, PChar(Filename), nil, 0, nil) > 0) then + exit; + + // Retrieve stream information + if (av_find_stream_info(pFormatCtx) < 0) then + exit; + + dump_format(pFormatCtx, 0, pchar(Filename), 0); + + ffmpegStreamID := FindAudioStreamID(pFormatCtx); + if (ffmpegStreamID < 0) then + exit; + + //Log.LogStatus('Audio Stream ID is : '+ inttostr(ffmpegStreamID), 'UAudio_FFMpeg'); + + ffmpegStream := pFormatCtx.streams[ffmpegStreamID]; + pCodecCtx := ffmpegStream^.codec; + + pCodec := avcodec_find_decoder(pCodecCtx^.codec_id); + if (pCodec = nil) then + begin + Log.LogStatus('Unsupported codec!', 'UAudio_FFMpeg'); + exit; + end; + + avcodec_open(pCodecCtx, pCodec); + //writeln( 'Opened the codec' ); + + stream := TFFMpegDecodeStream.Create(pFormatCtx, pCodecCtx, pCodec, + ffmpegStreamID, ffmpegStream); + + result := stream; +end; + + +{ TPacketQueue } + +constructor TPacketQueue.Create(); +begin + inherited; + + firstPkt := nil; + lastPkt := nil; + nbPackets := 0; + size := 0; + + mutex := SDL_CreateMutex(); + cond := SDL_CreateCond(); +end; + +destructor TPacketQueue.Destroy(); +begin + SDL_DestroyMutex(mutex); + SDL_DestroyCond(cond); + inherited; +end; + +function TPacketQueue.Put(pkt : PAVPacket): integer; +var + pkt1 : PAVPacketList; +begin + result := -1; + + if (pkt <> @EOSPacket) then + if (av_dup_packet(pkt) < 0) then + exit; + + pkt1 := av_malloc(sizeof(TAVPacketList)); + if (pkt1 = nil) then + exit; + + pkt1^.pkt := pkt^; + pkt1^.next := nil; + + + SDL_LockMutex(Self.mutex); + try + + if (Self.lastPkt = nil) then + Self.firstPkt := pkt1 + else + Self.lastPkt^.next := pkt1; + + Self.lastPkt := pkt1; + inc(Self.nbPackets); + + //Writeln('Put: ' + inttostr(nbPackets)); + + Self.size := Self.size + pkt1^.pkt.size; + SDL_CondSignal(Self.cond); + + finally + SDL_UnlockMutex(Self.mutex); + end; + + result := 0; +end; + +function TPacketQueue.Get(var pkt: TAVPacket; block: boolean): integer; +var + pkt1 : PAVPacketList; +begin + result := -1; + + SDL_LockMutex(Self.mutex); + try + while true do + begin + if (quit) then + exit; + + pkt1 := Self.firstPkt; + + if (pkt1 <> nil) then + begin + Self.firstPkt := pkt1.next; + if (Self.firstPkt = nil) then + Self.lastPkt := nil; + dec(Self.nbPackets); + + //Writeln('Get: ' + inttostr(nbPackets)); + + Self.size := Self.size - pkt1^.pkt.size; + pkt := pkt1^.pkt; + av_free(pkt1); + + result := 1; + break; + end + else + if (not block) then + begin + result := 0; + break; + end + else + begin + SDL_CondWait(Self.cond, Self.mutex); + end; + end; + finally + SDL_UnlockMutex(Self.mutex); + end; +end; + + + +initialization + singleton_AudioDecoderFFMpeg := TAudioDecoder_FFMpeg.create(); + + //writeln( 'UAudioDecoder_FFMpeg - Register Decoder' ); + AudioManager.add( singleton_AudioDecoderFFMpeg ); + +finalization + AudioManager.Remove( singleton_AudioDecoderFFMpeg ); + + +end. diff --git a/Game/Code/Classes/UAudioInput_Bass.pas b/Game/Code/Classes/UAudioInput_Bass.pas new file mode 100644 index 00000000..9807ffc3 --- /dev/null +++ b/Game/Code/Classes/UAudioInput_Bass.pas @@ -0,0 +1,286 @@ +unit UAudioInput_Bass; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + + +uses Classes, + {$IFDEF win32} + windows, + {$ENDIF} + SysUtils, + bass, + ULog, + UMusic; + +implementation + +uses + {$IFDEF LAZARUS} + lclintf, + {$ENDIF} + URecord, + UIni, + UMain, + UCommon, + UThemes; + +type + TAudioInput_Bass = class( TInterfacedObject, IAudioInput) + private + public + function GetName: String; + + {IAudioInput interface} + procedure InitializeRecord; + + procedure CaptureStart; + procedure CaptureStop; + procedure CaptureCard(Card: byte; CaptureSoundLeft, CaptureSoundRight: TSound); + procedure StopCard(Card: byte); + end; + + TBassSoundCard = class(TGenericSoundCard) + RecordStream: HSTREAM; + end; + +var + singleton_AudioInputBass : IAudioInput; + + +function TAudioInput_Bass.GetName: String; +begin + result := 'BASS_Input'; +end; + +procedure TAudioInput_Bass.InitializeRecord; +var + device: integer; + Descr: string; + input: integer; + input2: integer; + InputName: PChar; + Flags: integer; + mic: array[0..15] of integer; + SC: integer; // soundcard + SCI: integer; // soundcard input + No: integer; + +function isDuplicate(Desc: String): Boolean; +var + I: Integer; +begin + Result := False; + //Check for Soundcard with same Description + For I := 0 to SC-1 do + begin + if (Recording.SoundCard[I].Description = Desc) then + begin + Result := True; + Break; + end; + end; +end; + +begin + with Recording do + begin + // checks for recording devices and puts them into an array + SetLength(SoundCard, 0); + + SC := 0; + Descr := BASS_RecordGetDeviceDescription(SC); + + while (Descr <> '') do + begin + //If there is another SoundCard with the Same ID, Search an available Name + if (IsDuplicate(Descr)) then + begin + No:= 1; //Count of SoundCards with same Name + Repeat + Inc(No) + Until not IsDuplicate(Descr + ' (' + InttoStr(No) + ')'); + + //Set Description + Descr := Descr + ' (' + InttoStr(No) + ')'; + end; + + SetLength(SoundCard, SC+1); + + // TODO: free object on termination + SoundCard[SC] := TBassSoundCard.Create(); + SoundCard[SC].Description := Descr; + + //Get Recording Inputs + SCI := 0; + BASS_RecordInit(SC); + + InputName := BASS_RecordGetInputName(SCI); + + {$IFDEF DARWIN} + // Under MacOSX the SingStar Mics have an empty + // InputName. So, we have to add a hard coded + // Workaround for this problem + if (InputName = nil) and (Pos( 'USBMIC Serial#', Descr) > 0) then + begin + InputName := 'Microphone'; + end; + {$ENDIF} + + SetLength(SoundCard[SC].Input, 1); + SoundCard[SC].Input[SCI].Name := InputName; + + // process each input + while (InputName <> nil) do + begin + Flags := BASS_RecordGetInput(SCI); + if (SCI >= 1) {AND (Flags AND BASS_INPUT_OFF = 0)} then + begin + SetLength(SoundCard[SC].Input, SCI+1); + SoundCard[SC].Input[SCI].Name := InputName; + end; + + //Set Mic Index + if ((Flags and BASS_INPUT_TYPE_MIC) = 1) then + SoundCard[SC].MicInput := SCI; + + Inc(SCI); + InputName := BASS_RecordGetInputName(SCI); + end; + + BASS_RecordFree; + + Inc(SC); + Descr := BASS_RecordGetDeviceDescription(SC); + end; // while + end; // with Recording +end; + +// TODO: code is used by all IAudioInput implementors +// -> move to a common superclass (TAudioInput_Generic?) +procedure TAudioInput_Bass.CaptureStart; +var + S: integer; + SC: integer; + PlayerLeft, PlayerRight: integer; + CaptureSoundLeft, CaptureSoundRight: TSound; +begin + for S := 0 to High(Recording.Sound) do + Recording.Sound[S].BufferLong[0].Clear; + + for SC := 0 to High(Ini.CardList) do begin + PlayerLeft := Ini.CardList[SC].ChannelL-1; + PlayerRight := Ini.CardList[SC].ChannelR-1; + if PlayerLeft >= PlayersPlay then PlayerLeft := -1; + if PlayerRight >= PlayersPlay then PlayerRight := -1; + if (PlayerLeft > -1) or (PlayerRight > -1) then begin + if (PlayerLeft > -1) then + CaptureSoundLeft := Recording.Sound[PlayerLeft] + else + CaptureSoundLeft := nil; + if (PlayerRight > -1) then + CaptureSoundRight := Recording.Sound[PlayerRight] + else + CaptureSoundRight := nil; + + CaptureCard(SC, CaptureSoundLeft, CaptureSoundRight); + end; + end; +end; + +// TODO: code is used by all IAudioInput implementors +// -> move to a common superclass (TAudioInput_Generic?) +procedure TAudioInput_Bass.CaptureStop; +var + SC: integer; + PlayerLeft: integer; + PlayerRight: integer; +begin + + for SC := 0 to High(Ini.CardList) do begin + PlayerLeft := Ini.CardList[SC].ChannelL-1; + PlayerRight := Ini.CardList[SC].ChannelR-1; + if PlayerLeft >= PlayersPlay then PlayerLeft := -1; + if PlayerRight >= PlayersPlay then PlayerRight := -1; + if (PlayerLeft > -1) or (PlayerRight > -1) then + StopCard(SC); + end; + +end; + +{* + * Bass input capture callback. + * Params: + * stream - BASS input stream + * buffer - buffer of captured samples + * len - size of buffer in bytes + * user - players associated with left/right channels + *} +function MicrophoneCallback(stream: HSTREAM; buffer: Pointer; + len: Cardinal; Card: Cardinal): boolean; stdcall; +begin + Recording.HandleMicrophoneData(buffer, len, Recording.SoundCard[Card]); + Result := true; +end; + +{* + * Start input-capturing on Soundcard specified by Card. + * Params: + * Card - soundcard index in Recording.SoundCard array + * CaptureSoundLeft - sound(-buffer) used for left channel capture data + * CaptureSoundRight - sound(-buffer) used for right channel capture data + *} +procedure TAudioInput_Bass.CaptureCard(Card: byte; CaptureSoundLeft, CaptureSoundRight: TSound); +var + Error: integer; + ErrorMsg: string; + bassSoundCard: TBassSoundCard; +begin + if not BASS_RecordInit(Card) then + begin + Error := BASS_ErrorGetCode; + ErrorMsg := IntToStr(Error); + if Error = BASS_ERROR_DX then ErrorMsg := 'No DX5'; + if Error = BASS_ERROR_ALREADY then ErrorMsg := 'The device has already been initialized'; + if Error = BASS_ERROR_DEVICE then ErrorMsg := 'The device number specified is invalid'; + if Error = BASS_ERROR_DRIVER then ErrorMsg := 'There is no available device driver'; + Log.LogError('Error initializing record [' + IntToStr(Card) + ']'); + Log.LogError('TAudio_bass.CaptureCard: Error initializing record: ' + ErrorMsg); + end + else + begin + bassSoundCard := TBassSoundCard(Recording.SoundCard[Card]); + bassSoundCard.CaptureSoundLeft := CaptureSoundLeft; + bassSoundCard.CaptureSoundRight := CaptureSoundRight; + + // capture in 44.1kHz/stereo/16bit and a 20ms callback period + bassSoundCard.RecordStream := + BASS_RecordStart(44100, 2, MakeLong(0, 20) , @MicrophoneCallback, Card); + end; +end; + +{* + * Stop input-capturing on Soundcard specified by Card. + * Params: + * Card - soundcard index in Recording.SoundCard array + *} +procedure TAudioInput_Bass.StopCard(Card: byte); +begin + BASS_RecordSetDevice(Card); + BASS_RecordFree; +end; + + +initialization + singleton_AudioInputBass := TAudioInput_Bass.create(); + AudioManager.add( singleton_AudioInputBass ); + +finalization + AudioManager.Remove( singleton_AudioInputBass ); + +end. diff --git a/Game/Code/Classes/UAudioInput_Portaudio.pas b/Game/Code/Classes/UAudioInput_Portaudio.pas new file mode 100644 index 00000000..853fc35b --- /dev/null +++ b/Game/Code/Classes/UAudioInput_Portaudio.pas @@ -0,0 +1,420 @@ +unit UAudioInput_Portaudio; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + + +uses Classes, + SysUtils, + portaudio, + {$IFDEF UsePortmixer} + portmixer, + {$ENDIF} + ULog, + UMusic; + +implementation + +uses + {$IFDEF LAZARUS} + lclintf, + {$ENDIF} + URecord, + UIni, + UMain, + UCommon, + UThemes; +{ +type + TPaHostApiIndex = PaHostApiIndex; + TPaDeviceIndex = PaDeviceIndex; + PPaStream = ^PaStreamPtr; + PPaStreamCallbackTimeInfo = ^PaStreamCallbackTimeInfo; + TPaStreamCallbackFlags = PaStreamCallbackFlags; + TPaHostApiTypeId = PaHostApiTypeId; + PPaHostApiInfo = ^PaHostApiInfo; + PPaDeviceInfo = ^PaDeviceInfo; + TPaError = PaError; + TPaStreamParameters = PaStreamParameters; +} +type + TAudioInput_Portaudio = class( TInterfacedObject, IAudioInput ) + private + function GetPreferredApiIndex(): TPaHostApiIndex; + public + function GetName: String; + procedure InitializeRecord; + + procedure CaptureStart; + procedure CaptureStop; + + procedure CaptureCard(Card: byte; CaptureSoundLeft, CaptureSoundRight: TSound); + procedure StopCard(Card: byte); + end; + + TPortaudioSoundCard = class(TGenericSoundCard) + RecordStream: PPaStream; + DeviceIndex: TPaDeviceIndex; + end; + +function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword; + timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags; + inputDevice: Pointer): Integer; cdecl; forward; + +var + singleton_AudioInputPortaudio : IAudioInput; + +const + sampleRate: Double = 44100.; + +{* the default API used by Portaudio is the least common denominator + * and might lack efficiency. ApiPreferenceOrder defines the order of + * preferred APIs to use. The first API-type in the list is tried first. If it's + * not available the next is tried, ... + * If none of the preferred APIs was found the default API is used. + * Pascal doesn't permit zero-length static arrays, so you can use paDefaultApi + * as an array's only member if you do not have any preferences. + * paDefaultApi also terminates a preferences list but this is optional. + *} +const + paDefaultApi = -1; +var + ApiPreferenceOrder: +{$IF Defined(WIN32)} + // Note1: Portmixer has no mixer support for paASIO and paWASAPI at the moment + // Note2: Windows Default-API is MME + //array[0..0] of TPaHostApiTypeId = ( paDirectSound, paMME ); + array[0..0] of TPaHostApiTypeId = ( paDirectSound ); +{$ELSEIF Defined(LINUX)} + // Note1: Portmixer has no mixer support for paJACK at the moment + // Note2: Not tested, but ALSA might be better than OSS. + array[0..1] of TPaHostApiTypeId = ( paALSA, paOSS ); +{$ELSEIF Defined(DARWIN)} + // Note: Not tested. + //array[0..0] of TPaHostApiTypeId = ( paCoreAudio ); + array[0..0] of TPaHostApiTypeId = ( paDefaultApi ); +{$ELSE} + array[0..0] of TPaHostApiTypeId = ( paDefaultApi ); +{$IFEND} + +function TAudioInput_Portaudio.GetName: String; +begin + result := 'Portaudio'; +end; + +function TAudioInput_Portaudio.GetPreferredApiIndex(): TPaHostApiIndex; +var + i: integer; +begin + result := -1; + + // select preferred sound-API + for i:= 0 to High(ApiPreferenceOrder) do + begin + if(ApiPreferenceOrder[i] <> paDefaultApi) then begin + // check if API is available + result := Pa_HostApiTypeIdToHostApiIndex(ApiPreferenceOrder[i]); + if(result >= 0) then + break; + end; + end; + + // None of the preferred APIs is available -> use default + if(result < 0) then begin + result := Pa_GetDefaultHostApi(); + end; +end; + +// TODO: should be a function with boolean return type +procedure TAudioInput_Portaudio.InitializeRecord; +var + i: integer; + apiIndex: TPaHostApiIndex; + apiInfo: PPaHostApiInfo; + deviceName: string; + deviceIndex: TPaDeviceIndex; + deviceInfo: PPaDeviceInfo; + inputCnt: integer; + inputName: string; + SC: integer; // soundcard + SCI: integer; // soundcard input + err: TPaError; + errMsg: string; + paSoundCard: TPortaudioSoundCard; + inputParams: TPaStreamParameters; + stream: PPaStream; + {$IFDEF UsePortmixer} + mixer: PPxMixer; + {$ENDIF} +begin + // TODO: call Pa_Terminate() on termination + err := Pa_Initialize(); + if(err <> paNoError) then begin + Log.CriticalError('Portaudio.InitializeRecord: ' + Pa_GetErrorText(err)); + //Log.LogError('Portaudio.InitializeRecord: ' + Pa_GetErrorText(err)); + // result := false; + Exit; + end; + + apiIndex := GetPreferredApiIndex(); + apiInfo := Pa_GetHostApiInfo(apiIndex); + + SC := 0; + + // init array-size to max. input-devices count + SetLength(Recording.SoundCard, apiInfo^.deviceCount); // fix deviceCountL + for i:= 0 to High(Recording.SoundCard) do + begin + // convert API-specific device-index to global index + deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(apiIndex, i); + deviceInfo := Pa_GetDeviceInfo(deviceIndex); + + // current device is no input device -> skip + if(deviceInfo^.maxInputChannels <= 0) then + continue; + + // TODO: free object on termination + paSoundCard := TPortaudioSoundCard.Create(); + Recording.SoundCard[SC] := paSoundCard; + + // retrieve device-name + deviceName := deviceInfo^.name; + paSoundCard.Description := deviceName; + paSoundCard.DeviceIndex := deviceIndex; + + // setup desired input parameters + with inputParams do begin + device := deviceIndex; + channelCount := 2; + sampleFormat := paInt16; + suggestedLatency := deviceInfo^.defaultLowInputLatency; + hostApiSpecificStreamInfo := nil; + end; + + // check if device supports our input-format + err := Pa_IsFormatSupported(@inputParams, nil, sampleRate); + if(err <> 0) then begin + // format not supported -> skip + errMsg := Pa_GetErrorText(err); + Log.LogError('Portaudio.InitializeRecord, device: "'+ deviceName +'" ' + + '('+ errMsg +')'); + paSoundCard.Free(); + continue; + end; + + // TODO: retry with mono if stereo is not supported + // TODO: retry with input-latency set to 20ms (defaultLowInputLatency might + // not be set correctly in OSS) + + err := Pa_OpenStream(stream, @inputParams, nil, sampleRate, + paFramesPerBufferUnspecified, paNoFlag, @MicrophoneCallback, nil); + if(err <> paNoError) then begin + // unable to open device -> skip + errMsg := Pa_GetErrorText(err); + Log.LogError('Portaudio.InitializeRecord, device: "'+ deviceName +'" ' + + '('+ errMsg +')'); + paSoundCard.Free(); + continue; + end; + + + {$IFDEF UsePortmixer} + + // use default mixer + mixer := Px_OpenMixer(stream, 0); + + // get input count + inputCnt := Px_GetNumInputSources(mixer); + SetLength(paSoundCard.Input, inputCnt); + + // get input names + for SCI := 0 to inputCnt-1 do + begin + inputName := Px_GetInputSourceName(mixer, SCI); + paSoundCard.Input[SCI].Name := inputName; + end; + + Px_CloseMixer(mixer); + + {$ELSE} // !UsePortmixer + + //Pa_StartStream(stream); + // TODO: check if callback was called (this problem may occur on some devices) + //Pa_StopStream(stream); + + Pa_CloseStream(stream); + + // create a standard input source + SetLength(paSoundCard.Input, 1); + paSoundCard.Input[0].Name := 'Standard'; + + {$ENDIF} + + // use default input source + paSoundCard.InputSelected := 0; + + Inc(SC); + end; + + // adjust size to actual input-device count + SetLength(Recording.SoundCard, SC); + + Log.LogStatus('#Soundcards: ' + inttostr(SC), 'Portaudio'); + + { + SoundCard[SC].InputSelected := Mic[Device]; + } +end; + +// TODO: code is used by all IAudioInput implementors +// -> move to a common superclass (TAudioInput_Generic?) +procedure TAudioInput_Portaudio.CaptureStart; +var + S: integer; + SC: integer; + PlayerLeft, PlayerRight: integer; + CaptureSoundLeft, CaptureSoundRight: TSound; +begin + for S := 0 to High(Recording.Sound) do + Recording.Sound[S].BufferLong[0].Clear; + + for SC := 0 to High(Ini.CardList) do begin + PlayerLeft := Ini.CardList[SC].ChannelL-1; + PlayerRight := Ini.CardList[SC].ChannelR-1; + if PlayerLeft >= PlayersPlay then PlayerLeft := -1; + if PlayerRight >= PlayersPlay then PlayerRight := -1; + if (PlayerLeft > -1) or (PlayerRight > -1) then begin + if (PlayerLeft > -1) then + CaptureSoundLeft := Recording.Sound[PlayerLeft] + else + CaptureSoundLeft := nil; + if (PlayerRight > -1) then + CaptureSoundRight := Recording.Sound[PlayerRight] + else + CaptureSoundRight := nil; + + CaptureCard(SC, CaptureSoundLeft, CaptureSoundRight); + end; + end; +end; + +// TODO: code is used by all IAudioInput implementors +// -> move to a common superclass (TAudioInput_Generic?) +procedure TAudioInput_Portaudio.CaptureStop; +var + SC: integer; + PlayerLeft: integer; + PlayerRight: integer; +begin + + for SC := 0 to High(Ini.CardList) do begin + PlayerLeft := Ini.CardList[SC].ChannelL-1; + PlayerRight := Ini.CardList[SC].ChannelR-1; + if PlayerLeft >= PlayersPlay then PlayerLeft := -1; + if PlayerRight >= PlayersPlay then PlayerRight := -1; + if (PlayerLeft > -1) or (PlayerRight > -1) then + StopCard(SC); + end; + +end; + +{* + * Portaudio input capture callback. + *} +function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword; + timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags; + inputDevice: Pointer): Integer; cdecl; +begin + Recording.HandleMicrophoneData(input, frameCount*4, inputDevice); + result := paContinue; +end; + +{* + * Start input-capturing on Soundcard specified by Card. + * Params: + * Card - soundcard index in Recording.SoundCard array + * CaptureSoundLeft - sound(-buffer) used for left channel capture data + * CaptureSoundRight - sound(-buffer) used for right channel capture data + *} +procedure TAudioInput_Portaudio.CaptureCard(Card: byte; CaptureSoundLeft, CaptureSoundRight: TSound); +var + Error: TPaError; + ErrorMsg: string; + inputParams: TPaStreamParameters; + deviceInfo: PPaDeviceInfo; + stream: PPaStream; + paSoundCard: TPortaudioSoundCard; +begin + paSoundCard := TPortaudioSoundCard(Recording.SoundCard[Card]); + paSoundCard.CaptureSoundLeft := CaptureSoundLeft; + paSoundCard.CaptureSoundRight := CaptureSoundRight; + + // get input latency info + deviceInfo := Pa_GetDeviceInfo(paSoundCard.DeviceIndex); + + // set input stream parameters + with inputParams do begin + device := paSoundCard.DeviceIndex; + channelCount := 2; + sampleFormat := paInt16; + suggestedLatency := deviceInfo^.defaultLowInputLatency; + hostApiSpecificStreamInfo := nil; + end; + + Log.LogStatus(inttostr(paSoundCard.DeviceIndex), 'Portaudio'); + Log.LogStatus(floattostr(deviceInfo^.defaultLowInputLatency), 'Portaudio'); + + // open input stream + Error := Pa_OpenStream(stream, @inputParams, nil, sampleRate, + paFramesPerBufferUnspecified, paNoFlag, + @MicrophoneCallback, Pointer(paSoundCard)); + if(Error <> paNoError) then begin + ErrorMsg := Pa_GetErrorText(Error); + Log.CriticalError('TAudio_Portaudio.CaptureCard('+ IntToStr(Card) +'): Error opening stream: ' + ErrorMsg); + //Halt; + end; + + paSoundCard.RecordStream := stream; + + // start capture + Error := Pa_StartStream(stream); + if(Error <> paNoError) then begin + Pa_CloseStream(stream); + ErrorMsg := Pa_GetErrorText(Error); + Log.CriticalError('TAudio_Portaudio.CaptureCard('+ IntToStr(Card) +'): Error starting stream: ' + ErrorMsg); + //Halt; + end; +end; + +{* + * Stop input-capturing on Soundcard specified by Card. + * Params: + * Card - soundcard index in Recording.SoundCard array + *} +procedure TAudioInput_Portaudio.StopCard(Card: byte); +var + stream: PPaStream; + paSoundCard: TPortaudioSoundCard; +begin + paSoundCard := TPortaudioSoundCard(Recording.SoundCard[Card]); + stream := paSoundCard.RecordStream; + if(stream <> nil) then begin + Pa_StopStream(stream); + Pa_CloseStream(stream); + end; +end; + + +initialization + singleton_AudioInputPortaudio := TAudioInput_Portaudio.create(); + AudioManager.add( singleton_AudioInputPortaudio ); + +finalization + AudioManager.Remove( singleton_AudioInputPortaudio ); + +end. diff --git a/Game/Code/Classes/UAudioPlayback_Bass.pas b/Game/Code/Classes/UAudioPlayback_Bass.pas new file mode 100644 index 00000000..ee6b1ef9 --- /dev/null +++ b/Game/Code/Classes/UAudioPlayback_Bass.pas @@ -0,0 +1,460 @@ +unit UAudioPlayback_Bass; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + + +uses Classes, + {$IFDEF win32} + windows, + {$ENDIF} + SysUtils, + bass, + ULog, + UMusic; + +implementation + +uses + {$IFDEF LAZARUS} + lclintf, + {$ENDIF} + URecord, + UIni, + UMain, + UCommon, + UThemes; + +type + TMPModes = (mpNotReady, mpStopped, mpPlaying, mpRecording, mpSeeking, + mpPaused, mpOpen); + +const + ModeStr: array[TMPModes] of string = ('Not ready', 'Stopped', 'Playing', 'Recording', 'Seeking', 'Paused', 'Open'); + +type + TBassOutputStream = class(TAudioOutputStream) + Handle: HSTREAM; + + constructor Create(); overload; + constructor Create(stream: HSTREAM); overload; + end; + +type + TAudioPlayback_Bass = class( TInterfacedObject, IAudioPlayback) + private + MusicStream: HSTREAM; + + StartSoundStream: HSTREAM; + BackSoundStream: HSTREAM; + SwooshSoundStream: HSTREAM; + ChangeSoundStream: HSTREAM; + OptionSoundStream: HSTREAM; + ClickSoundStream: HSTREAM; + DrumSoundStream: HSTREAM; + HihatSoundStream: HSTREAM; + ClapSoundStream: HSTREAM; + ShuffleSoundStream: HSTREAM; + + //Custom Sounds + CustomSounds: array of TCustomSoundEntry; + Loaded: boolean; + Loop: boolean; + + public + function GetName: String; + + {IAudioOutput interface} + + procedure InitializePlayback; + procedure SetVolume(Volume: integer); + procedure SetMusicVolume(Volume: integer); + procedure SetLoop(Enabled: boolean); + + function Open(Name: string): boolean; // true if succeed + + procedure Rewind; + procedure Play; + procedure Pause; //Pause Mod + procedure Stop; + procedure Close; + function Finished: boolean; + function Length: real; + function GetPosition: real; + procedure SetPosition(Time: real); + + procedure PlayStart; + procedure PlayBack; + procedure PlaySwoosh; + procedure PlayChange; + procedure PlayOption; + procedure PlayClick; + procedure PlayDrum; + procedure PlayHihat; + procedure PlayClap; + procedure PlayShuffle; + procedure StopShuffle; + + function LoadSoundFromFile(var stream: HSTREAM; Name: string): boolean; + + //Equalizer + function GetFFTData: TFFTData; + + // Interface for Visualizer + function GetPCMData(var data: TPCMData): Cardinal; + + //Custom Sounds + function LoadCustomSound(const Filename: String): Cardinal; + procedure PlayCustomSound(const Index: Cardinal ); + end; + +var + singleton_AudioPlaybackBass : IAudioPlayback; + + +constructor TBassOutputStream.Create(); +begin + inherited; +end; + +constructor TBassOutputStream.Create(stream: HSTREAM); +begin + Create(); + Handle := stream; +end; + + +function TAudioPlayback_Bass.GetName: String; +begin + result := 'BASS_Playback'; +end; + +procedure TAudioPlayback_Bass.InitializePlayback; +var + Pet: integer; + S: integer; +begin +// Log.BenchmarkStart(4); +// Log.LogStatus('Initializing Playback Subsystem', 'Music Initialize'); + + Loaded := false; + Loop := false; + + if not BASS_Init(1, 44100, 0, 0, nil) then + begin + Log.LogError('Could not initialize BASS', 'Error'); + Exit; + end; + +// Log.BenchmarkEnd(4); Log.LogBenchmark('--> Bass Init', 4); + + // config playing buffer +// BASS_SetConfig(BASS_CONFIG_UPDATEPERIOD, 10); +// BASS_SetConfig(BASS_CONFIG_BUFFER, 100); + +// Log.LogStatus('Loading Sounds', 'Music Initialize'); + +// Log.BenchmarkStart(4); + LoadSoundFromFile(StartSoundStream, SoundPath + 'Common Start.mp3'); + LoadSoundFromFile(BackSoundStream, SoundPath + 'Common Back.mp3'); + LoadSoundFromFile(SwooshSoundStream, SoundPath + 'menu swoosh.mp3'); + LoadSoundFromFile(ChangeSoundStream, SoundPath + 'select music change music 50.mp3'); + LoadSoundFromFile(OptionSoundStream, SoundPath + 'option change col.mp3'); + LoadSoundFromFile(ClickSoundStream, SoundPath + 'rimshot022b.mp3'); + +// LoadSoundFromFile(DrumSoundStream, SoundPath + 'bassdrumhard076b.mp3'); +// LoadSoundFromFile(HihatSoundStream, SoundPath + 'hihatclosed068b.mp3'); +// LoadSoundFromFile(ClapSoundStream, SoundPath + 'claps050b.mp3'); + +// LoadSoundFromFile(ShuffleSoundStream, SoundPath + 'Shuffle.mp3'); + +// Log.BenchmarkEnd(4); +// Log.LogBenchmark('--> Loading Sounds', 4); +end; + +procedure TAudioPlayback_Bass.SetVolume(Volume: integer); +begin + //Old Sets Wave Volume + //BASS_SetVolume(Volume); + //New: Sets Volume only for this Application + + BASS_SetConfig(BASS_CONFIG_GVOL_SAMPLE, Volume); + BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Volume); + BASS_SetConfig(BASS_CONFIG_GVOL_MUSIC, Volume); +end; + +procedure TAudioPlayback_Bass.SetMusicVolume(Volume: Integer); +begin + //Max Volume Prevention + if Volume > 100 then + Volume := 100; + + if Volume < 0 then + Volume := 0; + + //Set Volume + BASS_ChannelSetAttributes (MusicStream, -1, Volume, -101); +end; + +procedure TAudioPlayback_Bass.SetLoop(Enabled: boolean); +begin + Loop := Enabled; +end; + +function TAudioPlayback_Bass.Open(Name: string): boolean; +begin + Loaded := false; + if FileExists(Name) then + begin + MusicStream := Bass_StreamCreateFile(false, pchar(Name), 0, 0, 0); + + Loaded := true; + //Set Max Volume + SetMusicVolume (100); + end; + + Result := Loaded; +end; + +procedure TAudioPlayback_Bass.Rewind; +begin + if Loaded then begin + end; +end; + +procedure TAudioPlayback_Bass.SetPosition(Time: real); +var + bytes: integer; +begin + bytes := BASS_ChannelSeconds2Bytes(MusicStream, Time); + BASS_ChannelSetPosition(MusicStream, bytes); +end; + +procedure TAudioPlayback_Bass.Play; +begin + if Loaded then + begin + if Loop then + BASS_ChannelPlay(MusicStream, True); // start from beginning... actually bass itself does not loop, nor does this TAudio_bass Class + + BASS_ChannelPlay(MusicStream, False); // for setting position before playing + end; +end; + +procedure TAudioPlayback_Bass.Pause; //Pause Mod +begin + if Loaded then begin + BASS_ChannelPause(MusicStream); // Pauses Song + end; +end; + +procedure TAudioPlayback_Bass.Stop; +begin + Bass_ChannelStop(MusicStream); +end; + +procedure TAudioPlayback_Bass.Close; +begin + Bass_StreamFree(MusicStream); +end; + +function TAudioPlayback_Bass.Length: real; +var + bytes: integer; +begin + Result := 60; + + bytes := BASS_ChannelGetLength(MusicStream); + Result := BASS_ChannelBytes2Seconds(MusicStream, bytes); +end; + +function TAudioPlayback_Bass.getPosition: real; +var + bytes: integer; +begin + Result := 0; + + bytes := BASS_ChannelGetPosition(MusicStream); + Result := BASS_ChannelBytes2Seconds(MusicStream, bytes); +end; + +function TAudioPlayback_Bass.Finished: boolean; +begin + Result := false; + + if BASS_ChannelIsActive(MusicStream) = BASS_ACTIVE_STOPPED then + begin + Result := true; + end; +end; + +procedure TAudioPlayback_Bass.PlayStart; +begin + BASS_ChannelPlay(StartSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayBack; +begin + BASS_ChannelPlay(BackSoundStream, True);// then +end; + +procedure TAudioPlayback_Bass.PlaySwoosh; +begin + BASS_ChannelPlay(SwooshSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayChange; +begin + BASS_ChannelPlay(ChangeSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayOption; +begin + BASS_ChannelPlay(OptionSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayClick; +begin + BASS_ChannelPlay(ClickSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayDrum; +begin + BASS_ChannelPlay(DrumSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayHihat; +begin + BASS_ChannelPlay(HihatSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayClap; +begin + BASS_ChannelPlay(ClapSoundStream, True); +end; + +procedure TAudioPlayback_Bass.PlayShuffle; +begin + BASS_ChannelPlay(ShuffleSoundStream, True); +end; + +procedure TAudioPlayback_Bass.StopShuffle; +begin + BASS_ChannelStop(ShuffleSoundStream); +end; + +function TAudioPlayback_Bass.LoadSoundFromFile(var stream: HSTREAM; Name: string): boolean; +var + L: Integer; +begin + if FileExists(Name) then + begin + Log.LogStatus('Loading Sound: "' + Name + '"', 'LoadSoundFromFile'); + try + stream := BASS_StreamCreateFile(False, pchar(Name), 0, 0, 0); + + //Add CustomSound + L := High(CustomSounds) + 1; + SetLength (CustomSounds, L + 1); + CustomSounds[L].Filename := Name; + CustomSounds[L].Stream := TBassOutputStream.Create(stream); + except + Log.LogError('Failed to open using BASS', 'LoadSoundFromFile'); + end; + end + else + begin + Log.LogError('Sound not found: "' + Name + '"', 'LoadSoundFromFile'); + exit; + end; +end; + +//Equalizer +function TAudioPlayback_Bass.GetFFTData: TFFTData; +var + Data: TFFTData; +begin + //Get Channel Data Mono and 256 Values + BASS_ChannelGetData(MusicStream, @Result, BASS_DATA_FFT512); +end; + +{* + * Copies interleaved PCM 16bit uint (maybe fake) stereo samples into data. + * Returns the number of frames (= stereo/mono sample) + *} +function TAudioPlayback_Bass.GetPCMData(var data: TPCMData): Cardinal; +var + info: BASS_CHANNELINFO; + nBytes: DWORD; +begin + //Get Channel Data Mono and 256 Values + BASS_ChannelGetInfo(MusicStream, info); + ZeroMemory(@data, sizeof(TPCMData)); + + if (info.chans = 1) then + begin + // mono file -> add stereo channel + { + nBytes := BASS_ChannelGetData(Bass, @data[0], samples*sizeof(Smallint)); + // interleave data + //CopyMemory(@data[1], @data[0], samples*sizeof(Smallint)); + } + result := 0; + end + else + begin + // stereo file + nBytes := BASS_ChannelGetData(MusicStream, @data, sizeof(TPCMData)); + end; + if(nBytes <= 0) then + result := 0 + else + result := nBytes div sizeof(TPCMStereoSample); +end; + +function TAudioPlayback_Bass.LoadCustomSound(const Filename: String): Cardinal; +var + S: hStream; + I: Integer; + F: String; +begin + //Search for Sound in already loaded Sounds + F := UpperCase(SoundPath + FileName); + For I := 0 to High(CustomSounds) do + begin + if (UpperCase(CustomSounds[I].Filename) = F) then + begin + Result := I; + Exit; + end; + end; + + if LoadSoundFromFile(S, SoundPath + Filename) then + Result := High(CustomSounds) + else + Result := 0; +end; + +procedure TAudioPlayback_Bass.PlayCustomSound(const Index: Cardinal ); +begin + if Index <= High(CustomSounds) then + with CustomSounds[Index].Stream as TBassOutputStream do + begin + BASS_ChannelPlay(Handle, True); + end; +end; + + +initialization + singleton_AudioPlaybackBass := TAudioPlayback_Bass.create(); + AudioManager.add( singleton_AudioPlaybackBass ); + +finalization + AudioManager.Remove( singleton_AudioPlaybackBass ); + +end. diff --git a/Game/Code/Classes/UAudioPlayback_Portaudio.pas b/Game/Code/Classes/UAudioPlayback_Portaudio.pas new file mode 100644 index 00000000..36bebc8a --- /dev/null +++ b/Game/Code/Classes/UAudioPlayback_Portaudio.pas @@ -0,0 +1,664 @@ +unit UAudioPlayback_Portaudio; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + + +uses Classes, + SysUtils, + UMusic; + +implementation + +uses + {$IFDEF LAZARUS} + lclintf, + {$ifndef win32} + libc, + {$endif} + {$ENDIF} + portaudio, + ULog, + UIni, + UMain; + +type + TPortaudioPlaybackStream = class(TAudioPlaybackStream) + private + status: TStreamStatus; + Loaded: boolean; + Loop: boolean; + public + decodeStream: TAudioDecodeStream; + + constructor Create(decodeStream: TAudioDecodeStream); + 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 ReadData(Buffer: PChar; BufSize: integer): integer; + + function IsLoaded(): boolean; + end; + +type + TAudioMixerStream = class + private + activeStreams: TList; + public + constructor Create(); + destructor Destroy(); override; + procedure AddStream(stream: TAudioPlaybackStream); + procedure RemoveStream(stream: TAudioPlaybackStream); + function ReadData(Buffer: PChar; BufSize: integer): integer; + end; + +type + TAudioPlayback_Portaudio = class( TInterfacedObject, IAudioPlayback ) + private + MusicStream: TPortaudioPlaybackStream; + + StartSoundStream: TPortaudioPlaybackStream; + BackSoundStream: TPortaudioPlaybackStream; + SwooshSoundStream: TPortaudioPlaybackStream; + ChangeSoundStream: TPortaudioPlaybackStream; + OptionSoundStream: TPortaudioPlaybackStream; + ClickSoundStream: TPortaudioPlaybackStream; + DrumSoundStream: TPortaudioPlaybackStream; + HihatSoundStream: TPortaudioPlaybackStream; + ClapSoundStream: TPortaudioPlaybackStream; + ShuffleSoundStream: TPortaudioPlaybackStream; + + //Custom Sounds + CustomSounds: array of TCustomSoundEntry; + + mixerStream: TAudioMixerStream; + paStream: PPaStream; + public + FrameSize: integer; + + function GetName: String; + + function InitializePortaudio(): boolean; + function StartPortaudioStream(): boolean; + + procedure InitializePlayback(); + procedure SetVolume(Volume: integer); + procedure SetMusicVolume(Volume: integer); + procedure SetLoop(Enabled: boolean); + function Open(Filename: string): boolean; // true if succeed + function Load(Filename: string): TPortaudioPlaybackStream; + procedure Rewind; + procedure SetPosition(Time: real); + procedure Play; + procedure Pause; //Pause Mod + + procedure Stop; + procedure Close; + function Finished: boolean; + function Length: real; + function GetPosition: real; + procedure PlayStart; + procedure PlayBack; + procedure PlaySwoosh; + procedure PlayChange; + procedure PlayOption; + procedure PlayClick; + procedure PlayDrum; + procedure PlayHihat; + procedure PlayClap; + procedure PlayShuffle; + procedure StopShuffle; + + //Equalizer + function GetFFTData: TFFTData; + + // Interface for Visualizer + function GetPCMData(var data: TPCMData): Cardinal; + + //Custom Sounds + function LoadCustomSound(const Filename: String): Cardinal; + procedure PlayCustomSound(const Index: Cardinal ); + end; + + +function AudioCallback(input: Pointer; output: Pointer; frameCount: Longword; + timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags; + userData: Pointer): Integer; cdecl; forward; + +var + singleton_AudioPlaybackPortaudio : IAudioPlayback; + + +{ TAudioMixerStream } + +constructor TAudioMixerStream.Create(); +begin + activeStreams := TList.Create; +end; + +destructor TAudioMixerStream.Destroy(); +begin + activeStreams.Free; +end; + +procedure TAudioMixerStream.AddStream(stream: TAudioPlaybackStream); +begin + // check if stream is already in list to avoid duplicates + if (activeStreams.IndexOf(Pointer(stream)) = -1) then + activeStreams.Add(Pointer(stream)); +end; + +procedure TAudioMixerStream.RemoveStream(stream: TAudioPlaybackStream); +begin + activeStreams.Remove(Pointer(stream)); +end; + +function TAudioMixerStream.ReadData(Buffer: PChar; BufSize: integer): integer; +var + i: integer; + stream: TPortaudioPlaybackStream; + dataAvailable: boolean; +begin + dataAvailable := false; + + // search for playing stream + if (activeStreams.Count > 0) then + begin + for i := 0 to activeStreams.Count-1 do + begin + stream := TPortaudioPlaybackStream(activeStreams[i]); + if (stream.getStatus = sPlaying) then + begin + dataAvailable := true; + break; + end; + end; + + // fetch data from current stream + if (dataAvailable) then + begin + result := stream.ReadData(Buffer, BufSize); + end; + end; + + // return silence + if ((not dataAvailable) or (result = -1)) then + begin + FillChar(Buffer^, BufSize, 0); + result := BufSize; + end; +end; + + +{ TPortaudioPlaybackStream } + +constructor TPortaudioPlaybackStream.Create(decodeStream: TAudioDecodeStream); +begin + inherited Create(); + status := sStopped; + if (decodeStream <> nil) then + begin + Self.decodeStream := decodeStream; + Loaded := true; + end; +end; + +procedure TPortaudioPlaybackStream.Play(); +begin + status := sPlaying; +end; + +procedure TPortaudioPlaybackStream.Pause(); +begin + status := sPaused; +end; + +procedure TPortaudioPlaybackStream.Stop(); +begin + status := sStopped; +end; + +procedure TPortaudioPlaybackStream.Close(); +begin + Stop(); +end; + +function TPortaudioPlaybackStream.IsLoaded(): boolean; +begin + result := Loaded; +end; + +function TPortaudioPlaybackStream.GetLoop(): boolean; +begin + result := Loop; +end; + +procedure TPortaudioPlaybackStream.SetLoop(Enabled: boolean); +begin + Loop := Enabled; +end; + +function TPortaudioPlaybackStream.GetLength(): real; +begin + result := decodeStream.Length; +end; + +function TPortaudioPlaybackStream.GetStatus(): TStreamStatus; +begin + result := status; +end; + +function TPortaudioPlaybackStream.ReadData(Buffer: PChar; BufSize: integer): integer; +begin + result := decodeStream.ReadData(Buffer, BufSize); + // end-of-stream reached -> stop playback + if (decodeStream.EOS) then + begin + status := sStopped; + end; +end; + + +{ TAudioPlayback_Portaudio } + +function AudioCallback(input: Pointer; output: Pointer; frameCount: Longword; + timeInfo: PPaStreamCallbackTimeInfo; statusFlags: TPaStreamCallbackFlags; + userData: Pointer): Integer; cdecl; +var + playback : TAudioPlayback_Portaudio; + playbackStream : TPortaudioPlaybackStream; + decodeStream : TAudioDecodeStream; +begin + playback := TAudioPlayback_Portaudio(userData); + with playback do + begin + mixerStream.ReadData(output, frameCount * FrameSize); + end; + result := paContinue; +end; + + +function TAudioPlayback_Portaudio.GetName: String; +begin + result := 'Portaudio_Playback'; +end; + +function TAudioPlayback_Portaudio.InitializePortaudio(): boolean; +var + paApi : TPaHostApiIndex; + paApiInfo : PPaHostApiInfo; + paOutParams : TPaStreamParameters; + paOutDevice : TPaDeviceIndex; + paOutDeviceInfo : PPaDeviceInfo; + err : TPaError; +begin + result := false; + + Pa_Initialize(); + + // FIXME: determine automatically + {$IFDEF WIN32} + paApi := Pa_HostApiTypeIdToHostApiIndex(paDirectSound); + {$ELSE} + paApi := Pa_HostApiTypeIdToHostApiIndex(paALSA); + {$ENDIF} + if (paApi < 0) then + begin + Log.LogStatus('Pa_HostApiTypeIdToHostApiIndex: '+Pa_GetErrorText(paApi), 'UAudioPlayback_Portaudio'); + exit; + end; + + paApiInfo := Pa_GetHostApiInfo(paApi); + paOutDevice := paApiInfo^.defaultOutputDevice; + paOutDeviceInfo := Pa_GetDeviceInfo(paOutDevice); + + with paOutParams do begin + device := paOutDevice; + channelCount := 2; + sampleFormat := paInt16; + suggestedLatency := paOutDeviceInfo^.defaultLowOutputLatency; + hostApiSpecificStreamInfo := nil; + end; + + FrameSize := 2 * sizeof(Smallint); + + err := Pa_OpenStream(paStream, nil, @paOutParams, 44100, + paFramesPerBufferUnspecified, + paNoFlag, @AudioCallback, Self); + if(err <> paNoError) then begin + Log.LogStatus('Pa_OpenStream: '+Pa_GetErrorText(err), 'UAudioPlayback_Portaudio'); + exit; + end; + + Log.LogStatus('Opened audio device', 'UAudioPlayback_Portaudio'); + + result := true; +end; + +function TAudioPlayback_Portaudio.StartPortaudioStream(): boolean; +var + err: TPaError; +begin + result := false; + + err := Pa_StartStream(paStream); + if(err <> paNoError) then + begin + Log.LogStatus('Pa_StartStream: '+Pa_GetErrorText(err), 'UAudioPlayback_Portaudio'); + exit; + end; + + result := true; +end; + +procedure TAudioPlayback_Portaudio.InitializePlayback; +begin + Log.LogStatus('InitializePlayback', 'UAudioPlayback_Portaudio'); + + InitializePortaudio(); + mixerStream := TAudioMixerStream.Create; + + StartSoundStream := Load(SoundPath + 'Common start.mp3'); + BackSoundStream := Load(SoundPath + 'Common back.mp3'); + SwooshSoundStream := Load(SoundPath + 'menu swoosh.mp3'); + ChangeSoundStream := Load(SoundPath + 'select music change music 50.mp3'); + OptionSoundStream := Load(SoundPath + 'option change col.mp3'); + ClickSoundStream := Load(SoundPath + 'rimshot022b.mp3'); + +// DrumSoundStream := Load(SoundPath + 'bassdrumhard076b.mp3'); +// HihatSoundStream := Load(SoundPath + 'hihatclosed068b.mp3'); +// ClapSoundStream := Load(SoundPath + 'claps050b.mp3'); + +// ShuffleSoundStream := Load(SoundPath + 'Shuffle.mp3'); + + StartPortaudioStream(); +end; + + +procedure TAudioPlayback_Portaudio.SetVolume(Volume: integer); +begin + //New: Sets Volume only for this Application +(* + BASS_SetConfig(BASS_CONFIG_GVOL_SAMPLE, Volume); + BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Volume); + BASS_SetConfig(BASS_CONFIG_GVOL_MUSIC, Volume); +*) +end; + +procedure TAudioPlayback_Portaudio.SetMusicVolume(Volume: Integer); +begin + //Max Volume Prevention + if Volume > 100 then + Volume := 100; + + if Volume < 0 then + Volume := 0; + + //Set Volume +// BASS_ChannelSetAttributes (Bass, -1, Volume, -101); +end; + +procedure TAudioPlayback_Portaudio.SetLoop(Enabled: boolean); +begin + MusicStream.SetLoop(Enabled); +end; + +function TAudioPlayback_Portaudio.Open(Filename: string): boolean; +var + decodeStream: TAudioDecodeStream; +begin + decodeStream := AudioDecoder.Open(Filename); + MusicStream := TPortaudioPlaybackStream.Create(decodeStream); + + if(MusicStream.IsLoaded()) then + begin + //Set Max Volume + SetMusicVolume(100); + end; + + Result := MusicStream.IsLoaded(); +end; + +procedure TAudioPlayback_Portaudio.Rewind; +begin + SetPosition(0); +end; + +procedure TAudioPlayback_Portaudio.SetPosition(Time: real); +var + bytes: integer; +begin + if (MusicStream.IsLoaded) then + begin + MusicStream.SetPosition(Time); + end; +end; + +procedure TAudioPlayback_Portaudio.Play; +begin + if (MusicStream <> nil) then + if (MusicStream.IsLoaded()) then + begin + if (MusicStream.GetLoop()) then + begin + end; + // start from beginning... + // actually bass itself does not loop, nor does this TAudio_FFMpeg Class + MusicStream.Play(); + end; +end; + +procedure TAudioPlayback_Portaudio.Pause; //Pause Mod +begin + if (MusicStream <> nil) then + if (MusicStream.IsLoaded()) then begin + MusicStream.Pause(); // Pauses Song + end; +end; + +procedure TAudioPlayback_Portaudio.Stop; +begin + if MusicStream <> nil then + MusicStream.Stop(); +end; + +procedure TAudioPlayback_Portaudio.Close; +begin + if MusicStream <> nil then + MusicStream.Close(); +end; + +function TAudioPlayback_Portaudio.Length: real; +begin + Result := 0; + if assigned( MusicStream ) then + begin + Result := MusicStream.GetLength(); + end; +end; + +function TAudioPlayback_Portaudio.getPosition: real; +var + bytes: integer; +begin + Result := 0; + +(* + bytes := BASS_ChannelGetPosition(BASS); + Result := BASS_ChannelBytes2Seconds(BASS, bytes); +*) +end; + +function TAudioPlayback_Portaudio.Finished: boolean; +begin + Result := false; + +(* + if BASS_ChannelIsActive(BASS) = BASS_ACTIVE_STOPPED then + begin + Result := true; + end; +*) +end; + +procedure TAudioPlayback_Portaudio.PlayStart; +begin + if StartSoundStream <> nil then + StartSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayBack; +begin + if BackSoundStream <> nil then + BackSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlaySwoosh; +begin + if SwooshSoundStream <> nil then + SwooshSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayChange; +begin + if ChangeSoundStream <> nil then + ChangeSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayOption; +begin + if OptionSoundStream <> nil then + OptionSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayClick; +begin + if ClickSoundStream <> nil then + ClickSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayDrum; +begin + if DrumSoundStream <> nil then + DrumSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayHihat; +begin + if HihatSoundStream <> nil then + HihatSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayClap; +begin + if ClapSoundStream <> nil then + ClapSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.PlayShuffle; +begin + if ShuffleSoundStream <> nil then + ShuffleSoundStream.Play(); +end; + +procedure TAudioPlayback_Portaudio.StopShuffle; +begin + if ShuffleSoundStream <> nil then + ShuffleSoundStream.Stop(); +end; + +//Equalizer +function TAudioPlayback_Portaudio.GetFFTData: TFFTData; +var + data: TFFTData; +begin + //Get Channel Data Mono and 256 Values +// BASS_ChannelGetData(Bass, @Result, BASS_DATA_FFT512); + result := data; +end; + +// Interface for Visualizer +function TAudioPlayback_Portaudio.GetPCMData(var data: TPCMData): Cardinal; +begin + result := 0; +end; + +function TAudioPlayback_Portaudio.Load(Filename: string): TPortaudioPlaybackStream; +var + decodeStream : TAudioDecodeStream; + playbackStream : TPortaudioPlaybackStream; + csIndex : integer; +begin + result := nil; + + decodeStream := AudioDecoder.Open(Filename); + if (decodeStream = nil) then + begin + Log.LogStatus('LoadSoundFromFile: Sound not found "' + Filename + '"', 'UAudioPlayback_Portaudio'); + exit; + end; + + playbackStream := TPortaudioPlaybackStream.Create(decodeStream); + mixerStream.AddStream(playbackStream); + + //Add CustomSound + csIndex := High(CustomSounds) + 1; + SetLength(CustomSounds, csIndex + 1); + CustomSounds[csIndex].Filename := Filename; + CustomSounds[csIndex].Stream := playbackStream; + + result := playbackStream; +end; + +function TAudioPlayback_Portaudio.LoadCustomSound(const Filename: String): Cardinal; +var + S: TAudioPlaybackStream; + I: Integer; + F: String; +begin + //Search for Sound in already loaded Sounds + F := UpperCase(SoundPath + FileName); + For I := 0 to High(CustomSounds) do + begin + if (UpperCase(CustomSounds[I].Filename) = F) then + begin + Result := I; + Exit; + end; + end; + + S := Load(SoundPath + Filename); + if (S <> nil) then + Result := High(CustomSounds) + else + Result := 0; +end; + +procedure TAudioPlayback_Portaudio.PlayCustomSound(const Index: Cardinal ); +begin + if (Index <= High(CustomSounds)) then + CustomSounds[Index].Stream.Play(); +end; + + + +initialization + singleton_AudioPlaybackPortaudio := TAudioPlayback_Portaudio.create(); + AudioManager.add( singleton_AudioPlaybackPortaudio ); + +finalization + AudioManager.Remove( singleton_AudioPlaybackPortaudio ); + + +end. -- cgit v1.2.3