text: /** @file
    Copyright (C) 2018-2021, John Davis, Mike Beaton. All rights reserved.<BR>
    SPDX-License-Identifier: BSD-3-Clause
  **/
  
  #include "HdaCodec.h"
  #include <Protocol/AudioIo.h>
  #include <Library/OcMiscLib.h>
  #include <Library/PcdLib.h>
  
  //
  // Cache playback setup.
  //
  STATIC UINT64                     mOutputIndexMask = 0;
  STATIC UINT8                      mVolume = 0;
  STATIC EFI_AUDIO_IO_PROTOCOL_FREQ mFreq = 0;
  STATIC EFI_AUDIO_IO_PROTOCOL_BITS mBits = 0;
  STATIC UINT8                      mChannels = 0xFFU;
  
  // HDA I/O Stream callback.
  VOID
  EFIAPI
  HdaCodecHdaIoStreamCallback(
    IN EFI_HDA_IO_PROTOCOL_TYPE Type,
    IN VOID *Context1,
    IN VOID *Context2,
    IN VOID *Context3) {
    DEBUG((DEBUG_VERBOSE, "HdaCodecHdaIoStreamCallback(): start\n"));
  
    // Create variables.
    EFI_AUDIO_IO_PROTOCOL *AudioIo = (EFI_AUDIO_IO_PROTOCOL*)Context1;
    EFI_AUDIO_IO_CALLBACK AudioIoCallback = (EFI_AUDIO_IO_CALLBACK)Context2;
  
    // Ensure required parameters are valid.
    if ((AudioIo == NULL) || (AudioIoCallback == NULL))
      return;
  
    // Invoke callback.
    AudioIoCallback(AudioIo, Context3);
  }
  
  /**
    Gets the collection of output ports.
  
    @param[in]  This              A pointer to the EFI_AUDIO_IO_PROTOCOL instance.
    @param[out] OutputPorts       A pointer to a buffer where the output ports will be placed.
    @param[out] OutputPortsCount  The number of ports in OutputPorts.
  
    @retval EFI_SUCCESS           The audio data was played successfully.
    @retval EFI_INVALID_PARAMETER One or more parameters are invalid.
  **/
  EFI_STATUS
  EFIAPI
  HdaCodecAudioIoGetOutputs(
    IN  EFI_AUDIO_IO_PROTOCOL *This,
    OUT EFI_AUDIO_IO_PROTOCOL_PORT **OutputPorts,
    OUT UINTN *OutputPortsCount) {
    DEBUG((DEBUG_VERBOSE, "HdaCodecAudioIoGetOutputs(): start\n"));
  
    // Create variables.
    EFI_STATUS Status;
    AUDIO_IO_PRIVATE_DATA *AudioIoPrivateData;
    HDA_CODEC_DEV *HdaCodecDev;
    EFI_AUDIO_IO_PROTOCOL_PORT *HdaOutputPorts;
    UINT32 SupportedRates;
  
    // If a parameter is invalid, return error.
    if ((This == NULL) || (OutputPorts == NULL) ||
      (OutputPortsCount == NULL))
      return EFI_INVALID_PARAMETER;
  
    // Get private data.
    AudioIoPrivateData = AUDIO_IO_PRIVATE_DATA_FROM_THIS(This);
    HdaCodecDev = AudioIoPrivateData->HdaCodecDev;
  
    // Allocate buffer.
    HdaOutputPorts = AllocateZeroPool(sizeof(EFI_AUDIO_IO_PROTOCOL_PORT) * HdaCodecDev->OutputPortsCount);
    if (HdaOutputPorts == NULL)
      return EFI_OUT_OF_RESOURCES;
  
    // Get output ports.
    for (UINTN i = 0; i < HdaCodecDev->OutputPortsCount; i++) {
      // Port is an output.
      HdaOutputPorts[i].Type = EfiAudioIoTypeOutput;
  
      // Get device type.
      switch (HDA_VERB_GET_CONFIGURATION_DEFAULT_DEVICE(HdaCodecDev->OutputPorts[i]->DefaultConfiguration)) {
        case HDA_CONFIG_DEFAULT_DEVICE_LINE_OUT:
        case HDA_CONFIG_DEFAULT_DEVICE_LINE_IN:
          HdaOutputPorts[i].Device = EfiAudioIoDeviceLine;
          break;
  
        case HDA_CONFIG_DEFAULT_DEVICE_SPEAKER:
          HdaOutputPorts[i].Device = EfiAudioIoDeviceSpeaker;
          break;
  
        case HDA_CONFIG_DEFAULT_DEVICE_HEADPHONE_OUT:
          HdaOutputPorts[i].Device = EfiAudioIoDeviceHeadphones;
          break;
  
        case HDA_CONFIG_DEFAULT_DEVICE_SPDIF_OUT:
        case HDA_CONFIG_DEFAULT_DEVICE_SPDIF_IN:
          HdaOutputPorts[i].Device = EfiAudioIoDeviceSpdif;
          break;
  
        case HDA_CONFIG_DEFAULT_DEVICE_MIC_IN:
          HdaOutputPorts[i].Device = EfiAudioIoDeviceMic;
          break;
  
        default:
          if (HdaCodecDev->OutputPorts[i]->PinCapabilities & HDA_PARAMETER_PIN_CAPS_HDMI)
            HdaOutputPorts[i].Device = EfiAudioIoDeviceHdmi;
          else
            HdaOutputPorts[i].Device = EfiAudioIoDeviceOther;
      }
  
      // Get location.
      switch (HDA_VERB_GET_CONFIGURATION_DEFAULT_LOC(HdaCodecDev->OutputPorts[i]->DefaultConfiguration)) {
        case HDA_CONFIG_DEFAULT_LOC_SPEC_NA:
          HdaOutputPorts[i].Location = EfiAudioIoLocationNone;
          break;
  
        case HDA_CONFIG_DEFAULT_LOC_SPEC_REAR:
          HdaOutputPorts[i].Location = EfiAudioIoLocationRear;
          break;
  
        case HDA_CONFIG_DEFAULT_LOC_SPEC_FRONT:
          HdaOutputPorts[i].Location = EfiAudioIoLocationFront;
          break;
  
        case HDA_CONFIG_DEFAULT_LOC_SPEC_LEFT:
          HdaOutputPorts[i].Location = EfiAudioIoLocationLeft;
          break;
  
        case HDA_CONFIG_DEFAULT_LOC_SPEC_RIGHT:
          HdaOutputPorts[i].Location = EfiAudioIoLocationRight;
          break;
  
        case HDA_CONFIG_DEFAULT_LOC_SPEC_TOP:
          HdaOutputPorts[i].Location = EfiAudioIoLocationTop;
          break;
  
        case HDA_CONFIG_DEFAULT_LOC_SPEC_BOTTOM:
          HdaOutputPorts[i].Location = EfiAudioIoLocationBottom;
          break;
  
        default:
          HdaOutputPorts[i].Location = EfiAudioIoLocationOther;
      }
  
      // Get surface.
      switch (HDA_VERB_GET_CONFIGURATION_DEFAULT_SURF(HdaCodecDev->OutputPorts[i]->DefaultConfiguration)) {
        case HDA_CONFIG_DEFAULT_LOC_SURF_EXTERNAL:
          HdaOutputPorts[i].Surface = EfiAudioIoSurfaceExternal;
          break;
  
        case HDA_CONFIG_DEFAULT_LOC_SURF_INTERNAL:
          HdaOutputPorts[i].Surface = EfiAudioIoSurfaceInternal;
          break;
  
        default:
          HdaOutputPorts[i].Surface = EfiAudioIoSurfaceOther;
      }
  
      // Get supported stream formats.
      Status = HdaCodecGetSupportedPcmRates(HdaCodecDev->OutputPorts[i], &SupportedRates);
      if (EFI_ERROR(Status))
        return Status;
  
      // Get supported bit depths.
      HdaOutputPorts[i].SupportedBits = 0;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_8BIT)
        HdaOutputPorts[i].SupportedBits |= EfiAudioIoBits8;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_16BIT)
        HdaOutputPorts[i].SupportedBits |= EfiAudioIoBits16;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_20BIT)
        HdaOutputPorts[i].SupportedBits |= EfiAudioIoBits20;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_24BIT)
        HdaOutputPorts[i].SupportedBits |= EfiAudioIoBits24;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_32BIT)
        HdaOutputPorts[i].SupportedBits |= EfiAudioIoBits32;
  
      // Get supported sample rates.
      HdaOutputPorts[i].SupportedFreqs = 0;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_8KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq8kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_11KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq11kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_16KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq16kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_22KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq22kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_32KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq32kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_44KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq44kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_48KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq48kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_88KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq88kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_96KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq96kHz;
      if (SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_192KHZ)
        HdaOutputPorts[i].SupportedFreqs |= EfiAudioIoFreq192kHz;
    }
  
    // Ports gotten successfully.
    *OutputPorts = HdaOutputPorts;
    *OutputPortsCount = HdaCodecDev->OutputPortsCount;
    return EFI_SUCCESS;
  }
  
  /**
    Sets up the device to play audio data. Basic caching is implemented: no actions are taken
    the second and subsequent times that set up is called again with exactly the same paremeters. 
  
    @param[in] This               A pointer to the EFI_AUDIO_IO_PROTOCOL instance.
    @param[in] OutputIndexMask    A mask indicating the desired outputs.
    @param[in] Volume             The volume (0-100) to use.
    @param[in] Bits               The width in bits of the source data.
    @param[in] Freq               The frequency of the source data.
    @param[in] Channels           The number of channels the source data contains.
    @param[in] PlaybackDelay      The required delay before playback after a change in setup.
  
    @retval EFI_SUCCESS           The audio data was played successfully.
    @retval EFI_INVALID_PARAMETER One or more parameters are invalid.
  **/
  EFI_STATUS
  EFIAPI
  HdaCodecAudioIoSetupPlayback(
    IN EFI_AUDIO_IO_PROTOCOL *This,
    IN UINT64 OutputIndexMask,
    IN UINT8 Volume,
    IN EFI_AUDIO_IO_PROTOCOL_FREQ Freq,
    IN EFI_AUDIO_IO_PROTOCOL_BITS Bits,
    IN UINT8 Channels,
    IN UINTN PlaybackDelay
    )
  {
    EFI_STATUS Status;
    AUDIO_IO_PRIVATE_DATA *AudioIoPrivateData;
    HDA_CODEC_DEV *HdaCodecDev;
    EFI_HDA_IO_PROTOCOL *HdaIo;
    UINTN Index;
    UINT64 IndexMask;
    UINT32 Response;
    UINT8 NumGpios;
    UINT8 Payload;
  
    // Widgets.
    HDA_WIDGET_DEV *PinWidget;
    HDA_WIDGET_DEV *OutputWidget;
    UINT32 SupportedRates;
    UINT8 HdaStreamId;
  
    // Stream.
    UINT8 StreamBits;
    UINT8 StreamDiv;
    UINT8 StreamMult;
    BOOLEAN StreamBase44kHz;
    UINT16 StreamFmt;
  
    DEBUG((DEBUG_VERBOSE, "HdaCodecAudioIoSetupPlayback(): start\n"));
  
    // Basic settings caching.
    if (mOutputIndexMask == OutputIndexMask
        && mVolume == Volume
        && mFreq == Freq
        && mBits == Bits
        && mChannels == Channels) {
      return EFI_SUCCESS;
    }
  
    mOutputIndexMask = OutputIndexMask;
    mVolume = Volume;
    mFreq = Freq;
    mBits = Bits;
    mChannels = Channels;
  
    // If a parameter is invalid, return error.
    if ((This == NULL) || (Volume > EFI_AUDIO_IO_PROTOCOL_MAX_VOLUME))
      return EFI_INVALID_PARAMETER;
  
    // Get private data.
    AudioIoPrivateData = AUDIO_IO_PRIVATE_DATA_FROM_THIS(This);
    HdaCodecDev = AudioIoPrivateData->HdaCodecDev;
    HdaIo = HdaCodecDev->HdaIo;
  
    // Mask to only outputs which are within bounds.
    if (HdaCodecDev->OutputPortsCount > sizeof (UINT64) * OC_CHAR_BIT) {
      return EFI_UNSUPPORTED;
    }
    OutputIndexMask &= ~LShiftU64(MAX_UINT64, HdaCodecDev->OutputPortsCount);
  
    // Fail visibily if nothing is requested.
    if (PcdGetBool (PcdAudioCodecErrorOnNoOutputs) && OutputIndexMask == 0) {
      return EFI_INVALID_PARAMETER;
    }
  
    // Avoid Coverity warnings (the bit mask checks actually ensure that these cannot be used uninitialised).
    StreamBits = 0;
    StreamDiv = 0;
    StreamMult = 0;
    StreamBase44kHz = FALSE;
  
    // Expand the requested stream frequency and sample size params,
    // and check that every requested channel can support them.
    for (Index = 0, IndexMask = 1; Index < HdaCodecDev->OutputPortsCount; Index++, IndexMask <<= 1) {
      if ((OutputIndexMask & IndexMask) == 0) {
        continue;
      }
  
      PinWidget = HdaCodecDev->OutputPorts[Index];
  
      // Get the output DAC for the path.
      Status = HdaCodecGetOutputDac(PinWidget, &OutputWidget);
      if (EFI_ERROR(Status))
        return Status;
  
      // Get supported stream formats.
      Status = HdaCodecGetSupportedPcmRates(OutputWidget, &SupportedRates);
      if (EFI_ERROR(Status)) {
        DEBUG((DEBUG_INFO, "HdaCodecGetSupportedPcmRates(): failure - %r\n", Status));
        return Status;
      }
  
      // Determine bitness of samples, ensuring desired bitness is supported.
      switch (Bits) {
        // 8-bit.
        case EfiAudioIoBits8:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_8BIT))
            return EFI_UNSUPPORTED;
          StreamBits = HDA_CONVERTER_FORMAT_BITS_8;
          break;
  
        // 16-bit.
        case EfiAudioIoBits16:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_16BIT))
            return EFI_UNSUPPORTED;
          StreamBits = HDA_CONVERTER_FORMAT_BITS_16;
          break;
  
        // 20-bit.
        case EfiAudioIoBits20:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_20BIT))
            return EFI_UNSUPPORTED;
          StreamBits = HDA_CONVERTER_FORMAT_BITS_20;
          break;
  
        // 24-bit.
        case EfiAudioIoBits24:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_24BIT))
            return EFI_UNSUPPORTED;
          StreamBits = HDA_CONVERTER_FORMAT_BITS_24;
          break;
  
        // 32-bit.
        case EfiAudioIoBits32:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_32BIT))
            return EFI_UNSUPPORTED;
          StreamBits = HDA_CONVERTER_FORMAT_BITS_32;
          break;
  
        // Others.
        default:
          return EFI_INVALID_PARAMETER;
      }
  
      // Determine base, divisor, and multipler.
      switch (Freq) {
        // 8 kHz.
        case EfiAudioIoFreq8kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_8KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE;
          StreamDiv = 6;
          StreamMult = 1;
          break;
  
        // 11.025 kHz.
        case EfiAudioIoFreq11kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_11KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE; ///< TODO: Why does Clover has TRUE here?
          StreamDiv = 4;
          StreamMult = 1;
          break;
  
        // 16 kHz.
        case EfiAudioIoFreq16kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_16KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE;
          StreamDiv = 3;
          StreamMult = 1;
          break;
  
        // 22.05 kHz.
        case EfiAudioIoFreq22kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_22KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE; ///< TODO: Why does Clover has TRUE here?
          StreamDiv = 2;
          StreamMult = 1;
          break;
  
        // 32 kHz.
        case EfiAudioIoFreq32kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_32KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE;
          StreamDiv = 3;
          StreamMult = 2;
          break;
  
        // 44.1 kHz.
        case EfiAudioIoFreq44kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_44KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = TRUE;
          StreamDiv = 1;
          StreamMult = 1;
          break;
  
        // 48 kHz.
        case EfiAudioIoFreq48kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_48KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE;
          StreamDiv = 1;
          StreamMult = 1;
          break;
  
        // 88 kHz.
        case EfiAudioIoFreq88kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_88KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = TRUE;
          StreamDiv = 1;
          StreamMult = 2;
          break;
  
        // 96 kHz.
        case EfiAudioIoFreq96kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_96KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE;
          StreamDiv = 1;
          StreamMult = 2;
          break;
  
        // 192 kHz.
        case EfiAudioIoFreq192kHz:
          if (!(SupportedRates & HDA_PARAMETER_SUPPORTED_PCM_SIZE_RATES_192KHZ))
            return EFI_UNSUPPORTED;
          StreamBase44kHz = FALSE;
          StreamDiv = 1;
          StreamMult = 4;
          break;
  
        // Others.
        default:
          return EFI_INVALID_PARAMETER;
      }
    }
  
    // Disable all non-selected widget paths.
    for (Index = 0, IndexMask = 1; Index < HdaCodecDev->OutputPortsCount; Index++, IndexMask <<= 1) {
      if ((OutputIndexMask & IndexMask) == 0) {
        continue;
      }
      Status = HdaCodecDisableWidgetPath(HdaCodecDev->OutputPorts[Index]);
      if (EFI_ERROR(Status))
        return Status;
    }
  
    // Close stream first.
    Status = HdaIo->CloseStream(HdaIo, EfiHdaIoTypeOutput);
    if (EFI_ERROR(Status))
      return Status;
  
    // Save requested outputs.
    AudioIoPrivateData->SelectedOutputIndexMask = OutputIndexMask;
  
    // Nothing to play.
    if (OutputIndexMask == 0) {
      return EFI_SUCCESS;
    }
  
    // Calculate stream format and setup stream.
    StreamFmt = HDA_CONVERTER_FORMAT_SET(Channels - 1, StreamBits,
      StreamDiv - 1, StreamMult - 1, StreamBase44kHz);
    DEBUG((DEBUG_VERBOSE, "HdaCodecAudioIoPlay(): Stream format 0x%X\n", StreamFmt));
    Status = HdaIo->SetupStream(HdaIo, EfiHdaIoTypeOutput, StreamFmt, &HdaStreamId);
    if (EFI_ERROR(Status))
      return Status;
  
    // Setup widget path for desired outputs.
    for (Index = 0, IndexMask = 1; Index < HdaCodecDev->OutputPortsCount; Index++, IndexMask <<= 1) {
      if ((OutputIndexMask & IndexMask) == 0) {
        continue;
      }
      Status = HdaCodecEnableWidgetPath(HdaCodecDev->OutputPorts[Index], Volume, HdaStreamId, StreamFmt);
      if (EFI_ERROR(Status))
        goto CLOSE_STREAM;
    }
  
    //
    // Enable any GPIO pins. This is similar to how Linux drivers enable audio on e.g. Cirrus Logic and
    // Realtek devices on Mac, by enabling specific GPIO pins as required, but this is an attempt to
    // make a more general purpose system.
    // REF:
    // - https://github.com/torvalds/linux/blob/6f513529296fd4f696afb4354c46508abe646541/sound/pci/hda/patch_cirrus.c#L43-L57
    // - https://github.com/torvalds/linux/blob/6f513529296fd4f696afb4354c46508abe646541/sound/pci/hda/patch_cirrus.c#L493-L517
    // - https://github.com/torvalds/linux/blob/6f513529296fd4f696afb4354c46508abe646541/sound/pci/hda/patch_realtek.c#L244-L258
    //
    NumGpios = HDA_PARAMETER_GPIO_COUNT_NUM_GPIOS (HdaCodecDev->AudioFuncGroup->GpioCapabilities);
    //
    // According to Intel HDA spec this can be from 0 to 7, however we
    // have seen 8 in the wild, and values up to 8 are perfectly usable.
    // REF: https://github.com/acidanthera/bugtracker/issues/740#issuecomment-1005279476
    //
    if (NumGpios > 8) {
      Status = EFI_INVALID_PARAMETER;
      goto CLOSE_STREAM;
    }
    if (NumGpios) {
      Payload = (1 << NumGpios) - 1; ///< Enable all available pins
      Status = HdaIo->SendCommand (
        HdaIo,
        HdaCodecDev->AudioFuncGroup->NodeId,
        HDA_CODEC_VERB (HDA_VERB_SET_GPIO_ENABLE_MASK, Payload),
        &Response
        );
      if (!EFI_ERROR (Status)) {
        Status = HdaIo->SendCommand (
          HdaIo,
          HdaCodecDev->AudioFuncGroup->NodeId,
          HDA_CODEC_VERB (HDA_VERB_SET_GPIO_DIRECTION, Payload),
          &Response
          );
      }
      if (!EFI_ERROR (Status)) {
        gBS->Stall (MS_TO_MICROSECONDS (1));
        Status = HdaIo->SendCommand (
          HdaIo,
          HdaCodecDev->AudioFuncGroup->NodeId,
          HDA_CODEC_VERB (HDA_VERB_SET_GPIO_DATA, Payload),
          &Response
          );
      }
      DEBUG ((
        EFI_ERROR (Status) ? DEBUG_WARN : DEBUG_INFO,
        "HDA: GPIO output enable 0x%02X - %r\n",
        Payload,
        Status
        ));
      if (EFI_ERROR(Status))
        goto CLOSE_STREAM;
    }
    
    //
    // We are required to wait for some time after codec setup on some systems.
    // REF: https://github.com/acidanthera/bugtracker/issues/971
    //
    if (PlaybackDelay > 0) {
      gBS->Stall (PlaybackDelay);
    }
  
    return EFI_SUCCESS;
  
  CLOSE_STREAM:
    // Close stream.
    HdaIo->CloseStream(HdaIo, EfiHdaIoTypeOutput);
    return Status;
  }
  
  /**
    Begins playback on the device and waits for playback to complete.
  
    @param[in] This               A pointer to the EFI_AUDIO_IO_PROTOCOL instance.
    @param[in] Data               A pointer to the buffer containing the audio data to play.
    @param[in] DataLength         The size, in bytes, of the data buffer specified by Data.
    @param[in] Position           The position in the buffer to start at.
  
    @retval EFI_SUCCESS           The audio data was played successfully.
    @retval EFI_INVALID_PARAMETER One or more parameters are invalid.
  **/
  EFI_STATUS
  EFIAPI
  HdaCodecAudioIoStartPlayback(
    IN EFI_AUDIO_IO_PROTOCOL *This,
    IN VOID *Data,
    IN UINTN DataLength,
    IN UINTN Position OPTIONAL) {
    DEBUG((DEBUG_VERBOSE, "HdaCodecAudioIoStartPlayback(): start\n"));
  
    // Create variables.
    EFI_STATUS Status;
    AUDIO_IO_PRIVATE_DATA *AudioIoPrivateData;
    EFI_HDA_IO_PROTOCOL *HdaIo;
    BOOLEAN StreamRunning;
  
    // If a parameter is invalid, return error.
    if ((This == NULL) || (Data == NULL) || (DataLength == 0))
      return EFI_INVALID_PARAMETER;
  
    // Get private data.
    AudioIoPrivateData = AUDIO_IO_PRIVATE_DATA_FROM_THIS(This);
    HdaIo = AudioIoPrivateData->HdaCodecDev->HdaIo;
  
    // Nothing to play.
    if (AudioIoPrivateData->SelectedOutputIndexMask == 0) {
      return EFI_SUCCESS;
    }
  
    // Start stream.
    Status = HdaIo->StartStream(HdaIo, EfiHdaIoTypeOutput, Data, DataLength, Position,
      NULL, NULL, NULL, NULL);
    if (EFI_ERROR(Status))
      return Status;
  
    // Wait for stream to stop.
    StreamRunning = TRUE;
    while (StreamRunning) {
      Status = HdaIo->GetStream(HdaIo, EfiHdaIoTypeOutput, &StreamRunning);
      if (EFI_ERROR(Status)) {
        HdaIo->StopStream(HdaIo, EfiHdaIoTypeOutput);
        return Status;
      }
  
      // Wait 100ms.
      //gBS->Stall (MS_TO_MICROSECONDS (100));
    }
    return EFI_SUCCESS;
  }
  
  /**
    Begins playback on the device asynchronously.
  
    @param[in] This               A pointer to the EFI_AUDIO_IO_PROTOCOL instance.
    @param[in] Data               A pointer to the buffer containing the audio data to play.
    @param[in] DataLength         The size, in bytes, of the data buffer specified by Data.
    @param[in] Position           The position in the buffer to start at.
    @param[in] Callback           A pointer to an optional callback to be invoked when playback is complete.
    @param[in] Context            A pointer to data to be passed to the callback function.
  
    @retval EFI_SUCCESS           The audio data was played successfully.
    @retval EFI_INVALID_PARAMETER One or more parameters are invalid.
  **/
  EFI_STATUS
  EFIAPI
  HdaCodecAudioIoStartPlaybackAsync(
    IN EFI_AUDIO_IO_PROTOCOL *This,
    IN VOID *Data,
    IN UINTN DataLength,
    IN UINTN Position OPTIONAL,
    IN EFI_AUDIO_IO_CALLBACK Callback OPTIONAL,
    IN VOID *Context OPTIONAL) {
    DEBUG((DEBUG_VERBOSE, "HdaCodecAudioIoStartPlaybackAsync(): start\n"));
  
    // Create variables.
    EFI_STATUS Status;
    AUDIO_IO_PRIVATE_DATA *AudioIoPrivateData;
    EFI_HDA_IO_PROTOCOL *HdaIo;
  
    // If a parameter is invalid, return error.
    if ((This == NULL) || (Data == NULL) || (DataLength == 0))
      return EFI_INVALID_PARAMETER;
  
    // Get private data.
    AudioIoPrivateData = AUDIO_IO_PRIVATE_DATA_FROM_THIS(This);
    HdaIo = AudioIoPrivateData->HdaCodecDev->HdaIo;
  
    // Start stream.
    Status = HdaIo->StartStream(HdaIo, EfiHdaIoTypeOutput, Data, DataLength, Position,
      HdaCodecHdaIoStreamCallback, (VOID*)This, (VOID*)Callback, Context);
    return Status;
  }
  
  /**
    Stops playback on the device.
  
    @param[in] This               A pointer to the EFI_AUDIO_IO_PROTOCOL instance.
  
    @retval EFI_SUCCESS           The audio data was played successfully.
    @retval EFI_INVALID_PARAMETER One or more parameters are invalid.
  **/
  EFI_STATUS
  EFIAPI
  HdaCodecAudioIoStopPlayback(
    IN EFI_AUDIO_IO_PROTOCOL *This) {
    DEBUG((DEBUG_VERBOSE, "HdaCodecAudioIoStopPlayback(): start\n"));
  
    // Create variables.
    AUDIO_IO_PRIVATE_DATA *AudioIoPrivateData;
    EFI_HDA_IO_PROTOCOL *HdaIo;
  
    // If a parameter is invalid, return error.
    if (This == NULL)
      return EFI_INVALID_PARAMETER;
  
    // Get private data.
    AudioIoPrivateData = AUDIO_IO_PRIVATE_DATA_FROM_THIS(This);
    HdaIo = AudioIoPrivateData->HdaCodecDev->HdaIo;
  
    // Stop stream.
    return HdaIo->StopStream(HdaIo, EfiHdaIoTypeOutput);
  }
  