

# IVS Broadcast SDK: 사용자 지정 오디오 소스 \$1 실시간 스트리밍
<a name="broadcast-custom-audio-sources"></a>

**참고:** 이 가이드는 IVS 실시간 스트리밍 Android Broadcast SDK에만 적용됩니다. iOS 및 웹 SDK에 대한 정보는 향후 게시될 예정입니다.

사용자 지정 오디오 입력 소스를 사용하면 애플리케이션이 디바이스의 내장 마이크로 제한되지 않고 Broadcast SDK에 자체 오디오 입력을 제공할 수 있습니다. 사용자 지정 오디오 소스를 사용하면 애플리케이션이 처리된 오디오를 효과와 함께 스트리밍하거나, 여러 오디오 스트림을 혼합하거나, 타사 오디오 처리 라이브러리와 통합할 수 있습니다.

사용자 지정 오디오 입력 소스를 사용하는 경우 Broadcast SDK는 더 이상 마이크를 직접 관리할 책임이 없습니다. 대신 애플리케이션은 오디오 데이터를 캡처, 처리 및 사용자 지정 소스에 제출할 책임이 있습니다.

사용자 지정 오디오 소스 워크플로는 다음 단계를 따릅니다.

1. 오디오 입력 - 지정된 오디오 형식(샘플 속도, 채널, 형식)으로 사용자 지정 오디오 소스를 생성합니다.

1. 처리 - 오디오 처리 파이프라인에서 오디오 데이터를 캡처하거나 생성합니다.

1. 사용자 지정 오디오 소스 - `appendBuffer()`를 사용하여 사용자 지정 소스에 오디오 버퍼를 제출합니다.

1. 스테이지 - `LocalStageStream`을 래핑하고 `StageStrategy`를 통해 스테이지에 게시합니다.

1. 참가자 - 스테이지 참가자는 처리된 오디오를 실시간으로 수신합니다.

## Android
<a name="custom-audio-sources-android"></a>

### 사용자 지정 오디오 소스 생성
<a name="custom-audio-sources-android-creating-a-custom-audio-source"></a>

`DeviceDiscovery` 세션을 생성한 후 오디오 입력 소스를 생성합니다.

```
DeviceDiscovery deviceDiscovery = new DeviceDiscovery(context); 
 
// Create custom audio source with specific format 
CustomAudioSource customAudioSource = deviceDiscovery.createAudioInputSource( 
   2,  // Number of channels (1 = mono, 2 = stereo) 
   BroadcastConfiguration.AudioSampleRate.RATE_48000,  // Sample rate 
   AudioDevice.Format.INT16  // Audio format (16-bit PCM) 
);
```

이 메서드는 원시 PCM 오디오 데이터를 허용하는 `CustomAudioSource`를 반환합니다. 사용자 지정 오디오 소스는 오디오 처리 파이프라인이 생성하는 것과 동일한 오디오 형식으로 구성되어야 합니다.

#### 지원되는 오디오 형식
<a name="custom-audio-sources-android-submitting-audio-data-supportedi-audio-formats"></a>


| 파라미터 | 옵션 | 설명 | 
| --- | --- | --- | 
| 채널 | 1(모노), 2(스테레오) | 오디오 채널의 수입니다. | 
| 샘플 속도 | RATE\$116000, RATE\$144100, RATE\$148000 | Hz 단위의 오디오 샘플 속도. 고품질의 경우 48kHz가 권장됩니다. | 
| 형식 | INT16, FLOAT32 | 오디오 샘플 형식. INT16은 16비트 고정 지점 PCM이고 FLOAT32는 32비트 부동 소수점 PCM입니다. 인터리브 형식과 평면 형식을 모두 사용할 수 있습니다. | 

### 오디오 데이터 제출
<a name="custom-audio-sources-android-submitting-audio-data"></a>

오디오 데이터를 사용자 지정 소스에 제출하려면 `appendBuffer()` 메서드를 사용합니다.

```
// Prepare audio data in a ByteBuffer 
ByteBuffer audioBuffer = ByteBuffer.allocateDirect(bufferSize); 
audioBuffer.put(pcmAudioData);  // Your processed audio data 
 
// Calculate the number of bytes 
long byteCount = pcmAudioData.length; 
 
// Submit audio to the custom source 
// presentationTimeUs should be generated by and come from your audio source
int samplesProcessed = customAudioSource.appendBuffer( 
   audioBuffer, 
   byteCount, 
   presentationTimeUs 
); 
 
if (samplesProcessed > 0) { 
   Log.d(TAG, "Successfully submitted " + samplesProcessed + " samples"); 
} else { 
   Log.w(TAG, "Failed to submit audio samples"); 
} 
 
// Clear buffer for reuse 
audioBuffer.clear();
```

**중요 고려 사항:**
+ 오디오 데이터는 사용자 지정 소스를 생성할 때 지정된 형식이어야 합니다.
+ 원활한 오디오 재생을 위해서는 타임스탬프가 단조롭게 증가하고 오디오 소스에서 제공해야 합니다.
+ 스트림의 격차를 방지하려면 오디오를 정기적으로 제출합니다.
+ 메서드는 처리된 샘플 수를 반환합니다(0은 실패를 나타냄).

### 스테이지에 게시
<a name="custom-audio-sources-android-publishing-to-a-stage"></a>

`AudioLocalStageStream`에서 `CustomAudioSource`를 래핑하고 `StageStrategy`에서 반환합니다.

```
// Create the audio stream from custom source 
AudioLocalStageStream audioStream = new AudioLocalStageStream(customAudioSource); 
 
// Define your stage strategy 
Strategy stageStrategy = new Strategy() { 
   @NonNull 
   @Override 
   public List<LocalStageStream> stageStreamsToPublishForParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      List<LocalStageStream> streams = new ArrayList<>(); 
      streams.add(audioStream);  // Publish custom audio 
      return streams; 
   } 
 
   @Override 
   public boolean shouldPublishFromParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      return true;  // Control when to publish 
   } 
 
   @Override 
   public Stage.SubscribeType shouldSubscribeToParticipant( 
         @NonNull Stage stage, 
         @NonNull ParticipantInfo participantInfo) { 
      return Stage.SubscribeType.AUDIO_VIDEO; 
   } 
}; 
 
// Create and join the stage 
Stage stage = new Stage(context, stageToken, stageStrategy);
```

### 전체 예제: 오디오 처리 통합
<a name="custom-audio-sources-android-complete-example"></a>

다음은 오디오 처리 SDK와의 통합을 보여주는 전체 예제입니다.

```
public class AudioStreamingActivity extends AppCompatActivity { 
   private DeviceDiscovery deviceDiscovery; 
   private CustomAudioSource customAudioSource; 
   private AudioLocalStageStream audioStream; 
   private Stage stage; 
 
   @Override 
   protected void onCreate(Bundle savedInstanceState) { 
      super.onCreate(savedInstanceState); 
 
      // Configure audio manager 
      StageAudioManager.getInstance(this) 
         .setPreset(StageAudioManager.UseCasePreset.VIDEO_CHAT); 
 
      // Initialize IVS components 
      initializeIVSStage(); 
 
      // Initialize your audio processing SDK 
      initializeAudioProcessing(); 
   } 
 
   private void initializeIVSStage() { 
      deviceDiscovery = new DeviceDiscovery(this); 
 
      // Create custom audio source (48kHz stereo, 16-bit) 
      customAudioSource = deviceDiscovery.createAudioInputSource( 
         2,  // Stereo 
         BroadcastConfiguration.AudioSampleRate.RATE_48000, 
         AudioDevice.Format.INT16 
      ); 
 
      // Create audio stream 
      audioStream = new AudioLocalStageStream(customAudioSource); 
 
      // Create stage with strategy 
      Strategy strategy = new Strategy() { 
         @NonNull 
         @Override 
         public List<LocalStageStream> stageStreamsToPublishForParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return Collections.singletonList(audioStream); 
         } 
 
         @Override 
         public boolean shouldPublishFromParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return true; 
         } 
 
         @Override 
         public Stage.SubscribeType shouldSubscribeToParticipant( 
               @NonNull Stage stage, 
               @NonNull ParticipantInfo participantInfo) { 
            return Stage.SubscribeType.AUDIO_VIDEO; 
         } 
      }; 
 
      stage = new Stage(this, getStageToken(), strategy); 
   } 
 
   private void initializeAudioProcessing() { 
      // Initialize your audio processing SDK 
      // Set up callback to receive processed audio 
      yourAudioSDK.setAudioCallback(new AudioCallback() { 
         @Override 
         public void onProcessedAudio(byte[] audioData, int sampleRate, 
                                     int channels, long timestamp) { 
            // Submit processed audio to IVS Stage 
            submitAudioToStage(audioData, timestamp); 
         } 
      }); 
   } 
 
   // The timestamp is required to come from your audio source and you  
   // should not be generating one on your own, unless your audio source 
   // does not provide one. If that is the case, create your own epoch  
   // timestamp and manually calculate the duration between each sample  
   // using the number of frames and frame size. 

   private void submitAudioToStage(byte[] audioData, long timestamp) { 
      try { 
         // Allocate direct buffer 
         ByteBuffer buffer = ByteBuffer.allocateDirect(audioData.length); 
         buffer.put(audioData); 
 
         // Submit to custom audio source 
         int samplesProcessed = customAudioSource.appendBuffer( 
            buffer, 
            audioData.length, 
            timestamp > 0 ? timestamp : System.nanoTime() / 1000 
         ); 
 
         if (samplesProcessed <= 0) { 
            Log.w(TAG, "Failed to submit audio samples"); 
         } 
 
         buffer.clear(); 
      } catch (Exception e) { 
         Log.e(TAG, "Error submitting audio: " + e.getMessage(), e); 
      } 
   } 
 
   @Override 
   protected void onDestroy() { 
      super.onDestroy(); 
      if (stage != null) { 
          stage.release(); 
      } 
   } 
}
```

### 모범 사례
<a name="custom-audio-sources-android-best-practices"></a>

#### 오디오 형식 일관성
<a name="custom-audio-sources-android-best-practices-audio-format-consistency"></a>

제출하는 오디오 형식이 사용자 지정 소스를 생성할 때 지정된 형식과 일치하는지 확인합니다.

```
// If you create with 48kHz stereo INT16 
customAudioSource = deviceDiscovery.createAudioInputSource( 
   2, RATE_48000, INT16 
); 
 
// Your audio data must be: 
// - 2 channels (stereo) 
// - 48000 Hz sample rate 
// - 16-bit interleaved PCM format
```

#### 버퍼 관리
<a name="custom-audio-sources-android-best-practices-buffer-managemetn"></a>

직접 `ByteBuffers`를 사용하고 재사용하여 폐영역 회수를 최소화합니다.

```
// Allocate once 
private ByteBuffer audioBuffer = ByteBuffer.allocateDirect(BUFFER_SIZE); 
 
// Reuse in callback 
public void onAudioData(byte[] data) { 
   audioBuffer.clear(); 
   audioBuffer.put(data); 
   customAudioSource.appendBuffer(audioBuffer, data.length, getTimestamp()); 
   audioBuffer.clear(); 
}
```

#### 타이밍 및 동기화
<a name="custom-audio-sources-android-best-practices-timing-and-synchronization"></a>

원활한 오디오 재생을 위해 오디오 소스에서 제공하는 타임스탬프를 사용해야 합니다. 오디오 소스가 자체 타임스탬프를 제공하지 않는 경우 자체 에포크 타임스탬프를 생성하고 프레임 수와 프레임 크기를 사용하여 각 샘플 간의 기간을 수동으로 계산합니다.

```
// "audioFrameTimestamp" should be generated by your audio source
// Consult your audio source’s documentation for information on how to get this 
long timestamp = audioFrameTimestamp;
```

#### 오류 처리
<a name="custom-audio-sources-android-best-practices-error-handling"></a>

항상 `appendBuffer()`의 반환 값을 확인합니다.

```
int samplesProcessed = customAudioSource.appendBuffer(buffer, count, timestamp); 
 
if (samplesProcessed <= 0) { 
   Log.w(TAG, "Audio submission failed - buffer may be full or format mismatch"); 
   // Handle error: check format, reduce submission rate, etc. 
}
```