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.