aboutsummaryrefslogtreecommitdiffstats
path: root/src/classes/URecord.pas
diff options
context:
space:
mode:
Diffstat (limited to 'src/classes/URecord.pas')
-rw-r--r--src/classes/URecord.pas766
1 files changed, 0 insertions, 766 deletions
diff --git a/src/classes/URecord.pas b/src/classes/URecord.pas
deleted file mode 100644
index 8a537dc9..00000000
--- a/src/classes/URecord.pas
+++ /dev/null
@@ -1,766 +0,0 @@
-unit URecord;
-
-interface
-
-{$IFDEF FPC}
- {$MODE Delphi}
-{$ENDIF}
-
-{$I switches.inc}
-
-uses Classes,
- Math,
- SysUtils,
- sdl,
- UCommon,
- UMusic,
- UIni;
-
-const
- BaseToneFreq = 65.4064; // lowest (half-)tone to analyze (C2 = 65.4064 Hz)
- NumHalftones = 36; // C2-B4 (for Whitney and my high voice)
-
-type
- TCaptureBuffer = class
- private
- VoiceStream: TAudioVoiceStream; // stream for voice passthrough
- AnalysisBufferLock: PSDL_Mutex;
-
- function GetToneString: string; // converts a tone to its string represenatation;
-
- procedure BoostBuffer(Buffer: PChar; Size: Cardinal);
- procedure ProcessNewBuffer(Buffer: PChar; BufferSize: integer);
-
- // we call it to analyze sound by checking Autocorrelation
- procedure AnalyzeByAutocorrelation;
- // use this to check one frequency by Autocorrelation
- function AnalyzeAutocorrelationFreq(Freq: real): real;
- public
- AnalysisBuffer: array[0..4095] of smallint; // newest 4096 samples
- AnalysisBufferSize: integer; // number of samples of BufferArray to analyze
-
- LogBuffer: TMemoryStream; // full buffer
-
- AudioFormat: TAudioFormatInfo;
-
- // pitch detection
- // TODO: remove ToneValid, set Tone/ToneAbs=-1 if invalid instead
- ToneValid: boolean; // true if Tone contains a valid value (otherwise it contains noise)
- Tone: integer; // tone relative to one octave (e.g. C2=C3=C4). Range: 0-11
- ToneAbs: integer; // absolute (full range) tone (e.g. C2<>C3). Range: 0..NumHalftones-1
-
- // methods
- constructor Create;
- destructor Destroy; override;
-
- procedure Clear;
-
- // use to analyze sound from buffers to get new pitch
- procedure AnalyzeBuffer;
- procedure LockAnalysisBuffer(); {$IFDEF HasInline}inline;{$ENDIF}
- procedure UnlockAnalysisBuffer(); {$IFDEF HasInline}inline;{$ENDIF}
-
- function MaxSampleVolume: Single;
- property ToneString: string READ GetToneString;
- end;
-
-const
- DEFAULT_SOURCE_NAME = '[Default]';
-
-type
- TAudioInputSource = record
- Name: string;
- end;
-
- // soundcard input-devices information
- TAudioInputDevice = class
- public
- CfgIndex: integer; // index of this device in Ini.InputDeviceConfig
- 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
-
- 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;
- function Stop(): boolean; virtual; abstract;
-
- function GetVolume(): single; virtual; abstract;
- procedure SetVolume(Volume: single); virtual; abstract;
- end;
-
- TAudioInputProcessor = class
- public
- Sound: array of TCaptureBuffer; // sound-buffers for every player
- DeviceList: array of TAudioInputDevice;
-
- constructor Create;
- destructor Destroy; override;
-
- procedure UpdateInputDeviceConfig;
-
- // handle microphone input
- procedure HandleMicrophoneData(Buffer: PChar; Size: Cardinal;
- InputDevice: TAudioInputDevice);
- end;
-
- TAudioInputBase = class( TInterfacedObject, IAudioInput )
- private
- Started: boolean;
- protected
- function UnifyDeviceName(const name: string; deviceIndex: integer): string;
- public
- function GetName: String; virtual; abstract;
- function InitializeRecord: boolean; virtual; abstract;
- function FinalizeRecord: boolean; virtual;
-
- procedure CaptureStart;
- procedure CaptureStop;
- end;
-
-
- TSmallIntArray = array [0..(MaxInt div SizeOf(SmallInt))-1] of SmallInt;
- PSmallIntArray = ^TSmallIntArray;
-
- function AudioInputProcessor(): TAudioInputProcessor;
-
-implementation
-
-uses
- ULog,
- UMain;
-
-var
- singleton_AudioInputProcessor : TAudioInputProcessor = nil;
-
-
-{ Global }
-
-function AudioInputProcessor(): TAudioInputProcessor;
-begin
- if singleton_AudioInputProcessor = nil then
- singleton_AudioInputProcessor := TAudioInputProcessor.create();
-
- result := singleton_AudioInputProcessor;
-end;
-
-
-{ TAudioInputDevice }
-
-destructor TAudioInputDevice.Destroy;
-begin
- Stop();
- Source := nil;
- CaptureChannel := nil;
- FreeAndNil(AudioFormat);
- inherited Destroy;
-end;
-
-procedure TAudioInputDevice.LinkCaptureBuffer(ChannelIndex: integer; Sound: TCaptureBuffer);
-var
- DeviceCfg: PInputDeviceConfig;
- OldSound: TCaptureBuffer;
-begin
- // check bounds
- if ((ChannelIndex < 0) or (ChannelIndex > High(CaptureChannel))) then
- Exit;
-
- // reset previously assigned (old) capture-buffer
- OldSound := CaptureChannel[ChannelIndex];
- if (OldSound <> nil) then
- begin
- // close voice stream
- FreeAndNil(OldSound.VoiceStream);
- // free old audio-format info
- FreeAndNil(OldSound.AudioFormat);
- end;
-
- // set audio-format of new capture-buffer
- if (Sound <> nil) then
- begin
- // copy the input-device audio-format ...
- Sound.AudioFormat := AudioFormat.Copy;
- // and adjust it because capture buffers are always mono
- Sound.AudioFormat.Channels := 1;
- DeviceCfg := @Ini.InputDeviceConfig[CfgIndex];
-
- if (Ini.VoicePassthrough = 1) then
- begin
- // TODO: map odd players to the left and even players to the right speaker
- Sound.VoiceStream := AudioPlayback.CreateVoiceStream(CHANNELMAP_FRONT, AudioFormat);
- end;
- end;
-
- // replace old with new buffer (Note: Sound might be nil)
- CaptureChannel[ChannelIndex] := Sound;
-end;
-
-{ TSound }
-
-constructor TCaptureBuffer.Create;
-begin
- inherited;
- LogBuffer := TMemoryStream.Create;
- AnalysisBufferLock := SDL_CreateMutex();
- AnalysisBufferSize := Length(AnalysisBuffer);
-end;
-
-destructor TCaptureBuffer.Destroy;
-begin
- FreeAndNil(LogBuffer);
- FreeAndNil(VoiceStream);
- FreeAndNil(AudioFormat);
- SDL_DestroyMutex(AnalysisBufferLock);
- inherited;
-end;
-
-procedure TCaptureBuffer.LockAnalysisBuffer();
-begin
- SDL_mutexP(AnalysisBufferLock);
-end;
-
-procedure TCaptureBuffer.UnlockAnalysisBuffer();
-begin
- SDL_mutexV(AnalysisBufferLock);
-end;
-
-procedure TCaptureBuffer.Clear;
-begin
- if assigned(LogBuffer) then
- LogBuffer.Clear;
- LockAnalysisBuffer();
- FillChar(AnalysisBuffer[0], Length(AnalysisBuffer) * SizeOf(SmallInt), 0);
- UnlockAnalysisBuffer();
-end;
-
-procedure TCaptureBuffer.ProcessNewBuffer(Buffer: PChar; BufferSize: integer);
-var
- BufferOffset: integer;
- SampleCount: integer;
- i: integer;
-begin
- // apply software boost
- //BoostBuffer(Buffer, Size);
-
- // voice passthrough (send data to playback-device)
- if (assigned(VoiceStream)) then
- VoiceStream.WriteData(Buffer, BufferSize);
-
- // we assume that samples are in S16Int format
- // TODO: support float too
- if (AudioFormat.Format <> asfS16) then
- Exit;
-
- // process BufferArray
- BufferOffset := 0;
-
- SampleCount := BufferSize div SizeOf(SmallInt);
-
- // check if we have more new samples than we can store
- if (SampleCount > Length(AnalysisBuffer)) then
- begin
- // discard the oldest of the new samples
- BufferOffset := (SampleCount - Length(AnalysisBuffer)) * SizeOf(SmallInt);
- SampleCount := Length(AnalysisBuffer);
- end;
-
-
- LockAnalysisBuffer();
- try
-
- // move old samples to the beginning of the array (if necessary)
- for i := 0 to High(AnalysisBuffer)-SampleCount do
- AnalysisBuffer[i] := AnalysisBuffer[i+SampleCount];
-
- // copy new samples to analysis buffer
- Move(Buffer[BufferOffset], AnalysisBuffer[Length(AnalysisBuffer)-SampleCount],
- SampleCount * SizeOf(SmallInt));
-
- finally
- UnlockAnalysisBuffer();
- end;
-
-
- // save capture-data to BufferLong if enabled
- 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.
- LogBuffer.WriteBuffer(Buffer, BufferSize);
- end;
-end;
-
-procedure TCaptureBuffer.AnalyzeBuffer;
-var
- Volume: single;
- MaxVolume: single;
- SampleIndex: integer;
- Threshold: single;
-begin
- ToneValid := false;
- ToneAbs := -1;
- Tone := -1;
-
- LockAnalysisBuffer();
- try
-
- // find maximum volume of first 1024 samples
- MaxVolume := 0;
- for SampleIndex := 0 to 1023 do
- begin
- Volume := Abs(AnalysisBuffer[SampleIndex]) / -Low(Smallint);
- if Volume > MaxVolume then
- MaxVolume := Volume;
- end;
-
- Threshold := IThresholdVals[Ini.ThresholdIndex];
-
- // check if signal has an acceptable volume (ignore background-noise)
- if MaxVolume >= Threshold then
- begin
- // analyse the current voice pitch
- AnalyzeByAutocorrelation;
- ToneValid := true;
- end;
-
- finally
- UnlockAnalysisBuffer();
- end;
-end;
-
-procedure TCaptureBuffer.AnalyzeByAutocorrelation;
-var
- ToneIndex: integer;
- CurFreq: real;
- CurWeight: real;
- MaxWeight: real;
- MaxTone: integer;
-const
- HalftoneBase = 1.05946309436; // 2^(1/12) -> HalftoneBase^12 = 2 (one octave)
-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
- // at 44.1 (or 48kHz) only 6 (or 5) samples are compared, this might be
- // too few samples -> use a bigger buffer-size
- for ToneIndex := 0 to NumHalftones-1 do
- begin
- CurFreq := BaseToneFreq * Power(HalftoneBase, ToneIndex);
- CurWeight := AnalyzeAutocorrelationFreq(CurFreq);
-
- // TODO: prefer higher frequencies (use >= or use downto)
- if (CurWeight > MaxWeight) then
- begin
- // this frequency has a higher weight
- MaxWeight := CurWeight;
- MaxTone := ToneIndex;
- end;
- end;
-
- ToneAbs := MaxTone;
- Tone := MaxTone mod 12;
-end;
-
-// result medium difference
-function TCaptureBuffer.AnalyzeAutocorrelationFreq(Freq: real): real;
-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(AudioFormat.SampleRate/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(AnalysisBuffer[SampleIndex] - AnalysisBuffer[CorrelatingSampleIndex]) /
- High(Word);
- AccumDist := AccumDist + Dist;
- Inc(SampleIndex);
- Inc(CorrelatingSampleIndex);
- end;
-
- // return "inverse" average distance (=correlation)
- Result := 1 - AccumDist / AnalysisBufferSize;
-end;
-
-function TCaptureBuffer.MaxSampleVolume: Single;
-var
- lSampleIndex: Integer;
- lMaxVol : Longint;
-begin;
- LockAnalysisBuffer();
- try
- lMaxVol := 0;
- for lSampleIndex := 0 to High(AnalysisBuffer) do
- begin
- if Abs(AnalysisBuffer[lSampleIndex]) > lMaxVol then
- lMaxVol := Abs(AnalysisBuffer[lSampleIndex]);
- end;
- finally
- UnlockAnalysisBuffer();
- end;
-
- result := lMaxVol / -Low(Smallint);
-end;
-
-const
- ToneStrings: array[0..11] of string = (
- 'C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B'
- );
-
-function TCaptureBuffer.GetToneString: string;
-begin
- if (ToneValid) then
- Result := ToneStrings[Tone] + IntToStr(ToneAbs div 12 + 2)
- else
- Result := '-';
-end;
-
-procedure TCaptureBuffer.BoostBuffer(Buffer: PChar; Size: Cardinal);
-var
- i: integer;
- Value: Longint;
- SampleCount: integer;
- SampleBuffer: PSmallIntArray; // buffer handled as array of samples
- Boost: byte;
-begin
- // TODO: set boost per device
- {
- case Ini.MicBoost of
- 0: Boost := 1;
- 1: Boost := 2;
- 2: Boost := 4;
- 3: Boost := 8;
- else Boost := 1;
- end;
- }
- Boost := 1;
-
- // at the moment we will boost SInt16 data only
- if (AudioFormat.Format = asfS16) then
- begin
- // interpret buffer as buffer of bytes
- SampleBuffer := PSmallIntArray(Buffer);
- SampleCount := Size div AudioFormat.FrameSize;
-
- // boost buffer
- for i := 0 to SampleCount-1 do
- begin
- Value := SampleBuffer^[i] * 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^[i] := Value;
- end;
- end;
-end;
-
-
-{ TAudioInputProcessor }
-
-constructor TAudioInputProcessor.Create;
-var
- i: integer;
-begin
- inherited;
- SetLength(Sound, 6 {max players});//Ini.Players+1);
- for i := 0 to High(Sound) do
- Sound[i] := TCaptureBuffer.Create;
-end;
-
-destructor TAudioInputProcessor.Destroy;
-var
- i: integer;
-begin
- for i := 0 to High(Sound) do
- Sound[i].Free;
- SetLength(Sound, 0);
- inherited;
-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;
-
-{*
- * Handles 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: PChar; Size: Cardinal; InputDevice: TAudioInputDevice);
-var
- MultiChannelBuffer: PChar; // buffer handled as array of bytes (offset relative to channel)
- SingleChannelBuffer: PChar; // temporary buffer for new samples per channel
- SingleChannelBufferSize: integer;
- ChannelIndex: integer;
- CaptureChannel: TCaptureBuffer;
- AudioFormat: TAudioFormatInfo;
- SampleSize: integer;
- SampleCount: integer;
- SamplesPerChannel: integer;
- i: integer;
-begin
- AudioFormat := InputDevice.AudioFormat;
- SampleSize := AudioSampleSize[AudioFormat.Format];
- SampleCount := Size div SampleSize;
- SamplesPerChannel := Size div AudioFormat.FrameSize;
-
- SingleChannelBufferSize := SamplesPerChannel * SampleSize;
- GetMem(SingleChannelBuffer, SingleChannelBufferSize);
-
- // process channels
- for ChannelIndex := 0 to High(InputDevice.CaptureChannel) do
- begin
- CaptureChannel := InputDevice.CaptureChannel[ChannelIndex];
- // check if a capture buffer was assigned, otherwise there is nothing to do
- if (CaptureChannel <> nil) then
- begin
- // set offset according to channel index
- MultiChannelBuffer := @Buffer[ChannelIndex * SampleSize];
- // seperate channel-data from interleaved multi-channel (e.g. stereo) data
- for i := 0 to SamplesPerChannel-1 do
- begin
- Move(MultiChannelBuffer[i*AudioFormat.FrameSize],
- SingleChannelBuffer[i*SampleSize],
- SampleSize);
- end;
- CaptureChannel.ProcessNewBuffer(SingleChannelBuffer, SingleChannelBufferSize);
- end;
- end;
-
- FreeMem(SingleChannelBuffer);
-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;
- Result := true;
-end;
-
-{*
- * 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();
-
- // reset buffers
- for S := 0 to High(AudioInputProcessor.Sound) do
- AudioInputProcessor.Sound[S].Clear;
-
- // start capturing on each used device
- for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do
- begin
- Device := AudioInputProcessor.DeviceList[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.LinkCaptureBuffer(ChannelIndex, nil);
- end
- else
- begin
- Device.LinkCaptureBuffer(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;
-
- Started := true;
-end;
-
-{*
- * Stop input-capturing on all soundcards.
- *}
-procedure TAudioInputBase.CaptureStop;
-var
- DeviceIndex: integer;
- ChannelIndex: integer;
- Device: TAudioInputDevice;
- DeviceCfg: PInputDeviceConfig;
-begin
- for DeviceIndex := 0 to High(AudioInputProcessor.DeviceList) do
- begin
- Device := AudioInputProcessor.DeviceList[DeviceIndex];
- if not assigned(Device) then
- continue;
-
- Device.Stop();
-
- // disconnect capture buffers
- DeviceCfg := @Ini.InputDeviceConfig[Device.CfgIndex];
- for ChannelIndex := 0 to High(DeviceCfg.ChannelToPlayerMap) do
- Device.LinkCaptureBuffer(ChannelIndex, nil);
- 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.DeviceList[i].Name = 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;
-
-end.
-
-
-