unit URecord; interface {$IFDEF FPC} {$MODE Delphi} {$ENDIF} {$I switches.inc} uses Classes, Math, SysUtils, UCommon, UMusic, UIni; type TSound = class private BufferNew: TMemoryStream; // buffer for newest samples public BufferArray: array[0..4095] of smallint; // newest 4096 samples BufferLong: array of TMemoryStream; // full buffer Index: integer; // index in TAudioInputProcessor.Sound[] (TODO: Remove if not used) AnalysisBufferSize: integer; // number of samples to analyze // pitch detection ToneValid: boolean; // true if Tone contains a valid value (otherwise it contains noise) //Peak: integer; // position of peak on horizontal pivot (TODO: Remove if not used) //ToneAccuracy: real; // tone accuracy (TODO: Remove if not used) Tone: integer; // TODO: should be a non-unified full range tone (e.g. C2<>C3). Range: 0..NumHalftones-1 // Note: at the moment it is the same as ToneUnified ToneUnified: integer; // tone unified to one octave (e.g. C2=C3=C4). Range: 0-11 //Scale: real; // FFT scale (TODO: Remove if not used) // procedures procedure ProcessNewBuffer; procedure AnalyzeBuffer; // use to analyze sound from buffers to get new pitch procedure AnalyzeByAutocorrelation; // we call it to analyze sound by checking Autocorrelation function AnalyzeAutocorrelationFreq(Freq: real): real; // use this to check one frequency by Autocorrelation end; TAudioInputDeviceSource = record Name: string; end; // soundcard input-devices information 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? MicInput: integer; // unused. What is this good for? SampleRate: integer; // capture sample-rate (e.g. 44.1kHz -> 44100) CaptureChannel: array[0..1] of TSound; // sound(-buffers) used for left/right channel's capture data procedure Start(); virtual; abstract; procedure Stop(); virtual; abstract; destructor Destroy; override; end; TAudioInputProcessor = class Sound: array of TSound; Device: array of TAudioInputDevice; constructor Create; // handle microphone input procedure HandleMicrophoneData(Buffer: Pointer; Size: Cardinal; InputDevice: TAudioInputDevice); function Volume( aChannel : byte ): byte; end; TAudioInputBase = class( TInterfacedObject, IAudioInput ) private 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; procedure CaptureStart; procedure CaptureStop; end; SmallIntArray = array [0..maxInt shr 1-1] of smallInt; PSmallIntArray = ^SmallIntArray; function AudioInputProcessor(): TAudioInputProcessor; implementation uses ULog, UMain; const CaptureFreq = 44100; BaseToneFreq = 65.4064; // lowest (half-)tone to analyze (C2 = 65.4064 Hz) NumHalftones = 36; // C2-B4 (for Whitney and my high voice) var singleton_AudioInputProcessor : TAudioInputProcessor = nil; // FIXME: Race-Conditions between Callback-thread and main-thread // on BufferArray (maybe BufferNew also). // Use SDL-mutexes to solve this problem. { Global } function AudioInputProcessor(): TAudioInputProcessor; begin if singleton_AudioInputProcessor = nil then singleton_AudioInputProcessor := TAudioInputProcessor.create(); result := singleton_AudioInputProcessor; end; { TAudioInputDevice } destructor TAudioInputDevice.Destroy; var i: integer; begin Stop(); Source := nil; for i := 0 to High(CaptureChannel) do CaptureChannel[i] := nil; inherited Destroy; end; { TSound } procedure TSound.ProcessNewBuffer; var SkipCount: integer; NumSamples: integer; SampleIndex: integer; begin // process BufferArray SkipCount := 0; NumSamples := BufferNew.Size div 2; // check if we have more new samples than we can store if NumSamples > Length(BufferArray) then begin // discard the oldest of the new samples SkipCount := NumSamples - Length(BufferArray); NumSamples := Length(BufferArray); end; // move old samples to the beginning of the array (if necessary) for SampleIndex := NumSamples to High(BufferArray) do BufferArray[SampleIndex-NumSamples] := BufferArray[SampleIndex]; // skip samples if necessary BufferNew.Seek(2*SkipCount, soBeginning); // copy samples BufferNew.ReadBuffer(BufferArray[Length(BufferArray)-NumSamples], 2*NumSamples); // save capture-data to BufferLong if neccessary if Ini.SavePlayback = 1 then begin BufferNew.Seek(0, soBeginning); BufferLong[0].CopyFrom(BufferNew, BufferNew.Size); end; end; procedure TSound.AnalyzeBuffer; begin AnalyzeByAutocorrelation; end; procedure TSound.AnalyzeByAutocorrelation; var ToneIndex: integer; Freq: real; Wages: array[0..NumHalftones-1] of real; MaxTone: integer; MaxWage: real; Volume: real; MaxVolume: real; SampleIndex: integer; Threshold: real; const HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave) begin ToneValid := false; // find maximum volume of first 1024 samples MaxVolume := 0; for SampleIndex := 0 to 1023 do begin Volume := Abs(BufferArray[SampleIndex]) / -Low(Smallint); // was $10000 (65536) before but must be 32768 if Volume > MaxVolume then MaxVolume := Volume; end; // prepare to analyze MaxWage := 0; // analyze halftones for ToneIndex := 0 to NumHalftones-1 do begin Freq := BaseToneFreq * Power(HalftoneBase, ToneIndex); Wages[ToneIndex] := AnalyzeAutocorrelationFreq(Freq); if Wages[ToneIndex] > MaxWage then begin // this frequency has better wage MaxWage := Wages[ToneIndex]; MaxTone := ToneIndex; end; end; Threshold := 0.2; case Ini.Threshold of 0: Threshold := 0.1; 1: Threshold := 0.2; 2: Threshold := 0.3; 3: Threshold := 0.4; end; // check if signal has an acceptable volume (ignore background-noise) if MaxVolume >= Threshold then begin ToneValid := true; ToneUnified := MaxTone mod 12; Tone := MaxTone mod 12; end; end; function TSound.AnalyzeAutocorrelationFreq(Freq: real): real; // result medium difference var Dist: real; // distance (0=equal .. 1=totally different) between correlated samples AccumDist: real; // accumulated distances SampleIndex: integer; // index of sample to analyze CorrelatingSampleIndex: integer; // index of sample one period ahead SamplesPerPeriod: integer; // samples in one period begin SampleIndex := 0; SamplesPerPeriod := Round(CaptureFreq/Freq); CorrelatingSampleIndex := SampleIndex + SamplesPerPeriod; AccumDist := 0; // compare correlating samples while (CorrelatingSampleIndex < AnalysisBufferSize) do begin // calc distance (correlation: 1-dist) to corresponding sample in next period Dist := Abs(BufferArray[SampleIndex] - BufferArray[CorrelatingSampleIndex]) / High(Word); // was $10000 (65536) before but must be 65535 AccumDist := AccumDist + Dist; Inc(SampleIndex); Inc(CorrelatingSampleIndex); end; // return "inverse" average distance (=correlation) Result := 1 - AccumDist / AnalysisBufferSize; end; { TAudioInputProcessor } {* * Handle captured microphone input data. * Params: * Buffer - buffer of signed 16bit interleaved stereo PCM-samples. * Interleaved means that a right-channel sample follows a left- * channel sample and vice versa (0:left[0],1:right[0],2:left[1],...). * Length - number of bytes in Buffer * Input - Soundcard-Input used for capture *} procedure TAudioInputProcessor.HandleMicrophoneData(Buffer: Pointer; Size: Cardinal; InputDevice: TAudioInputDevice); var NumSamples: integer; // number of samples SampleIndex: integer; Value: integer; ByteBuffer: PByteArray; // buffer handled as array of bytes SampleBuffer: PSmallIntArray; // buffer handled as array of samples Offset: integer; Boost: byte; ChannelCount: integer; ChannelIndex: integer; CaptureChannel: TSound; SampleSize: integer; begin // set boost case Ini.MicBoost of 0: Boost := 1; 1: Boost := 2; 2: Boost := 4; 3: Boost := 8; end; // boost buffer NumSamples := Size div 2; SampleBuffer := Buffer; for SampleIndex := 0 to NumSamples-1 do begin Value := SampleBuffer^[SampleIndex] * Boost; // TODO : JB - This will clip the audio... cant we reduce the "Boost" if the data clips ?? if Value > High(Smallint) then Value := High(Smallint); if Value < Low(Smallint) then Value := Low(Smallint); SampleBuffer^[SampleIndex] := Value; end; // number of channels ChannelCount := Length(InputDevice.CaptureChannel); // size of one sample SampleSize := ChannelCount * SizeOf(SmallInt); // samples per channel NumSamples := Size div SampleSize; // interpret buffer as buffer of bytes ByteBuffer := Buffer; // process channels for ChannelIndex := 0 to High(InputDevice.CaptureChannel) do begin CaptureChannel := InputDevice.CaptureChannel[ChannelIndex]; if (CaptureChannel <> nil) then begin Offset := ChannelIndex * SizeOf(SmallInt); // TODO: remove BufferNew and write to BufferArray directly CaptureChannel.BufferNew.Clear; for SampleIndex := 0 to NumSamples-1 do begin CaptureChannel.BufferNew.Write(ByteBuffer^[Offset + SampleIndex*SampleSize], SizeOf(SmallInt)); end; CaptureChannel.ProcessNewBuffer(); end; end; end; constructor TAudioInputProcessor.Create; var i: integer; begin SetLength(Sound, 6 {max players});//Ini.Players+1); for i := 0 to High(Sound) do begin Sound[i] := TSound.Create; Sound[i].Index := i; Sound[i].BufferNew := TMemoryStream.Create; SetLength(Sound[i].BufferLong, 1); Sound[i].BufferLong[0] := TMemoryStream.Create; Sound[i].AnalysisBufferSize := Min(4*1024, Length(Sound[i].BufferArray)); end; end; function TAudioInputProcessor.Volume( aChannel : byte ): byte; var lSampleIndex: Integer; lMaxVol : Word; begin; with AudioInputProcessor.Sound[aChannel] do begin lMaxVol := BufferArray[0]; for lSampleIndex := 1 to High(BufferArray) do begin if Abs(BufferArray[lSampleIndex]) > lMaxVol then lMaxVol := Abs(BufferArray[lSampleIndex]); end; end; result := trunc( ( 255 / -Low(Smallint) ) * lMaxVol ); end; { TAudioInputBase } {* * Start capturing on all used input-device. *} procedure TAudioInputBase.CaptureStart; var S: integer; DeviceIndex: integer; ChannelIndex: integer; Device: TAudioInputDevice; DeviceCfg: PInputDeviceConfig; DeviceUsed: boolean; Player: integer; begin if (Started) then CaptureStop(); Log.BenchmarkStart(1); // reset buffers for S := 0 to High(AudioInputProcessor.Sound) do AudioInputProcessor.Sound[S].BufferLong[0].Clear; // start capturing on each used device for DeviceIndex := 0 to High(AudioInputProcessor.Device) do begin Device := AudioInputProcessor.Device[DeviceIndex]; if not assigned(Device) then continue; DeviceCfg := @Ini.InputDeviceConfig[Device.CfgIndex]; DeviceUsed := false; // check if device is used for ChannelIndex := 0 to High(DeviceCfg.ChannelToPlayerMap) do begin Player := DeviceCfg.ChannelToPlayerMap[ChannelIndex]-1; if (Player < 0) or (Player >= PlayersPlay) then begin Device.CaptureChannel[ChannelIndex] := nil; end else begin Device.CaptureChannel[ChannelIndex] := AudioInputProcessor.Sound[Player]; DeviceUsed := true; end; end; // start device if used if (DeviceUsed) then begin Log.BenchmarkStart(2); Device.Start(); Log.BenchmarkEnd(2); Log.LogBenchmark('Device.Start', 2) ; end; end; Log.BenchmarkEnd(1); Log.LogBenchmark('CaptureStart', 1) ; Started := true; end; {* * Stop input-capturing on all soundcards. *} procedure TAudioInputBase.CaptureStop; var DeviceIndex: integer; Player: integer; Device: TAudioInputDevice; DeviceCfg: PInputDeviceConfig; begin for DeviceIndex := 0 to High(AudioInputProcessor.Device) do begin Device := AudioInputProcessor.Device[DeviceIndex]; if not assigned(Device) then continue; Device.Stop(); end; Started := false; end; function TAudioInputBase.UnifyDeviceName(const name: string; deviceIndex: integer): string; var count: integer; // count of devices with this name function IsDuplicate(const name: string): boolean; var i: integer; begin Result := False; // search devices with same description For i := 0 to deviceIndex-1 do begin if (AudioInputProcessor.Device[i].Description = name) then begin Result := True; Break; end; end; end; begin count := 1; result := name; // if there is another device with the same ID, search for an available name while (IsDuplicate(result)) do begin Inc(count); // set description result := name + ' ('+IntToStr(count)+')'; 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; 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.