public final class

OggFileAudioBufferSink

extends ForwardingAudioSink

 java.lang.Object

androidx.media3.exoplayer.audio.ForwardingAudioSink

↳androidx.media3.test.utils.OggFileAudioBufferSink

Gradle dependencies

compile group: 'androidx.media3', name: 'media3-test-utils', version: '1.5.0-alpha01'

  • groupId: androidx.media3
  • artifactId: media3-test-utils
  • version: 1.5.0-alpha01

Artifact androidx.media3:media3-test-utils:1.5.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

A sink for audio buffers that writes output audio as .ogg files with a given path prefix. When new audio data is handled after flushing the audio packetizer, a counter is incremented and its value is appended to the output file name.

Note: if writing to external storage it's necessary to grant the WRITE_EXTERNAL_STORAGE permission.

Summary

Fields
public static final intSAMPLE_RATE

Opus streams are always 48000 Hz.

Constructors
publicOggFileAudioBufferSink(AudioSink audioSink, java.lang.String outputFileNamePrefix)

Creates an instance.

Methods
public voidflush()

public booleanhandleBuffer(java.nio.ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount)

public voidreset()

from ForwardingAudioSinkconfigure, disableTunneling, enableTunnelingV21, getAudioAttributes, getCurrentPositionUs, getFormatOffloadSupport, getFormatSupport, getPlaybackParameters, getSkipSilenceEnabled, handleDiscontinuity, hasPendingData, isEnded, pause, play, playToEndOfStream, release, setAudioAttributes, setAudioSessionId, setAuxEffectInfo, setClock, setListener, setOffloadDelayPadding, setOffloadMode, setOutputStreamOffsetUs, setPlaybackParameters, setPlayerId, setPreferredDevice, setSkipSilenceEnabled, setVolume, supportsFormat
from java.lang.Objectclone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait

Fields

public static final int SAMPLE_RATE

Opus streams are always 48000 Hz.

Constructors

public OggFileAudioBufferSink(AudioSink audioSink, java.lang.String outputFileNamePrefix)

Creates an instance.

Parameters:

audioSink: The base audioSink calls are forwarded to.
outputFileNamePrefix: The prefix for output files.

Methods

public void flush()

public void reset()

public boolean handleBuffer(java.nio.ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount)

Source

/*
 * Copyright (C) 2023 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package androidx.media3.test.utils;

import static java.lang.Math.min;

import android.os.Environment;
import androidx.annotation.Nullable;
import androidx.media3.common.util.Assertions;
import androidx.media3.common.util.Log;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.audio.ForwardingAudioSink;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

/**
 * A sink for audio buffers that writes output audio as .ogg files with a given path prefix. When
 * new audio data is handled after flushing the audio packetizer, a counter is incremented and its
 * value is appended to the output file name.
 *
 * <p>Note: if writing to external storage it's necessary to grant the {@code
 * WRITE_EXTERNAL_STORAGE} permission.
 */
@UnstableApi
public final class OggFileAudioBufferSink extends ForwardingAudioSink {

  /** Opus streams are always 48000 Hz. */
  public static final int SAMPLE_RATE = 48_000;

  private static final String TAG = "OggFileAudioBufferSink";
  private static final int OGG_ID_HEADER_LENGTH = 47;
  private static final int OGG_COMMENT_HEADER_LENGTH = 52;

  private final byte[] scratchBuffer;
  private final ByteBuffer scratchByteBuffer;
  private final String outputFileNamePrefix;

  @Nullable private RandomAccessFile randomAccessFile;
  private int counter;

  /**
   * Creates an instance.
   *
   * @param audioSink The base audioSink calls are forwarded to.
   * @param outputFileNamePrefix The prefix for output files.
   */
  public OggFileAudioBufferSink(AudioSink audioSink, String outputFileNamePrefix) {
    super(audioSink);
    this.outputFileNamePrefix = outputFileNamePrefix;
    counter = 0;
    scratchBuffer = new byte[1024];
    scratchByteBuffer = ByteBuffer.wrap(scratchBuffer).order(ByteOrder.LITTLE_ENDIAN);
  }

  @Override
  public void flush() {
    super.flush();
    try {
      resetInternal();
    } catch (IOException e) {
      Log.e(TAG, "Error resetting", e);
    }
  }

  @Override
  public void reset() {
    super.reset();
    try {
      resetInternal();
    } catch (IOException e) {
      Log.e(TAG, "Error resetting", e);
    }
  }

  @Override
  public boolean handleBuffer(
      ByteBuffer buffer, long presentationTimeUs, int encodedAccessUnitCount)
      throws InitializationException, WriteException {
    handleBuffer(buffer);
    return super.handleBuffer(buffer, presentationTimeUs, encodedAccessUnitCount);
  }

  private void handleBuffer(ByteBuffer buffer) {
    try {
      maybePrepareFile();
      writeBuffer(buffer);
    } catch (IOException e) {
      Log.e(TAG, "Error writing data", e);
    }
  }

  private void maybePrepareFile() throws IOException {
    if (randomAccessFile != null) {
      return;
    }
    RandomAccessFile randomAccessFile = new RandomAccessFile(getNextOutputFileName(), "rw");
    scratchByteBuffer.clear();
    writeIdHeaderPacket();
    writeCommentHeaderPacket();
    randomAccessFile.write(scratchBuffer, 0, scratchByteBuffer.position());
    this.randomAccessFile = randomAccessFile;
  }

  private void writeOggPacketHeader(int pageSequenceNumber, boolean isIdHeaderPacket) {
    // Capture Pattern for Page [OggS]
    scratchByteBuffer.put((byte) 'O');
    scratchByteBuffer.put((byte) 'g');
    scratchByteBuffer.put((byte) 'g');
    scratchByteBuffer.put((byte) 'S');

    // StreamStructure Version
    scratchByteBuffer.put((byte) 0);

    // header-type
    scratchByteBuffer.put(isIdHeaderPacket ? (byte) 0x02 : (byte) 0x00);

    // granule_position
    scratchByteBuffer.putLong((long) 0);

    // bitstream_serial_number
    scratchByteBuffer.putInt(0);

    // page_sequence_number
    scratchByteBuffer.putInt(pageSequenceNumber);

    // CRC_checksum
    scratchByteBuffer.putInt(0);

    // number_page_segments
    scratchByteBuffer.put((byte) 1);
  }

  private void writeIdHeaderPacket() {
    // Id Header
    writeOggPacketHeader(/* pageSequenceNumber= */ 0, /* isIdHeaderPacket= */ true);

    // Payload Size = 19
    scratchByteBuffer.put((byte) 19);

    // OggOpus Id Header Capture Pattern 8
    scratchByteBuffer.put((byte) 'O');
    scratchByteBuffer.put((byte) 'p');
    scratchByteBuffer.put((byte) 'u');
    scratchByteBuffer.put((byte) 's');
    scratchByteBuffer.put((byte) 'H');
    scratchByteBuffer.put((byte) 'e');
    scratchByteBuffer.put((byte) 'a');
    scratchByteBuffer.put((byte) 'd');

    // version
    scratchByteBuffer.put((byte) 1);

    // output channel count
    scratchByteBuffer.put((byte) 2);

    // pre-skip
    scratchByteBuffer.putShort((short) 312);

    // input sample rate
    scratchByteBuffer.putInt(SAMPLE_RATE);

    // Output Gain
    scratchByteBuffer.putShort((short) 0);

    // channel mapping family
    scratchByteBuffer.put((byte) 0);

    int checksum =
        Util.crc32(scratchBuffer, /* start= */ 0, OGG_ID_HEADER_LENGTH, /* initialValue= */ 0);
    scratchByteBuffer.putInt(/* index= */ 22, checksum);
    scratchByteBuffer.position(OGG_ID_HEADER_LENGTH);
  }

  private void writeCommentHeaderPacket() {
    // Id Header
    writeOggPacketHeader(/* pageSequenceNumber= */ 1, /* isIdHeaderPacket= */ false);

    // Payload Size = 24
    scratchByteBuffer.put((byte) 24);

    // Comment Header Opus Capture Pattern 8
    scratchByteBuffer.put((byte) 'O');
    scratchByteBuffer.put((byte) 'p');
    scratchByteBuffer.put((byte) 'u');
    scratchByteBuffer.put((byte) 's');
    scratchByteBuffer.put((byte) 'T');
    scratchByteBuffer.put((byte) 'a');
    scratchByteBuffer.put((byte) 'g');
    scratchByteBuffer.put((byte) 's');

    // Vendor Comment String Length
    scratchByteBuffer.putInt(8);

    // Vendor Comment String
    scratchByteBuffer.put((byte) 'G');
    scratchByteBuffer.put((byte) 'o');
    scratchByteBuffer.put((byte) 'o');
    scratchByteBuffer.put((byte) 'g');
    scratchByteBuffer.put((byte) 'l');
    scratchByteBuffer.put((byte) 'e');
    scratchByteBuffer.put((byte) 'r');
    scratchByteBuffer.put((byte) 's');

    // UserCommentList Length
    scratchByteBuffer.putInt(0);

    int checksum =
        Util.crc32(
            scratchBuffer,
            OGG_ID_HEADER_LENGTH,
            OGG_ID_HEADER_LENGTH + OGG_COMMENT_HEADER_LENGTH,
            /* initialValue= */ 0);

    scratchByteBuffer.putInt(/* index= */ 69, checksum);

    scratchByteBuffer.position(OGG_ID_HEADER_LENGTH + OGG_COMMENT_HEADER_LENGTH);
  }

  private void writeBuffer(ByteBuffer buffer) throws IOException {
    RandomAccessFile randomAccessFile = Assertions.checkNotNull(this.randomAccessFile);
    while (buffer.hasRemaining()) {
      int bytesToWrite = min(buffer.remaining(), scratchBuffer.length);
      buffer.get(scratchBuffer, /* offset= */ 0, bytesToWrite);
      randomAccessFile.write(scratchBuffer, /* off= */ 0, bytesToWrite);
    }
  }

  private void resetInternal() throws IOException {
    @Nullable RandomAccessFile randomAccessFile = this.randomAccessFile;
    if (randomAccessFile == null) {
      return;
    }

    try {
      randomAccessFile.close();
    } finally {
      this.randomAccessFile = null;
    }
  }

  private String getNextOutputFileName() {
    return Util.formatInvariant(
        "%s/%s-%04d.ogg",
        Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC)
            .getAbsolutePath(),
        outputFileNamePrefix,
        counter++);
  }
}