From b5a738fa52c8b0f2212deb5febd2d7f0b8f6544f Mon Sep 17 00:00:00 2001 From: tobigun Date: Fri, 9 May 2008 19:19:28 +0000 Subject: - input-source selection works now (with bass or portaudio with portmixer) - audio-effects (DSP) interface for audio-playback plus a simple voice removal example (does not sound that good) - FFMpeg support for BASS - audio-clock for FFMpeg for GetPosition and synchronisation - more compatible seeking in FFMpeg - clean termination of the audio interfaces/streams (especially ffmpeg) - Audio output device enumeration (selection will be added later to the sounds option screen) - display of threshold and volume in the record-options screen - threshold and volume can be changed with the 'T' (threshold) and '+'/'-' (source volume) keys - added a FadeIn() method to the IAudioPlayback interface - some minor changes to the audio classes/screens - new base-class for audio-playback classes (used by bass, portaudio and sdl) git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/trunk@1078 b956fd51-792f-4845-bead-9b4dfca2ff2c --- Game/Code/Classes/UAudioCore_Bass.pas | 27 +- Game/Code/Classes/UAudioCore_Portaudio.pas | 25 +- Game/Code/Classes/UAudioDecoder_FFMpeg.pas | 546 +++++++++++++++--------- Game/Code/Classes/UAudioInput_Bass.pas | 329 +++++++++++---- Game/Code/Classes/UAudioInput_Portaudio.pas | 255 +++++++++--- Game/Code/Classes/UAudioPlaybackBase.pas | 220 ++++++++++ Game/Code/Classes/UAudioPlayback_Bass.pas | 549 ++++++++++++++++--------- Game/Code/Classes/UAudioPlayback_Portaudio.pas | 248 +++++++++-- Game/Code/Classes/UAudioPlayback_SDL.pas | 46 ++- Game/Code/Classes/UAudioPlayback_SoftMixer.pas | 370 ++++++++--------- Game/Code/Classes/UIni.pas | 166 +++----- Game/Code/Classes/UMain.pas | 11 +- Game/Code/Classes/UMedia_dummy.pas | 38 +- Game/Code/Classes/UMusic.pas | 162 +++++++- Game/Code/Classes/URecord.pas | 167 +++++--- Game/Code/Screens/UScreenOptionsRecord.pas | 532 +++++++++++++++++------- Game/Code/Screens/UScreenOptionsSound.pas | 2 +- Game/Code/Screens/UScreenSong.pas | 95 +++-- Game/Code/UltraStar.dpr | 2 +- 19 files changed, 2628 insertions(+), 1162 deletions(-) create mode 100644 Game/Code/Classes/UAudioPlaybackBase.pas (limited to 'Game/Code') diff --git a/Game/Code/Classes/UAudioCore_Bass.pas b/Game/Code/Classes/UAudioCore_Bass.pas index 9c13b461..442c999b 100644 --- a/Game/Code/Classes/UAudioCore_Bass.pas +++ b/Game/Code/Classes/UAudioCore_Bass.pas @@ -9,8 +9,12 @@ interface {$I switches.inc} uses + {$IFDEF MSWINDOWS} + Windows, + {$ENDIF} Classes, - SysUtils; + SysUtils, + UMusic; type TAudioCore_Bass = class @@ -18,6 +22,7 @@ type public class function ErrorGetString(): string; overload; class function ErrorGetString(errCode: integer): string; overload; + class function ConvertAudioFormatToBASSFlags(Format: TAudioSampleFormat; out Flags: DWORD): boolean; end; @@ -113,4 +118,22 @@ begin end; end; -end. \ No newline at end of file +class function TAudioCore_Bass.ConvertAudioFormatToBASSFlags(Format: TAudioSampleFormat; out Flags: DWORD): boolean; +begin + case Format of + asfS16: + Flags := 0; + asfFloat: + Flags := BASS_SAMPLE_FLOAT; + asfU8: + Flags := BASS_SAMPLE_8BITS; + else begin + Result := false; + Exit; + end; + end; + + Result := true; +end; + +end. diff --git a/Game/Code/Classes/UAudioCore_Portaudio.pas b/Game/Code/Classes/UAudioCore_Portaudio.pas index bb0635b3..cd228982 100644 --- a/Game/Code/Classes/UAudioCore_Portaudio.pas +++ b/Game/Code/Classes/UAudioCore_Portaudio.pas @@ -16,9 +16,12 @@ uses type TAudioCore_Portaudio = class + private + constructor Create(); public - class function GetPreferredApiIndex(): TPaHostApiIndex; - class function TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: Double): boolean; + class function GetInstance(): TAudioCore_Portaudio; + function GetPreferredApiIndex(): TPaHostApiIndex; + function TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: Double): boolean; end; implementation @@ -57,10 +60,24 @@ const array[0..0] of TPaHostApiTypeId = ( paDefaultApi ); {$IFEND} +var + Instance: TAudioCore_Portaudio; { TAudioInput_Portaudio } -class function TAudioCore_Portaudio.GetPreferredApiIndex(): TPaHostApiIndex; +constructor TAudioCore_Portaudio.Create(); +begin + inherited; +end; + +class function TAudioCore_Portaudio.GetInstance(): TAudioCore_Portaudio; +begin + if not assigned(Instance) then + Instance := TAudioCore_Portaudio.Create(); + Result := Instance; +end; + +function TAudioCore_Portaudio.GetPreferredApiIndex(): TPaHostApiIndex; var i: integer; apiIndex: TPaHostApiIndex; @@ -148,7 +165,7 @@ end; * So we have to provide the possibility to manually select an output device * in the UltraStar options if we want to use portaudio instead of SDL. *) -class function TAudioCore_Portaudio.TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: Double): boolean; +function TAudioCore_Portaudio.TestDevice(inParams, outParams: PPaStreamParameters; var sampleRate: Double): boolean; var stream: PPaStream; err: TPaError; diff --git a/Game/Code/Classes/UAudioDecoder_FFMpeg.pas b/Game/Code/Classes/UAudioDecoder_FFMpeg.pas index 209e1838..f1c9b364 100644 --- a/Game/Code/Classes/UAudioDecoder_FFMpeg.pas +++ b/Game/Code/Classes/UAudioDecoder_FFMpeg.pas @@ -36,8 +36,10 @@ uses avutil, avio, // used for url_ferror mathematics, // used for av_rescale_q + rational, SDL, ULog, + UCommon, UConfig; type @@ -50,35 +52,49 @@ type size : integer; mutex : PSDL_Mutex; cond : PSDL_Cond; - quit : boolean; + abortRequest: boolean; public constructor Create(); destructor Destroy(); override; function Put(pkt : PAVPacket): integer; + function PutStatus(statusFlag: integer; statusInfo: Pointer): integer; function Get(var pkt: TAVPacket; block: boolean): integer; procedure Flush(); + procedure Abort(); end; const MAX_AUDIOQ_SIZE = (5 * 16 * 1024); -var - EOFPacket: TAVPacket; - FlushPacket: TAVPacket; +const + STATUS_PACKET: PChar = 'STATUS_PACKET'; +const + PKT_STATUS_FLAG_EOF = 1; + PKT_STATUS_FLAG_FLUSH = 2; + PKT_STATUS_FLAG_ERROR = 3; type PAudioBuffer = ^TAudioBuffer; + // TODO: should (or must?) be aligned at a 2-byte boundary. + // ffmpeg provides a C-macro called DECLARE_ALIGNED for this. + // Records can be aligned with the $PACKRECORDS compiler-directive but + // are already aligned at a 2-byte boundary by default in FPC. + // But what about arrays, are they aligned by a 2-byte boundary too? + // Or maybe we have to define a fake record with only an array in it? TAudioBuffer = array[0 .. (AVCODEC_MAX_AUDIO_FRAME_SIZE * 3 div 2)-1] of byte; type TFFMpegDecodeStream = class(TAudioDecodeStream) private - _EOF: boolean; // end-of-stream flag - _EOF_lock : PSDL_Mutex; + decoderLock : PSDL_Mutex; + parserLock : PSDL_Mutex; + myint: integer; + + EOFState: boolean; // end-of-stream flag + ErrorState: boolean; - internalLock : PSDL_Mutex; resumeCond : PSDL_Cond; quitRequest : boolean; @@ -99,23 +115,30 @@ type ffmpegStreamIndex : Integer; ffmpegStream : PAVStream; + audioClock: double; // stream position in seconds + // state-vars for DecodeFrame pkt : TAVPacket; audio_pkt_data : PChar; audio_pkt_size : integer; // state-vars for AudioCallback - audio_buf_index : cardinal; - audio_buf_size : cardinal; + audio_buf_index : integer; + audio_buf_size : integer; audio_buf : TAudioBuffer; - procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} - procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} - function GetLockMutex(): PSDL_Mutex; {$IFDEF HasInline}inline;{$ENDIF} + procedure LockParser(); {$IFDEF HasInline}inline;{$ENDIF} + procedure UnlockParser(); {$IFDEF HasInline}inline;{$ENDIF} + function GetParserMutex(): PSDL_Mutex; {$IFDEF HasInline}inline;{$ENDIF} + + procedure LockDecoder(); {$IFDEF HasInline}inline;{$ENDIF} + procedure UnlockDecoder(); {$IFDEF HasInline}inline;{$ENDIF} + + procedure SetEOF(state: boolean); {$IFDEF HasInline}inline;{$ENDIF} + procedure SetError(state: boolean); {$IFDEF HasInline}inline;{$ENDIF} procedure ParseAudio(); function DecodeFrame(var buffer: TAudioBuffer; bufSize: integer): integer; - procedure SetEOF(state: boolean); public constructor Create(pFormatCtx: PAVFormatContext; pCodecCtx: PAVCodecContext; pCodec: PAVCodec; @@ -129,6 +152,7 @@ type function GetPosition: real; override; procedure SetPosition(Time: real); override; function IsEOF(): boolean; override; + function IsError(): boolean; override; function ReadData(Buffer: PChar; BufSize: integer): integer; override; end; @@ -141,6 +165,7 @@ type function GetName: String; function InitializeDecoder(): boolean; + function FinalizeDecoder(): boolean; function Open(const Filename: string): TAudioDecodeStream; end; @@ -155,6 +180,8 @@ var constructor TFFMpegDecodeStream.Create(pFormatCtx: PAVFormatContext; pCodecCtx: PAVCodecContext; pCodec: PAVCodec; ffmpegStreamIndex : Integer; ffmpegStream: PAVStream); +var + sampleFormat: TAudioSampleFormat; begin inherited Create(); @@ -174,41 +201,45 @@ begin Self.ffmpegStreamIndex := ffmpegStreamIndex; Self.ffmpegStream := ffmpegStream; + case pCodecCtx^.sample_fmt of + SAMPLE_FMT_U8: sampleFormat := asfU8; + SAMPLE_FMT_S16: sampleFormat := asfS16; + SAMPLE_FMT_S24: sampleFormat := asfS24; + SAMPLE_FMT_S32: sampleFormat := asfS32; + SAMPLE_FMT_FLT: sampleFormat := asfFloat; + else sampleFormat := asfS16; // try standard format + end; + formatInfo := TAudioFormatInfo.Create( pCodecCtx^.channels, pCodecCtx^.sample_rate, - // pCodecCtx^.sample_fmt not yet used by FFMpeg -> use FFMpeg's standard format - asfS16 + sampleFormat ); - _EOF := false; - _EOF_lock := SDL_CreateMutex(); - - internalLock := SDL_CreateMutex(); + EOFState := false; + ErrorState := false; + decoderLock := SDL_CreateMutex(); + parserLock := SDL_CreateMutex(); resumeCond := SDL_CreateCond(); parseThread := SDL_CreateThread(@DecodeThreadMain, Self); end; +{* + * Frees the decode-stream data. + * IMPORTANT: call Close() before freeing the decode-stream to avoid dead-locks. + * This wakes-up every waiting audio-thread waiting in the packet-queue while + * performing a ReadData() request. + * Then assure that no thread uses ReadData anymore (e.g. by stopping the audio-callback). + * Now you can free the decode-stream. + *} destructor TFFMpegDecodeStream.Destroy(); begin + // wake-up and terminate threads + // Note: should be called by the caller before Destroy() was called instead + // to wake-up a waiting audio-callback thread in the packet-queue. + // Otherwise dead-locks are possible. Close(); - inherited; -end; - -procedure TFFMpegDecodeStream.Close(); -var - status: integer; -begin - Lock(); - quitRequest := true; - SDL_CondSignal(resumeCond); - Unlock(); - - if (parseThread <> nil) then - begin - SDL_WaitThread(parseThread, status); - end; // Close the codec if (pCodecCtx <> nil) then @@ -226,21 +257,64 @@ begin FreeAndNil(packetQueue); FreeAndNil(formatInfo); + + SDL_DestroyMutex(decoderLock); + decoderLock := nil; + SDL_DestroyMutex(parserLock); + parserLock := nil; + SDL_DestroyCond(resumeCond); + resumeCond := nil; + + inherited; end; -procedure TFFMpegDecodeStream.Lock(); +procedure TFFMpegDecodeStream.Close(); +var + status: integer; begin - SDL_mutexP(internalLock); + // wake threads waiting for packet-queue data + packetQueue.Abort(); + + // abort parse-thread + LockParser(); + quitRequest := true; + SDL_CondSignal(resumeCond); + UnlockParser(); + // and wait until it terminates + if (parseThread <> nil) then + begin + SDL_WaitThread(parseThread, status); + parseThread := nil; + end; + + // NOTE: we cannot free the codecCtx or formatCtx here because + // a formerly waiting thread in the packet-queue might require them + // and crash if it tries to access them. +end; + +procedure TFFMpegDecodeStream.LockParser(); +begin + SDL_mutexP(parserLock); +end; + +procedure TFFMpegDecodeStream.UnlockParser(); +begin + SDL_mutexV(parserLock); end; -procedure TFFMpegDecodeStream.Unlock(); +function TFFMpegDecodeStream.GetParserMutex(): PSDL_Mutex; begin - SDL_mutexV(internalLock); + Result := parserLock; end; -function TFFMpegDecodeStream.GetLockMutex(): PSDL_Mutex; +procedure TFFMpegDecodeStream.LockDecoder(); begin - Result := internalLock; + SDL_mutexP(decoderLock); +end; + +procedure TFFMpegDecodeStream.UnlockDecoder(); +begin + SDL_mutexV(decoderLock); end; function TFFMpegDecodeStream.GetLength(): real; @@ -255,37 +329,59 @@ end; function TFFMpegDecodeStream.IsEOF(): boolean; begin - SDL_mutexP(_EOF_lock); - Result := _EOF; - SDL_mutexV(_EOF_lock); + LockDecoder(); + Result := EOFState; + UnlockDecoder(); end; procedure TFFMpegDecodeStream.SetEOF(state: boolean); begin - SDL_mutexP(_EOF_lock); - _EOF := state; - SDL_mutexV(_EOF_lock); + LockDecoder(); + EOFState := state; + UnlockDecoder(); +end; + +function TFFMpegDecodeStream.IsError(): boolean; +begin + LockDecoder(); + Result := ErrorState; + UnlockDecoder(); +end; + +procedure TFFMpegDecodeStream.SetError(state: boolean); +begin + LockDecoder(); + ErrorState := state; + UnlockDecoder(); end; function TFFMpegDecodeStream.GetPosition(): real; -//var -// bytes: integer; begin + // FIXME: the audio-clock might not be that accurate // see: tutorial on synching (audio-clock) - Result := 0; + Result := audioClock; end; procedure TFFMpegDecodeStream.SetPosition(Time: real); -//var -// bytes: integer; begin - Lock(); + LockParser(); + seekPos := Trunc(Time * AV_TIME_BASE); - // FIXME: seek_flags = rel < 0 ? AVSEEK_FLAG_BACKWARD : 0 - seekFlags := 0;//AVSEEK_FLAG_BACKWARD; + + seekFlags := 0; + // Note: the BACKWARD-flag seeks to the first position <= the position + // searched for. Otherwise e.g. position 0 might not be seeked correct. + // For some reason ffmpeg sometimes doesn't use position 0 but the key-frame + // following. In streams with few key-frames (like many flv-files) the next + // key-frame after 0 might be 5secs ahead. + if (Time < audioClock) then + seekFlags := AVSEEK_FLAG_BACKWARD; + seekFlags := AVSEEK_FLAG_ANY; + seekRequest := true; SDL_CondSignal(resumeCond); - Unlock(); + + UnlockParser(); end; function DecodeThreadMain(streamPtr: Pointer): integer; cdecl; @@ -300,68 +396,71 @@ end; procedure TFFMpegDecodeStream.ParseAudio(); var packet: TAVPacket; + statusPacket: PAVPacket; seekTarget: int64; - eofState: boolean; + stopParsing: boolean; pbIOCtx: PByteIOContext; + err: integer; + index: integer; begin - eofState := false; + stopParsing := false; while (true) do begin - Lock(); + LockParser(); + // wait if end-of-file reached - if (eofState) then + if (stopParsing) then begin - if (not (seekRequest or quitRequest)) then - begin - // signal end-of-file - packetQueue.put(@EOFPacket); - // wait for reuse or destruction of stream - repeat - SDL_CondWait(resumeCond, GetLockMutex()); - until (seekRequest or quitRequest); - end; - eofState := false; - SetEOF(false); + // wait for reuse or destruction of stream + while not (seekRequest or quitRequest) do + SDL_CondWait(resumeCond, GetParserMutex()); end; if (quitRequest) then begin + UnlockParser(); break; end; // handle seek-request - if(seekRequest) then + if (seekRequest) then begin - // TODO: Do we need this? - // The position is converted to AV_TIME_BASE and then to the stream-specific base. - // Why not convert to the stream-specific one from the beginning. + // reset status + SetEOF(false); + SetError(false); + stopParsing := false; + seekTarget := av_rescale_q(seekPos, AV_TIME_BASE_Q, ffmpegStream^.time_base); - if(av_seek_frame(pFormatCtx, ffmpegStreamIndex, - seekTarget, seekFlags) < 0) then + err := av_seek_frame(pFormatCtx, ffmpegStreamIndex, seekTarget, seekFlags); + // seeking failed -> retry with the default stream (necessary for flv-videos and some ogg-files) + if (err < 0) then + err := av_seek_frame(pFormatCtx, -1, seekPos, seekFlags); + // check if seeking failed + if (err < 0) then begin - // this will crash in FPC due to a bug - //Log.LogStatus({pFormatCtx^.filename +} ': error while seeking', 'UAudioDecoder_FFMpeg'); + Log.LogStatus('Seek Error in "'+pFormatCtx^.filename+'"', 'UAudioDecoder_FFMpeg'); end else begin packetQueue.Flush(); - packetQueue.Put(@FlushPacket); + packetQueue.PutStatus(PKT_STATUS_FLAG_FLUSH, nil); end; seekRequest := false; end; - Unlock(); + UnlockParser(); - - if(packetQueue.size > MAX_AUDIOQ_SIZE) then + if (packetQueue.size > MAX_AUDIOQ_SIZE) then begin SDL_Delay(10); continue; end; - if(av_read_frame(pFormatCtx, packet) < 0) then + if (av_read_frame(pFormatCtx, packet) < 0) then begin + // failed to read a frame, check reason + {$IF (LIBAVFORMAT_VERSION_MAJOR >= 52)} pbIOCtx := pFormatCtx^.pb; {$ELSE} @@ -369,39 +468,30 @@ begin {$IFEND} // check for end-of-file (eof is not an error) - if(url_feof(pbIOCtx) <> 0) then + if (url_feof(pbIOCtx) <> 0) then begin - {$IFDEF DebugFFMpegDecode} - DebugWriteln('feof'); - {$ENDIF} - eofState := true; + // signal end-of-file + packetQueue.putStatus(PKT_STATUS_FLAG_EOF, nil); + stopParsing := true; continue; end; // check for errors - if(url_ferror(pbIOCtx) = 0) then + if (url_ferror(pbIOCtx) <> 0) then begin - {$IFDEF DebugFFMpegDecode} - DebugWriteln('Errorf'); - {$ENDIF} - // no error -> wait for user input - SDL_Delay(100); - continue; - end - else - begin - // an error occured -> abort - // TODO: eof or quit? - eofState := true; + // an error occured -> abort and wait for repositioning or termination + packetQueue.putStatus(PKT_STATUS_FLAG_ERROR, nil); + stopParsing := true; continue; end; - end; - //DebugWriteln( 'ffmpeg - av_read_frame' ); + // no error -> wait for user input + SDL_Delay(100); + continue; + end; - if(packet.stream_index = ffmpegStreamIndex) then + if (packet.stream_index = ffmpegStreamIndex) then begin - //DebugWriteln( 'packet_queue_put' ); packetQueue.put(@packet); end else @@ -425,20 +515,22 @@ begin begin while (audio_pkt_size > 0) do begin - //DebugWriteln( 'got audio packet' ); data_size := bufSize; - {$IF LIBAVCODEC_VERSION >= 51030000} // 51.30.0 - len1 := avcodec_decode_audio2(pCodecCtx, @buffer, - data_size, audio_pkt_data, audio_pkt_size); - {$ELSE} - // FIXME: with avcodec_decode_audio a package could contain several frames - // this is not handled yet - len1 := avcodec_decode_audio(pCodecCtx, @buffer, - data_size, audio_pkt_data, audio_pkt_size); - {$IFEND} - - //DebugWriteln('avcodec_decode_audio : ' + inttostr( len1 )); + try + {$IF LIBAVCODEC_VERSION >= 51030000} // 51.30.0 + len1 := avcodec_decode_audio2(pCodecCtx, @buffer, + data_size, audio_pkt_data, audio_pkt_size); + {$ELSE} + // FIXME: with avcodec_decode_audio a package could contain several frames + // this is not handled yet + len1 := avcodec_decode_audio(pCodecCtx, @buffer, + data_size, audio_pkt_data, audio_pkt_size); + {$IFEND} + except + Log.LogError('Exception at avcodec_decode_audio(2)!', 'TFFMpegDecodeStream.DecodeFrame'); + len1 := -1; + end; if(len1 < 0) then begin @@ -459,6 +551,10 @@ begin continue; end; + //pts := audioClock; + audioClock := audioClock + data_size / + (1.0 * formatInfo.FrameSize * formatInfo.SampleRate); + // we have data, return it and come back for more later result := data_size; exit; @@ -469,87 +565,104 @@ begin av_free_packet(@pkt); end; - if (packetQueue.quit) then + // do not use an aborted queue + if (packetQueue.abortRequest) then exit; + // request a new packet and block if non available. + // If this fails, the queue was aborted. if (packetQueue.Get(pkt, true) < 0) then exit; - audio_pkt_data := PChar(pkt.data); - audio_pkt_size := pkt.size; - - if (audio_pkt_data = PChar(FlushPacket.data)) then + // handle Status-packet + if (PChar(pkt.data) = STATUS_PACKET) then begin - avcodec_flush_buffers(pCodecCtx); - {$IFDEF DebugFFMpegDecode} - DebugWriteln('Flush'); - {$ENDIF} + pkt.data := nil; + audio_pkt_data := nil; + audio_pkt_size := 0; + + case (pkt.flags) of + PKT_STATUS_FLAG_FLUSH: + begin + avcodec_flush_buffers(pCodecCtx); + end; + PKT_STATUS_FLAG_EOF: // end-of-file + begin + SetEOF(true); + // buffer contains no data -> result = -1 + exit; + end; + PKT_STATUS_FLAG_ERROR: + begin + SetError(true); + Log.LogStatus('I/O Error', 'TFFMpegDecodeStream.DecodeFrame'); + exit; + end; + else + begin + Log.LogStatus('Unknown status', 'TFFMpegDecodeStream.DecodeFrame'); + end; + end; + continue; end; - // check for end-of-file - if (audio_pkt_data = PChar(EOFPacket.data)) then + audio_pkt_data := PChar(pkt.data); + audio_pkt_size := pkt.size; + + // if available, update the audio clock with pts + if(pkt.pts <> AV_NOPTS_VALUE) then begin - // end-of-file reached -> set EOF-flag - SetEOF(true); - {$IFDEF DebugFFMpegDecode} - DebugWriteln('EOF'); - {$ENDIF} - // note: buffer is not (even partially) filled -> no data to return - exit; + audioClock := av_q2d(ffmpegStream^.time_base) * pkt.pts; end; - - //DebugWriteln( 'Audio Packet Size - ' + inttostr(audio_pkt_size) ); end; end; function TFFMpegDecodeStream.ReadData(Buffer : PChar; BufSize: integer): integer; var - //outStream : TFFMpegDecodeStream; - len1, - audio_size : integer; - len : integer; + nBytesCopy: integer; // number of bytes to copy + nBytesRemain: integer; // number of bytes left (remaining) to read begin - len := BufSize; result := -1; - // end-of-file reached + // init number of bytes left to copy to the output buffer + nBytesRemain := BufSize; + + // leave if end-of-file was reached previously if EOF then exit; - while (len > 0) do begin + // copy data to output buffer + while (nBytesRemain > 0) do begin + // check if we need more data 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)); - //DebugWriteln('audio_decode_frame : '+ inttostr(audio_size)); - - if(audio_size < 0) then + audio_buf_size := DecodeFrame(audio_buf, sizeof(TAudioBuffer)); + // check for errors or EOF + if(audio_buf_size < 0) then begin - // if error, output silence + // fill decode-buffer with silence audio_buf_size := 1024; FillChar(audio_buf, audio_buf_size, #0); - //DebugWriteln( '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; + // calc number of new bytes in the decode-buffer + nBytesCopy := audio_buf_size - audio_buf_index; + // resize copy-count if more bytes available than needed (remaining bytes are used the next time) + if (nBytesCopy > nBytesRemain) then + nBytesCopy := nBytesRemain; - Move(audio_buf[audio_buf_index], Buffer[0], len1); + Move(audio_buf[audio_buf_index], Buffer[0], nBytesCopy); - Dec(len, len1); - Inc(Buffer, len1); - Inc(audio_buf_index, len1); + Dec(nBytesRemain, nBytesCopy); + Inc(Buffer, nBytesCopy); + Inc(audio_buf_index, nBytesCopy); end; - result := BufSize; + Result := BufSize; end; @@ -557,7 +670,7 @@ end; function TAudioDecoder_FFMpeg.GetName: String; begin - result := 'FFMpeg_Decoder'; + Result := 'FFMpeg_Decoder'; end; function TAudioDecoder_FFMpeg.InitializeDecoder: boolean; @@ -566,15 +679,12 @@ begin av_register_all(); - // init end-of-file package - av_init_packet(EOFPacket); - EOFPacket.data := Pointer(PChar('EOF')); - - // init flush package - av_init_packet(FlushPacket); - FlushPacket.data := Pointer(PChar('FLUSH')); + Result := true; +end; - result := true; +function TAudioDecoder_FFMpeg.FinalizeDecoder(): boolean; +begin + Result := true; end; class function TAudioDecoder_FFMpeg.FindAudioStreamIndex(pFormatCtx : PAVFormatContext): integer; @@ -599,7 +709,7 @@ begin end; end; - result := streamIndex; + Result := streamIndex; end; function TAudioDecoder_FFMpeg.Open(const Filename: string): TAudioDecodeStream; @@ -609,11 +719,9 @@ var pCodec : PAVCodec; ffmpegStreamID : Integer; ffmpegStream : PAVStream; -// wanted_spec, - //csIndex : integer; stream : TFFMpegDecodeStream; begin - result := nil; + Result := nil; if (not FileExists(Filename)) then begin @@ -622,19 +730,24 @@ begin end; // open audio file - if (av_open_input_file(pFormatCtx, PChar(Filename), nil, 0, nil) > 0) then + if (av_open_input_file(pFormatCtx, PChar(Filename), nil, 0, nil) <> 0) then exit; + // TODO: do we need to generate PTS values if they do not exist? + //pFormatCtx^.flags := pFormatCtx^.flags or AVFMT_FLAG_GENPTS; + // retrieve stream information if (av_find_stream_info(pFormatCtx) < 0) then exit; + // FIXME: hack used by ffplay. Maybe should not use url_feof() to test for the end + pFormatCtx^.pb.eof_reached := 0; + {$IFDEF DebugFFMpegDecode} dump_format(pFormatCtx, 0, pchar(Filename), 0); {$ENDIF} ffmpegStreamID := FindAudioStreamIndex(pFormatCtx); - //Writeln('ID: ' + inttostr(ffmpegStreamID)); if (ffmpegStreamID < 0) then exit; @@ -656,26 +769,25 @@ begin // detect bug-workarounds automatically pCodecCtx^.workaround_bugs := FF_BUG_AUTODETECT; + // error resilience strategy (careful/compliant/agressive/very_aggressive) + //pCodecCtx^.error_resilience := FF_ER_CAREFUL; //FF_ER_COMPLIANT; + // allow non spec compliant speedup tricks. + //pCodecCtx^.flags2 := pCodecCtx^.flags2 or CODEC_FLAG2_FAST; - // TODO: Not sure if these fields are for audio too - //pCodecCtx^.lowres := lowres; - //if (fast) then pCodecCtx^.flags2 := pCodecCtx^.flags2 or CODEC_FLAG2_FAST; - //pCodecCtx^.skip_frame := skip_frame; - //pCodecCtx^.skip_loop_filter := skip_loop_filter; - //pCodecCtx^.error_resilience := error_resilience; - //pCodecCtx^.error_concealment := error_concealment; - + // Note: avcodec_open() is not thread-safe! if (avcodec_open(pCodecCtx, pCodec) < 0) then begin Log.LogStatus('avcodec_open failed!', 'UAudio_FFMpeg'); exit; end; - //WriteLn( 'Opened the codec' ); + + // TODO: what about pCodecCtx^.start_time? Should we seek to this position here? + // ... stream := TFFMpegDecodeStream.Create(pFormatCtx, pCodecCtx, pCodec, ffmpegStreamID, ffmpegStream); - result := stream; + Result := stream; end; @@ -702,15 +814,30 @@ begin inherited; end; +procedure TPacketQueue.Abort(); +begin + SDL_LockMutex(mutex); + + abortRequest := true; + + SDL_CondSignal(cond); + SDL_UnlockMutex(mutex); +end; + function TPacketQueue.Put(pkt : PAVPacket): integer; var pkt1 : PAVPacketList; begin result := -1; - if ((pkt <> @EOFPacket) and (pkt <> @FlushPacket)) then + if (pkt = nil) then + exit; + + if (PChar(pkt^.data) <> STATUS_PACKET) then + begin if (av_dup_packet(pkt) < 0) then exit; + end; pkt1 := av_malloc(sizeof(TAVPacketList)); if (pkt1 = nil) then @@ -719,10 +846,8 @@ begin pkt1^.pkt := pkt^; pkt1^.next := nil; - SDL_LockMutex(Self.mutex); try - if (Self.lastPkt = nil) then Self.firstPkt := pkt1 else @@ -731,42 +856,58 @@ begin Self.lastPkt := pkt1; inc(Self.nbPackets); - //DebugWriteln('Put: ' + inttostr(nbPackets)); - Self.size := Self.size + pkt1^.pkt.size; SDL_CondSignal(Self.cond); - finally SDL_UnlockMutex(Self.mutex); end; - result := 0; + Result := 0; +end; + +function TPacketQueue.PutStatus(statusFlag: integer; statusInfo: Pointer): integer; +var + pkt: PAVPacket; +begin + // create temp. package + pkt := av_malloc(SizeOf(TAVPacket)); + if (pkt = nil) then + begin + Result := -1; + Exit; + end; + // init package + av_init_packet(pkt^); + pkt^.data := Pointer(STATUS_PACKET); + pkt^.flags := statusFlag; + pkt^.priv := statusInfo; + // put a copy of the package into the queue + Result := Put(pkt); + // data has been copied -> delete temp. package + av_free(pkt); end; function TPacketQueue.Get(var pkt: TAVPacket; block: boolean): integer; var pkt1 : PAVPacketList; begin - result := -1; + Result := -1; SDL_LockMutex(Self.mutex); try while true do begin - if (quit) then + if (abortRequest) then exit; pkt1 := Self.firstPkt; - if (pkt1 <> nil) then begin - Self.firstPkt := pkt1.next; + Self.firstPkt := pkt1^.next; if (Self.firstPkt = nil) then Self.lastPkt := nil; dec(Self.nbPackets); - //DebugWriteln('Get: ' + inttostr(nbPackets)); - Self.size := Self.size - pkt1^.pkt.size; pkt := pkt1^.pkt; av_free(pkt1); @@ -774,8 +915,7 @@ begin result := 1; break; end - else - if (not block) then + else if (not block) then begin result := 0; break; diff --git a/Game/Code/Classes/UAudioInput_Bass.pas b/Game/Code/Classes/UAudioInput_Bass.pas index a62ff22e..db49749e 100644 --- a/Game/Code/Classes/UAudioInput_Bass.pas +++ b/Game/Code/Classes/UAudioInput_Bass.pas @@ -27,21 +27,32 @@ uses type TAudioInput_Bass = class(TAudioInputBase) + private + function EnumDevices(): boolean; public function GetName: String; override; function InitializeRecord: boolean; override; - destructor Destroy; override; + function FinalizeRecord: boolean; override; end; TBassInputDevice = class(TAudioInputDevice) - public - DeviceIndex: integer; // index in TAudioInputProcessor.Device[] - BassDeviceID: integer; // DeviceID used by BASS + private RecordStream: HSTREAM; + BassDeviceID: DWORD; // DeviceID used by BASS + SingleIn: boolean; + + DeviceIndex: integer; // index in TAudioInputProcessor.Device[] - function Init(): boolean; + function SetInputSource(SourceIndex: integer): boolean; + function GetInputSource(): integer; + public + function Open(): boolean; + function Close(): boolean; function Start(): boolean; override; - procedure Stop(); override; + function Stop(): boolean; override; + + function GetVolume(): integer; override; + procedure SetVolume(Volume: integer); override; end; var @@ -62,83 +73,226 @@ function MicrophoneCallback(stream: HSTREAM; buffer: Pointer; len: Cardinal; Card: Cardinal): boolean; stdcall; begin AudioInputProcessor.HandleMicrophoneData(buffer, len, - AudioInputProcessor.Device[Card]); + AudioInputProcessor.DeviceList[Card]); Result := true; end; { TBassInputDevice } -function TBassInputDevice.Init(): boolean; +function TBassInputDevice.GetInputSource(): integer; +var + SourceCnt: integer; + i: integer; +begin + // get input-source config (subtract virtual device to get BASS indices) + SourceCnt := Length(Source)-1; + + // find source + Result := -1; + for i := 0 to SourceCnt-1 do + begin + // check if current source is selected + if ((BASS_RecordGetInput(i) and BASS_INPUT_OFF) = 0) then + begin + // selected source found + Result := i; + Exit; + end; + end; +end; + +function TBassInputDevice.SetInputSource(SourceIndex: integer): boolean; +var + SourceCnt: integer; + i: integer; begin Result := false; - // TODO: Call once. Otherwise it's to slow - if not BASS_RecordInit(BassDeviceID) then + // check for invalid source index + if (SourceIndex < 0) then + Exit; + + // get input-source config (subtract virtual device to get BASS indices) + SourceCnt := Length(Source)-1; + + // turn on selected source (turns off the others for single-in devices) + if (not BASS_RecordSetInput(SourceIndex, BASS_INPUT_ON)) then begin - Log.LogError('TBassInputDevice.Start: Error initializing device['+IntToStr(DeviceIndex)+']: ' + - TAudioCore_Bass.ErrorGetString()); + Log.LogError('BASS_RecordSetInput: ' + TAudioCore_Bass.ErrorGetString(), 'TBassInputDevice.Start'); Exit; end; + // turn off all other sources (not needed for single-in devices) + if (not SingleIn) then + begin + for i := 0 to SourceCnt-1 do + begin + if (i = SourceIndex) then + continue; + // deselect source if selected + if ((BASS_RecordGetInput(i) and BASS_INPUT_OFF) = 0) then + BASS_RecordSetInput(i, BASS_INPUT_OFF); + end; + end; + Result := true; end; -{* - * Start input-capturing on this device. - * TODO: call BASS_RecordInit only once - *} -function TBassInputDevice.Start(): boolean; +function TBassInputDevice.Open(): boolean; var - flags: Word; + FormatFlags: DWORD; + SourceIndex: integer; const latency = 20; // 20ms callback period (= latency) begin Result := false; - // recording already started -> stop first - if (RecordStream <> 0) then - Stop(); - - if not Init() then + if not BASS_RecordInit(BassDeviceID) then + begin + Log.LogError('BASS_RecordInit[device:'+IntToStr(DeviceIndex)+']: ' + + TAudioCore_Bass.ErrorGetString(), 'TBassInputDevice.Open'); Exit; + end; - case AudioFormat.Format of - asfS16: flags := 0; - asfFloat: flags := BASS_SAMPLE_FLOAT; - asfU8: flags := BASS_SAMPLE_8BITS; - else begin - Log.LogError('Unhandled sample-format', 'TBassInputDevice.Start'); - Exit; - end; + if (not TAudioCore_Bass.ConvertAudioFormatToBASSFlags(AudioFormat.Format, FormatFlags)) then + begin + Log.LogError('Unhandled sample-format', 'TBassInputDevice.Open'); + Exit; end; - // start capturing + // start capturing in paused state RecordStream := BASS_RecordStart(Round(AudioFormat.SampleRate), AudioFormat.Channels, - MakeLong(flags, latency), + MakeLong(FormatFlags or BASS_RECORD_PAUSE, latency), @MicrophoneCallback, DeviceIndex); if (RecordStream = 0) then begin + Log.LogError('BASS_RecordStart: ' + TAudioCore_Bass.ErrorGetString(), 'TBassInputDevice.Open'); BASS_RecordFree; Exit; end; + // save current source selection and select new source + SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1; + if (SourceIndex = -1) then + begin + // nothing to do if default source is used + SourceRestore := -1; + end + else + begin + // store current source-index and select new source + SourceRestore := GetInputSource(); + SetInputSource(SourceIndex); + end; + Result := true; end; -{* - * Stop input-capturing on this device. - *} -procedure TBassInputDevice.Stop(); +{* Start input-capturing on this device. *} +function TBassInputDevice.Start(): boolean; +begin + Result := false; + + // recording already started -> stop first + if (RecordStream <> 0) then + Stop(); + + // TODO: Do not open the device here (takes too much time). + if not Open() then + Exit; + + if (not BASS_ChannelPlay(RecordStream, true)) then + begin + Log.LogError('BASS_ChannelPlay: ' + TAudioCore_Bass.ErrorGetString(), 'TBassInputDevice.Start'); + Exit; + end; + + Result := true; +end; + +{* Stop input-capturing on this device. *} +function TBassInputDevice.Stop(): boolean; begin + Result := false; + if (RecordStream = 0) then Exit; - // TODO: Don't free the device. Do this on close - if (BASS_RecordSetDevice(BassDeviceID)) then - BASS_RecordFree; + if (not BASS_RecordSetDevice(BassDeviceID)) then + Exit; + + if (not BASS_ChannelStop(RecordStream)) then + begin + Log.LogError('BASS_ChannelStop: ' + TAudioCore_Bass.ErrorGetString(), 'TBassInputDevice.Stop'); + end; + + // TODO: Do not close the device here (takes too much time). + Result := Close(); +end; + +function TBassInputDevice.Close(): boolean; +begin + // restore source selection + if (SourceRestore >= 0) then + begin + SetInputSource(SourceRestore); + end; + + // free data + if (not BASS_RecordFree()) then + begin + Log.LogError('BASS_RecordFree: ' + TAudioCore_Bass.ErrorGetString(), 'TBassInputDevice.Close'); + Result := false; + end + else + begin + Result := true; + end; + RecordStream := 0; end; +function TBassInputDevice.GetVolume(): integer; +var + SourceIndex: integer; +begin + SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1; + if (SourceIndex = -1) then + begin + // if default source used find selected source + SourceIndex := GetInputSource(); + if (SourceIndex = -1) then + begin + Result := 0; + Exit; + end; + end; + + Result := LOWORD(BASS_RecordGetInput(SourceIndex)); +end; + +procedure TBassInputDevice.SetVolume(Volume: integer); +var + SourceIndex: integer; +begin + SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1; + if (SourceIndex = -1) then + begin + // if default source used find selected source + SourceIndex := GetInputSource(); + if (SourceIndex = -1) then + Exit; + end; + + // clip volume to valid range + if (Volume > 100) then + Volume := 100 + else if (Volume < 0) then + Volume := 0; + + BASS_RecordSetInput(SourceIndex, BASS_INPUT_LEVEL or Volume); +end; + { TAudioInput_Bass } @@ -147,7 +301,7 @@ begin result := 'BASS_Input'; end; -function TAudioInput_Bass.InitializeRecord(): boolean; +function TAudioInput_Bass.EnumDevices(): boolean; var Descr: PChar; SourceName: PChar; @@ -157,12 +311,13 @@ var DeviceIndex: integer; SourceIndex: integer; RecordInfo: BASS_RECORDINFO; + SelectedSourceIndex: integer; begin result := false; DeviceIndex := 0; BassDeviceID := 0; - SetLength(AudioInputProcessor.Device, 0); + SetLength(AudioInputProcessor.DeviceList, 0); // checks for recording devices and puts them into an array while true do @@ -171,7 +326,7 @@ begin if (Descr = nil) then break; - // try to intialize the device + // try to initialize the device if not BASS_RecordInit(BassDeviceID) then begin Log.LogStatus('Failed to initialize BASS Capture-Device['+inttostr(BassDeviceID)+']', @@ -179,26 +334,25 @@ begin end else begin - SetLength(AudioInputProcessor.Device, DeviceIndex+1); + SetLength(AudioInputProcessor.DeviceList, DeviceIndex+1); // TODO: free object on termination BassDevice := TBassInputDevice.Create(); - AudioInputProcessor.Device[DeviceIndex] := BassDevice; + AudioInputProcessor.DeviceList[DeviceIndex] := BassDevice; BassDevice.DeviceIndex := DeviceIndex; BassDevice.BassDeviceID := BassDeviceID; - BassDevice.Description := UnifyDeviceName(Descr, DeviceIndex); + BassDevice.Name := UnifyDeviceName(Descr, DeviceIndex); // retrieve recording device info BASS_RecordGetInfo(RecordInfo); - // FIXME: does BASS use LSB/MSB or system integer values for 16bit? - // check if BASS has capture-freq. info if (RecordInfo.freq > 0) then begin // use current input sample rate (available only on Windows Vista and OSX). // Recording at this rate will give the best quality and performance, as no resampling is required. + // FIXME: does BASS use LSB/MSB or system integer values for 16bit? BassDevice.AudioFormat := TAudioFormatInfo.Create(2, RecordInfo.freq, asfS16) end else @@ -209,36 +363,59 @@ begin BassDevice.AudioFormat := TAudioFormatInfo.Create(2, 44100, asfS16) end; + // get info if multiple input-sources can be selected at once + BassDevice.SingleIn := RecordInfo.singlein; + + // init list for capture buffers per channel SetLength(BassDevice.CaptureChannel, BassDevice.AudioFormat.Channels); - // get input sources - SourceIndex := 0; - BassDevice.MicSource := 0; + BassDevice.MicSource := -1; + BassDevice.SourceRestore := -1; + + // add a virtual default source (will not change mixer-settings) + SetLength(BassDevice.Source, 1); + BassDevice.Source[0].Name := DEFAULT_SOURCE_NAME; + + // add real input sources + SourceIndex := 1; // process each input while true do begin - SourceName := BASS_RecordGetInputName(SourceIndex); - {$IFDEF DARWIN} - // Patch for SingStar USB-Microphones: - if ((SourceName = nil) and (SourceIndex = 0) and (Pos('Serial#', Descr) > 0)) then - SourceName := 'Microphone' - else - break; - {$ELSE} - if (SourceName = nil) then - break; - {$ENDIF} - - SetLength(BassDevice.Source, SourceIndex+1); - BassDevice.Source[SourceIndex].Name := - UnifyDeviceSourceName(SourceName, BassDevice.Description); - - // set mic index + SourceName := BASS_RecordGetInputName(SourceIndex-1); + + {$IFDEF DARWIN} + // Under MacOSX the SingStar Mics have an empty InputName. + // So, we have to add a hard coded Workaround for this problem + // FIXME: - Do we need this anymore? Doesn't the (new) default source already solve this problem? + // - Normally a nil return value of BASS_RecordGetInputName() means end-of-list, so maybe + // BASS is not able to detect any mic-sources (the default source will work then). + // - Does BASS_RecordGetInfo() return true or false? If it returns true in this case + // we could use this value to check if the device exists. + // Please check that, eddie. + // If it returns false, then the source is not detected and it does not make sense to add a second + // fake device here. + // What about BASS_RecordGetInput()? Does it return a value <> -1? + // - Does it even work at all with this fake source-index, now that input switching works? + // This info was not used before (sources were never switched), so it did not matter what source-index was used. + // But now BASS_RecordSetInput() will probably fail. + if ((SourceName = nil) and (SourceIndex = 1) and (Pos('USBMIC Serial#', Descr) > 0)) then + SourceName := 'Microphone' + {$ENDIF} + + if (SourceName = nil) then + break; + + SetLength(BassDevice.Source, Length(BassDevice.Source)+1); + BassDevice.Source[SourceIndex].Name := SourceName; + + // get input-source info Flags := BASS_RecordGetInput(SourceIndex); - if ((Flags <> -1) and ((Flags and BASS_INPUT_TYPE_MIC) <> 0)) then + if (Flags <> -1) then begin - BassDevice.MicSource := SourceIndex; + // is the current source a mic-source? + if ((Flags and BASS_INPUT_TYPE_MIC) <> 0) then + BassDevice.MicSource := SourceIndex; end; Inc(SourceIndex); @@ -250,16 +427,22 @@ begin Inc(DeviceIndex); end; - + Inc(BassDeviceID); end; result := true; end; -destructor TAudioInput_Bass.Destroy; +function TAudioInput_Bass.InitializeRecord(): boolean; +begin + Result := EnumDevices(); +end; + +function TAudioInput_Bass.FinalizeRecord(): boolean; begin - inherited; + CaptureStop; + Result := inherited FinalizeRecord; end; diff --git a/Game/Code/Classes/UAudioInput_Portaudio.pas b/Game/Code/Classes/UAudioInput_Portaudio.pas index 90ac41b1..183c482d 100644 --- a/Game/Code/Classes/UAudioInput_Portaudio.pas +++ b/Game/Code/Classes/UAudioInput_Portaudio.pas @@ -29,19 +29,30 @@ uses type TAudioInput_Portaudio = class(TAudioInputBase) + private + AudioCore: TAudioCore_Portaudio; + function EnumDevices(): boolean; public function GetName: String; override; function InitializeRecord: boolean; override; - destructor Destroy; override; + function FinalizeRecord: boolean; override; end; TPortaudioInputDevice = class(TAudioInputDevice) - public - RecordStream: PPaStream; + private + RecordStream: PPaStream; + {$IFDEF UsePortmixer} + Mixer: PPxMixer; + {$ENDIF} PaDeviceIndex: TPaDeviceIndex; - + public + function Open(): boolean; + function Close(): boolean; function Start(): boolean; override; - procedure Stop(); override; + function Stop(): boolean; override; + + function GetVolume(): integer; override; + procedure SetVolume(Volume: integer); override; end; function MicrophoneCallback(input: Pointer; output: Pointer; frameCount: Longword; @@ -58,12 +69,12 @@ var { TPortaudioInputDevice } -function TPortaudioInputDevice.Start(): boolean; +function TPortaudioInputDevice.Open(): boolean; var Error: TPaError; - ErrorMsg: string; inputParams: TPaStreamParameters; deviceInfo: PPaDeviceInfo; + SourceIndex: integer; begin Result := false; @@ -90,35 +101,148 @@ begin @MicrophoneCallback, Pointer(Self)); if(Error <> paNoError) then begin - ErrorMsg := Pa_GetErrorText(Error); - Log.LogError('Error opening stream: ' + ErrorMsg, 'TPortaudioInputDevice.Start'); + Log.LogError('Error opening stream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Open'); Exit; end; + {$IFDEF UsePortmixer} + // open default mixer + Mixer := Px_OpenMixer(RecordStream, 0); + if (Mixer = nil) then + begin + Log.LogError('Error opening mixer: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Open'); + end + else + begin + // save current source selection and select new source + SourceIndex := Ini.InputDeviceConfig[CfgIndex].Input-1; + if (SourceIndex = -1) then + begin + // nothing to do if default source is used + SourceRestore := -1; + end + else + begin + // store current source-index and select new source + SourceRestore := Px_GetCurrentInputSource(Mixer); // -1 in error case + Px_SetCurrentInputSource(Mixer, SourceIndex); + end; + end; + {$ENDIF} + + Result := true; +end; + +function TPortaudioInputDevice.Start(): boolean; +var + Error: TPaError; +begin + Result := false; + + // recording already started -> stop first + if (RecordStream <> nil) then + Stop(); + + // TODO: Do not open the device here (takes too much time). + if (not Open()) then + Exit; + // start capture Error := Pa_StartStream(RecordStream); if(Error <> paNoError) then begin - Pa_CloseStream(RecordStream); - ErrorMsg := Pa_GetErrorText(Error); - Log.LogError('Error starting stream: ' + ErrorMsg, 'TPortaudioInputDevice.Start'); + Log.LogError('Error starting stream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Start'); + Close(); + RecordStream := nil; Exit; end; - + Result := true; end; -procedure TPortaudioInputDevice.Stop(); +function TPortaudioInputDevice.Stop(): boolean; +var + Error: TPaError; begin - if assigned(RecordStream) then + Result := false; + + if (RecordStream = nil) then + Exit; + + // Note: do NOT call Pa_StopStream here! + // It gets stuck on devices with non-working callback as Pa_StopStream + // waits until all buffers have been handled (which never occurs in that case). + Error := Pa_AbortStream(RecordStream); + if (Error <> paNoError) then begin - // Note: do NOT call Pa_StopStream here! - // It gets stuck on devices with non-working callback as Pa_StopStream - // waits until all buffers have been handled (which never occurs in that - // case). - // Pa_CloseStream internally calls Pa_AbortStream which works as expected. - Pa_CloseStream(RecordStream); + Log.LogError('Pa_AbortStream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Stop'); end; + + Result := Close(); +end; + +function TPortaudioInputDevice.Close(): boolean; +var + Error: TPaError; +begin + {$IFDEF UsePortmixer} + if (Mixer <> nil) then + begin + // restore source selection + if (SourceRestore >= 0) then + begin + Px_SetCurrentInputSource(Mixer, SourceRestore); + end; + + // close mixer + Px_CloseMixer(Mixer); + Mixer := nil; + end; + {$ENDIF} + + Error := Pa_CloseStream(RecordStream); + if (Error <> paNoError) then + begin + Log.LogError('Pa_CloseStream: ' + Pa_GetErrorText(Error), 'TPortaudioInputDevice.Close'); + Result := false; + end + else + begin + Result := true; + end; + + RecordStream := nil; +end; + +function TPortaudioInputDevice.GetVolume(): integer; +begin + Result := 0; + {$IFDEF UsePortmixer} + if (Mixer <> nil) then + begin + Result := Round(Px_GetInputVolume(Mixer) * 100); + // clip to valid range + if (Result > 100) then + Result := 100 + else if (Result < 0) then + Result := 0; + end; + {$ENDIF} +end; + +procedure TPortaudioInputDevice.SetVolume(Volume: integer); +begin + {$IFDEF UsePortmixer} + if (Mixer <> nil) then + begin + // clip to valid range + if (Volume > 100) then + Volume := 100 + else if (Volume < 0) then + Volume := 0; + Px_SetInputVolume(Mixer, Volume / 100); + end; + {$ENDIF} end; @@ -129,7 +253,7 @@ begin result := 'Portaudio'; end; -function TAudioInput_Portaudio.InitializeRecord(): boolean; +function TAudioInput_Portaudio.EnumDevices(): boolean; var i: integer; paApiIndex: TPaHostApiIndex; @@ -148,29 +272,21 @@ var sampleRate: double; latency: TPaTime; {$IFDEF UsePortmixer} - sourceIndex: integer; mixer: PPxMixer; sourceCnt: integer; + sourceIndex: integer; sourceName: string; {$ENDIF} cbPolls: integer; cbWorks: boolean; begin - result := false; - - // initialize portaudio - err := Pa_Initialize(); - if(err <> paNoError) then - begin - Log.LogError(Pa_GetErrorText(err), 'TAudioInput_Portaudio.InitializeRecord'); - Exit; - end; + Result := false; // choose the best available Audio-API - paApiIndex := TAudioCore_Portaudio.GetPreferredApiIndex(); + paApiIndex := AudioCore.GetPreferredApiIndex(); if(paApiIndex = -1) then begin - Log.LogError('No working Audio-API found', 'TAudioInput_Portaudio.InitializeRecord'); + Log.LogError('No working Audio-API found', 'TAudioInput_Portaudio.EnumDevices'); Exit; end; @@ -179,8 +295,8 @@ begin SC := 0; // init array-size to max. input-devices count - SetLength(AudioInputProcessor.Device, paApiInfo^.deviceCount); - for i:= 0 to High(AudioInputProcessor.Device) do + SetLength(AudioInputProcessor.DeviceList, paApiInfo^.deviceCount); + for i:= 0 to High(AudioInputProcessor.DeviceList) do begin // convert API-specific device-index to global index deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(paApiIndex, i); @@ -199,11 +315,11 @@ begin channelCnt := 2; paDevice := TPortaudioInputDevice.Create(); - AudioInputProcessor.Device[SC] := paDevice; + AudioInputProcessor.DeviceList[SC] := paDevice; // retrieve device-name deviceName := deviceInfo^.name; - paDevice.Description := deviceName; + paDevice.Name := deviceName; paDevice.PaDeviceIndex := deviceIndex; sampleRate := deviceInfo^.defaultSampleRate; @@ -213,6 +329,8 @@ begin latency := deviceInfo^.defaultLowInputLatency; // setup desired input parameters + // TODO: retry with input-latency set to 20ms (defaultLowInputLatency might + // not be set correctly in OSS) with inputParams do begin device := deviceIndex; @@ -223,10 +341,10 @@ begin end; // check souncard and adjust sample-rate - if (not TAudioCore_Portaudio.TestDevice(@inputParams, nil, sampleRate)) then + if (not AudioCore.TestDevice(@inputParams, nil, sampleRate)) then begin // ignore device if it does not work - Log.LogError('Device "'+paDevice.Description+'" does not work', + Log.LogError('Device "'+paDevice.Name+'" does not work', 'TAudioInput_Portaudio.EnumDevices'); paDevice.Free(); continue; @@ -266,11 +384,19 @@ begin ); SetLength(paDevice.CaptureChannel, paDevice.AudioFormat.Channels); - Log.LogStatus('InputDevice "'+paDevice.Description+'"@' + + Log.LogStatus('InputDevice "'+paDevice.Name+'"@' + IntToStr(paDevice.AudioFormat.Channels)+'x'+ FloatToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+ FloatTostr(inputParams.suggestedLatency)+'sec)' , - 'Portaudio.InitializeRecord'); + 'Portaudio.EnumDevices'); + + // portaudio does not provide a source-type check + paDevice.MicSource := -1; + paDevice.SourceRestore := -1; + + // add a virtual default source (will not change mixer-settings) + SetLength(paDevice.Source, 1); + paDevice.Source[0].Name := DEFAULT_SOURCE_NAME; {$IFDEF UsePortmixer} // use default mixer @@ -278,52 +404,55 @@ begin // get input count sourceCnt := Px_GetNumInputSources(mixer); - SetLength(paDevice.Source, sourceCnt); + SetLength(paDevice.Source, sourceCnt+1); // get input names - for sourceIndex := 0 to sourceCnt-1 do + for sourceIndex := 1 to sourceCnt do begin - sourceName := Px_GetInputSourceName(mixer, sourceIndex); + sourceName := Px_GetInputSourceName(mixer, sourceIndex-1); paDevice.Source[sourceIndex].Name := sourceName; end; Px_CloseMixer(mixer); - {$ELSE} // not UsePortmixer - // create a standard input source - SetLength(paDevice.Source, 1); - paDevice.Source[0].Name := 'Standard'; {$ENDIF} // close test-stream Pa_CloseStream(stream); - // use default input source - paDevice.SourceSelected := 0; - Inc(SC); end; // adjust size to actual input-device count - SetLength(AudioInputProcessor.Device, SC); + SetLength(AudioInputProcessor.DeviceList, SC); - Log.LogStatus('#Soundcards: ' + inttostr(SC), 'Portaudio'); + Log.LogStatus('#Input-Devices: ' + inttostr(SC), 'Portaudio'); - result := true; + Result := true; end; -destructor TAudioInput_Portaudio.Destroy; +function TAudioInput_Portaudio.InitializeRecord(): boolean; var - i: integer; - //paSoundCard: TPortaudioInputDevice; + err: TPaError; begin - Pa_Terminate(); - for i := 0 to High(AudioInputProcessor.Device) do + AudioCore := TAudioCore_Portaudio.GetInstance(); + + // initialize portaudio + err := Pa_Initialize(); + if(err <> paNoError) then begin - AudioInputProcessor.Device[i].Free(); + Log.LogError(Pa_GetErrorText(err), 'TAudioInput_Portaudio.InitializeRecord'); + Result := false; + Exit; end; - AudioInputProcessor.Device := nil; - inherited Destroy; + Result := EnumDevices(); +end; + +function TAudioInput_Portaudio.FinalizeRecord: boolean; +begin + CaptureStop; + Pa_Terminate(); + Result := inherited FinalizeRecord(); end; {* diff --git a/Game/Code/Classes/UAudioPlaybackBase.pas b/Game/Code/Classes/UAudioPlaybackBase.pas new file mode 100644 index 00000000..f1fe26f9 --- /dev/null +++ b/Game/Code/Classes/UAudioPlaybackBase.pas @@ -0,0 +1,220 @@ +unit UAudioPlaybackBase; + +interface + +{$IFDEF FPC} + {$MODE Delphi} +{$ENDIF} + +{$I switches.inc} + +uses + UMusic; + +type + TAudioPlaybackBase = class(TInterfacedObject, IAudioPlayback) + protected + OutputDeviceList: TAudioOutputDeviceList; + MusicStream: TAudioPlaybackStream; + // open sound or music stream (used by Open() and OpenSound()) + function OpenStream(const Filename: string): TAudioPlaybackStream; virtual; abstract; + procedure ClearOutputDeviceList(); + public + function GetName: String; virtual; abstract; + + function Open(const Filename: string): boolean; // true if succeed + procedure Close; + + procedure Play; + procedure Pause; + procedure Stop; + procedure FadeIn(Time: real; TargetVolume: integer); + + procedure SetPosition(Time: real); + function GetPosition: real; + + function InitializePlayback: boolean; virtual; abstract; + function FinalizePlayback: boolean; virtual; + + // function SetOutputDevice(Device: TAudioOutputDevice): boolean; + function GetOutputDeviceList(): TAudioOutputDeviceList; + + procedure SetAppVolume(Volume: integer); virtual; abstract; + procedure SetVolume(Volume: integer); + procedure SetLoop(Enabled: boolean); + + procedure Rewind; + function Finished: boolean; + function Length: real; + + // Sounds + function OpenSound(const Filename: String): TAudioPlaybackStream; + procedure PlaySound(stream: TAudioPlaybackStream); + procedure StopSound(stream: TAudioPlaybackStream); + + // Equalizer + procedure GetFFTData(var data: TFFTData); + + // Interface for Visualizer + function GetPCMData(var data: TPCMData): Cardinal; + end; + + +implementation + +uses + SysUtils; + +{ TAudioPlaybackBase } + +function TAudioPlaybackBase.FinalizePlayback: boolean; +begin + FreeAndNil(MusicStream); + ClearOutputDeviceList(); +end; + +function TAudioPlaybackBase.Open(const Filename: string): boolean; +begin + // free old MusicStream + MusicStream.Free; + + MusicStream := OpenStream(Filename); + if not assigned(MusicStream) then + begin + Result := false; + Exit; + end; + + //MusicStream.AddSoundEffect(TVoiceRemoval.Create()); + + Result := true; +end; + +procedure TAudioPlaybackBase.Close; +begin + if assigned(MusicStream) then + MusicStream.Close(); +end; + +procedure TAudioPlaybackBase.Play; +begin + if assigned(MusicStream) then + MusicStream.Play(); +end; + +procedure TAudioPlaybackBase.Pause; +begin + if assigned(MusicStream) then + MusicStream.Pause(); +end; + +procedure TAudioPlaybackBase.Stop; +begin + if assigned(MusicStream) then + MusicStream.Stop(); +end; + +function TAudioPlaybackBase.Length: real; +begin + if assigned(MusicStream) then + Result := MusicStream.Length + else + Result := 0; +end; + +function TAudioPlaybackBase.GetPosition: real; +begin + if assigned(MusicStream) then + Result := MusicStream.Position + else + Result := 0; +end; + +procedure TAudioPlaybackBase.SetPosition(Time: real); +begin + if assigned(MusicStream) then + MusicStream.Position := Time; +end; + +procedure TAudioPlaybackBase.Rewind; +begin + SetPosition(0); +end; + +function TAudioPlaybackBase.Finished: boolean; +begin + if assigned(MusicStream) then + Result := (MusicStream.Status = ssStopped) + else + Result := true; +end; + +procedure TAudioPlaybackBase.SetVolume(Volume: Integer); +begin + if assigned(MusicStream) then + MusicStream.Volume := Volume; +end; + +procedure TAudioPlaybackBase.FadeIn(Time: real; TargetVolume: integer); +begin + if assigned(MusicStream) then + MusicStream.FadeIn(Time, TargetVolume); +end; + +procedure TAudioPlaybackBase.SetLoop(Enabled: boolean); +begin + if assigned(MusicStream) then + MusicStream.Loop := Enabled; +end; + +// Equalizer +procedure TAudioPlaybackBase.GetFFTData(var data: TFFTData); +begin + if assigned(MusicStream) then + MusicStream.GetFFTData(data); +end; + +{* + * Copies interleaved PCM SInt16 stereo samples into data. + * Returns the number of frames + *} +function TAudioPlaybackBase.GetPCMData(var data: TPCMData): Cardinal; +begin + if assigned(MusicStream) then + Result := MusicStream.GetPCMData(data) + else + Result := 0; +end; + +function TAudioPlaybackBase.OpenSound(const Filename: string): TAudioPlaybackStream; +begin + Result := OpenStream(Filename); +end; + +procedure TAudioPlaybackBase.PlaySound(stream: TAudioPlaybackStream); +begin + if assigned(stream) then + stream.Play(); +end; + +procedure TAudioPlaybackBase.StopSound(stream: TAudioPlaybackStream); +begin + if assigned(stream) then + stream.Stop(); +end; + +procedure TAudioPlaybackBase.ClearOutputDeviceList(); +var + DeviceIndex: integer; +begin + for DeviceIndex := 0 to High(OutputDeviceList) do + OutputDeviceList[DeviceIndex].Free(); + SetLength(OutputDeviceList, 0); +end; + +function TAudioPlaybackBase.GetOutputDeviceList(): TAudioOutputDeviceList; +begin + Result := OutputDeviceList; +end; + +end. diff --git a/Game/Code/Classes/UAudioPlayback_Bass.pas b/Game/Code/Classes/UAudioPlayback_Bass.pas index d47990a8..0fca9c72 100644 --- a/Game/Code/Classes/UAudioPlayback_Bass.pas +++ b/Game/Code/Classes/UAudioPlayback_Bass.pas @@ -8,36 +8,40 @@ interface {$I switches.inc} +implementation uses Classes, SysUtils, - UMusic; - -implementation - -uses UIni, UMain, - ULog, + UMusic, + UAudioPlaybackBase, UAudioCore_Bass, + ULog, bass; type + PHDSP = ^HDSP; + +type + // Playback-stream decoded internally by BASS TBassPlaybackStream = class(TAudioPlaybackStream) private Handle: HSTREAM; - Loop: boolean; public - constructor Create(); overload; - constructor Create(stream: HSTREAM); overload; + constructor Create(stream: HSTREAM); + destructor Destroy(); override; procedure Reset(); procedure Play(); override; procedure Pause(); override; procedure Stop(); override; + procedure FadeIn(Time: real; TargetVolume: integer); override; + procedure Close(); override; + function GetLoop(): boolean; override; procedure SetLoop(Enabled: boolean); override; function GetLength(): real; override; @@ -45,79 +49,95 @@ type function GetVolume(): integer; override; procedure SetVolume(volume: integer); override; - function GetPosition: real; - procedure SetPosition(Time: real); + procedure AddSoundEffect(effect: TSoundEffect); override; + procedure RemoveSoundEffect(effect: TSoundEffect); override; + + function GetPosition: real; override; + procedure SetPosition(Time: real); override; - function IsLoaded(): boolean; + procedure GetFFTData(var data: TFFTData); override; + function GetPCMData(var data: TPCMData): Cardinal; override; end; -type - TAudioPlayback_Bass = class( TInterfacedObject, IAudioPlayback) + // Playback-stream decoded by an external decoder e.g. FFmpeg + TBassExtDecoderPlaybackStream = class(TBassPlaybackStream) private - MusicStream: TBassPlaybackStream; - - function Load(const Filename: string): TBassPlaybackStream; + DecodeStream: TAudioDecodeStream; public - function GetName: String; - - {IAudioOutput interface} - - function InitializePlayback(): boolean; - procedure SetVolume(Volume: integer); - procedure SetMusicVolume(Volume: integer); - procedure SetLoop(Enabled: boolean); - - function Open(const Filename: 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 Stop(); override; + procedure Close(); override; + function GetLength(): real; override; + function GetPosition: real; override; + procedure SetPosition(Time: real); override; - //Equalizer - procedure GetFFTData(var data: TFFTData); + function SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; + end; - // Interface for Visualizer - function GetPCMData(var data: TPCMData): Cardinal; +type + TAudioPlayback_Bass = class(TAudioPlaybackBase) + private + function EnumDevices(): boolean; + protected + function OpenStream(const Filename: string): TAudioPlaybackStream; override; + public + function GetName: String; override; + function InitializePlayback(): boolean; override; + function FinalizePlayback: boolean; override; + procedure SetAppVolume(Volume: integer); override; + end; - // Sounds - function OpenSound(const Filename: String): TAudioPlaybackStream; - procedure PlaySound(stream: TAudioPlaybackStream); - procedure StopSound(stream: TAudioPlaybackStream); + TBassOutputDevice = class(TAudioOutputDevice) + private + BassDeviceID: DWORD; // DeviceID used by BASS end; var singleton_AudioPlaybackBass : IAudioPlayback; -constructor TBassPlaybackStream.Create(); +{ TBassPlaybackStream } + +constructor TBassPlaybackStream.Create(stream: HSTREAM); begin - inherited; + inherited Create(); Reset(); + Handle := stream; end; -constructor TBassPlaybackStream.Create(stream: HSTREAM); +destructor TBassPlaybackStream.Destroy(); begin - Create(); - Handle := stream; + Close(); + inherited; end; procedure TBassPlaybackStream.Reset(); begin - Loop := false; if (Handle <> 0) then + begin Bass_StreamFree(Handle); + end; Handle := 0; end; procedure TBassPlaybackStream.Play(); +var + restart: boolean; begin - BASS_ChannelPlay(Handle, Loop); + if (BASS_ChannelIsActive(Handle) = BASS_ACTIVE_PAUSED) then + restart := false // resume from last position + else + restart := true; // start from the beginning + + BASS_ChannelPlay(Handle, restart); +end; + +procedure TBassPlaybackStream.FadeIn(Time: real; TargetVolume: integer); +begin + // start stream + BASS_ChannelPlay(Handle, true); + + // start fade-in: slide from fadeStart- to fadeEnd-volume in FadeInTime + BASS_ChannelSlideAttributes(Handle, -1, TargetVolume, -101, Trunc(Time * 1000)); end; procedure TBassPlaybackStream.Pause(); @@ -136,9 +156,11 @@ begin end; function TBassPlaybackStream.GetVolume(): integer; +var + volume: cardinal; begin - Result := 0; - BASS_ChannelSetAttributes(Handle, PInteger(nil)^, Result, PInteger(nil)^); + BASS_ChannelGetAttributes(Handle, PCardinal(nil)^, volume, PInteger(nil)^); + Result := volume; end; procedure TBassPlaybackStream.SetVolume(volume: integer); @@ -168,19 +190,9 @@ begin BASS_ChannelSetPosition(Handle, bytes); end; -function TBassPlaybackStream.GetLoop(): boolean; -begin - result := Loop; -end; - -procedure TBassPlaybackStream.SetLoop(Enabled: boolean); -begin - Loop := Enabled; -end; - function TBassPlaybackStream.GetLength(): real; var - bytes: integer; + bytes: integer; begin bytes := BASS_ChannelGetLength(Handle); Result := BASS_ChannelBytes2Seconds(Handle, bytes); @@ -205,212 +217,367 @@ begin end; end; -function TBassPlaybackStream.IsLoaded(): boolean; -begin - Result := (Handle <> 0); -end; - - -function TAudioPlayback_Bass.GetName: String; +function TBassPlaybackStream.GetLoop(): boolean; +var + info: BASS_CHANNELINFO; begin - result := 'BASS_Playback'; + if not BASS_ChannelGetInfo(Handle, info) then + begin + Log.LogError('BASS_ChannelGetInfo: ' + TAudioCore_Bass.ErrorGetString(), 'TBassPlaybackStream.GetLoop'); + Result := false; + Exit; + end; + Result := (info.flags and BASS_SAMPLE_LOOP) <> 0; end; -function TAudioPlayback_Bass.InitializePlayback(): boolean; +procedure TBassPlaybackStream.SetLoop(Enabled: boolean); var - Pet: integer; - S: integer; + info: BASS_CHANNELINFO; begin - result := false; - - //Log.BenchmarkStart(4); - //Log.LogStatus('Initializing Playback Subsystem', 'Music Initialize'); - - if not BASS_Init(1, 44100, 0, 0, nil) then + // retrieve old flag-bits + if not BASS_ChannelGetInfo(Handle, info) then begin - Log.LogError('Could not initialize BASS', 'Error'); + Log.LogError('BASS_ChannelGetInfo:' + TAudioCore_Bass.ErrorGetString(), 'TBassPlaybackStream.SetLoop'); Exit; end; - //Log.BenchmarkEnd(4); Log.LogBenchmark('--> Bass Init', 4); + // set/unset loop-flag + if (Enabled) then + info.flags := info.flags or BASS_SAMPLE_LOOP + else + info.flags := info.flags and not BASS_SAMPLE_LOOP; - // config playing buffer - //BASS_SetConfig(BASS_CONFIG_UPDATEPERIOD, 10); - //BASS_SetConfig(BASS_CONFIG_BUFFER, 100); + // set new flag-bits + if not BASS_ChannelSetFlags(Handle, info.flags) then + begin + Log.LogError('BASS_ChannelSetFlags: ' + TAudioCore_Bass.ErrorGetString(), 'TBassPlaybackStream.SetLoop'); + Exit; + end; +end; - result := true; +procedure DSPProcHandler(handle: HDSP; channel: DWORD; buffer: Pointer; length: DWORD; user: DWORD); stdcall; +var + effect: TSoundEffect; +begin + effect := TSoundEffect(user); + if assigned(effect) then + effect.Callback(buffer, length); end; -function TAudioPlayback_Bass.Load(const Filename: string): TBassPlaybackStream; +procedure TBassPlaybackStream.AddSoundEffect(effect: TSoundEffect); var - L: Integer; - stream: HSTREAM; + dspHandle: HDSP; begin - Result := nil; + if assigned(effect.engineData) then + begin + Log.LogError('TSoundEffect.engineData already set', 'TBassPlaybackStream.AddSoundEffect'); + Exit; + end; - //Log.LogStatus('Loading Sound: "' + Filename + '"', 'LoadSoundFromFile'); - stream := BASS_StreamCreateFile(False, pchar(Filename), 0, 0, 0); - if (stream = 0) then + // FIXME: casting of a pointer to Uint32 will fail on 64bit systems + dspHandle := BASS_ChannelSetDSP(Handle, @DSPProcHandler, DWORD(effect), 0); + if (dspHandle = 0) then begin - Log.LogError('Failed to open "' + Filename + '", ' + - TAudioCore_Bass.ErrorGetString(BASS_ErrorGetCode()), 'TAudioPlayback_Bass.Load'); + Log.LogError(TAudioCore_Bass.ErrorGetString(), 'TBassPlaybackStream.AddSoundEffect'); Exit; end; - Result := TBassPlaybackStream.Create(stream); + GetMem(effect.engineData, SizeOf(HDSP)); + PHDSP(effect.engineData)^ := dspHandle; end; -procedure TAudioPlayback_Bass.SetVolume(Volume: integer); +procedure TBassPlaybackStream.RemoveSoundEffect(effect: TSoundEffect); 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; + if not assigned(effect.EngineData) then + begin + Log.LogError('TSoundEffect.engineData invalid', 'TBassPlaybackStream.RemoveSoundEffect'); + Exit; + end; -procedure TAudioPlayback_Bass.SetMusicVolume(Volume: Integer); -begin - if assigned(MusicStream) then - MusicStream.SetVolume(Volume); + if not BASS_ChannelRemoveDSP(Handle, PHDSP(effect.EngineData)^) then + begin + Log.LogError(TAudioCore_Bass.ErrorGetString(), 'TBassPlaybackStream.RemoveSoundEffect'); + Exit; + end; + + FreeMem(effect.engineData); + effect.engineData := nil; end; -procedure TAudioPlayback_Bass.SetLoop(Enabled: boolean); +procedure TBassPlaybackStream.GetFFTData(var data: TFFTData); begin - if assigned(MusicStream) then - MusicStream.Loop := Enabled; + // Get Channel Data Mono and 256 Values + BASS_ChannelGetData(Handle, @data, BASS_DATA_FFT512); end; -function TAudioPlayback_Bass.Open(const Filename: string): boolean; +{* + * Copies interleaved PCM SInt16 stereo samples into data. + * Returns the number of frames + *} +function TBassPlaybackStream.GetPCMData(var data: TPCMData): Cardinal; var - stream: HSTREAM; + info: BASS_CHANNELINFO; + nBytes: DWORD; begin - Result := false; + Result := 0; - // free old MusicStream - if assigned(MusicStream) then - MusicStream.Free; + // Get Channel Data Mono and 256 Values + BASS_ChannelGetInfo(Handle, info); + FillChar(data, sizeof(TPCMData), 0); - MusicStream := Load(Filename); - if not assigned(MusicStream) then + // no support for non-stereo files at the moment + if (info.chans <> 2) then Exit; - //Set Max Volume - SetMusicVolume(100); - - Result := true; + nBytes := BASS_ChannelGetData(Handle, @data, sizeof(TPCMData)); + if(nBytes <= 0) then + result := 0 + else + result := nBytes div sizeof(TPCMStereoSample); end; -procedure TAudioPlayback_Bass.Rewind; -begin - SetPosition(0); -end; -procedure TAudioPlayback_Bass.Play; -begin - if assigned(MusicStream) then - MusicStream.Play(); -end; +{ TBassExtDecoderPlaybackStream } -procedure TAudioPlayback_Bass.Pause; +procedure TBassExtDecoderPlaybackStream.Stop(); begin - if assigned(MusicStream) then - MusicStream.Pause(); + inherited; + // rewind + if assigned(DecodeStream) then + DecodeStream.Position := 0; end; -procedure TAudioPlayback_Bass.Stop; +procedure TBassExtDecoderPlaybackStream.Close(); begin - if assigned(MusicStream) then - MusicStream.Stop(); + // wake-up waiting audio-callback threads in the ReadData()-function + if assigned(decodeStream) then + DecodeStream.Close(); + // stop audio-callback on this stream + inherited; + // free decoder-data + FreeAndNil(DecodeStream); end; -procedure TAudioPlayback_Bass.Close; +function TBassExtDecoderPlaybackStream.GetLength(): real; begin - if assigned(MusicStream) then - MusicStream.Close(); + if assigned(DecodeStream) then + result := DecodeStream.Length + else + result := -1; end; -function TAudioPlayback_Bass.Length: real; -var - bytes: integer; +function TBassExtDecoderPlaybackStream.GetPosition: real; begin - if assigned(MusicStream) then - Result := MusicStream.GetLength() + if assigned(DecodeStream) then + result := DecodeStream.Position else - Result := -1; + result := -1; end; -function TAudioPlayback_Bass.GetPosition: real; +procedure TBassExtDecoderPlaybackStream.SetPosition(Time: real); begin - if assigned(MusicStream) then - Result := MusicStream.GetPosition() - else - Result := -1; + if assigned(DecodeStream) then + DecodeStream.Position := Time; end; -procedure TAudioPlayback_Bass.SetPosition(Time: real); +function TBassExtDecoderPlaybackStream.SetDecodeStream(decodeStream: TAudioDecodeStream): boolean; begin - if assigned(MusicStream) then - MusicStream.SetPosition(Time); + result := false; + + BASS_ChannelStop(Handle); + + if not assigned(decodeStream) then + Exit; + Self.DecodeStream := decodeStream; + + result := true; end; -function TAudioPlayback_Bass.Finished: boolean; + +{ TAudioPlayback_Bass } + +function TAudioPlayback_Bass.GetName: String; begin - if assigned(MusicStream) then - Result := (MusicStream.GetStatus() = ssStopped) - else - Result := true; + result := 'BASS_Playback'; end; -// Equalizer -procedure TAudioPlayback_Bass.GetFFTData(var data: TFFTData); +function TAudioPlayback_Bass.EnumDevices(): boolean; +var + BassDeviceID: DWORD; + DeviceIndex: integer; + Device: TBassOutputDevice; + Description: PChar; begin - // Get Channel Data Mono and 256 Values - BASS_ChannelGetData(MusicStream.Handle, @data, BASS_DATA_FFT512); + ClearOutputDeviceList(); + + // skip "no sound"-device (ID = 0) + BassDeviceID := 1; + + while true do + begin + // Check for device + Description := BASS_GetDeviceDescription(BassDeviceID); + if (Description = nil) then + break; + + // Set device info + Device := TBassOutputDevice.Create(); + Device.Name := Description; + Device.BassDeviceID := BassDeviceID; + + // Add device to list + SetLength(OutputDeviceList, BassDeviceID); + OutputDeviceList[BassDeviceID-1] := Device; + + Inc(BassDeviceID); + end; 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; +function TAudioPlayback_Bass.InitializePlayback(): boolean; var - info: BASS_CHANNELINFO; - nBytes: DWORD; + Pet: integer; + S: integer; begin - Result := 0; + result := false; - // Get Channel Data Mono and 256 Values - BASS_ChannelGetInfo(MusicStream.Handle, info); - FillChar(data, sizeof(TPCMData), 0); + EnumDevices(); - // no support for non-stereo files at the moment - if (info.chans <> 2) then + //Log.BenchmarkStart(4); + //Log.LogStatus('Initializing Playback Subsystem', 'Music Initialize'); + + if not BASS_Init(1, 44100, 0, 0, nil) then + begin + Log.LogError('Could not initialize BASS', 'Error'); Exit; + end; - nBytes := BASS_ChannelGetData(MusicStream.Handle, @data, sizeof(TPCMData)); - if(nBytes <= 0) then - result := 0 + //Log.BenchmarkEnd(4); Log.LogBenchmark('--> Bass Init', 4); + + // config playing buffer + //BASS_SetConfig(BASS_CONFIG_UPDATEPERIOD, 10); + //BASS_SetConfig(BASS_CONFIG_BUFFER, 100); + + result := true; +end; + +function DecodeStreamHandler(handle: HSTREAM; buffer: Pointer; length: DWORD; user: DWORD): DWORD; stdcall; +var + decodeStream: TAudioDecodeStream; + bytes: integer; +begin + decodeStream := TAudioDecodeStream(user); + bytes := decodeStream.ReadData(buffer, length); + // handle errors + if (bytes < 0) then + Result := BASS_STREAMPROC_END + // handle EOF + else if (DecodeStream.EOF) then + Result := bytes or BASS_STREAMPROC_END else - result := nBytes div sizeof(TPCMStereoSample); + Result := bytes; end; -function TAudioPlayback_Bass.OpenSound(const Filename: string): TAudioPlaybackStream; +function TAudioPlayback_Bass.FinalizePlayback(): boolean; begin - result := Load(Filename); + Close; + BASS_Free; + inherited FinalizePlayback(); + Result := true; end; -procedure TAudioPlayback_Bass.PlaySound(stream: TAudioPlaybackStream); +function TAudioPlayback_Bass.OpenStream(const Filename: string): TAudioPlaybackStream; +var + L: Integer; + stream: HSTREAM; + playbackStream: TBassExtDecoderPlaybackStream; + decodeStream: TAudioDecodeStream; + formatInfo: TAudioFormatInfo; + formatFlags: DWORD; + channelInfo: BASS_CHANNELINFO; + fileExt: string; begin - if assigned(stream) then - stream.Play(); + Result := nil; + + //Log.LogStatus('Loading Sound: "' + Filename + '"', 'LoadSoundFromFile'); + stream := BASS_StreamCreateFile(False, PChar(Filename), 0, 0, 0); + + // check if BASS opened some erroneously recognized file-formats + if (stream <> 0) then + begin + if BASS_ChannelGetInfo(stream, channelInfo) then + begin + fileExt := ExtractFileExt(Filename); + // BASS opens FLV-files although it cannot handle them + if ((fileExt = '.flv') and (channelInfo.ctype = BASS_CTYPE_STREAM_MP1)) then + begin + BASS_StreamFree(stream); + stream := 0; + end; + end; + end; + + // Check if BASS can handle the format or try another decoder otherwise + if (stream <> 0) then + begin + Result := TBassPlaybackStream.Create(stream); + end + else + begin + if (AudioDecoder = nil) then + begin + Log.LogError('Failed to open "' + Filename + '", ' + + TAudioCore_Bass.ErrorGetString(BASS_ErrorGetCode()), 'TAudioPlayback_Bass.Load'); + Exit; + end; + + decodeStream := AudioDecoder.Open(Filename); + if not assigned(decodeStream) then + begin + Log.LogStatus('Sound not found "' + Filename + '"', 'TAudioPlayback_Bass.Load'); + Exit; + end; + + formatInfo := decodeStream.GetAudioFormatInfo(); + if (not TAudioCore_Bass.ConvertAudioFormatToBASSFlags(formatInfo.Format, formatFlags)) then + begin + Log.LogError('Unhandled sample-format in "' + Filename + '"', 'TAudioPlayback_Bass.Load'); + FreeAndNil(decodeStream); + Exit; + end; + + // FIXME: casting of a pointer to Uint32 will fail on 64bit systems + stream := BASS_StreamCreate(Round(formatInfo.SampleRate), formatInfo.Channels, formatFlags, + @DecodeStreamHandler, DWORD(decodeStream)); + if (stream = 0) then + begin + Log.LogError('Failed to open "' + Filename + '", ' + + TAudioCore_Bass.ErrorGetString(BASS_ErrorGetCode()), 'TAudioPlayback_Bass.Load'); + FreeAndNil(decodeStream); + Exit; + end; + + playbackStream := TBassExtDecoderPlaybackStream.Create(stream); + if (not assigned(playbackStream)) then + begin + FreeAndNil(decodeStream); + Exit; + end; + + if (not playbackStream.SetDecodeStream(decodeStream)) then + begin + FreeAndNil(playbackStream); + FreeAndNil(decodeStream); + Exit; + end; + + Result := playbackStream; + end; end; -procedure TAudioPlayback_Bass.StopSound(stream: TAudioPlaybackStream); +procedure TAudioPlayback_Bass.SetAppVolume(Volume: integer); begin - if assigned(stream) then - stream.Stop(); + // Sets Volume only for this Application + BASS_SetConfig(BASS_CONFIG_GVOL_STREAM, Volume); end; diff --git a/Game/Code/Classes/UAudioPlayback_Portaudio.pas b/Game/Code/Classes/UAudioPlayback_Portaudio.pas index 431fbd43..2c52c41e 100644 --- a/Game/Code/Classes/UAudioPlayback_Portaudio.pas +++ b/Game/Code/Classes/UAudioPlayback_Portaudio.pas @@ -28,12 +28,21 @@ type TAudioPlayback_Portaudio = class(TAudioPlayback_SoftMixer) private paStream: PPaStream; + AudioCore: TAudioCore_Portaudio; + function OpenDevice(deviceIndex: TPaDeviceIndex): boolean; + function EnumDevices(): boolean; protected function InitializeAudioPlaybackEngine(): boolean; override; function StartAudioPlaybackEngine(): boolean; override; procedure StopAudioPlaybackEngine(); override; + function FinalizeAudioPlaybackEngine(): boolean; override; public - function GetName: String; override; + function GetName: String; override; + end; + + TPortaudioOutputDevice = class(TAudioOutputDevice) + private + PaDeviceIndex: TPaDeviceIndex; end; var @@ -58,50 +67,39 @@ begin result := 'Portaudio_Playback'; end; -function TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine(): boolean; +function TAudioPlayback_Portaudio.OpenDevice(deviceIndex: TPaDeviceIndex): boolean; var - paApiIndex : TPaHostApiIndex; - paApiInfo : PPaHostApiInfo; - paOutParams : TPaStreamParameters; - paOutDevice : TPaDeviceIndex; - paOutDeviceInfo : PPaDeviceInfo; - err : TPaError; - sampleRate : double; + deviceInfo : PPaDeviceInfo; + sampleRate : double; + outParams : TPaStreamParameters; + err : TPaError; begin - result := false; + Result := false; - Pa_Initialize(); + deviceInfo := Pa_GetDeviceInfo(deviceIndex); - paApiIndex := TAudioCore_Portaudio.GetPreferredApiIndex(); - if(paApiIndex = -1) then - begin - Log.LogError('No working Audio-API found', 'TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine'); - Exit; - end; - - paApiInfo := Pa_GetHostApiInfo(paApiIndex); - paOutDevice := paApiInfo^.defaultOutputDevice; - paOutDeviceInfo := Pa_GetDeviceInfo(paOutDevice); + Log.LogInfo('Audio-Output Device: ' + deviceInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice'); - sampleRate := paOutDeviceInfo^.defaultSampleRate; + sampleRate := deviceInfo^.defaultSampleRate; - with paOutParams do begin - device := paOutDevice; + with outParams do + begin + device := deviceIndex; channelCount := 2; sampleFormat := paInt16; - suggestedLatency := paOutDeviceInfo^.defaultLowOutputLatency; + suggestedLatency := deviceInfo^.defaultLowOutputLatency; hostApiSpecificStreamInfo := nil; end; // check souncard and adjust sample-rate - if not TAudioCore_Portaudio.TestDevice(nil, @paOutParams, sampleRate) then + if not AudioCore.TestDevice(nil, @outParams, sampleRate) then begin Log.LogStatus('TestDevice failed!', 'TAudioPlayback_Portaudio.OpenDevice'); exit; end; // open output stream - err := Pa_OpenStream(paStream, nil, @paOutParams, sampleRate, + err := Pa_OpenStream(paStream, nil, @outParams, sampleRate, paFramesPerBufferUnspecified, paNoFlag, @PortaudioAudioCallback, Self); if(err <> paNoError) then @@ -112,12 +110,199 @@ begin end; FormatInfo := TAudioFormatInfo.Create( - paOutParams.channelCount, + outParams.channelCount, sampleRate, asfS16 // FIXME: is paInt16 system-dependant or -independant? ); - Log.LogStatus('Opened audio device', 'UAudioPlayback_Portaudio'); + Result := true; +end; + +function TAudioPlayback_Portaudio.EnumDevices(): boolean; +var + i: integer; + paApiIndex: TPaHostApiIndex; + paApiInfo: PPaHostApiInfo; + deviceName: string; + deviceIndex: TPaDeviceIndex; + deviceInfo: PPaDeviceInfo; + channelCnt: integer; + SC: integer; // soundcard + err: TPaError; + errMsg: string; + paDevice: TPortaudioOutputDevice; + inputParams: TPaStreamParameters; + stream: PPaStream; + streamInfo: PPaStreamInfo; + sampleRate: double; + latency: TPaTime; + cbPolls: integer; + cbWorks: boolean; +begin + Result := false; +(* + // choose the best available Audio-API + paApiIndex := AudioCore.GetPreferredApiIndex(); + if(paApiIndex = -1) then + begin + Log.LogError('No working Audio-API found', 'TAudioPlayback_Portaudio.EnumDevices'); + Exit; + end; + + paApiInfo := Pa_GetHostApiInfo(paApiIndex); + + SC := 0; + + // init array-size to max. output-devices count + SetLength(OutputDeviceList, paApiInfo^.deviceCount); + for i:= 0 to High(OutputDeviceList) do + begin + // convert API-specific device-index to global index + deviceIndex := Pa_HostApiDeviceIndexToDeviceIndex(paApiIndex, i); + deviceInfo := Pa_GetDeviceInfo(deviceIndex); + + channelCnt := deviceInfo^.maxOutputChannels; + + // current device is no output device -> skip + if (channelCnt <= 0) then + continue; + + // portaudio returns a channel-count of 128 for some devices + // (e.g. the "default"-device), so we have to detect those + // fantasy channel counts. + if (channelCnt > 8) then + channelCnt := 2; + + paDevice := TPortaudioOutputDevice.Create(); + OutputDeviceList[SC] := paDevice; + + // retrieve device-name + deviceName := deviceInfo^.name; + paDevice.Name := deviceName; + paDevice.PaDeviceIndex := deviceIndex; + + if (deviceInfo^.defaultSampleRate > 0) then + sampleRate := deviceInfo^.defaultSampleRate + else + sampleRate := 44100; + + // on vista and xp the defaultLowInputLatency may be set to 0 but it works. + // TODO: correct too low latencies (what is a too low latency, maybe < 10ms?) + latency := deviceInfo^.defaultLowInputLatency; + + // setup desired input parameters + // TODO: retry with input-latency set to 20ms (defaultLowInputLatency might + // not be set correctly in OSS) + with inputParams do + begin + device := deviceIndex; + channelCount := channelCnt; + sampleFormat := paInt16; + suggestedLatency := latency; + hostApiSpecificStreamInfo := nil; + end; + + // check if mic-callback works (might not be called on some devices) + if (not TAudioCore_Portaudio.TestDevice(@inputParams, nil, sampleRate)) then + begin + // ignore device if callback did not work + Log.LogError('Device "'+paDevice.Name+'" does not respond', + 'TAudioInput_Portaudio.InitializeRecord'); + paDevice.Free(); + continue; + end; + + // open device for further info + err := Pa_OpenStream(stream, @inputParams, nil, sampleRate, + paFramesPerBufferUnspecified, paNoFlag, @MicrophoneTestCallback, nil); + if(err <> paNoError) then + begin + // unable to open device -> skip + errMsg := Pa_GetErrorText(err); + Log.LogError('Device error: "'+ deviceName +'" ('+ errMsg +')', + 'TAudioInput_Portaudio.InitializeRecord'); + paDevice.Free(); + continue; + end; + + // adjust sample-rate (might be changed by portaudio) + streamInfo := Pa_GetStreamInfo(stream); + if (streamInfo <> nil) then + begin + if (sampleRate <> streamInfo^.sampleRate) then + begin + Log.LogStatus('Portaudio changed Samplerate from ' + FloatToStr(sampleRate) + + ' to ' + FloatToStr(streamInfo^.sampleRate), + 'TAudioInput_Portaudio.InitializeRecord'); + sampleRate := streamInfo^.sampleRate; + end; + end; + + // create audio-format info and resize capture-buffer array + paDevice.AudioFormat := TAudioFormatInfo.Create( + channelCnt, + sampleRate, + asfS16 + ); + SetLength(paDevice.CaptureChannel, paDevice.AudioFormat.Channels); + + Log.LogStatus('InputDevice "'+paDevice.Name+'"@' + + IntToStr(paDevice.AudioFormat.Channels)+'x'+ + FloatToStr(paDevice.AudioFormat.SampleRate)+'Hz ('+ + FloatTostr(inputParams.suggestedLatency)+'sec)' , + 'Portaudio.InitializeRecord'); + + // close test-stream + Pa_CloseStream(stream); + + Inc(SC); + end; + + // adjust size to actual input-device count + SetLength(OutputDeviceList, SC); + + Log.LogStatus('#Output-Devices: ' + inttostr(SC), 'Portaudio'); + + Result := true; + *) +end; + +function TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine(): boolean; +var + paApiIndex : TPaHostApiIndex; + paApiInfo : PPaHostApiInfo; + paOutDevice : TPaDeviceIndex; + err: TPaError; +begin + result := false; + + AudioCore := TAudioCore_Portaudio.GetInstance(); + + // initialize portaudio + err := Pa_Initialize(); + if(err <> paNoError) then + begin + Log.LogError(Pa_GetErrorText(err), 'TAudioInput_Portaudio.InitializeRecord'); + Exit; + end; + + paApiIndex := AudioCore.GetPreferredApiIndex(); + if(paApiIndex = -1) then + begin + Log.LogError('No working Audio-API found', 'TAudioPlayback_Portaudio.InitializeAudioPlaybackEngine'); + Exit; + end; + + EnumDevices(); + + paApiInfo := Pa_GetHostApiInfo(paApiIndex); + Log.LogInfo('Audio-Output API-Type: ' + paApiInfo^.name, 'TAudioPlayback_Portaudio.OpenDevice'); + + paOutDevice := paApiInfo^.defaultOutputDevice; + if (not OpenDevice(paOutDevice)) then + begin + Exit; + end; result := true; end; @@ -147,6 +332,11 @@ begin Pa_StopStream(paStream); end; +function TAudioPlayback_Portaudio.FinalizeAudioPlaybackEngine(): boolean; +begin + Pa_Terminate(); + Result := true; +end; initialization diff --git a/Game/Code/Classes/UAudioPlayback_SDL.pas b/Game/Code/Classes/UAudioPlayback_SDL.pas index 6fc22242..ed5a208b 100644 --- a/Game/Code/Classes/UAudioPlayback_SDL.pas +++ b/Game/Code/Classes/UAudioPlayback_SDL.pas @@ -25,12 +25,15 @@ uses type TAudioPlayback_SDL = class(TAudioPlayback_SoftMixer) + private + function EnumDevices(): boolean; protected function InitializeAudioPlaybackEngine(): boolean; override; function StartAudioPlaybackEngine(): boolean; override; procedure StopAudioPlaybackEngine(); override; + function FinalizeAudioPlaybackEngine(): boolean; override; public - function GetName: String; override; + function GetName: String; override; procedure MixBuffers(dst, src: PChar; size: Cardinal; volume: Integer); override; end; @@ -53,14 +56,37 @@ begin result := 'SDL_Playback'; end; +function TAudioPlayback_SDL.EnumDevices(): boolean; +begin + // Note: SDL does not provide Device-Selection capabilities (will be introduced in 1.3) + ClearOutputDeviceList(); + SetLength(OutputDeviceList, 1); + OutputDeviceList[0] := TAudioOutputDevice.Create(); + OutputDeviceList[0].Name := '[SDL Default-Device]'; + Result := true; +end; + function TAudioPlayback_SDL.InitializeAudioPlaybackEngine(): boolean; var desiredAudioSpec, obtainedAudioSpec: TSDL_AudioSpec; -// err: integer; // Auto Removed, Unused Variable + SampleBufferSize: integer; begin result := false; - SDL_InitSubSystem(SDL_INIT_AUDIO); + EnumDevices(); + + if (SDL_InitSubSystem(SDL_INIT_AUDIO) = -1) then + begin + Log.LogError('SDL_InitSubSystem failed!', 'TAudioPlayback_SDL.InitializeAudioPlaybackEngine'); + exit; + end; + + SampleBufferSize := IAudioOutputBufferSizeVals[Ini.AudioOutputBufferSizeIndex]; + if (SampleBufferSize <= 0) then + begin + // Automatic setting defaults to 1024 samples + SampleBufferSize := 1024; + end; FillChar(desiredAudioSpec, sizeof(desiredAudioSpec), 0); with desiredAudioSpec do @@ -68,14 +94,14 @@ begin freq := 44100; format := AUDIO_S16SYS; channels := 2; - samples := Ini.SDLBufferSize; + samples := SampleBufferSize; callback := @SDLAudioCallback; userdata := Self; end; if(SDL_OpenAudio(@desiredAudioSpec, @obtainedAudioSpec) = -1) then begin - Log.LogStatus('SDL_OpenAudio: ' + SDL_GetError(), 'UAudioPlayback_SDL'); + Log.LogStatus('SDL_OpenAudio: ' + SDL_GetError(), 'TAudioPlayback_SDL.InitializeAudioPlaybackEngine'); exit; end; @@ -85,7 +111,7 @@ begin asfS16 ); - Log.LogStatus('Opened audio device', 'UAudioPlayback_SDL'); + Log.LogStatus('Opened audio device', 'TAudioPlayback_SDL.InitializeAudioPlaybackEngine'); result := true; end; @@ -97,8 +123,15 @@ begin end; procedure TAudioPlayback_SDL.StopAudioPlaybackEngine(); +begin + SDL_PauseAudio(1); +end; + +function TAudioPlayback_SDL.FinalizeAudioPlaybackEngine(): boolean; begin SDL_CloseAudio(); + SDL_QuitSubSystem(SDL_INIT_AUDIO); + Result := true; end; procedure TAudioPlayback_SDL.MixBuffers(dst, src: PChar; size: Cardinal; volume: Integer); @@ -116,5 +149,4 @@ initialization finalization AudioManager.Remove( singleton_AudioPlaybackSDL ); - end. diff --git a/Game/Code/Classes/UAudioPlayback_SoftMixer.pas b/Game/Code/Classes/UAudioPlayback_SoftMixer.pas index f65b3d9a..431653d0 100644 --- a/Game/Code/Classes/UAudioPlayback_SoftMixer.pas +++ b/Game/Code/Classes/UAudioPlayback_SoftMixer.pas @@ -13,7 +13,8 @@ uses Classes, SysUtils, sdl, - UMusic; + UMusic, + UAudioPlaybackBase; type TAudioPlayback_SoftMixer = class; @@ -35,9 +36,14 @@ type InternalLock: PSDL_Mutex; + SoundEffects: TList; + + FadeInStartTime, FadeInTime: cardinal; + FadeInStartVolume, FadeInTargetVolume: integer; + procedure Reset(); - class function ConvertAudioFormatToSDL(fmt: TAudioSampleFormat): UInt16; + class function ConvertAudioFormatToSDL(Format: TAudioSampleFormat; out SDLFormat: UInt16): boolean; function InitFormatConversion(): boolean; procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} @@ -51,24 +57,26 @@ type procedure Play(); override; procedure Pause(); override; procedure Stop(); override; + procedure FadeIn(Time: real; TargetVolume: integer); 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; + function GetLoop(): boolean; override; + procedure SetLoop(Enabled: boolean); override; + function GetPosition: real; override; + procedure SetPosition(Time: real); 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); + function GetPCMData(var data: TPCMData): Cardinal; override; + procedure GetFFTData(var data: TFFTData); override; + + procedure AddSoundEffect(effect: TSoundEffect); override; + procedure RemoveSoundEffect(effect: TSoundEffect); override; end; TAudioMixerStream = class @@ -79,7 +87,7 @@ type mixerBuffer: PChar; internalLock: PSDL_Mutex; - _volume: integer; + appVolume: integer; procedure Lock(); {$IFDEF HasInline}inline;{$ENDIF} procedure Unlock(); {$IFDEF HasInline}inline;{$ENDIF} @@ -96,9 +104,8 @@ type property Volume: integer READ GetVolume WRITE SetVolume; end; - TAudioPlayback_SoftMixer = class( TInterfacedObject, IAudioPlayback ) + TAudioPlayback_SoftMixer = class(TAudioPlaybackBase) private - MusicStream: TGenericPlaybackStream; MixerStream: TAudioMixerStream; protected FormatInfo: TAudioFormatInfo; @@ -106,45 +113,21 @@ type function InitializeAudioPlaybackEngine(): boolean; virtual; abstract; function StartAudioPlaybackEngine(): boolean; virtual; abstract; procedure StopAudioPlaybackEngine(); virtual; abstract; + function FinalizeAudioPlaybackEngine(): boolean; 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); + function OpenStream(const Filename: String): TAudioPlaybackStream; override; + public + function GetName: String; override; abstract; + function InitializePlayback(): boolean; override; + function FinalizePlayback: boolean; override; - // Interface for Visualizer - function GetPCMData(var data: TPCMData): Cardinal; + procedure SetAppVolume(Volume: integer); override; 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 @@ -161,11 +144,13 @@ uses constructor TAudioMixerStream.Create(Engine: TAudioPlayback_SoftMixer); begin + inherited Create(); + Self.Engine := Engine; activeStreams := TList.Create; internalLock := SDL_CreateMutex(); - _volume := 100; + appVolume := 100; end; destructor TAudioMixerStream.Destroy(); @@ -174,6 +159,7 @@ begin Freemem(mixerBuffer); activeStreams.Free; SDL_DestroyMutex(internalLock); + inherited; end; procedure TAudioMixerStream.Lock(); @@ -189,14 +175,14 @@ end; function TAudioMixerStream.GetVolume(): integer; begin Lock(); - result := _volume; + result := appVolume; Unlock(); end; procedure TAudioMixerStream.SetVolume(volume: integer); begin Lock(); - _volume := volume; + appVolume := volume; Unlock(); end; @@ -270,8 +256,8 @@ begin 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); + // Note: use Self.appVolume instead of Self.Volume to prevent recursive locking + Engine.MixBuffers(Buffer, mixerBuffer, size, appVolume * stream.Volume div 100); end; end; @@ -292,6 +278,8 @@ begin inherited Create(); Self.Engine := Engine; internalLock := SDL_CreateMutex(); + SoundEffects := TList.Create; + Status := ssStopped; Reset(); end; @@ -299,20 +287,34 @@ destructor TGenericPlaybackStream.Destroy(); begin Close(); SDL_DestroyMutex(internalLock); - inherited Destroy(); + FreeAndNil(SoundEffects); + inherited; end; procedure TGenericPlaybackStream.Reset(); begin + // wake-up sleeping audio-callback threads in the ReadData()-function + if assigned(decodeStream) then + decodeStream.Close(); + + // stop audio-callback on this stream Stop(); + + // reset and/or free data + Loop := false; + // TODO: use DecodeStream.Unref() instead of Free(); FreeAndNil(DecodeStream); + FreeMem(SampleBuffer); SampleBuffer := nil; SampleBufferPos := 0; BytesAvail := 0; + _volume := 0; + SoundEffects.Clear; + FadeInTime := 0; end; procedure TGenericPlaybackStream.Lock(); @@ -325,24 +327,27 @@ begin SDL_mutexV(internalLock); end; -class function TGenericPlaybackStream.ConvertAudioFormatToSDL(fmt: TAudioSampleFormat): UInt16; +class function TGenericPlaybackStream.ConvertAudioFormatToSDL(Format: TAudioSampleFormat; out SDLFormat: UInt16): boolean; 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; + case Format of + asfU8: SDLFormat := AUDIO_U8; + asfS8: SDLFormat := AUDIO_S8; + asfU16LSB: SDLFormat := AUDIO_U16LSB; + asfS16LSB: SDLFormat := AUDIO_S16LSB; + asfU16MSB: SDLFormat := AUDIO_U16MSB; + asfS16MSB: SDLFormat := AUDIO_S16MSB; + asfU16: SDLFormat := AUDIO_U16; + asfS16: SDLFormat := AUDIO_S16; + else begin + Result := false; + Exit; + end; end; + Result := true; end; function TGenericPlaybackStream.InitFormatConversion(): boolean; var - //err: integer; srcFormat: UInt16; dstFormat: UInt16; srcFormatInfo: TAudioFormatInfo; @@ -353,10 +358,8 @@ begin srcFormatInfo := DecodeStream.GetAudioFormatInfo(); dstFormatInfo := Engine.GetAudioFormatInfo(); - srcFormat := ConvertAudioFormatToSDL(srcFormatInfo.Format); - dstFormat := ConvertAudioFormatToSDL(dstFormatInfo.Format); - - if ((srcFormat = 0) or (dstFormat = 0)) then + if (not ConvertAudioFormatToSDL(srcFormatInfo.Format, srcFormat) or + not ConvertAudioFormatToSDL(dstFormatInfo.Format, dstFormat)) then begin Log.LogError('Audio-format not supported by SDL', 'TSoftMixerPlaybackStream.InitFormatConversion'); Exit; @@ -399,7 +402,7 @@ procedure TGenericPlaybackStream.Play(); var mixer: TAudioMixerStream; begin - if (status <> ssPaused) then + if (status = ssPlaying) then begin // rewind if assigned(DecodeStream) then @@ -412,6 +415,15 @@ begin mixer.AddStream(Self); end; +procedure TGenericPlaybackStream.FadeIn(Time: real; TargetVolume: integer); +begin + FadeInTime := Trunc(Time * 1000); + FadeInStartTime := SDL_GetTicks(); + FadeInStartVolume := _volume; + FadeInTargetVolume := TargetVolume; + Play(); +end; + procedure TGenericPlaybackStream.Pause(); var mixer: TAudioMixerStream; @@ -427,16 +439,18 @@ procedure TGenericPlaybackStream.Stop(); var mixer: TAudioMixerStream; begin + if (status = ssStopped) then + Exit; + status := ssStopped; mixer := Engine.GetMixer(); if (mixer <> nil) then mixer.RemoveStream(Self); -end; -function TGenericPlaybackStream.IsLoaded(): boolean; -begin - result := assigned(DecodeStream); + // rewind (note: DecodeStream might be closed already, but this is not a problem) + if assigned(DecodeStream) then + DecodeStream.Position := 0; end; function TGenericPlaybackStream.GetLoop(): boolean; @@ -480,6 +494,7 @@ var remFrameBytes: integer; copyCnt: integer; BytesNeeded: integer; + i: integer; begin Result := -1; @@ -543,7 +558,9 @@ begin // end-of-file reached -> stop playback if (DecodeStream.EOF) then + begin Stop(); + end; // resample decoded data cvt.buf := PUint8(SampleBuffer); @@ -552,6 +569,15 @@ begin Exit; SampleBufferCount := cvt.len_cvt; + + // apply effects + for i := 0 to SoundEffects.Count-1 do + begin + if (SoundEffects[i] <> nil) then + begin + TSoundEffect(SoundEffects[i]).Callback(SampleBuffer, SampleBufferCount); + end; + end; finally Unlock(); end; @@ -664,11 +690,28 @@ begin // 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; +procedure TGenericPlaybackStream.AddSoundEffect(effect: TSoundEffect); +begin + if (not assigned(effect)) then + Exit; + Lock(); + // check if effect is already in list to avoid duplicates + if (SoundEffects.IndexOf(Pointer(effect)) = -1) then + SoundEffects.Add(Pointer(effect)); + Unlock(); +end; + +procedure TGenericPlaybackStream.RemoveSoundEffect(effect: TSoundEffect); +begin + Lock(); + SoundEffects.Remove(effect); + Unlock(); +end; + function TGenericPlaybackStream.GetPosition: real; begin if assigned(DecodeStream) then @@ -684,12 +727,37 @@ begin end; function TGenericPlaybackStream.GetVolume(): integer; +var + FadeAmount: Single; begin - result := _volume; + Lock(); + // adjust volume if fading is enabled + if (FadeInTime > 0) then + begin + FadeAmount := (SDL_GetTicks() - FadeInStartTime) / FadeInTime; + // check if fade-target is reached + if (FadeAmount >= 1) then + begin + // target reached -> stop fading + FadeInTime := 0; + _volume := FadeInTargetVolume; + end + else + begin + // fading in progress + _volume := Trunc(FadeAmount*FadeInTargetVolume + (1-FadeAmount)*FadeInStartVolume); + end; + end; + // return current volume + Result := _volume; + Unlock(); end; procedure TGenericPlaybackStream.SetVolume(volume: integer); begin + Lock(); + // stop fading + FadeInTime := 0; // clamp volume if (volume > 100) then _volume := 100 @@ -697,6 +765,7 @@ begin _volume := 0 else _volume := volume; + Unlock(); end; @@ -719,15 +788,17 @@ begin result := true; end; -destructor TAudioPlayback_SoftMixer.Destroy; +function TAudioPlayback_SoftMixer.FinalizePlayback: boolean; begin + Close; StopAudioPlaybackEngine(); - FreeAndNil(MusicStream); FreeAndNil(MixerStream); FreeAndNil(FormatInfo); - inherited Destroy(); + FinalizeAudioPlaybackEngine(); + inherited FinalizePlayback; + Result := true; end; procedure TAudioPlayback_SoftMixer.AudioCallback(buffer: PChar; size: integer); @@ -745,13 +816,16 @@ begin Result := FormatInfo; end; -function TAudioPlayback_SoftMixer.Load(const Filename: String): TGenericPlaybackStream; +function TAudioPlayback_SoftMixer.OpenStream(const Filename: String): TAudioPlaybackStream; var decodeStream: TAudioDecodeStream; playbackStream: TGenericPlaybackStream; begin Result := nil; + if (AudioDecoder = nil) then + Exit; + decodeStream := AudioDecoder.Open(Filename); if not assigned(decodeStream) then begin @@ -760,110 +834,28 @@ begin end; playbackStream := TGenericPlaybackStream.Create(Self); + if (not assigned(playbackStream)) then + begin + FreeAndNil(decodeStream); + Exit; + end; + if (not playbackStream.SetDecodeStream(decodeStream)) then + begin + FreeAndNil(playbackStream); + FreeAndNil(decodeStream); Exit; + end; result := playbackStream; end; -procedure TAudioPlayback_SoftMixer.SetVolume(Volume: integer); +procedure TAudioPlayback_SoftMixer.SetAppVolume(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; @@ -916,38 +908,4 @@ begin 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. diff --git a/Game/Code/Classes/UIni.pas b/Game/Code/Classes/UIni.pas index 292a485a..bed93a83 100644 --- a/Game/Code/Classes/UIni.pas +++ b/Game/Code/Classes/UIni.pas @@ -65,8 +65,8 @@ type ClickAssist: integer; BeatClick: integer; SavePlayback: integer; - Threshold: integer; - SDLBufferSize: integer; + ThresholdIndex: integer; + AudioOutputBufferSizeIndex: integer; //Song Preview PreviewVolume: integer; @@ -105,14 +105,11 @@ type LPT: integer; procedure Load(); - procedure LoadSoundSettings(); - procedure Save(); procedure SaveNames; procedure SaveLevel; end; - var Ini: TIni; IResolution: array of string; @@ -121,7 +118,9 @@ var ISkin: array of string; const - IPlayers: array[0..4] of string = ('1', '2', '3', '4', '6'); + IPlayers: array[0..4] of string = ('1', '2', '3', '4', '6'); + IPlayersVals: array[0..4] of integer = ( 1 , 2 , 3 , 4 , 6 ); + IDifficulty: array[0..2] of string = ('Easy', 'Medium', 'Hard'); ITabs: array[0..1] of string = ('Off', 'On'); @@ -151,16 +150,25 @@ const ISpectrograph: array[0..1] of string = ('Off', 'On'); IMovieSize: array[0..2] of string = ('Half', 'Full [Vid]', 'Full [BG+Vid]'); - IMicBoost: array[0..3] of string = ('Off', '+6dB', '+12dB', '+18dB'); IClickAssist: array[0..1] of string = ('Off', 'On'); IBeatClick: array[0..1] of string = ('Off', 'On'); ISavePlayback: array[0..1] of string = ('Off', 'On'); + IThreshold: array[0..3] of string = ('5%', '10%', '15%', '20%'); - ISDLBufferSize: array[0..8] of string = ('256', '512', '1024', '2048', '4096', '8192', '16384', '32768', '65536'); - + IThresholdVals: array[0..3] of single = (0.05, 0.10, 0.15, 0.20); + + IAudioOutputBufferSize: array[0..9] of string = ('Auto', '256', '512', '1024', '2048', '4096', '8192', '16384', '32768', '65536'); + IAudioOutputBufferSizeVals: array[0..9] of integer = ( 0, 256, 512 , 1024 , 2048 , 4096 , 8192 , 16384 , 32768 , 65536 ); + + IAudioInputBufferSize: array[0..9] of string = ('Auto', '256', '512', '1024', '2048', '4096', '8192', '16384', '32768', '65536'); + IAudioInputBufferSizeVals: array[0..9] of integer = ( 0, 256, 512 , 1024 , 2048 , 4096 , 8192 , 16384 , 32768 , 65536 ); + //Song Preview - IPreviewVolume: array[0..10] of string = ('Off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', '100%'); - IPreviewFading: array[0..5] of string = ('Off', '1 Sec', '2 Secs', '3 Secs', '4 Secs', '5 Secs'); + IPreviewVolume: array[0..10] of string = ('Off', '10%', '20%', '30%', '40%', '50%', '60%', '70%', '80%', '90%', '100%'); + IPreviewVolumeVals: array[0..10] of single = ( 0, 0.10, 0.20, 0.30, 0.40, 0.50, 0.60, 0.70, 0.80, 0.90, 1.00 ); + + IPreviewFading: array[0..5] of string = ('Off', '1 Sec', '2 Secs', '3 Secs', '4 Secs', '5 Secs'); + IPreviewFadingVals: array[0..5] of integer = ( 0, 1, 2, 3, 4, 5 ); ILyricsFont: array[0..2] of string = ('Plain', 'OLine1', 'OLine2'); @@ -182,7 +190,9 @@ const IJoypad: array[0..1] of string = ('Off', 'On'); ILPT: array[0..2] of string = ('Off', 'LCD', 'Lights'); - IChannel: array[0..6] of string = ('Off', '1', '2', '3', '4', '5', '6'); + // Recording options + IChannelPlayer: array[0..6] of string = ('Off', '1', '2', '3', '4', '5', '6'); + IMicBoost: array[0..3] of string = ('Off', '+6dB', '+12dB', '+18dB'); implementation @@ -232,12 +242,10 @@ procedure TIni.LoadInputDeviceCfg(IniFile: TMemIniFile); var deviceIndex: integer; deviceCfg: PInputDeviceConfig; - device: TAudioInputDevice; deviceIniIndex: integer; deviceIniStr: string; channelCount: integer; channelIndex: integer; - newDevice: boolean; recordKeys: TStringList; i: integer; begin @@ -289,63 +297,10 @@ begin recordKeys.Free(); - // Input devices - append detected soundcards - for deviceIndex := 0 to High(AudioInputProcessor.Device) do - begin - newDevice := true; - for deviceIniIndex := 0 to High(InputDeviceConfig) do - begin //Search for Card in List - deviceCfg := @InputDeviceConfig[deviceIniIndex]; - device := AudioInputProcessor.Device[deviceIndex]; - - if (deviceCfg.Name = Trim(device.Description)) then - begin - newDevice := false; - - // store highest channel index as an offset for the new channels - channelIndex := High(deviceCfg.ChannelToPlayerMap); - // add missing channels or remove non-existing ones - SetLength(deviceCfg.ChannelToPlayerMap, device.AudioFormat.Channels); - // initialize added channels to 0 - for i := channelIndex+1 to High(deviceCfg.ChannelToPlayerMap) do - begin - deviceCfg.ChannelToPlayerMap[i] := 0; - end; - - // associate ini-index with device - device.CfgIndex := deviceIniIndex; - break; - end; - end; - - //If not in List -> Add - if newDevice then - begin - // resize list - SetLength(InputDeviceConfig, Length(InputDeviceConfig)+1); - deviceCfg := @InputDeviceConfig[High(InputDeviceConfig)]; - device := AudioInputProcessor.Device[deviceIndex]; - - // associate ini-index with device - device.CfgIndex := High(InputDeviceConfig); - - deviceCfg.Name := Trim(device.Description); - deviceCfg.Input := 0; - - channelCount := device.AudioFormat.Channels; - SetLength(deviceCfg.ChannelToPlayerMap, channelCount); - - for channelIndex := 0 to channelCount-1 do - begin - // set default at first start of USDX (1st device, 1st channel -> player1) - if ((channelIndex = 0) and (device.CfgIndex = 0)) then - deviceCfg.ChannelToPlayerMap[0] := 1 - else - deviceCfg.ChannelToPlayerMap[channelIndex] := 0; - end; - end; - end; - + // MicBoost + //MicBoost := GetArrayIndex(IMicBoost, IniFile.ReadString('Record', 'MicBoost', 'Off')); + // Threshold + // ThresholdIndex := GetArrayIndex(IThreshold, IniFile.ReadString('Record', 'Threshold', IThreshold[1])); end; procedure TIni.SaveInputDeviceCfg(IniFile: TIniFile); @@ -369,6 +324,11 @@ begin IntToStr(InputDeviceConfig[deviceIndex].ChannelToPlayerMap[channelIndex])); end; end; + + // MicBoost + //IniFile.WriteString('Record', 'MicBoost', IMicBoost[MicBoost]); + // Threshold + //IniFile.WriteString('Record', 'Threshold', IThreshold[ThresholdIndex]); end; procedure TIni.Load(); @@ -385,14 +345,14 @@ var //Result := copy (S,0,StrRScan (PChar(S),char('.'))+1); Result := copy (S,0,Pos ('.ini',S)-1); end; - + // get index of value V in array a, returns -1 if value is not in array function GetArrayIndex(const A: array of String; V: String; caseInsensitiv: Boolean = False): Integer; var i: Integer; begin Result := -1; - + for i := 0 To High(A) do if (A[i] = V) or (caseInsensitiv and (UpperCase(A[i]) = UpperCase(V))) then begin @@ -400,7 +360,20 @@ var break; end; end; - + + // get index of IniSeaction:IniProperty in array SearchArray or return the default value + function ReadArrayIndex(const SearchArray: array of String; IniSection: String; IniProperty: String; Default: Integer): Integer; + var + StrValue: string; + begin + StrValue := IniFile.ReadString('Sound', 'AudioOutputBufferSize', SearchArray[Default]); + Result := GetArrayIndex(SearchArray, StrValue); + if (Result = -1) then + begin + Result := Default; + end; + end; + // swap two strings procedure swap(var s1, s2: String); var @@ -564,9 +537,6 @@ begin // MovieSize MovieSize := GetArrayIndex(IMovieSize, IniFile.ReadString('Graphics', 'MovieSize', IMovieSize[2])); - // MicBoost - MicBoost := GetArrayIndex(IMicBoost, IniFile.ReadString('Sound', 'MicBoost', 'Off')); - // ClickAssist ClickAssist := GetArrayIndex(IClickAssist, IniFile.ReadString('Sound', 'ClickAssist', 'Off')); @@ -575,17 +545,10 @@ begin // SavePlayback SavePlayback := GetArrayIndex(ISavePlayback, IniFile.ReadString('Sound', 'SavePlayback', ISavePlayback[0])); - - // Threshold - Threshold := GetArrayIndex(IThreshold, IniFile.ReadString('Sound', 'Threshold', IThreshold[1])); - // SDLBufferSize - SDLBufferSize := GetArrayIndex(ISDLBufferSize, IniFile.ReadString('Sound', 'SDLBufferSize', '1024')); - if (SDLBufferSize = -1) then - SDLBufferSize := 1024 - else - SDLBufferSize := StrToInt(ISDLBufferSize[SDLBufferSize]); - + // AudioOutputBufferSize + AudioOutputBufferSizeIndex := ReadArrayIndex(IAudioOutputBufferSize, 'Sound', 'AudioOutputBufferSize', 0); + //Preview Volume PreviewVolume := GetArrayIndex(IPreviewVolume, IniFile.ReadString('Sound', 'PreviewVolume', IPreviewVolume[7])); @@ -638,15 +601,17 @@ begin Theme := GetArrayIndex(ITheme, IniFile.ReadString('Themes', 'Theme', 'DELUXE'), True); if (Theme = -1) then Theme := 0; - + // Skin Skin.onThemeChange; - + SkinNo := GetArrayIndex(ISkin, IniFile.ReadString('Themes', 'Skin', ISkin[0])); // Color Color := GetArrayIndex(IColor, IniFile.ReadString('Themes', 'Color', IColor[0])); - + + LoadInputDeviceCfg(IniFile); + // LoadAnimation LoadAnimation := GetArrayIndex(ILoadAnimation, IniFile.ReadString('Advanced', 'LoadAnimation', 'On')); @@ -740,20 +705,14 @@ begin // Movie Size IniFile.WriteString('Graphics', 'MovieSize', IMovieSize[MovieSize]); - // MicBoost - IniFile.WriteString('Sound', 'MicBoost', IMicBoost[MicBoost]); - // ClickAssist IniFile.WriteString('Sound', 'ClickAssist', IClickAssist[ClickAssist]); // BeatClick IniFile.WriteString('Sound', 'BeatClick', IBeatClick[BeatClick]); - // Threshold - IniFile.WriteString('Sound', 'Threshold', IThreshold[Threshold]); - - // SDLBufferSize - IniFile.WriteString('Sound', 'SDLBufferSize', IntToStr(SDLBufferSize)); + // AudioOutputBufferSize + IniFile.WriteString('Sound', 'AudioOutputBufferSize', IAudioOutputBufferSize[AudioOutputBufferSizeIndex]); // Song Preview IniFile.WriteString('Sound', 'PreviewVolume', IPreviewVolume[PreviewVolume]); @@ -813,17 +772,8 @@ begin IniFile.WriteString('Controller', 'Joypad', IJoypad[Joypad]); IniFile.Free; - - end; -end; -procedure TIni.LoadSoundSettings; -var - IniFile: TMemIniFile; -begin - IniFile := TMemIniFile.Create(Filename); - LoadInputDeviceCfg(IniFile); - IniFile.Free; + end; end; procedure TIni.SaveNames; diff --git a/Game/Code/Classes/UMain.pas b/Game/Code/Classes/UMain.pas index 39265a6d..9c867a3c 100644 --- a/Game/Code/Classes/UMain.pas +++ b/Game/Code/Classes/UMain.pas @@ -223,13 +223,6 @@ begin Log.BenchmarkEnd(1); Log.LogBenchmark('Initializing Sound', 1); - // Load Sound Settings from Ini - Log.BenchmarkStart(1); - Log.LogStatus('Load Sound Settings', 'Initialization'); - Ini.LoadSoundSettings; - Log.BenchmarkEnd(1); - Log.LogBenchmark('Load Sound Settings', 1); - // Theme Log.BenchmarkStart(1); Log.LogStatus('Load Themes', 'Initialization'); @@ -352,7 +345,9 @@ begin // call an uninitialize routine for every initialize step // or at least use the corresponding Free-Methods - //TTF_quit(); + FinalizeSound(); + + TTF_Quit(); SDL_Quit(); (* diff --git a/Game/Code/Classes/UMedia_dummy.pas b/Game/Code/Classes/UMedia_dummy.pas index dcdcd710..ad3aa94e 100644 --- a/Game/Code/Classes/UMedia_dummy.pas +++ b/Game/Code/Classes/UMedia_dummy.pas @@ -30,8 +30,9 @@ var singleton_dummy : IVideoPlayback; type - Tmedia_dummy = class( TInterfacedObject, IVideoPlayback, IVideoVisualization, IAudioPlayback, IAudioInput ) + TMedia_dummy = class( TInterfacedObject, IVideoPlayback, IVideoVisualization, IAudioPlayback, IAudioInput ) private + DummyOutputDeviceList: TAudioOutputDeviceList; public constructor create(); function GetName: String; @@ -53,6 +54,7 @@ type // IAudioInput function InitializeRecord: boolean; + function FinalizeRecord: boolean; procedure CaptureStart; procedure CaptureStop; procedure GetFFTData(var data: TFFTData); @@ -60,8 +62,12 @@ type // IAudioPlayback function InitializePlayback: boolean; + function FinalizePlayback: boolean; + + function GetOutputDeviceList(): TAudioOutputDeviceList; + procedure FadeIn(Time: real; TargetVolume: integer); + procedure SetAppVolume(Volume: integer); procedure SetVolume(Volume: integer); - procedure SetMusicVolume(Volume: integer); procedure SetLoop(Enabled: boolean); procedure Rewind; @@ -124,7 +130,7 @@ procedure Tmedia_dummy.SetPosition(Time: real); begin end; -function Tmedia_dummy.getPosition: real; +function Tmedia_dummy.GetPosition: real; begin result := 0; end; @@ -135,6 +141,11 @@ begin result := true; end; +function Tmedia_dummy.FinalizeRecord: boolean; +begin + result := true; +end; + procedure Tmedia_dummy.CaptureStart; begin end; @@ -155,14 +166,27 @@ end; // IAudioPlayback function Tmedia_dummy.InitializePlayback: boolean; begin + SetLength(DummyOutputDeviceList, 1); + DummyOutputDeviceList[0] := TAudioOutputDevice.Create(); + DummyOutputDeviceList[0].Name := '[Dummy Device]'; result := true; end; -procedure Tmedia_dummy.SetVolume(Volume: integer); +function Tmedia_dummy.FinalizePlayback: boolean; +begin + result := true; +end; + +function Tmedia_dummy.GetOutputDeviceList(): TAudioOutputDeviceList; +begin + Result := DummyOutputDeviceList; +end; + +procedure Tmedia_dummy.SetAppVolume(Volume: integer); begin end; -procedure Tmedia_dummy.SetMusicVolume(Volume: integer); +procedure Tmedia_dummy.SetVolume(Volume: integer); begin end; @@ -170,6 +194,10 @@ procedure Tmedia_dummy.SetLoop(Enabled: boolean); begin end; +procedure Tmedia_dummy.FadeIn(Time: real; TargetVolume: integer); +begin +end; + procedure Tmedia_dummy.Rewind; begin end; diff --git a/Game/Code/Classes/UMusic.pas b/Game/Code/Classes/UMusic.pas index 4e0291cf..9977661f 100644 --- a/Game/Code/Classes/UMusic.pas +++ b/Game/Code/Classes/UMusic.pas @@ -80,6 +80,7 @@ type TFFTData = array[0..(FFTSize div 2)-1] of Single; type + PPCMStereoSample = ^TPCMStereoSample; TPCMStereoSample = array[0..1] of SmallInt; TPCMData = array[0..511] of TPCMStereoSample; @@ -123,6 +124,18 @@ type constructor Create(Channels: byte; SampleRate: double; Format: TAudioSampleFormat); end; +type + TSoundEffect = class + public + EngineData: Pointer; // can be used for engine-specific data + procedure Callback(Buffer: PChar; BufSize: integer); virtual; abstract; + end; + + TVoiceRemoval = class(TSoundEffect) + public + procedure Callback(Buffer: PChar; BufSize: integer); override; + end; + type TAudioProcessingStream = class public @@ -131,40 +144,40 @@ type TAudioPlaybackStream = class(TAudioProcessingStream) protected - function GetLoop(): boolean; virtual; abstract; - procedure SetLoop(Enabled: boolean); virtual; abstract; + function GetPosition: real; virtual; abstract; + procedure SetPosition(Time: real); virtual; abstract; function GetLength(): real; virtual; abstract; function GetStatus(): TStreamStatus; virtual; abstract; function GetVolume(): integer; virtual; abstract; - procedure SetVolume(volume: integer); virtual; abstract; + procedure SetVolume(Volume: integer); virtual; abstract; + function GetLoop(): boolean; virtual; abstract; + procedure SetLoop(Enabled: boolean); virtual; abstract; public procedure Play(); virtual; abstract; procedure Pause(); virtual; abstract; procedure Stop(); virtual; abstract; + procedure FadeIn(Time: real; TargetVolume: integer); virtual; abstract; + + procedure GetFFTData(var data: TFFTData); virtual; abstract; + function GetPCMData(var data: TPCMData): Cardinal; virtual; abstract; + + procedure AddSoundEffect(effect: TSoundEffect); virtual; abstract; + procedure RemoveSoundEffect(effect: TSoundEffect); virtual; abstract; - property Loop: boolean READ GetLoop WRITE SetLoop; property Length: real READ GetLength; + property Position: real READ GetPosition WRITE SetPosition; property Status: TStreamStatus READ GetStatus; property Volume: integer READ GetVolume WRITE SetVolume; + property Loop: boolean READ GetLoop WRITE SetLoop; end; - (* - TAudioMixerStream = class(TAudioProcessingStream) - procedure AddStream(stream: TAudioProcessingStream); - procedure RemoveStream(stream: TAudioProcessingStream); - procedure SetMasterVolume(volume: cardinal); - function GetMasterVolume(): cardinal; - procedure SetStreamVolume(stream: TAudioProcessingStream; volume: cardinal); - function GetStreamVolume(stream: TAudioProcessingStream): cardinal; - end; - *) - TAudioDecodeStream = class(TAudioProcessingStream) protected function GetLength(): real; virtual; abstract; function GetPosition(): real; virtual; abstract; procedure SetPosition(Time: real); virtual; abstract; function IsEOF(): boolean; virtual; abstract; + function IsError(): boolean; virtual; abstract; public function ReadData(Buffer: PChar; BufSize: integer): integer; virtual; abstract; function GetAudioFormatInfo(): TAudioFormatInfo; virtual; abstract; @@ -174,6 +187,19 @@ type property EOF: boolean READ IsEOF; end; +type + TAudioVoiceStream = class(TAudioProcessingStream) + public + function ReadData(Buffer: PChar; BufSize: integer): integer; virtual; abstract; + function GetAudioFormatInfo(): TAudioFormatInfo; virtual; abstract; + end; + // soundcard output-devices information + TAudioOutputDevice = class + public + Name: string; // soundcard name + end; + TAudioOutputDeviceList = array of TAudioOutputDevice; + type IGenericPlayback = Interface ['{63A5EBC3-3F4D-4F23-8DFB-B5165FCE33DD}'] @@ -208,9 +234,13 @@ type IAudioPlayback = Interface( IGenericPlayback ) ['{E4AE0B40-3C21-4DC5-847C-20A87E0DFB96}'] function InitializePlayback: boolean; + function FinalizePlayback: boolean; + + function GetOutputDeviceList(): TAudioOutputDeviceList; + procedure SetAppVolume(Volume: integer); procedure SetVolume(Volume: integer); - procedure SetMusicVolume(Volume: integer); procedure SetLoop(Enabled: boolean); + procedure FadeIn(Time: real; TargetVolume: integer); procedure Rewind; function Finished: boolean; @@ -232,6 +262,7 @@ type ['{557B0E9A-604D-47E4-B826-13769F3E10B7}'] function GetName(): String; function InitializeDecoder(): boolean; + function FinalizeDecoder(): boolean; //function IsSupported(const Filename: string): boolean; end; @@ -251,6 +282,7 @@ type ['{A5C8DA92-2A0C-4AB2-849B-2F7448C6003A}'] function GetName: String; function InitializeRecord: boolean; + function FinalizeRecord(): boolean; procedure CaptureStart; procedure CaptureStop; @@ -288,6 +320,7 @@ var // TODO : JB --- THESE SHOULD NOT BE GLOBAL procedure InitializeSound; +procedure FinalizeSound; function Visualization(): IVideoPlayback; function VideoPlayback(): IVideoPlayback; @@ -360,14 +393,32 @@ begin result := singleton_AudioDecoder; end; -procedure AssignSingletonObjects(); +procedure FilterInterfaceList(const IID: TGUID; InList, OutList: TInterfaceList); +var + i: integer; + obj: IInterface; +begin + if (not assigned(OutList)) then + Exit; + + OutList.Clear; + for i := 0 to InList.Count-1 do + begin + if assigned(InList[i]) then + begin + // add object to list if it implements the interface searched for + if (InList[i].QueryInterface(IID, obj) = 0) then + OutList.Add(obj); + end; + end; +end; + +procedure AssignSingletonObjects(); var lTmpInterface : IInterface; iCount : Integer; begin lTmpInterface := nil; - - for iCount := 0 to AudioManager.Count - 1 do begin @@ -410,7 +461,6 @@ begin end; end; - end; procedure InitializeSound; @@ -460,6 +510,8 @@ begin end; end; + // Update input-device list with registered devices + AudioInputProcessor.UpdateInputDeviceConfig(); // Load in-game sounds SoundLib := TSoundLibrary.Create; @@ -480,6 +532,50 @@ begin end; end; +procedure FinalizeSound; +var + i: integer; + AudioIntfList: TInterfaceList; +begin + // stop, close and free sounds + SoundLib.Free; + + // stop and close music stream + if (AudioPlayback <> nil) then + AudioPlayback.Close; + + // stop any active captures + if (AudioInput <> nil) then + AudioInput.CaptureStop; + + singleton_AudioPlayback := nil; + singleton_AudioDecoder := nil; + singleton_AudioInput := nil; + + // create temporary interface list + AudioIntfList := TInterfaceList.Create(); + + // finalize audio playback interfaces (should be done before the decoders) + FilterInterfaceList(IAudioPlayback, AudioManager, AudioIntfList); + for i := 0 to AudioIntfList.Count-1 do + IAudioPlayback(AudioIntfList[i]).FinalizePlayback(); + + // finalize audio input interfaces + FilterInterfaceList(IAudioInput, AudioManager, AudioIntfList); + for i := 0 to AudioIntfList.Count-1 do + IAudioInput(AudioIntfList[i]).FinalizeRecord(); + + // finalize audio decoder interfaces + FilterInterfaceList(IAudioDecoder, AudioManager, AudioIntfList); + for i := 0 to AudioIntfList.Count-1 do + IAudioDecoder(AudioIntfList[i]).FinalizeDecoder(); + + AudioIntfList.Free; + + // free audio interfaces + while (AudioManager.Count > 0) do + AudioManager.Delete(0); +end; { TSoundLibrary } @@ -535,6 +631,32 @@ begin //Shuffle.Free; end; +procedure TVoiceRemoval.Callback(Buffer: PChar; BufSize: integer); +var + FrameIndex, FrameSize: integer; + Value: integer; + Sample: PPCMStereoSample; +begin + FrameSize := 2 * SizeOf(SmallInt); + for FrameIndex := 0 to (BufSize div FrameSize)-1 do + begin + Sample := PPCMStereoSample(Buffer); + // channel difference + Value := Sample[0] - Sample[1]; + // clip + if (Value > High(SmallInt)) then + Value := High(SmallInt) + else if (Value < Low(SmallInt)) then + Value := Low(SmallInt); + // assign result + Sample[0] := Value; + Sample[1] := Value; + // increase to next frame + Inc(Buffer, FrameSize); + end; +end; + + initialization begin singleton_AudioManager := TInterfaceList.Create(); diff --git a/Game/Code/Classes/URecord.pas b/Game/Code/Classes/URecord.pas index 6faac2b6..55dedd1f 100644 --- a/Game/Code/Classes/URecord.pas +++ b/Game/Code/Classes/URecord.pas @@ -55,7 +55,11 @@ type property ToneString: string READ GetToneString; end; - TAudioInputDeviceSource = record +const + DEFAULT_SOURCE_NAME = '[Default]'; + +type + TAudioInputSource = record Name: string; end; @@ -63,10 +67,10 @@ type TAudioInputDevice = class public CfgIndex: integer; // index of this device in Ini.InputDeviceConfig - Description: string; // soundcard name/description - Source: array of TAudioInputDeviceSource; // soundcard input(-source)s - SourceSelected: integer; // unused. What is this good for? - MicSource: integer; // unused. What is this good for? + Name: string; // soundcard name + Source: array of TAudioInputSource; // soundcard input-sources + SourceRestore: integer; // source-index that will be selected after capturing (-1: not detected) + MicSource: integer; // source-index of mic (-1: none detected) AudioFormat: TAudioFormatInfo; // capture format info (e.g. 44.1kHz SInt16 stereo) CaptureChannel: array of TCaptureBuffer; // sound-buffer references used for mono or stereo channel's capture data @@ -74,18 +78,26 @@ type destructor Destroy; override; procedure LinkCaptureBuffer(ChannelIndex: integer; Sound: TCaptureBuffer); - + + // TODO: add Open/Close functions so Start/Stop becomes faster + //function Open(): boolean; virtual; abstract; + //function Close(): boolean; virtual; abstract; function Start(): boolean; virtual; abstract; - procedure Stop(); virtual; abstract; + function Stop(): boolean; virtual; abstract; + + function GetVolume(): integer; virtual; abstract; + procedure SetVolume(Volume: integer); virtual; abstract; end; TAudioInputProcessor = class public Sound: array of TCaptureBuffer; // sound-buffers for every player - Device: array of TAudioInputDevice; + DeviceList: array of TAudioInputDevice; constructor Create; + procedure UpdateInputDeviceConfig; + // handle microphone input procedure HandleMicrophoneData(Buffer: Pointer; Size: Cardinal; InputDevice: TAudioInputDevice); @@ -96,10 +108,10 @@ type Started: boolean; protected function UnifyDeviceName(const name: string; deviceIndex: integer): string; - function UnifyDeviceSourceName(const name: string; const deviceName: string): string; public function GetName: String; virtual; abstract; function InitializeRecord: boolean; virtual; abstract; + function FinalizeRecord: boolean; virtual; procedure CaptureStart; procedure CaptureStop; @@ -140,8 +152,6 @@ end; { TAudioInputDevice } destructor TAudioInputDevice.Destroy; -//var -// i: integer; // Auto Removed, Unused Variable begin Stop(); Source := nil; @@ -214,6 +224,7 @@ begin end; // move old samples to the beginning of the array (if necessary) + // TODO: should be a ring-buffer instead for SampleIndex := NumSamples to High(BufferArray) do BufferArray[SampleIndex-NumSamples] := BufferArray[SampleIndex]; @@ -225,6 +236,10 @@ begin // save capture-data to BufferLong if neccessary if (Ini.SavePlayback = 1) then begin + // this is just for debugging (approx 15MB per player for a 3min song!!!) + // For an in-game replay-mode we need to compress data so we do not + // waste that much memory. Maybe ogg-vorbis with voice-preset in fast-mode? + // Or we could use a faster but not that efficient lossless compression. BufferNew.Seek(0, soBeginning); BufferLong.CopyFrom(BufferNew, BufferNew.Size); end; @@ -250,14 +265,8 @@ begin MaxVolume := Volume; end; - case Ini.Threshold of - 0: Threshold := 0.05; - 1: Threshold := 0.1; - 2: Threshold := 0.15; - 3: Threshold := 0.2; - else Threshold := 0.1; - end; - + Threshold := IThresholdVals[Ini.ThresholdIndex]; + // check if signal has an acceptable volume (ignore background-noise) if MaxVolume >= Threshold then begin @@ -279,6 +288,7 @@ const begin // prepare to analyze MaxWeight := -1; + MaxTone := 0; // this is not needed, but it satifies the compiler // analyze halftones // Note: at the lowest tone (~65Hz) and a buffer-size of 4096 @@ -376,6 +386,78 @@ begin end; end; +// updates InputDeviceConfig with current input-device information +// See: TIni.LoadInputDeviceCfg() +procedure TAudioInputProcessor.UpdateInputDeviceConfig; +var + deviceIndex: integer; + newDevice: boolean; + deviceIniIndex: integer; + deviceCfg: PInputDeviceConfig; + device: TAudioInputDevice; + channelCount: integer; + channelIndex: integer; + i: integer; +begin + // Input devices - append detected soundcards + for deviceIndex := 0 to High(DeviceList) do + begin + newDevice := true; + //Search for Card in List + for deviceIniIndex := 0 to High(Ini.InputDeviceConfig) do + begin + deviceCfg := @Ini.InputDeviceConfig[deviceIniIndex]; + device := DeviceList[deviceIndex]; + + if (deviceCfg.Name = Trim(device.Name)) then + begin + newDevice := false; + + // store highest channel index as an offset for the new channels + channelIndex := High(deviceCfg.ChannelToPlayerMap); + // add missing channels or remove non-existing ones + SetLength(deviceCfg.ChannelToPlayerMap, device.AudioFormat.Channels); + // initialize added channels to 0 + for i := channelIndex+1 to High(deviceCfg.ChannelToPlayerMap) do + begin + deviceCfg.ChannelToPlayerMap[i] := 0; + end; + + // associate ini-index with device + device.CfgIndex := deviceIniIndex; + break; + end; + end; + + //If not in List -> Add + if newDevice then + begin + // resize list + SetLength(Ini.InputDeviceConfig, Length(Ini.InputDeviceConfig)+1); + deviceCfg := @Ini.InputDeviceConfig[High(Ini.InputDeviceConfig)]; + device := DeviceList[deviceIndex]; + + // associate ini-index with device + device.CfgIndex := High(Ini.InputDeviceConfig); + + deviceCfg.Name := Trim(device.Name); + deviceCfg.Input := 0; + + channelCount := device.AudioFormat.Channels; + SetLength(deviceCfg.ChannelToPlayerMap, channelCount); + + for channelIndex := 0 to channelCount-1 do + begin + // set default at first start of USDX (1st device, 1st channel -> player1) + if ((channelIndex = 0) and (device.CfgIndex = 0)) then + deviceCfg.ChannelToPlayerMap[0] := 1 + else + deviceCfg.ChannelToPlayerMap[channelIndex] := 0; + end; + end; + end; +end; + {* * Handle captured microphone input data. * Params: @@ -391,9 +473,7 @@ var ChannelBuffer: PChar; // buffer handled as array of bytes (offset relative to channel) SampleBuffer: PSmallIntArray; // buffer handled as array of samples Boost: byte; -// ChannelCount: integer; // Auto Removed, Unused Variable ChannelIndex: integer; -// ChannelOffset: integer; // Auto Removed, Unused Variable CaptureChannel: TCaptureBuffer; AudioFormat: TAudioFormatInfo; FrameSize: integer; @@ -418,7 +498,7 @@ begin // results in the analysis phase otherwise) if (AudioFormat.Format <> asfS16) then begin - // this only occurs if a developer choosed a wrong input sample-format + // this only occurs if a developer choosed an unsupported input sample-format Log.CriticalError('TAudioInputProcessor.HandleMicrophoneData: Wrong sample-format'); Exit; end; @@ -472,6 +552,15 @@ end; { TAudioInputBase } +function TAudioInputBase.FinalizeRecord: boolean; +var + i: integer; +begin + for i := 0 to High(AudioInputProcessor.DeviceList) do + AudioInputProcessor.DeviceList[i].Free(); + AudioInputProcessor.DeviceList := nil; +end; + {* * Start capturing on all used input-device. *} @@ -493,9 +582,9 @@ begin AudioInputProcessor.Sound[S].Clear; // start capturing on each used device - for DeviceIndex := 0 to High(AudioInputProcessor.Device) do + for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do begin - Device := AudioInputProcessor.Device[DeviceIndex]; + Device := AudioInputProcessor.DeviceList[DeviceIndex]; if not assigned(Device) then continue; DeviceCfg := @Ini.InputDeviceConfig[Device.CfgIndex]; @@ -536,13 +625,11 @@ end; procedure TAudioInputBase.CaptureStop; var DeviceIndex: integer; -// Player: integer; // Auto Removed, Unused Variable Device: TAudioInputDevice; -// DeviceCfg: PInputDeviceConfig; // Auto Removed, Unused Variable begin - for DeviceIndex := 0 to High(AudioInputProcessor.Device) do + for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do begin - Device := AudioInputProcessor.Device[DeviceIndex]; + Device := AudioInputProcessor.DeviceList[DeviceIndex]; if not assigned(Device) then continue; Device.Stop(); @@ -563,7 +650,7 @@ var // search devices with same description For i := 0 to deviceIndex-1 do begin - if (AudioInputProcessor.Device[i].Description = name) then + if (AudioInputProcessor.DeviceList[i].Name = name) then begin Result := True; Break; @@ -583,28 +670,6 @@ begin end; end; -{* - * Unifies an input-device's source name. - * Note: the description member of the device must already be set when - * calling this function. - *} -function TAudioInputBase.UnifyDeviceSourceName(const name: string; const deviceName: string): string; -//var -// Descr: string; // Auto Removed, Unused Variable -begin - result := name; - - {$IFDEF DARWIN} - // Under MacOSX the SingStar Mics have an empty - // InputName. So, we have to add a hard coded - // Workaround for this problem - if (name = '') and (Pos( 'USBMIC Serial#', deviceName) > 0) then - begin - result := 'Microphone'; - end; - {$ENDIF} -end; - end. diff --git a/Game/Code/Screens/UScreenOptionsRecord.pas b/Game/Code/Screens/UScreenOptionsRecord.pas index b9000991..8e3d0f67 100644 --- a/Game/Code/Screens/UScreenOptionsRecord.pas +++ b/Game/Code/Screens/UScreenOptionsRecord.pas @@ -15,6 +15,17 @@ uses UMenu; type + TDrawState = record + ChannelIndex: integer; + R, G, B: real; // mapped player color (normal) + RD, GD, BD: real; // mapped player color (dark) + end; + + TPeakInfo = record + Volume: single; + Time: cardinal; + end; + TScreenOptionsRecord = class(TMenu) private // max. count of input-channels determined for all devices @@ -32,11 +43,11 @@ type SelectSlideChannelTheme: array of TThemeSelectSlide; // indices for widget-updates - SelectSlideInputID: integer; + SelectInputSourceID: integer; SelectSlideChannelID: array of integer; TextPitchID: array of integer; - // interaction IDs + // interaction IDs ExitButtonIID: integer; // dummy data for non-available channels @@ -44,10 +55,19 @@ type // preview channel-buffers PreviewChannel: array of TCaptureBuffer; + ChannelPeak: array of TPeakInfo; + + // Device source volume + SourceVolume: single; + NextVolumePollTime: cardinal; procedure StartPreview; procedure StopPreview; - procedure UpdateCard; + procedure UpdateInputDevice; + procedure ChangeVolume(VolumeChange: integer); + procedure DrawVolume(x, y, Width, Height: single); + procedure DrawVUMeter(const State: TDrawState; x, y, Width, Height: single); + procedure DrawPitch(const State: TDrawState; x, y, Width, Height: single); public constructor Create; override; function Draw: boolean; override; @@ -56,10 +76,21 @@ type procedure onHide; override; end; +const + PeakDecay = 0.2; // strength of peak-decay (reduction after one sec) + +const + BarHeight = 11; // height of each bar (volume/vu-meter/pitch) + BarUpperSpacing = 1; // spacing between a bar-area and the previous widget + BarLowerSpacing = 3; // spacing between a bar-area and the next widget + SourceBarsTotalHeight = BarHeight + BarUpperSpacing + BarLowerSpacing; + ChannelBarsTotalHeight = 2*BarHeight + BarUpperSpacing + BarLowerSpacing; + implementation uses SysUtils, + Math, SDL, gl, UGraphic, @@ -84,8 +115,27 @@ begin Result := false; Exit; end; + '+': + begin + // FIXME: add a nice volume-slider instead + // or at least provide visualization and acceleration if the user holds the key pressed. + ChangeVolume(2); + end; + '-': + begin + // FIXME: add a nice volume-slider instead + // or at least provide visualization and acceleration if the user holds the key pressed. + ChangeVolume(-2); + end; + 'T': + begin + if ((SDL_GetModState() and KMOD_SHIFT) <> 0) then + Ini.ThresholdIndex := (Ini.ThresholdIndex + Length(IThresholdVals) - 1) mod Length(IThresholdVals) + else + Ini.ThresholdIndex := (Ini.ThresholdIndex + 1) mod Length(IThresholdVals); + end; end; - + // check special keys case PressedKey of SDLK_ESCAPE, @@ -115,7 +165,7 @@ begin AudioPlayback.PlaySound(SoundLib.Option); InteractInc; end; - UpdateCard; + UpdateInputDevice; end; SDLK_LEFT: begin @@ -124,7 +174,7 @@ begin AudioPlayback.PlaySound(SoundLib.Option); InteractDec; end; - UpdateCard; + UpdateInputDevice; end; end; end; @@ -139,13 +189,14 @@ var InputDeviceCfg: PInputDeviceConfig; ChannelTheme: ^TThemeSelectSlide; ButtonTheme: TThemeButton; + WidgetYPos: integer; begin inherited Create; LoadFromTheme(Theme.OptionsRecord); // set CurrentDeviceIndex to a valid device - if (Length(AudioInputProcessor.Device) > 0) then + if (Length(AudioInputProcessor.DeviceList) > 0) then CurrentDeviceIndex := 0 else CurrentDeviceIndex := -1; @@ -153,16 +204,16 @@ begin PreviewDeviceIndex := -1; // init sliders if at least one device was detected - if (Length(AudioInputProcessor.Device) > 0) then + if (Length(AudioInputProcessor.DeviceList) > 0) then begin - InputDevice := AudioInputProcessor.Device[CurrentDeviceIndex]; + InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex]; InputDeviceCfg := @Ini.InputDeviceConfig[InputDevice.CfgIndex]; // init device-selection slider - SetLength(InputDeviceNames, Length(AudioInputProcessor.Device)); - for DeviceIndex := 0 to High(AudioInputProcessor.Device) do + SetLength(InputDeviceNames, Length(AudioInputProcessor.DeviceList)); + for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do begin - InputDeviceNames[DeviceIndex] := AudioInputProcessor.Device[DeviceIndex].Description; + InputDeviceNames[DeviceIndex] := AudioInputProcessor.DeviceList[DeviceIndex].Name; end; // add device-selection slider (InteractionID: 0) AddSelectSlide(Theme.OptionsRecord.SelectSlideCard, CurrentDeviceIndex, InputDeviceNames); @@ -174,15 +225,20 @@ begin InputSourceNames[SourceIndex] := InputDevice.Source[SourceIndex].Name; end; // add source-selection slider (InteractionID: 1) - SelectSlideInputID := AddSelectSlide(Theme.OptionsRecord.SelectSlideInput, + SelectInputSourceID := AddSelectSlide(Theme.OptionsRecord.SelectSlideInput, InputDeviceCfg.Input, InputSourceNames); + // add space for source volume bar + WidgetYPos := Theme.OptionsRecord.SelectSlideInput.Y + + Theme.OptionsRecord.SelectSlideInput.H + + SourceBarsTotalHeight; + // find max. channel count of all devices MaxChannelCount := 0; - for DeviceIndex := 0 to High(AudioInputProcessor.Device) do + for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do begin - if (AudioInputProcessor.Device[DeviceIndex].AudioFormat.Channels > MaxChannelCount) then - MaxChannelCount := AudioInputProcessor.Device[DeviceIndex].AudioFormat.Channels; + if (AudioInputProcessor.DeviceList[DeviceIndex].AudioFormat.Channels > MaxChannelCount) then + MaxChannelCount := AudioInputProcessor.DeviceList[DeviceIndex].AudioFormat.Channels; end; // init channel-to-player mapping sliders @@ -198,7 +254,9 @@ begin // set current channel-theme ChannelTheme := @SelectSlideChannelTheme[ChannelIndex]; // adjust vertical position - ChannelTheme.Y := ChannelTheme.Y + ChannelIndex * ChannelTheme.H; + ChannelTheme.Y := WidgetYPos; + // calc size of next slide (add space for bars) + WidgetYPos := WidgetYPos + ChannelTheme.H + ChannelBarsTotalHeight; // append channel index to name ChannelTheme.Text := ChannelTheme.Text + IntToStr(ChannelIndex+1); @@ -215,7 +273,7 @@ begin // add slider SelectSlideChannelID[ChannelIndex] := AddSelectSlide(ChannelTheme^, - InputDeviceCfg.ChannelToPlayerMap[ChannelIndex], IChannel); + InputDeviceCfg.ChannelToPlayerMap[ChannelIndex], IChannelPlayer); end else begin @@ -223,52 +281,50 @@ begin // add slider but hide it and assign a dummy variable to it SelectSlideChannelID[ChannelIndex] := AddSelectSlide(ChannelTheme^, - ChannelToPlayerMapDummy, IChannel); + ChannelToPlayerMapDummy, IChannelPlayer); SelectsS[SelectSlideChannelID[ChannelIndex]].Visible := false; // hide pitch label Text[TextPitchID[ChannelIndex]].Visible := false; end; end; - - // TODO: move from sound-options to record-options (Themes must be changed first) - //AddSelect(Theme.OptionsSound.SelectMicBoost, Ini.MicBoost, IMicBoost); end; // add Exit-button ButtonTheme := Theme.OptionsRecord.ButtonExit; - ButtonTheme.Y := Theme.OptionsRecord.SelectSlideChannel.Y + - MaxChannelCount * - Theme.OptionsRecord.SelectSlideChannel.H; + ButtonTheme.Y := WidgetYPos; AddButton(ButtonTheme); if (Length(Button[0].Text) = 0) then AddButtonText(14, 20, Theme.Options.Description[7]); // store InteractionID - ExitButtonIID := MaxChannelCount + 2; + if (Length(AudioInputProcessor.DeviceList) > 0) then + ExitButtonIID := MaxChannelCount + 2 + else + ExitButtonIID := 0; // set focus Interaction := 0; end; -procedure TScreenOptionsRecord.UpdateCard; +procedure TScreenOptionsRecord.UpdateInputDevice; var SourceIndex: integer; InputDevice: TAudioInputDevice; InputDeviceCfg: PInputDeviceConfig; ChannelIndex: integer; begin - Log.LogStatus('Update input-device', 'TScreenOptionsRecord.UpdateCard') ; + //Log.LogStatus('Update input-device', 'TScreenOptionsRecord.UpdateCard') ; StopPreview(); // set CurrentDeviceIndex to a valid device - if (CurrentDeviceIndex > High(AudioInputProcessor.Device)) then + if (CurrentDeviceIndex > High(AudioInputProcessor.DeviceList)) then CurrentDeviceIndex := 0; // update sliders if at least one device was detected - if (Length(AudioInputProcessor.Device) > 0) then + if (Length(AudioInputProcessor.DeviceList) > 0) then begin - InputDevice := AudioInputProcessor.Device[CurrentDeviceIndex]; + InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex]; InputDeviceCfg := @Ini.InputDeviceConfig[InputDevice.CfgIndex]; // update source-selection slider @@ -277,7 +333,7 @@ begin begin InputSourceNames[SourceIndex] := InputDevice.Source[SourceIndex].Name; end; - UpdateSelectSlideOptions(Theme.OptionsRecord.SelectSlideInput, SelectSlideInputID, + UpdateSelectSlideOptions(Theme.OptionsRecord.SelectSlideInput, SelectInputSourceID, InputSourceNames, InputDeviceCfg.Input); // update channel-to-player mapping sliders @@ -290,7 +346,7 @@ begin // show slider UpdateSelectSlideOptions(SelectSlideChannelTheme[ChannelIndex], - SelectSlideChannelID[ChannelIndex], IChannel, + SelectSlideChannelID[ChannelIndex], IChannelPlayer, InputDeviceCfg.ChannelToPlayerMap[ChannelIndex]); SelectsS[SelectSlideChannelID[ChannelIndex]].Visible := true; @@ -303,7 +359,7 @@ begin // hide slider and assign a dummy variable to it UpdateSelectSlideOptions(SelectSlideChannelTheme[ChannelIndex], - SelectSlideChannelID[ChannelIndex], IChannel, + SelectSlideChannelID[ChannelIndex], IChannelPlayer, ChannelToPlayerMapDummy); SelectsS[SelectSlideChannelID[ChannelIndex]].Visible := false; @@ -316,6 +372,31 @@ begin StartPreview(); end; +procedure TScreenOptionsRecord.ChangeVolume(VolumeChange: integer); +var + InputDevice: TAudioInputDevice; + Volume: integer; +begin + // validate CurrentDeviceIndex + if ((CurrentDeviceIndex < 0) or + (CurrentDeviceIndex > High(AudioInputProcessor.DeviceList))) then + begin + Exit; + end; + + InputDevice := AudioInputProcessor.DeviceList[CurrentDeviceIndex]; + if not assigned(InputDevice) then + Exit; + + // set new volume + Volume := InputDevice.GetVolume() + VolumeChange; + InputDevice.SetVolume(Volume); + //DebugWriteln('Volume: ' + inttostr(InputDevice.GetVolume)); + + // volume must be polled again + NextVolumePollTime := 0; +end; + procedure TScreenOptionsRecord.onShow; var ChannelIndex: integer; @@ -329,6 +410,8 @@ begin for ChannelIndex := 0 to High(PreviewChannel) do PreviewChannel[ChannelIndex] := TCaptureBuffer.Create(); + SetLength(ChannelPeak, MaxChannelCount); + StartPreview(); end; @@ -342,6 +425,7 @@ begin for ChannelIndex := 0 to High(PreviewChannel) do PreviewChannel[ChannelIndex].Free; SetLength(PreviewChannel, 0); + SetLength(ChannelPeak, 0); end; procedure TScreenOptionsRecord.StartPreview; @@ -350,17 +434,21 @@ var Device: TAudioInputDevice; begin if ((CurrentDeviceIndex >= 0) and - (CurrentDeviceIndex <= High(AudioInputProcessor.Device))) then + (CurrentDeviceIndex <= High(AudioInputProcessor.DeviceList))) then begin - Device := AudioInputProcessor.Device[CurrentDeviceIndex]; + Device := AudioInputProcessor.DeviceList[CurrentDeviceIndex]; // set preview channel as active capture channel for ChannelIndex := 0 to High(Device.CaptureChannel) do begin PreviewChannel[ChannelIndex].Clear(); Device.LinkCaptureBuffer(ChannelIndex, PreviewChannel[ChannelIndex]); + FillChar(ChannelPeak[ChannelIndex], SizeOf(TPeakInfo), 0); end; Device.Start(); PreviewDeviceIndex := CurrentDeviceIndex; + + // volume must be polled again + NextVolumePollTime := 0; end; end; @@ -370,9 +458,9 @@ var Device: TAudioInputDevice; begin if ((PreviewDeviceIndex >= 0) and - (PreviewDeviceIndex <= High(AudioInputProcessor.Device))) then + (PreviewDeviceIndex <= High(AudioInputProcessor.DeviceList))) then begin - Device := AudioInputProcessor.Device[PreviewDeviceIndex]; + Device := AudioInputProcessor.DeviceList[PreviewDeviceIndex]; Device.Stop; for ChannelIndex := 0 to High(Device.CaptureChannel) do Device.CaptureChannel[ChannelIndex] := nil; @@ -380,137 +468,281 @@ begin PreviewDeviceIndex := -1; end; + +procedure TScreenOptionsRecord.DrawVolume(x, y, Width, Height: single); +var + x1, y1, x2, y2: single; + VolBarInnerWidth: integer; +const + VolBarInnerHSpacing = 2; + VolBarInnerVSpacing = 1; +begin + // coordinates for black rect + x1 := x; + y1 := y; + x2 := x1 + Width; + y2 := y1 + Height; + + // draw black background-rect + glColor4f(0, 0, 0, 0.8); + glBegin(GL_QUADS); + glVertex2f(x1, y1); + glVertex2f(x2, y1); + glVertex2f(x2, y2); + glVertex2f(x1, y2); + glEnd(); + + VolBarInnerWidth := Trunc(Width - 2*VolBarInnerHSpacing); + + // coordinates for first half of the volume bar + x1 := x + VolBarInnerHSpacing; + x2 := x1 + VolBarInnerWidth * SourceVolume; + y1 := y1 + VolBarInnerVSpacing; + y2 := y2 - VolBarInnerVSpacing; + + // draw volume-bar + glBegin(GL_QUADS); + // draw volume bar + glColor3f(0.4, 0.3, 0.3); + glVertex2f(x1, y1); + glVertex2f(x1, y2); + glColor3f(1, 0.1, 0.1); + glVertex2f(x2, y2); + glVertex2f(x2, y1); + glEnd(); + + { not needed anymore + // coordinates for separator + x1 := x + VolBarInnerHSpacing; + x2 := x1 + VolBarInnerWidth; + + // draw separator + glBegin(GL_LINE_STRIP); + glColor4f(0.1, 0.1, 0.1, 0.2); + glVertex2f(x1, y2); + glColor4f(0.4, 0.4, 0.4, 0.2); + glVertex2f((x1+x2)/2, y2); + glColor4f(0.1, 0.1, 0.1, 0.2); + glVertex2f(x2, y2); + glEnd(); + } +end; + +procedure TScreenOptionsRecord.DrawVUMeter(const State: TDrawState; x, y, Width, Height: single); +var + x1, y1, x2, y2: single; + Volume, PeakVolume: single; + Delta: single; + VolBarInnerWidth: integer; +const + VolBarInnerHSpacing = 2; + VolBarInnerVSpacing = 1; +begin + // coordinates for black rect + x1 := x; + y1 := y; + x2 := x1 + Width; + y2 := y1 + Height; + + // draw black background-rect + glColor4f(0, 0, 0, 0.8); + glBegin(GL_QUADS); + glVertex2f(x1, y1); + glVertex2f(x2, y1); + glVertex2f(x2, y2); + glVertex2f(x1, y2); + glEnd(); + + VolBarInnerWidth := Trunc(Width - 2*VolBarInnerHSpacing); + + // vertical positions + y1 := y1 + VolBarInnerVSpacing; + y2 := y2 - VolBarInnerVSpacing; + + // coordinates for bevel + x1 := x + VolBarInnerHSpacing; + x2 := x1 + VolBarInnerWidth; + + glBegin(GL_QUADS); + Volume := PreviewChannel[State.ChannelIndex].MaxSampleVolume(); + + // coordinates for volume bar + x1 := x + VolBarInnerHSpacing; + x2 := x1 + VolBarInnerWidth * Volume; + + // draw volume bar + glColor3f(State.RD, State.GD, State.BD); + glVertex2f(x1, y1); + glVertex2f(x1, y2); + glColor3f(State.R, State.G, State.B); + glVertex2f(x2, y2); + glVertex2f(x2, y1); + + Delta := (SDL_GetTicks() - ChannelPeak[State.ChannelIndex].Time)/1000; + PeakVolume := ChannelPeak[State.ChannelIndex].Volume - Delta*Delta*PeakDecay; + + // determine new peak-volume + if (Volume > PeakVolume) then + begin + PeakVolume := Volume; + ChannelPeak[State.ChannelIndex].Volume := Volume; + ChannelPeak[State.ChannelIndex].Time := SDL_GetTicks(); + end; + + x1 := x + VolBarInnerHSpacing + VolBarInnerWidth * PeakVolume; + x2 := x1 + 2; + + // draw peak + glColor3f(0.8, 0.8, 0.8); + glVertex2f(x1, y1); + glVertex2f(x1, y2); + glVertex2f(x2, y2); + glVertex2f(x2, y1); + + // draw threshold + x1 := x + VolBarInnerHSpacing; + x2 := x1 + VolBarInnerWidth * IThresholdVals[Ini.ThresholdIndex]; + + glColor4f(0.3, 0.3, 0.3, 0.6); + glVertex2f(x1, y1); + glVertex2f(x1, y2); + glVertex2f(x2, y2); + glVertex2f(x2, y1); + glEnd(); +end; + +procedure TScreenOptionsRecord.DrawPitch(const State: TDrawState; x, y, Width, Height: single); +var + x1, y1, x2, y2: single; + i: integer; + ToneBoxWidth: real; +const + PitchBarInnerHSpacing = 2; + PitchBarInnerVSpacing = 1; +begin + // calc tone pitch + PreviewChannel[State.ChannelIndex].AnalyzeBuffer(); + + // coordinates for black rect + x1 := x; + y1 := y; + x2 := x + Width; + y2 := y + Height; + + // draw black background-rect + glColor4f(0, 0, 0, 0.8); + glBegin(GL_QUADS); + glVertex2f(x1, y1); + glVertex2f(x2, y1); + glVertex2f(x2, y2); + glVertex2f(x1, y2); + glEnd(); + + // coordinates for tone boxes + ToneBoxWidth := Width / NumHalftones; + y1 := y1 + PitchBarInnerVSpacing; + y2 := y2 - PitchBarInnerVSpacing; + + glBegin(GL_QUADS); + // draw tone boxes + for i := 0 to NumHalftones-1 do + begin + x1 := x + i * ToneBoxWidth + PitchBarInnerHSpacing; + x2 := x1 + ToneBoxWidth - 2*PitchBarInnerHSpacing; + + if ((PreviewChannel[State.ChannelIndex].ToneValid) and + (PreviewChannel[State.ChannelIndex].ToneAbs = i)) then + begin + // highlight current tone-pitch + glColor3f(1, i / (NumHalftones-1), 0) + end + else + begin + // grey other tone-pitches + glColor3f(0.3, i / (NumHalftones-1) * 0.3, 0); + end; + + glVertex2f(x1, y1); + glVertex2f(x2, y1); + glVertex2f(x2, y2); + glVertex2f(x1, y2); + end; + glEnd(); + + // update tone-pitch label + Text[TextPitchID[State.ChannelIndex]].Text := + PreviewChannel[State.ChannelIndex].ToneString; +end; + function TScreenOptionsRecord.Draw: boolean; var i: integer; - x1, x2, y1, y2: real; - R, G, B, RD, GD, BD: real; - ChannelIndex: integer; Device: TAudioInputDevice; DeviceCfg: PInputDeviceConfig; SelectSlide: TSelectSlide; - ToneBoxWidth: real; - Volume: single; + BarXOffset, BarYOffset, BarWidth: real; + ChannelIndex: integer; + State: TDrawState; begin DrawBG; DrawFG; if ((PreviewDeviceIndex >= 0) and - (PreviewDeviceIndex <= High(AudioInputProcessor.Device))) then + (PreviewDeviceIndex <= High(AudioInputProcessor.DeviceList))) then begin - Device := AudioInputProcessor.Device[PreviewDeviceIndex]; + Device := AudioInputProcessor.DeviceList[PreviewDeviceIndex]; DeviceCfg := @Ini.InputDeviceConfig[Device.CfgIndex]; - glBegin(GL_QUADS); - for ChannelIndex := 0 to High(Device.CaptureChannel) do + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // update source volume + if (SDL_GetTicks() >= NextVolumePollTime) then + begin + NextVolumePollTime := SDL_GetTicks() + 500; // next poll in 500ms + SourceVolume := Device.GetVolume()/100; + end; + + // get source select slide + SelectSlide := SelectsS[SelectInputSourceID]; + BarXOffset := SelectSlide.TextureSBG.X; + BarYOffset := SelectSlide.TextureSBG.Y + SelectSlide.TextureSBG.H + BarUpperSpacing; + BarWidth := SelectSlide.TextureSBG.W; + DrawVolume(SelectSlide.TextureSBG.X, BarYOffset, BarWidth, BarHeight); + + for ChannelIndex := 0 to High(Device.CaptureChannel) do + begin + // load player color mapped to current input channel + if (DeviceCfg.ChannelToPlayerMap[ChannelIndex] > 0) then begin - // load player color mapped to current input channel - if (DeviceCfg.ChannelToPlayerMap[ChannelIndex] > 0) then - begin - // set mapped channel to corresponding player-color - LoadColor(R, G, B, 'P'+ IntToStr(DeviceCfg.ChannelToPlayerMap[ChannelIndex]) + 'Dark'); - end - else - begin - // set non-mapped channel to white - R := 1; G := 1; B := 1; - end; + // set mapped channel to corresponding player-color + LoadColor(State.R, State.G, State.B, 'P'+ IntToStr(DeviceCfg.ChannelToPlayerMap[ChannelIndex]) + 'Dark'); + end + else + begin + // set non-mapped channel to white + State.R := 1; State.G := 1; State.B := 1; + end; - // dark player colors - RD := 0.2 * R; - GD := 0.2 * G; - BD := 0.2 * B; - - // channel select slide - SelectSlide := SelectsS[SelectSlideChannelID[ChannelIndex]]; - - ////////// - // draw Volume - // - - // coordinates for black rect - x1 := SelectSlide.TextureSBG.X; - x2 := x1 + SelectSlide.TextureSBG.W; - y2 := SelectSlide.TextureSBG.Y + SelectSlide.TextureSBG.H; - y1 := y2 - 11; - - // draw black background-rect - glColor3f(0, 0, 0); - glVertex2f(x1, y1); - glVertex2f(x2, y1); - glVertex2f(x2, y2); - glVertex2f(x1, y2); - - Volume := PreviewChannel[ChannelIndex].MaxSampleVolume(); - - // coordinates for volume bar - x1 := x1 + 1; - x2 := x1 + Trunc((SelectSlide.TextureSBG.W-4) * Volume) + 1; - y1 := y1 + 1; - y2 := y2 - 1; - - // draw volume bar - glColor3f(RD, GD, BD); - glVertex2f(x1, y1); - glVertex2f(x1, y2); - glColor3f(R, G, B); - glVertex2f(x2, y2); - glVertex2f(x2, y1); - - ////////// - // draw Pitch - // - - // calc tone pitch - PreviewChannel[ChannelIndex].AnalyzeBuffer(); - - // coordinates for black rect - x1 := SelectSlide.TextureSBG.X; - x2 := x1 + SelectSlide.TextureSBG.W; - y1 := SelectSlide.TextureSBG.Y + SelectSlide.TextureSBG.H; - y2 := y1 + 11; - - // draw black background-rect - glColor3f(0, 0, 0); - glVertex2f(x1, y1); - glVertex2f(x2, y1); - glVertex2f(x2, y2); - glVertex2f(x1, y2); - - // coordinates for tone boxes - ToneBoxWidth := SelectSlide.TextureSBG.W / NumHalftones; - y1 := y1 + 1; - y2 := y2 - 1; - - // draw tone boxes - for i := 0 to NumHalftones-1 do - begin - x1 := SelectSlide.TextureSBG.X + i * ToneBoxWidth + 2; - x2 := x1 + ToneBoxWidth - 4; + // dark player colors + State.RD := 0.2 * State.R; + State.GD := 0.2 * State.G; + State.BD := 0.2 * State.B; - if ((PreviewChannel[ChannelIndex].ToneValid) and - (PreviewChannel[ChannelIndex].ToneAbs = i)) then - begin - // highlight current tone-pitch - glColor3f(1, i / (NumHalftones-1), 0) - end - else - begin - // grey other tone-pitches - glColor3f(0.3, i / (NumHalftones-1) * 0.3, 0); - end; + // channel select slide + SelectSlide := SelectsS[SelectSlideChannelID[ChannelIndex]]; - glVertex2f(x1, y1); - glVertex2f(x2, y1); - glVertex2f(x2, y2); - glVertex2f(x1, y2); - end; + BarXOffset := SelectSlide.TextureSBG.X; + BarYOffset := SelectSlide.TextureSBG.Y + SelectSlide.TextureSBG.H + BarUpperSpacing; + BarWidth := SelectSlide.TextureSBG.W; - // update tone-pitch label - Text[TextPitchID[ChannelIndex]].Text := - PreviewChannel[ChannelIndex].ToneString; - end; - glEnd; + State.ChannelIndex := ChannelIndex; + + DrawVUMeter(State, BarXOffset, BarYOffset, BarWidth, BarHeight); + DrawPitch(State, BarXOffset, BarYOffset+BarHeight, BarWidth, BarHeight); + end; + + glDisable(GL_BLEND); end; Result := True; diff --git a/Game/Code/Screens/UScreenOptionsSound.pas b/Game/Code/Screens/UScreenOptionsSound.pas index c3ef523b..2d807d02 100644 --- a/Game/Code/Screens/UScreenOptionsSound.pas +++ b/Game/Code/Screens/UScreenOptionsSound.pas @@ -87,7 +87,7 @@ begin AddSelect(Theme.OptionsSound.SelectMicBoost, Ini.MicBoost, IMicBoost); // TODO - This need moving to ScreenOptionsRecord AddSelect(Theme.OptionsSound.SelectClickAssist, Ini.ClickAssist, IClickAssist); AddSelect(Theme.OptionsSound.SelectBeatClick, Ini.BeatClick, IBeatClick); - AddSelect(Theme.OptionsSound.SelectThreshold, Ini.Threshold, IThreshold); + AddSelect(Theme.OptionsSound.SelectThreshold, Ini.ThresholdIndex, IThreshold); //Song Preview AddSelectSlide(Theme.OptionsSound.SelectSlidePreviewVolume, Ini.PreviewVolume, IPreviewVolume); diff --git a/Game/Code/Screens/UScreenSong.pas b/Game/Code/Screens/UScreenSong.pas index e52b4c98..82d5100a 100644 --- a/Game/Code/Screens/UScreenSong.pas +++ b/Game/Code/Screens/UScreenSong.pas @@ -34,6 +34,8 @@ type EqualizerData: TFFTData; // moved here to avoid stack overflows EqualizerBands: array of Byte; EqualizerTime: Cardinal; + + procedure StartMusicPreview(Song: TSong); public TextArtist: integer; TextTitle: integer; @@ -51,6 +53,8 @@ type HighSpeed: boolean; CoverFull: boolean; CoverTime: real; + MusicStartTime: cardinal; + CoverX: integer; CoverY: integer; CoverW: integer; @@ -1454,19 +1458,8 @@ begin if Length(CatSongs.Song) > 0 then begin //Load Music only when Song Preview is activated if ( Ini.PreviewVolume <> 0 ) then - begin - if(AudioPlayback.Open(CatSongs.Song[Interaction].Path + CatSongs.Song[Interaction].Mp3)) then - begin - AudioPlayback.SetLoop(false); - AudioPlayback.Position := AudioPlayback.Length / 4; - AudioPlayback.Play; - - //Set Preview Volume - AudioPlayback.SetMusicVolume (Ini.PreviewVolume * 10); - {//if Music Fade is activated, Set Volume to 0 % - if (Ini.PreviewFading <> 0) then - Music.SetMusicVolume(0);} - end; + begin // to - do : new Song management + StartMusicPreview(CatSongs.Song[Interaction]); end; SetScroll; @@ -1506,7 +1499,7 @@ procedure TScreenSong.onHide; begin //When Music Fading is activated, Turn Music to 100 % If (Ini.PreviewVolume <> 100) or (Ini.PreviewFading <> 0) then - AudioPlayback.SetMusicVolume(100); + AudioPlayback.SetVolume(100); //If Preview is deactivated: Load MUsicfile now If (Ini.PreviewVolume = 0) then @@ -1557,7 +1550,7 @@ begin //Fading Functions, Only if Covertime is under 5 Seconds If (CoverTime < 5) then begin - // 0.5.0: cover fade + // cover fade if (CoverTime < 1) and (CoverTime + TimeSkip >= 1) then begin // load new texture @@ -1568,18 +1561,17 @@ begin end; //Song Fade - if (CatSongs.VisibleSongs > 0) AND (Ini.PreviewVolume <> 0) AND (Not CatSongs.Song[Interaction].Main) AND (Ini.PreviewFading <> 0) then + if (CatSongs.VisibleSongs > 0) and + (not CatSongs.Song[Interaction].Main) and + (Ini.PreviewVolume <> 0) and + (Ini.PreviewFading <> 0) then begin //Start Song Fade after a little Time, to prevent Song to be Played on Scrolling - if (CoverTime < 0.2) and (CoverTime + TimeSkip >= 0.2) then - AudioPlayback.Play; - - //Update Song Volume - if (CoverTime < Ini.PreviewFading) then - AudioPlayback.SetMusicVolume(Round (CoverTime * Ini.PreviewVolume / Ini.PreviewFading * 10)) - else - AudioPlayback.SetMusicVolume(Ini.PreviewVolume * 10); - + if ((MusicStartTime > 0) and (SDL_GetTicks() >= MusicStartTime)) then + begin + MusicStartTime := 0; + StartMusicPreview(CatSongs.Song[Interaction]); + end; end; @@ -1588,7 +1580,8 @@ begin //Update Fading Texture Button[Interaction].Texture2.Alpha := (CoverTime - 1) * 1.5; - if Button[Interaction].Texture2.Alpha > 1 then Button[Interaction].Texture2.Alpha := 1; + if Button[Interaction].Texture2.Alpha > 1 then + Button[Interaction].Texture2.Alpha := 1; end; @@ -1694,30 +1687,52 @@ begin end; *) +procedure TScreenSong.StartMusicPreview(Song: TSong); +begin + AudioPlayback.Close(); + + if not assigned(Song) then + Exit; + + if AudioPlayback.Open(Song.Path + Song.Mp3) then + begin + AudioPlayback.Position := AudioPlayback.Length / 4; + // set preview volume + if (Ini.PreviewFading = 0) then + begin + // music fade disabled: start with full volume + AudioPlayback.SetVolume(Ini.PreviewVolume * 10); + AudioPlayback.Play() + end + else + begin + // music fade enabled: start muted and fade-in + AudioPlayback.SetVolume(0); + AudioPlayback.FadeIn(Ini.PreviewFading, Ini.PreviewVolume * 10); + end; + end; +end; + //Procedure Change current played Preview procedure TScreenSong.ChangeMusic; begin //When Music Preview is avtivated -> then Change Music if (Ini.PreviewVolume <> 0) then begin - if (NOT CatSongs.Song[Interaction].Main) AND(CatSongs.VisibleSongs > 0) then + // Stop previous song + AudioPlayback.Stop; + // Disable music start delay + MusicStartTime := 0; + + if (CatSongs.VisibleSongs > 0) then begin - AudioPlayback.Close; - if AudioPlayback.Open(CatSongs.Song[Interaction].Path + CatSongs.Song[Interaction].Mp3) then begin - AudioPlayback.Position := AudioPlayback.Length / 4; - //If Song Fading is activated then don't Play directly, and Set Volume to Null, else Play normal - if (Ini.PreviewFading = 0) then - AudioPlayback.Play - else - AudioPlayback.SetMusicVolume(0); - end; - end - else - AudioPlayback.Stop; + // delay start of music for 200ms (see Draw()) + MusicStartTime := SDL_GetTicks() + 200; + end; end; end; -procedure TScreenSong.SkipTo(Target: Cardinal); // 0.5.0 +procedure TScreenSong.SkipTo(Target: Cardinal); var // Skip: integer; // Auto Removed, Unused Variable I: integer; diff --git a/Game/Code/UltraStar.dpr b/Game/Code/UltraStar.dpr index b71ae5a1..e4697419 100644 --- a/Game/Code/UltraStar.dpr +++ b/Game/Code/UltraStar.dpr @@ -170,7 +170,7 @@ uses // TODO : these all need to be renamed like UMedia_******** for consistency UMusic in 'Classes\UMusic.pas', - //UAudioPlaybackBase in 'Classes\UAudioPlaybackBase.pas', + UAudioPlaybackBase in 'Classes\UAudioPlaybackBase.pas', UMedia_dummy in 'Classes\UMedia_dummy.pas', // Must be first UMedia Unit, all others will override available interfaces {$IFDEF UseProjectM} UVisualizer in 'Classes\UVisualizer.pas', // MUST be before Video... so video can override... -- cgit v1.2.3