From b5a738fa52c8b0f2212deb5febd2d7f0b8f6544f Mon Sep 17 00:00:00 2001 From: tobigun Date: Fri, 9 May 2008 19:19:28 +0000 Subject: - input-source selection works now (with bass or portaudio with portmixer) - audio-effects (DSP) interface for audio-playback plus a simple voice removal example (does not sound that good) - FFMpeg support for BASS - audio-clock for FFMpeg for GetPosition and synchronisation - more compatible seeking in FFMpeg - clean termination of the audio interfaces/streams (especially ffmpeg) - Audio output device enumeration (selection will be added later to the sounds option screen) - display of threshold and volume in the record-options screen - threshold and volume can be changed with the 'T' (threshold) and '+'/'-' (source volume) keys - added a FadeIn() method to the IAudioPlayback interface - some minor changes to the audio classes/screens - new base-class for audio-playback classes (used by bass, portaudio and sdl) git-svn-id: svn://svn.code.sf.net/p/ultrastardx/svn/trunk@1078 b956fd51-792f-4845-bead-9b4dfca2ff2c --- Game/Code/Classes/UAudioInput_Bass.pas | 329 +++++++++++++++++++++++++-------- 1 file changed, 256 insertions(+), 73 deletions(-) (limited to 'Game/Code/Classes/UAudioInput_Bass.pas') 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; -- cgit v1.2.3