TV 오디오

TIF(TV 입력 프레임워크) 관리자는 오디오 라우팅 API를 통한 유연한 오디오 경로 변경을 지원합니다. SoC(단일 칩 시스템)가 TV HAL(하드웨어 추상화 계층)을 구현할 때 각 TV 입력(HDMI 입력, 튜너 등)은 오디오 유형 및 주소에 관한 AudioPort 정보를 지정하는 TvInputHardwareInfo를 제공합니다.

  • 물리적 오디오 입출력 기기에는 이에 상응하는 AudioPort가 있습니다.
  • 소프트웨어 오디오 입출력 스트림은 AudioMixPort(AudioPort의 하위 클래스)로 표시됩니다.

그런 다음 TIF는 오디오 라우팅 API에서 AudioPort 정보를 사용합니다.

Android TV 입력 프레임워크(TIF)

그림 1. TV 입력 프레임워크(TIF)

요구사항

SoC는 다음과 같은 오디오 라우팅 API의 지원을 통해 오디오 HAL을 구현해야 합니다.

오디오 포트
  • TV 오디오 입력에는 이에 상응하는 오디오 소스 포트 구현이 있습니다.
  • TV 오디오 출력에는 이에 상응하는 오디오 싱크 포트 구현이 있습니다.
  • TV 입력 오디오 포트와 TV 출력 오디오 포트 간에 오디오 패치를 만들 수 있습니다.
기본 입력 Android TV에서 AUDIO_DEVICE_IN_DEFAULT를 획득하려면 AudioRecord(기본 입력 소스로 생성됨)는 가상 null 입력 소스를 점유해야 합니다.
기기 루프백 모든 TV 출력의 모든 오디오 출력(11Khz, 16비트 모노 또는 48Khz, 16비트 모노)을 완전히 믹스한 AUDIO_DEVICE_IN_LOOPBACK 입력이 필요합니다. 이는 오디오 캡처에만 사용됩니다.

TV 오디오 기기

Android는 TV 오디오 입출력에 다음 오디오 기기를 지원합니다.

system/media/audio/include/system/audio.h

참고: Android 5.1 이하에서 이 파일의 경로는 system/core/include/system/audio.h입니다.

/* output devices */
AUDIO_DEVICE_OUT_AUX_DIGITAL  = 0x400,
AUDIO_DEVICE_OUT_HDMI   = AUDIO_DEVICE_OUT_AUX_DIGITAL,
/* HDMI Audio Return Channel */
AUDIO_DEVICE_OUT_HDMI_ARC   = 0x40000,
/* S/PDIF out */
AUDIO_DEVICE_OUT_SPDIF    = 0x80000,
/* input devices */
AUDIO_DEVICE_IN_AUX_DIGITAL   = AUDIO_DEVICE_BIT_IN | 0x20,
AUDIO_DEVICE_IN_HDMI      = AUDIO_DEVICE_IN_AUX_DIGITAL,
/* TV tuner input */
AUDIO_DEVICE_IN_TV_TUNER    = AUDIO_DEVICE_BIT_IN | 0x4000,
/* S/PDIF in */
AUDIO_DEVICE_IN_SPDIF   = AUDIO_DEVICE_BIT_IN | 0x10000,
AUDIO_DEVICE_IN_LOOPBACK    = AUDIO_DEVICE_BIT_IN | 0x40000,

오디오 HAL 확장

오디오 라우팅 API의 오디오 HAL 확장은 다음과 같이 정의됩니다.

system/media/audio/include/system/audio.h

참고: Android 5.1 이하에서 이 파일의 경로는 system/core/include/system/audio.h입니다.

/* audio port configuration structure used to specify a particular configuration of an audio port */
struct audio_port_config {
    audio_port_handle_t      id;           /* port unique ID */
    audio_port_role_t        role;         /* sink or source */
    audio_port_type_t        type;         /* device, mix ... */
    unsigned int             config_mask;  /* e.g AUDIO_PORT_CONFIG_ALL */
    unsigned int             sample_rate;  /* sampling rate in Hz */
    audio_channel_mask_t     channel_mask; /* channel mask if applicable */
    audio_format_t           format;       /* format if applicable */
    struct audio_gain_config gain;         /* gain to apply if applicable */
    union {
        struct audio_port_config_device_ext  device;  /* device specific info */
        struct audio_port_config_mix_ext     mix;     /* mix specific info */
        struct audio_port_config_session_ext session; /* session specific info */
    } ext;
};
struct audio_port {
    audio_port_handle_t      id;                /* port unique ID */
    audio_port_role_t        role;              /* sink or source */
    audio_port_type_t        type;              /* device, mix ... */
    unsigned int             num_sample_rates;  /* number of sampling rates in following array */
    unsigned int             sample_rates[AUDIO_PORT_MAX_SAMPLING_RATES];
    unsigned int             num_channel_masks; /* number of channel masks in following array */
    audio_channel_mask_t     channel_masks[AUDIO_PORT_MAX_CHANNEL_MASKS];
    unsigned int             num_formats;       /* number of formats in following array */
    audio_format_t           formats[AUDIO_PORT_MAX_FORMATS];
    unsigned int             num_gains;         /* number of gains in following array */
    struct audio_gain        gains[AUDIO_PORT_MAX_GAINS];
    struct audio_port_config active_config;     /* current audio port configuration */
    union {
        struct audio_port_device_ext  device;
        struct audio_port_mix_ext     mix;
        struct audio_port_session_ext session;
    } ext;
};

hardware/libhardware/include/hardware/audio.h

struct audio_hw_device {
  :
    /**
     * Routing control
     */

    /* Creates an audio patch between several source and sink ports.
     * The handle is allocated by the HAL and should be unique for this
     * audio HAL module. */
    int (*create_audio_patch)(struct audio_hw_device *dev,
                               unsigned int num_sources,
                               const struct audio_port_config *sources,
                               unsigned int num_sinks,
                               const struct audio_port_config *sinks,
                               audio_patch_handle_t *handle);

    /* Release an audio patch */
    int (*release_audio_patch)(struct audio_hw_device *dev,
                               audio_patch_handle_t handle);

    /* Fills the list of supported attributes for a given audio port.
     * As input, "port" contains the information (type, role, address etc...)
     * needed by the HAL to identify the port.
     * As output, "port" contains possible attributes (sampling rates, formats,
     * channel masks, gain controllers...) for this port.
     */
    int (*get_audio_port)(struct audio_hw_device *dev,
                          struct audio_port *port);

    /* Set audio port configuration */
    int (*set_audio_port_config)(struct audio_hw_device *dev,
                         const struct audio_port_config *config);

DEVICE_IN_LOOPBACK 테스트

TV 모니터링을 위해 DEVICE_IN_LOOPBACK을 테스트하려면 다음 테스트 코드를 사용합니다. 테스트를 실행한 후 캡처된 오디오는 /sdcard/record_loopback.raw에 저장되고, FFmpeg를 사용하여 이를 들을 수 있습니다.

<uses-permission android:name="android.permission.MODIFY_AUDIO_ROUTING" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

   AudioRecord mRecorder;
   Handler mHandler = new Handler();
   int mMinBufferSize = AudioRecord.getMinBufferSize(RECORD_SAMPLING_RATE,
           AudioFormat.CHANNEL_IN_MONO,
           AudioFormat.ENCODING_PCM_16BIT);;
   static final int RECORD_SAMPLING_RATE = 48000;
   public void doCapture() {
       mRecorder = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, RECORD_SAMPLING_RATE,
               AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, mMinBufferSize * 10);
       AudioManager am = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
       ArrayList<AudioPort> audioPorts = new ArrayList<AudioPort>();
       am.listAudioPorts(audioPorts);
       AudioPortConfig srcPortConfig = null;
       AudioPortConfig sinkPortConfig = null;
       for (AudioPort audioPort : audioPorts) {
           if (srcPortConfig == null
                   && audioPort.role() == AudioPort.ROLE_SOURCE
                   && audioPort instanceof AudioDevicePort) {
               AudioDevicePort audioDevicePort = (AudioDevicePort) audioPort;
               if (audioDevicePort.type() == AudioManager.DEVICE_IN_LOOPBACK) {
                   srcPortConfig = audioPort.buildConfig(48000, AudioFormat.CHANNEL_IN_DEFAULT,
                           AudioFormat.ENCODING_DEFAULT, null);
                   Log.d(LOG_TAG, "Found loopback audio source port : " + audioPort);
               }
           }
           else if (sinkPortConfig == null
                   && audioPort.role() == AudioPort.ROLE_SINK
                   && audioPort instanceof AudioMixPort) {
               sinkPortConfig = audioPort.buildConfig(48000, AudioFormat.CHANNEL_OUT_DEFAULT,
                       AudioFormat.ENCODING_DEFAULT, null);
               Log.d(LOG_TAG, "Found recorder audio mix port : " + audioPort);
           }
       }
       if (srcPortConfig != null && sinkPortConfig != null) {
           AudioPatch[] patches = new AudioPatch[] { null };
           int status = am.createAudioPatch(
                   patches,
                   new AudioPortConfig[] { srcPortConfig },
                   new AudioPortConfig[] { sinkPortConfig });
           Log.d(LOG_TAG, "Result of createAudioPatch(): " + status);
       }
       mRecorder.startRecording();
       processAudioData();
       mRecorder.stop();
       mRecorder.release();
   }
   private void processAudioData() {
       OutputStream rawFileStream = null;
       byte data[] = new byte[mMinBufferSize];
       try {
           rawFileStream = new BufferedOutputStream(
                   new FileOutputStream(new File("/sdcard/record_loopback.raw")));
       } catch (FileNotFoundException e) {
           Log.d(LOG_TAG, "Can't open file.", e);
       }
       long startTimeMs = System.currentTimeMillis();
       while (System.currentTimeMillis() - startTimeMs < 5000) {
           int nbytes = mRecorder.read(data, 0, mMinBufferSize);
           if (nbytes <= 0) {
               continue;
           }
           try {
               rawFileStream.write(data);
           } catch (IOException e) {
               Log.e(LOG_TAG, "Error on writing raw file.", e);
           }
       }
       try {
           rawFileStream.close();
       } catch (IOException e) {
       }
       Log.d(LOG_TAG, "Exit audio recording.");
   }

/sdcard/record_loopback.raw에서 캡처된 오디오 파일 위치를 찾고 FFmpeg를 사용하여 이를 듣습니다.

adb pull /sdcard/record_loopback.raw
ffmpeg -f s16le -ar 48k -ac 1 -i record_loopback.raw record_loopback.wav
ffplay record_loopback.wav

사용 사례

이 섹션에는 TV 오디오의 일반적인 사용 사례가 포함되어 있습니다.

스피커 출력이 있는 TV 튜너

TV 튜너가 활성화되면 오디오 라우팅 API가 튜너와 기본 출력(예: 스피커) 사이에 오디오 패치를 만듭니다. 튜너 출력에는 디코딩이 필요하지 않지만, 최종 오디오 출력은 소프트웨어 output_stream과 믹스됩니다.

Android TV 튜너 오디오 패치

그림 2. 스피커 출력이 있는 TV 튜너용 오디오 패치

실시간 TV의 HDMI 출력

사용자가 실시간 TV를 시청한 다음, HDMI 오디오 출력(Intent.ACTION_HDMI_AUDIO_PLUG)으로 전환합니다. 모든 output_stream의 출력 기기가 HDMI_OUT 포트로 변경되고, TIF 관리자가 기존 튜너 오디오 패치의 싱크 포트를 HDMI_OUT 포트로 변경합니다.

Android TV HDMI-OUT 오디오 패치

그림 3. 실시간 TV의 HDMI 출력용 오디오 패치