feat(player): bitstream TrueHD through the MAT/IEC 61937 carrier
Copies the path Kodi uses, and replaces nothing-but-detection with a route that actually plays (#1804). Android will not bitstream raw TrueHD on the TV routes measured here. Both connected boxes report ENCODING_DOLBY_TRUEHD as offload-only while reporting ENCODING_IEC61937 at 192kHz/7.1 as bitstream-capable. Kodi models exactly that split: it packs the carrier itself and offers "AudioTrack (IEC)" as the recommended sink, treating raw TrueHD as a fallback that it still runs at 192kHz. Media3 only ever hands Android raw TrueHD at the stream rate, which on the reporter's box takes one write and then never advances the playback head. TrueHdCarrierSink routes TrueHD onto a dedicated delegate and leaves everything else on the existing processed sink. The split is deliberate rather than enforcing that the normal processors stay inactive: the carrier is a bit-exact byte stream shaped like PCM, so a downmix, Sonic pass or silence skip turns it into full-scale noise at the receiver. A delegate built with an empty AudioProcessorChain makes that impossible by construction, instead of putting the guarantee in a different class from the thing it protects. The carrier delegate keeps OutputConfig at PCM 16-bit so media3's position, pending-data and release accounting all stay on their mature PCM path — correct here, because after packing the stream really is a fixed-rate 192kHz 8-channel carrier. Only the AudioTrack itself is switched, through the builder modifier upstream applies just before AudioTrack.Builder.build(). That avoids reimplementing AudioOutput and avoids the encoded frame-domain mismatch in androidx/media#3329. Burst timestamps come from the carrier cadence rather than from whichever access unit closed the frame; anchoring on the closing unit drifts against the time the sink derives from written frames and reports a discontinuity on nearly every frame. Availability is Kodi's test, not media3's: getMinBufferSize for the exact 192kHz/7.1 IEC tuple, plus getDirectPlaybackSupport where it exists to confirm the route will bitstream rather than quietly decode. Speed changes, downmix, normalization and 44.1kHz-family streams all decline the carrier and decode. Verified on a SEI Robotics Box R 4K Plus (Android 14, armeabi-v7a): the carrier is selected, the AudioTrack is built as IEC61937 at 192kHz/7.1, no audio decoder is instantiated, zero timestamp discontinuities, and the clock tracks wall time with no frozen samples. The same box freezes for ten seconds on raw TrueHD.
This commit is contained in:
@@ -2,6 +2,9 @@ package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.content.Context
|
||||
import android.media.AudioFormat
|
||||
import android.media.AudioManager
|
||||
import android.media.AudioTrack
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.AudioAttributes
|
||||
@@ -86,3 +89,46 @@ internal fun supportedMpvSpdifCodecs(context: Context): String {
|
||||
}
|
||||
return mpvSpdifCodecs(capabilities::supportsEncoding)
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this route can carry TrueHD as MAT inside IEC 61937 (#1804).
|
||||
*
|
||||
* This is Kodi's test, and deliberately not media3's. Kodi asks the AudioTrack layer whether it can
|
||||
* size a buffer for one exact tuple — `getMinBufferSize(rate, mask, encoding) > 0` — and gates the
|
||||
* carrier on the 192kHz/7.1 IEC combination specifically. media3 instead asks the audio policy
|
||||
* layer about the encoding, which on the boxes measured for this issue answers "TrueHD is
|
||||
* offload-capable" and says nothing about whether a raw TrueHD track will ever drain.
|
||||
*
|
||||
* Both are consulted: `getMinBufferSize` proves a track can be built, and, where the API exists,
|
||||
* `getDirectPlaybackSupport` proves the route will actually bitstream it rather than silently
|
||||
* decode or wedge.
|
||||
*/
|
||||
internal fun supportsTrueHdMatCarrier(context: Context): Boolean {
|
||||
val rate = TrueHdMatPacker.CARRIER_SAMPLE_RATE
|
||||
val mask = AudioFormat.CHANNEL_OUT_7POINT1_SURROUND
|
||||
val sizedOk = try {
|
||||
AudioTrack.getMinBufferSize(rate, mask, AudioFormat.ENCODING_IEC61937) > 0
|
||||
} catch (error: Exception) {
|
||||
false
|
||||
}
|
||||
if (!sizedOk) return false
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return true
|
||||
|
||||
return try {
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.build()
|
||||
.getPlatformAudioAttributes()
|
||||
val probe = AudioFormat.Builder()
|
||||
.setEncoding(AudioFormat.ENCODING_IEC61937)
|
||||
.setChannelMask(mask)
|
||||
.setSampleRate(rate)
|
||||
.build()
|
||||
val support = AudioManager.getDirectPlaybackSupport(probe, audioAttributes)
|
||||
(support and AudioManager.DIRECT_PLAYBACK_BITSTREAM_SUPPORTED) != 0
|
||||
} catch (error: Exception) {
|
||||
Log.w(TAG, "IEC 61937 carrier probe failed; not offering the TrueHD carrier", error)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.annotation.OptIn
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.audio.AudioProcessor
|
||||
import androidx.media3.common.audio.ChannelMixingAudioProcessor
|
||||
import androidx.media3.common.audio.ChannelMixingMatrix
|
||||
import androidx.media3.common.util.Clock
|
||||
@@ -185,7 +186,7 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
|
||||
.setAudioOutputProvider(RawPositionOutputProvider(realProvider, rawPositionUs, audioDiagnosticsLogger))
|
||||
.build()
|
||||
|
||||
return PositionFixAudioSink(
|
||||
val processedSink = PositionFixAudioSink(
|
||||
defaultSink,
|
||||
rawPositionUs,
|
||||
audioDelayUs,
|
||||
@@ -193,7 +194,89 @@ class PlezyRenderersFactory(context: Context) : DefaultRenderersFactory(context)
|
||||
onAudioCapabilitiesChanged,
|
||||
audioDiagnosticsLogger
|
||||
)
|
||||
|
||||
return TrueHdCarrierSink(
|
||||
defaultSink = processedSink,
|
||||
carrierSink = buildCarrierSink(context, bufferSizeProvider),
|
||||
carrierRouteAvailable = { supportsTrueHdMatCarrier(context) },
|
||||
directOutputBlocked = { format -> shouldBlockDirectAudioOutput?.invoke(format) == true },
|
||||
log = audioDiagnosticsLogger
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* The delegate that carries packed TrueHD (#1804).
|
||||
*
|
||||
* Deliberately separate from the processed sink, and deliberately barren: an empty
|
||||
* [DefaultAudioSink.AudioProcessorChain] means no downmix, no Sonic, no silence skipping and no
|
||||
* float conversion can ever touch the bytes. The carrier only looks like PCM; a single mutated
|
||||
* sample reaches the receiver as full-scale noise rather than as a glitch.
|
||||
*
|
||||
* The output provider keeps `OutputConfig.encoding` at PCM 16-bit so every position, pending-data
|
||||
* and release accounting in media3 stays in its mature PCM path — which is correct here, because
|
||||
* after packing the stream genuinely is a fixed-rate 192kHz 8-channel carrier. Only the
|
||||
* `AudioTrack` itself is switched to `ENCODING_IEC61937`, via the builder modifier that upstream
|
||||
* applies immediately before `AudioTrack.Builder.build()`.
|
||||
*/
|
||||
private fun buildCarrierSink(
|
||||
context: Context,
|
||||
bufferSizeProvider: DefaultAudioTrackBufferSizeProvider
|
||||
): AudioSink {
|
||||
val provider = AudioTrackAudioOutputProvider.Builder(context)
|
||||
.setAudioTrackBufferSizeProvider(bufferSizeProvider)
|
||||
.apply {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
setAudioTrackBuilderModifier { builder, config ->
|
||||
builder.setAudioFormat(
|
||||
android.media.AudioFormat.Builder()
|
||||
.setEncoding(android.media.AudioFormat.ENCODING_IEC61937)
|
||||
.setChannelMask(android.media.AudioFormat.CHANNEL_OUT_7POINT1_SURROUND)
|
||||
.setSampleRate(config.sampleRate)
|
||||
.build()
|
||||
)
|
||||
// The AudioTrackConfig media3 reports upstream is built from OutputConfig, which stays
|
||||
// PCM so the accounting stays in the PCM domain — so it cannot show the real encoding.
|
||||
// This is the only place that can confirm what the AudioTrack was actually built as.
|
||||
audioDiagnosticsLogger?.invoke(
|
||||
"info",
|
||||
"audio",
|
||||
"Carrier AudioTrack: encoding=IEC61937 rate=${config.sampleRate} " +
|
||||
"mask=0x${android.media.AudioFormat.CHANNEL_OUT_7POINT1_SURROUND.toString(16)} " +
|
||||
"buffer=${config.bufferSize}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
return DefaultAudioSink.Builder(context)
|
||||
.setEnableFloatOutput(false)
|
||||
// Not DefaultAudioProcessorChain: that one appends silence-skipping and Sonic whatever it is
|
||||
// constructed with, and either would rewrite carrier bytes. This chain has nothing in it, so
|
||||
// AudioProcessingPipeline is a straight passthrough. Speed is reported as applied without
|
||||
// resampling because the carrier delegate routes playback parameters to the AudioTrack, and
|
||||
// the sink refuses the carrier outright at any speed other than 1.0x.
|
||||
.setAudioProcessorChain(EmptyAudioProcessorChain())
|
||||
.setAudioOutputProvider(provider)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An [AudioProcessorChain] that owns no processors, for the MAT carrier delegate (#1804).
|
||||
*
|
||||
* `DefaultAudioProcessorChain` always contributes silence-skipping and Sonic; both rewrite samples,
|
||||
* which is fatal to a bit-exact IEC 61937 carrier. Speed is reported back unchanged because the
|
||||
* carrier delegate applies playback parameters at the AudioTrack, and the routing sink declines the
|
||||
* carrier entirely at any speed other than 1.0x.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
private class EmptyAudioProcessorChain : DefaultAudioSink.AudioProcessorChain {
|
||||
override fun getAudioProcessors(): Array<AudioProcessor> = emptyArray()
|
||||
override fun applyPlaybackParameters(playbackParameters: PlaybackParameters): PlaybackParameters = playbackParameters
|
||||
override fun applySkipSilenceEnabled(skipSilenceEnabled: Boolean): Boolean = false
|
||||
override fun getMediaDuration(playoutDuration: Long): Long = playoutDuration
|
||||
override fun getSkippedOutputFrameCount(): Long = 0L
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
package com.edde746.plezy.exoplayer
|
||||
|
||||
import android.media.AudioDeviceInfo
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.AuxEffectInfo
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.Format
|
||||
import androidx.media3.common.MimeTypes
|
||||
import androidx.media3.common.PlaybackParameters
|
||||
import androidx.media3.common.util.Clock
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.analytics.PlayerId
|
||||
import androidx.media3.exoplayer.audio.AudioSink
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
/**
|
||||
* Routes Dolby TrueHD through a MAT/IEC 61937 carrier, and everything else through the normal sink
|
||||
* (#1804).
|
||||
*
|
||||
* Android will not bitstream raw TrueHD on the TV routes measured for this issue: the platform
|
||||
* reports `ENCODING_DOLBY_TRUEHD` as offload-only while reporting `ENCODING_IEC61937` at 192kHz/7.1
|
||||
* as bitstream-capable. Kodi models the same split and packs the carrier itself; media3 only ever
|
||||
* hands Android raw TrueHD, at the stream rate. This sink adds the missing path.
|
||||
*
|
||||
* **Why two delegates rather than one sink with the processors held inactive.** The carrier is a
|
||||
* bit-exact byte stream that happens to be shaped like PCM. Any sample mutation — downmix, Sonic,
|
||||
* silence skipping, float conversion — turns it into full-scale noise at the receiver. Keeping the
|
||||
* carrier on a delegate built with an empty [AudioProcessorChain] makes that impossible by
|
||||
* construction. The alternative, asserting the normal chain stays inactive, puts the guarantee in a
|
||||
* different class from the thing it protects, so adding a processor later would silently break
|
||||
* playback in the loudest possible way.
|
||||
*
|
||||
* [carrierSink] must therefore be built with no audio processors and with an output provider whose
|
||||
* `AudioTrack` is forced to `ENCODING_IEC61937`; see [PlezyRenderersFactory].
|
||||
*
|
||||
* Selection happens only in [configure]. Persistent controls and the listener are mirrored to both
|
||||
* delegates so either can be activated later; per-stream calls go to the active one alone.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
internal class TrueHdCarrierSink(
|
||||
private val defaultSink: AudioSink,
|
||||
private val carrierSink: AudioSink,
|
||||
/** Whether the current route can bitstream the carrier tuple. Evaluated per format. */
|
||||
private val carrierRouteAvailable: () -> Boolean,
|
||||
/** Whether policy currently forbids bitstreaming at all (downmix, normalization, user setting). */
|
||||
private val directOutputBlocked: (Format) -> Boolean,
|
||||
private val log: ((String, String, String) -> Unit)? = null
|
||||
) : AudioSink {
|
||||
|
||||
private companion object {
|
||||
/** One MAT frame is exactly one carrier period: 3840 frames at 192kHz. */
|
||||
const val CARRIER_BURST_DURATION_US =
|
||||
TrueHdMatPacker.MAT_PKT_OFFSET.toLong() / TrueHdMatPacker.CARRIER_BYTES_PER_FRAME *
|
||||
1_000_000L / TrueHdMatPacker.CARRIER_SAMPLE_RATE
|
||||
}
|
||||
|
||||
private val packer = TrueHdMatPacker()
|
||||
|
||||
private var active: AudioSink = defaultSink
|
||||
private var carrierActive = false
|
||||
|
||||
/** A burst the delegate refused; it must be placed before any further input is consumed. */
|
||||
private var pendingBurst: ByteBuffer? = null
|
||||
private var pendingBurstTimeUs: Long = 0
|
||||
|
||||
/**
|
||||
* Anchor for carrier timestamps, and how many bursts have been emitted since it.
|
||||
*
|
||||
* Each MAT frame is exactly one carrier period of audio, so timestamps are derived from the
|
||||
* cadence rather than from whichever access unit happened to close the frame. Handing the sink
|
||||
* the closing unit's own presentation time drifts against the time it derives from written
|
||||
* frames, which it reports as a discontinuity on nearly every frame.
|
||||
*/
|
||||
private var carrierAnchorUs: Long = C.TIME_UNSET
|
||||
private var burstsSinceAnchor: Long = 0
|
||||
|
||||
private var playbackParameters: PlaybackParameters = PlaybackParameters.DEFAULT
|
||||
|
||||
// --- Selection ---
|
||||
|
||||
/**
|
||||
* True when this format should ride the carrier.
|
||||
*
|
||||
* Speed changes are excluded deliberately: a bitstream cannot be resampled, so anything other
|
||||
* than 1.0x has to decode. The same is true of the existing downmix/normalization blocks, which
|
||||
* [directOutputBlocked] already reports.
|
||||
*/
|
||||
private fun shouldUseCarrier(format: Format): Boolean {
|
||||
if (format.sampleMimeType != MimeTypes.AUDIO_TRUEHD) return false
|
||||
if (packer.unsupportedRateFamily) return false
|
||||
if (playbackParameters.speed != 1f) return false
|
||||
if (directOutputBlocked(format)) return false
|
||||
return carrierRouteAvailable()
|
||||
}
|
||||
|
||||
/** The PCM-shaped format the carrier delegate is configured with. */
|
||||
private fun carrierFormat(): Format = Format.Builder()
|
||||
.setSampleMimeType(MimeTypes.AUDIO_RAW)
|
||||
.setPcmEncoding(C.ENCODING_PCM_16BIT)
|
||||
.setChannelCount(TrueHdMatPacker.CARRIER_CHANNEL_COUNT)
|
||||
.setSampleRate(TrueHdMatPacker.CARRIER_SAMPLE_RATE)
|
||||
.build()
|
||||
|
||||
override fun supportsFormat(format: Format): Boolean =
|
||||
if (shouldUseCarrier(format)) true else defaultSink.supportsFormat(format)
|
||||
|
||||
override fun getFormatSupport(format: Format): Int =
|
||||
if (shouldUseCarrier(format)) AudioSink.SINK_FORMAT_SUPPORTED_DIRECTLY else defaultSink.getFormatSupport(format)
|
||||
|
||||
override fun configure(inputFormat: Format, specifiedBufferSize: Int, outputChannels: IntArray?) {
|
||||
val useCarrier = shouldUseCarrier(inputFormat)
|
||||
if (useCarrier != carrierActive) {
|
||||
log?.invoke(
|
||||
"info",
|
||||
"audio",
|
||||
if (useCarrier) {
|
||||
"TrueHD via MAT/IEC 61937 carrier at ${TrueHdMatPacker.CARRIER_SAMPLE_RATE}Hz/" +
|
||||
"${TrueHdMatPacker.CARRIER_CHANNEL_COUNT}ch"
|
||||
} else {
|
||||
"Leaving the MAT carrier; audio returns to the normal sink"
|
||||
}
|
||||
)
|
||||
}
|
||||
carrierActive = useCarrier
|
||||
active = if (useCarrier) carrierSink else defaultSink
|
||||
discardCarrierState()
|
||||
|
||||
if (useCarrier) {
|
||||
carrierSink.configure(carrierFormat(), specifiedBufferSize, null)
|
||||
} else {
|
||||
defaultSink.configure(inputFormat, specifiedBufferSize, outputChannels)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Stream path: active delegate only ---
|
||||
|
||||
override fun handleBuffer(buffer: ByteBuffer, presentationTimeUs: Long, encodedAccessUnitCount: Int): Boolean {
|
||||
if (!carrierActive) return defaultSink.handleBuffer(buffer, presentationTimeUs, encodedAccessUnitCount)
|
||||
|
||||
// A refused burst blocks everything: placing it must come before consuming more input, or the
|
||||
// carrier would lose a frame.
|
||||
pendingBurst?.let { burst ->
|
||||
if (!carrierSink.handleBuffer(burst, pendingBurstTimeUs, 1)) return false
|
||||
pendingBurst = null
|
||||
}
|
||||
if (!buffer.hasRemaining()) return true
|
||||
|
||||
val sample = ByteArray(buffer.remaining())
|
||||
buffer.duplicate().get(sample)
|
||||
if (carrierAnchorUs == C.TIME_UNSET) carrierAnchorUs = presentationTimeUs
|
||||
|
||||
var offset = 0
|
||||
while (offset < sample.size) {
|
||||
val length = TrueHdMatPacker.accessUnitLength(sample, offset, sample.size)
|
||||
if (length == 0) break
|
||||
val burst = packer.packAccessUnit(sample, offset, length)
|
||||
offset += length
|
||||
if (burst == null) continue
|
||||
|
||||
val burstTimeUs = carrierAnchorUs + burstsSinceAnchor * CARRIER_BURST_DURATION_US
|
||||
burstsSinceAnchor++
|
||||
if (!carrierSink.handleBuffer(burst, burstTimeUs, 1)) {
|
||||
pendingBurst = burst
|
||||
pendingBurstTimeUs = burstTimeUs
|
||||
break
|
||||
}
|
||||
}
|
||||
if (packer.unsupportedRateFamily) {
|
||||
log?.invoke("info", "audio", "TrueHD is 44.1kHz-family; the MAT carrier does not cover it, decoding instead")
|
||||
}
|
||||
|
||||
// The sample is consumed either way: a stashed burst is placed on the next call.
|
||||
buffer.position(buffer.limit())
|
||||
return true
|
||||
}
|
||||
|
||||
override fun getCurrentPositionUs(sourceEnded: Boolean): Long = active.getCurrentPositionUs(sourceEnded)
|
||||
|
||||
override fun playToEndOfStream() {
|
||||
// A partially filled MAT frame cannot be emitted; up to 20ms is dropped at the end of a stream.
|
||||
active.playToEndOfStream()
|
||||
}
|
||||
|
||||
override fun isEnded(): Boolean = active.isEnded()
|
||||
|
||||
override fun hasPendingData(): Boolean = pendingBurst != null || active.hasPendingData()
|
||||
|
||||
override fun handleDiscontinuity() {
|
||||
discardCarrierState()
|
||||
active.handleDiscontinuity()
|
||||
}
|
||||
|
||||
override fun play() = active.play()
|
||||
|
||||
override fun pause() = active.pause()
|
||||
|
||||
override fun flush() {
|
||||
discardCarrierState()
|
||||
active.flush()
|
||||
}
|
||||
|
||||
override fun getAudioTrackBufferSizeUs(): Long = active.getAudioTrackBufferSizeUs()
|
||||
|
||||
private fun discardCarrierState() {
|
||||
packer.reset()
|
||||
pendingBurst = null
|
||||
// Re-anchor on the next burst: after a seek the carrier restarts from a new media time.
|
||||
carrierAnchorUs = C.TIME_UNSET
|
||||
burstsSinceAnchor = 0
|
||||
}
|
||||
|
||||
// --- Persistent state: mirrored, so either delegate can be activated later ---
|
||||
|
||||
override fun setListener(listener: AudioSink.Listener) {
|
||||
defaultSink.setListener(listener)
|
||||
carrierSink.setListener(listener)
|
||||
}
|
||||
|
||||
override fun setPlayerId(playerId: PlayerId?) {
|
||||
defaultSink.setPlayerId(playerId)
|
||||
carrierSink.setPlayerId(playerId)
|
||||
}
|
||||
|
||||
override fun setClock(clock: Clock) {
|
||||
defaultSink.setClock(clock)
|
||||
carrierSink.setClock(clock)
|
||||
}
|
||||
|
||||
override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
|
||||
this.playbackParameters = playbackParameters
|
||||
defaultSink.setPlaybackParameters(playbackParameters)
|
||||
carrierSink.setPlaybackParameters(playbackParameters)
|
||||
}
|
||||
|
||||
override fun getPlaybackParameters(): PlaybackParameters = active.getPlaybackParameters()
|
||||
|
||||
override fun setSkipSilenceEnabled(skipSilenceEnabled: Boolean) {
|
||||
// Only reaches the normal sink: the carrier delegate has no processors to skip silence with,
|
||||
// and dropping "silent" carrier bytes would break the frame cadence.
|
||||
defaultSink.setSkipSilenceEnabled(skipSilenceEnabled)
|
||||
}
|
||||
|
||||
override fun getSkipSilenceEnabled(): Boolean = defaultSink.getSkipSilenceEnabled()
|
||||
|
||||
override fun setAudioAttributes(audioAttributes: AudioAttributes) {
|
||||
defaultSink.setAudioAttributes(audioAttributes)
|
||||
carrierSink.setAudioAttributes(audioAttributes)
|
||||
}
|
||||
|
||||
override fun getAudioAttributes(): AudioAttributes? = active.audioAttributes
|
||||
|
||||
override fun setAudioSessionId(audioSessionId: Int) {
|
||||
defaultSink.setAudioSessionId(audioSessionId)
|
||||
carrierSink.setAudioSessionId(audioSessionId)
|
||||
}
|
||||
|
||||
override fun setAuxEffectInfo(auxEffectInfo: AuxEffectInfo) {
|
||||
defaultSink.setAuxEffectInfo(auxEffectInfo)
|
||||
carrierSink.setAuxEffectInfo(auxEffectInfo)
|
||||
}
|
||||
|
||||
override fun setPreferredDevice(audioDeviceInfo: AudioDeviceInfo?) {
|
||||
defaultSink.setPreferredDevice(audioDeviceInfo)
|
||||
carrierSink.setPreferredDevice(audioDeviceInfo)
|
||||
}
|
||||
|
||||
override fun setOutputStreamOffsetUs(outputStreamOffsetUs: Long) {
|
||||
defaultSink.setOutputStreamOffsetUs(outputStreamOffsetUs)
|
||||
carrierSink.setOutputStreamOffsetUs(outputStreamOffsetUs)
|
||||
}
|
||||
|
||||
override fun setVolume(volume: Float) {
|
||||
defaultSink.setVolume(volume)
|
||||
carrierSink.setVolume(volume)
|
||||
}
|
||||
|
||||
override fun enableTunnelingV21() {
|
||||
// Tunneling is a decoded-PCM/video-sync arrangement; the carrier never uses it.
|
||||
defaultSink.enableTunnelingV21()
|
||||
}
|
||||
|
||||
override fun disableTunneling() {
|
||||
defaultSink.disableTunneling()
|
||||
}
|
||||
|
||||
override fun reset() {
|
||||
discardCarrierState()
|
||||
defaultSink.reset()
|
||||
carrierSink.reset()
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
discardCarrierState()
|
||||
defaultSink.release()
|
||||
carrierSink.release()
|
||||
}
|
||||
}
|
||||
@@ -20,9 +20,8 @@ import org.junit.Test
|
||||
*/
|
||||
class TrueHdMatPackerTest {
|
||||
|
||||
private fun resource(name: String): ByteArray =
|
||||
checkNotNull(javaClass.classLoader?.getResourceAsStream(name)) { "missing fixture $name" }
|
||||
.use { it.readBytes() }
|
||||
private fun resource(name: String): ByteArray = checkNotNull(javaClass.classLoader?.getResourceAsStream(name)) { "missing fixture $name" }
|
||||
.use { it.readBytes() }
|
||||
|
||||
private val accessUnits by lazy { resource("truehd_access_units.bin") }
|
||||
private val golden by lazy { resource("truehd_iec61937_golden.bin") }
|
||||
@@ -116,7 +115,10 @@ class TrueHdMatPackerTest {
|
||||
while (offset < accessUnits.size) {
|
||||
val length = TrueHdMatPacker.accessUnitLength(accessUnits, offset, accessUnits.size)
|
||||
if (length == 0) break
|
||||
packer.packAccessUnit(accessUnits, offset, length)?.let { seen[it] = true; bursts++ }
|
||||
packer.packAccessUnit(accessUnits, offset, length)?.let {
|
||||
seen[it] = true
|
||||
bursts++
|
||||
}
|
||||
offset += length
|
||||
}
|
||||
assertTrue("expected several bursts, saw $bursts", bursts > 2)
|
||||
@@ -150,6 +152,5 @@ class TrueHdMatPackerTest {
|
||||
}
|
||||
}
|
||||
|
||||
private fun readLittleEndianShort(data: ByteArray, offset: Int): Int =
|
||||
(data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||
private fun readLittleEndianShort(data: ByteArray, offset: Int): Int = (data[offset].toInt() and 0xFF) or ((data[offset + 1].toInt() and 0xFF) shl 8)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user