diff options
author | tobigun <tobigun@b956fd51-792f-4845-bead-9b4dfca2ff2c> | 2008-05-09 19:19:28 +0000 |
---|---|---|
committer | tobigun <tobigun@b956fd51-792f-4845-bead-9b4dfca2ff2c> | 2008-05-09 19:19:28 +0000 |
commit | b5a738fa52c8b0f2212deb5febd2d7f0b8f6544f (patch) | |
tree | 3c2812cffdd035b385d5b0f0f8f5ea0702973739 | |
parent | 37744cee627605db0675efd3a6e0c42bd51c48d6 (diff) | |
download | usdx-b5a738fa52c8b0f2212deb5febd2d7f0b8f6544f.tar.gz usdx-b5a738fa52c8b0f2212deb5febd2d7f0b8f6544f.tar.xz usdx-b5a738fa52c8b0f2212deb5febd2d7f0b8f6544f.zip |
- 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
-rw-r--r-- | Game/Code/Classes/UAudioCore_Bass.pas | 27 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioCore_Portaudio.pas | 25 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioDecoder_FFMpeg.pas | 546 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioInput_Bass.pas | 329 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioInput_Portaudio.pas | 255 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioPlaybackBase.pas | 220 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioPlayback_Bass.pas | 549 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioPlayback_Portaudio.pas | 248 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioPlayback_SDL.pas | 46 | ||||
-rw-r--r-- | Game/Code/Classes/UAudioPlayback_SoftMixer.pas | 370 | ||||
-rw-r--r-- | Game/Code/Classes/UIni.pas | 166 | ||||
-rw-r--r-- | Game/Code/Classes/UMain.pas | 11 | ||||
-rw-r--r-- | Game/Code/Classes/UMedia_dummy.pas | 38 | ||||
-rw-r--r-- | Game/Code/Classes/UMusic.pas | 162 | ||||
-rw-r--r-- | Game/Code/Classes/URecord.pas | 167 | ||||
-rw-r--r-- | Game/Code/Screens/UScreenOptionsRecord.pas | 532 | ||||
-rw-r--r-- | Game/Code/Screens/UScreenOptionsSound.pas | 2 | ||||
-rw-r--r-- | Game/Code/Screens/UScreenSong.pas | 95 | ||||
-rw-r--r-- | Game/Code/UltraStar.dpr | 2 |
19 files changed, 2628 insertions, 1162 deletions
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;
@@ -98,7 +124,14 @@ 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; @@ -124,6 +125,18 @@ type 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 procedure Close(); virtual; abstract; @@ -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; @@ -175,6 +188,19 @@ type 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}'] function GetName: String; @@ -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...
|