{* UltraStar Deluxe - Karaoke Game
 *
 * UltraStar Deluxe is the legal property of its developers, whose names
 * are too numerous to list here. Please refer to the COPYRIGHT
 * file distributed with this source distribution.
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING. If not, write to
 * the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301, USA.
 *
 * $URL: svn://basisbit@svn.code.sf.net/p/ultrastardx/svn/trunk/src/media/UAudioInput_Bass.pas $
 * $Id: UAudioInput_Bass.pas 2475 2010-06-10 18:27:53Z brunzelchen $
 *}

unit UAudioInput_Bass;

interface

{$IFDEF FPC}
  {$MODE Delphi}
{$ENDIF}

{$I switches.inc}

uses
  Classes,
  SysUtils,
  URecord,
  UMusic;

implementation

uses
  UMain,
  UIni,
  ULog,
  UAudioCore_Bass,
  UTextEncoding,
  UCommon,    // (Note: for MakeLong on non-windows platforms)
  {$IFDEF MSWINDOWS}
  Windows,    // (Note: for MakeLong)
  {$ENDIF}
  bass;       // (Note: DWORD is redefined here -> insert after Windows-unit)

type
  TAudioInput_Bass = class(TAudioInputBase)
    private
      function EnumDevices(): boolean;
    public
      function GetName: String; override;
      function InitializeRecord(ScanMode: TAudioInputScanMode): boolean; override;
      function FinalizeRecord: boolean; override;
  end;

  TBassInputDevice = class(TAudioInputDevice)
    private
      RecordStream: HSTREAM;
      BassDeviceID: DWORD; // DeviceID used by BASS
      SingleIn: boolean;

      function SetInputSource(SourceIndex: integer): boolean;
      function GetInputSource(): integer;
    public
      function Open(): boolean;
      function Close(): boolean;
      function Start(): boolean; override;
      function Stop(): boolean;  override;

      function GetVolume(): single;        override;
      procedure SetVolume(Volume: single); override;
  end;

var
  BassCore: TAudioCore_Bass;


{ Global }

{*
 * Bass input capture callback.
 * Params:
 *   stream - BASS input stream
 *   buffer - buffer of captured samples
 *   len - size of buffer in bytes
 *   user - players associated with left/right channels
 *}
function MicrophoneCallback(stream: HSTREAM; buffer: Pointer;
    len: integer; inputDevice: Pointer): boolean; {$IFDEF MSWINDOWS}stdcall;{$ELSE}cdecl;{$ENDIF}
begin
  AudioInputProcessor.HandleMicrophoneData(buffer, len, inputDevice);
  Result := true;
end;


{ TBassInputDevice }

function TBassInputDevice.GetInputSource(): integer;
var
  SourceCnt: integer;
  i: integer;
  flags: DWORD;
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
    // get input settings
    flags := BASS_RecordGetInput(i, nil);
    if (flags = DWORD(-1)) then
    begin
      Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString(), 'TBassInputDevice.GetInputSource');
      Exit;
    end;

    // check if current source is selected
    if ((flags 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;
  flags: DWORD;
begin
  Result := false;

  // 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, -1)) then
  begin
    Log.LogError('BASS_RecordSetInput: ' + BassCore.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;
      // get input settings
      flags := BASS_RecordGetInput(i, nil);
      if (flags = DWORD(-1)) then
      begin
        Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString(), 'TBassInputDevice.GetInputSource');
        Exit;
      end;
      // deselect source if selected
      if ((flags and BASS_INPUT_OFF) = 0) then
        BASS_RecordSetInput(i, BASS_INPUT_OFF, -1);
    end;
  end;

  Result := true;
end;

function TBassInputDevice.Open(): boolean;
var
  FormatFlags: DWORD;
  SourceIndex: integer;
const
  latency = 20; // 20ms callback period (= latency)
begin
  Result := false;

  if (not BASS_RecordInit(BassDeviceID)) then
  begin
    Log.LogError('BASS_RecordInit['+Name+']: ' +
                 BassCore.ErrorGetString(), 'TBassInputDevice.Open');
    Exit;
  end;

  if (not BassCore.ConvertAudioFormatToBASSFlags(AudioFormat.Format, FormatFlags)) then
  begin
    Log.LogError('Unhandled sample-format', 'TBassInputDevice.Open');
    Exit;
  end;

  // start capturing in paused state
  RecordStream := BASS_RecordStart(Round(AudioFormat.SampleRate), AudioFormat.Channels,
                    MakeLong(FormatFlags or BASS_RECORD_PAUSE, latency),
                    @MicrophoneCallback, Self);
  if (RecordStream = 0) then
  begin
    Log.LogError('BASS_RecordStart: ' + BassCore.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;

{* 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: ' + BassCore.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;
  if (not BASS_RecordSetDevice(BassDeviceID)) then
    Exit;

  if (not BASS_ChannelStop(RecordStream)) then
  begin
    Log.LogError('BASS_ChannelStop: ' + BassCore.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: ' + BassCore.ErrorGetString(), 'TBassInputDevice.Close');
    Result := false;
  end
  else
  begin
    Result := true;
  end;

  RecordStream := 0;
end;

function TBassInputDevice.GetVolume(): single;
var
  SourceIndex: integer;
  lVolume: Single;
begin
  Result := 0;

  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;

  if (BASS_RecordGetInput(SourceIndex, @lVolume) = DWORD(-1)) then
  begin
    Log.LogError('BASS_RecordGetInput: ' + BassCore.ErrorGetString() , 'TBassInputDevice.GetVolume');
    Exit;
  end;
  Result := lVolume;
end;

procedure TBassInputDevice.SetVolume(Volume: single);
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 > 1.0) then
    Volume := 1.0
  else if (Volume < 0) then
    Volume := 0;

  if (not BASS_RecordSetInput(SourceIndex, 0, Volume)) then
  begin
    Log.LogError('BASS_RecordSetInput: ' + BassCore.ErrorGetString() , 'TBassInputDevice.SetVolume');
  end;
end;


{ TAudioInput_Bass }

function  TAudioInput_Bass.GetName: String;
begin
  result := 'BASS_Input';
end;

function TAudioInput_Bass.EnumDevices(): boolean;
var
  Descr:      UTF8String;
  SourceName: PChar;
  Flags:      integer;
  BassDeviceID: integer;
  BassDevice:   TBassInputDevice;
  DeviceIndex:  integer;
  DeviceInfo: BASS_DEVICEINFO;
  SourceIndex:  integer;
  RecordInfo: BASS_RECORDINFO;
  NumberOfSupportedChannels: byte;
  DeviceName: UTF8String;
begin
  result := false;

  DeviceIndex := 0;
  BassDeviceID := 0;
  SetLength(AudioInputProcessor.DeviceList, 0);

  // checks for recording devices and puts them into an array
  while true do
  begin
    if (not BASS_RecordGetDeviceInfo(BassDeviceID, DeviceInfo)) then
      break;

    if ((DeviceInfo.flags and BASS_DEVICE_TYPE_MASK) = BASS_DEVICE_TYPE_SPEAKERS) then
    begin
      Inc(BassDeviceID);
      continue;
    end;

    // BASS device names seem to be encoded with local encoding
    // TODO: works for windows, check Linux + Mac OS X
    Descr := DecodeStringUTF8(DeviceInfo.name, encAuto);
    DeviceName := UnifyDeviceName(Descr, BassDeviceID);

    if not ShouldProbeDevice(DeviceName) then
    begin
      Inc(BassDeviceID);
      continue;
    end;

    // try to initialize the device
    if not BASS_RecordInit(BassDeviceID) then
    begin
      Log.LogStatus('Failed to initialize BASS Capture-Device['+inttostr(BassDeviceID)+']',
                    'TAudioInput_Bass.InitializeRecord');
    end
    else
    begin
      SetLength(AudioInputProcessor.DeviceList, DeviceIndex+1);

      // TODO: free object on termination
      BassDevice := TBassInputDevice.Create();
      AudioInputProcessor.DeviceList[DeviceIndex] := BassDevice;

      BassDevice.BassDeviceID := BassDeviceID;

      BassDevice.Name := DeviceName;
      Log.LogStatus('Attempting to configure InputDevice "' + BassDevice.Name + '"', 'Bass.EnumDevices');

      // zero info-struct as some fields might not be set (e.g. freq is just set on Vista and macOS)
      FillChar(RecordInfo, SizeOf(RecordInfo), 0);
      // retrieve recording device info
      BASS_RecordGetInfo(RecordInfo);

      // BASS_RECORDINFO.formats is a DWORD that has the number of channels supported by the device
      // encoded in the high 8 bits (see BASS_RECORDINFO documentation)
      NumberOfSupportedChannels := hi(hi(RecordInfo.formats));
      // Now limit this number to sane values (highest number of channels on an interface I could
      // find was 32) for buggy devices returning fantasy channel counts
      if (NumberOfSupportedChannels > 32) or (NumberOfSupportedChannels = 0) then
         NumberOfSupportedChannels := 2;

      // check if BASS has capture-freq. info
      if (RecordInfo.freq > 0) then
      begin
        // use current input sample rate (available only on Windows Vista and macOS).
        // 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(NumberOfSupportedChannels, RecordInfo.freq, asfS16)
      end
      else
      begin
        BassDevice.AudioFormat := TAudioFormatInfo.Create(NumberOfSupportedChannels, 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);

      Log.LogStatus('InputDevice "'+BassDevice.Name+'"@' +
      IntToStr(BassDevice.AudioFormat.Channels)+'x'+
      FloatToStr(BassDevice.AudioFormat.SampleRate)+'Hz' ,
                     'Bass.EnumDevices');

      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-1);

        {$IFDEF DARWIN}
        // Under macOS 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);
        // BASS source names seem to be encoded with local encoding
        // TODO: works for windows, check Linux + macOS
        BassDevice.Source[SourceIndex].Name := DecodeStringUTF8(SourceName, encAuto);

        // get input-source info
        {$push} {$R-}
        Flags := BASS_RecordGetInput(SourceIndex, nil);
        {$POP}
        if (Flags <> -1) then
        begin
          // chech if current source is a mic (and none was set before)
          if ((Flags and BASS_INPUT_TYPE_MIC) <> 0) and
             (BassDevice.MicSource = -1) then
          begin
            BassDevice.MicSource := SourceIndex;
          end;
        end;

        Inc(SourceIndex);
      end;

      // FIXME: this call hangs in FPC (windows) every 2nd time USDX is called.
      //   Maybe because the sound-device was not released properly?
      BASS_RecordFree;

      Inc(DeviceIndex);
    end;

    Inc(BassDeviceID);
  end;

  result := true;
end;

function TAudioInput_Bass.InitializeRecord(ScanMode: TAudioInputScanMode): boolean;
begin
  BassCore := TAudioCore_Bass.GetInstance();
  if not BassCore.CheckVersion then
  begin
    Result := false;
    Exit;
  end;
  PrepareDeviceScan(ScanMode);
  Result := EnumDevices();
end;

function TAudioInput_Bass.FinalizeRecord(): boolean;
begin
  CaptureStop;
  Result := inherited FinalizeRecord;
end;


initialization
  MediaManager.Add(TAudioInput_Bass.Create);

end.
