Add general-purpose overflow-resistant divide+multiply util method

This is equivalent to the existing `scaleLargeTimestamp` method with the
following changes/improvements:
* No longer specific to timestamps (there was nothing inherently
  time-specific about the logic in `scaleLargeTimestamp`, but the name
  and docs suggested it shouldn't be used for non-timestamp use-cases).
* Additional 'perfect division' checks between `value` and `divisor`.
* The caller can now provide a `RoundingMode`.
* Robust against `multiplier == 0`.
* Some extra branches before falling through to (potentially lossy)
  floating-point math, including trying to simplify the fraction with
  greatest common divisor to reduce the chance of overflowing `long`.

This was discussed during review of 6e91f0d4c5

This change also includes some golden test file updates - these
represent a bug fix where floating-point maths had previously resulted
in a timestamp being incorrectly rounded down to the previous
microsecond. These changes are due to the 'some more branches' mentioned
above.

PiperOrigin-RevId: 564760748
This commit is contained in:
ibaker 2023-09-12 10:20:42 -07:00 committed by Copybara-Service
parent 320a45f7d6
commit 885ddb167e
26 changed files with 559 additions and 312 deletions

View File

@ -91,6 +91,8 @@ import androidx.media3.common.Player;
import androidx.media3.common.Player.Commands;
import com.google.common.base.Ascii;
import com.google.common.base.Charsets;
import com.google.common.math.DoubleMath;
import com.google.common.math.LongMath;
import com.google.common.util.concurrent.AsyncFunction;
import com.google.common.util.concurrent.Futures;
import com.google.common.util.concurrent.ListenableFuture;
@ -103,6 +105,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayDeque;
@ -1528,7 +1531,7 @@ public final class Util {
*/
@UnstableApi
public static long sampleCountToDurationUs(long sampleCount, int sampleRate) {
return (sampleCount * C.MICROS_PER_SECOND) / sampleRate;
return scaleLargeValue(sampleCount, C.MICROS_PER_SECOND, sampleRate, RoundingMode.FLOOR);
}
/**
@ -1545,7 +1548,7 @@ public final class Util {
*/
@UnstableApi
public static long durationUsToSampleCount(long durationUs, int sampleRate) {
return Util.ceilDivide(durationUs * sampleRate, C.MICROS_PER_SECOND);
return scaleLargeValue(durationUs, sampleRate, C.MICROS_PER_SECOND, RoundingMode.CEILING);
}
/**
@ -1638,11 +1641,213 @@ public final class Util {
return time;
}
/**
* Scales a large value by a multiplier and a divisor.
*
* <p>The order of operations in this implementation is designed to minimize the probability of
* overflow. The implementation tries to stay in integer arithmetic as long as possible, but falls
* through to floating-point arithmetic if the values can't be combined without overflowing signed
* 64-bit longs.
*
* <p>If the mathematical result would overflow or underflow a 64-bit long, the result will be
* either {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}, respectively.
*
* @param value The value to scale.
* @param multiplier The multiplier.
* @param divisor The divisor.
* @param roundingMode The rounding mode to use if the result of the division is not an integer.
* @return The scaled value.
*/
// LongMath.saturatedMultiply is @Beta in the version of Guava we currently depend on (31.1)
// but it is no longer @Beta from 32.0.0. This suppression is therefore safe because there's
// no version of Guava after 31.1 that doesn't contain this symbol.
// TODO(b/290045069): Remove this suppression when we depend on Guava 32+.
@SuppressWarnings("UnstableApiUsage")
@UnstableApi
public static long scaleLargeValue(
long value, long multiplier, long divisor, RoundingMode roundingMode) {
if (value == 0 || multiplier == 0) {
return 0;
}
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = LongMath.divide(divisor, multiplier, RoundingMode.UNNECESSARY);
return LongMath.divide(value, divisionFactor, roundingMode);
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = LongMath.divide(multiplier, divisor, RoundingMode.UNNECESSARY);
return LongMath.saturatedMultiply(value, multiplicationFactor);
} else if (divisor >= value && (divisor % value) == 0) {
long divisionFactor = LongMath.divide(divisor, value, RoundingMode.UNNECESSARY);
return LongMath.divide(multiplier, divisionFactor, roundingMode);
} else if (divisor < value && (value % divisor) == 0) {
long multiplicationFactor = LongMath.divide(value, divisor, RoundingMode.UNNECESSARY);
return LongMath.saturatedMultiply(multiplier, multiplicationFactor);
} else {
return scaleLargeValueFallback(value, multiplier, divisor, roundingMode);
}
}
/**
* Applies {@link #scaleLargeValue(long, long, long, RoundingMode)} to a list of unscaled values.
*
* @param values The values to scale.
* @param multiplier The multiplier.
* @param divisor The divisor.
* @param roundingMode The rounding mode to use if the result of the division is not an integer.
* @return The scaled values.
*/
// LongMath.saturatedMultiply is @Beta in the version of Guava we currently depend on (31.1)
// but it is no longer @Beta from 32.0.0. This suppression is therefore safe because there's
// no version of Guava after 31.1 that doesn't contain this symbol.
// TODO(b/290045069): Remove this suppression when we depend on Guava 32+.
@SuppressWarnings("UnstableApiUsage")
@UnstableApi
public static long[] scaleLargeValues(
List<Long> values, long multiplier, long divisor, RoundingMode roundingMode) {
long[] result = new long[values.size()];
if (multiplier == 0) {
// Array is initialized with all zeroes by default.
return result;
}
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = LongMath.divide(divisor, multiplier, RoundingMode.UNNECESSARY);
for (int i = 0; i < result.length; i++) {
result[i] = LongMath.divide(values.get(i), divisionFactor, roundingMode);
}
return result;
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = LongMath.divide(multiplier, divisor, RoundingMode.UNNECESSARY);
for (int i = 0; i < result.length; i++) {
result[i] = LongMath.saturatedMultiply(values.get(i), multiplicationFactor);
}
return result;
} else {
for (int i = 0; i < result.length; i++) {
long value = values.get(i);
if (value == 0) {
// Array is initialized with all zeroes by default.
continue;
}
if (divisor >= value && (divisor % value) == 0) {
long divisionFactor = LongMath.divide(divisor, value, RoundingMode.UNNECESSARY);
result[i] = LongMath.divide(multiplier, divisionFactor, roundingMode);
} else if (divisor < value && (value % divisor) == 0) {
long multiplicationFactor = LongMath.divide(value, divisor, RoundingMode.UNNECESSARY);
result[i] = LongMath.saturatedMultiply(multiplier, multiplicationFactor);
} else {
result[i] = scaleLargeValueFallback(value, multiplier, divisor, roundingMode);
}
}
return result;
}
}
/**
* Applies {@link #scaleLargeValue(long, long, long, RoundingMode)} to an array of unscaled
* values.
*
* @param values The values to scale.
* @param multiplier The multiplier.
* @param divisor The divisor.
* @param roundingMode The rounding mode to use if the result of the division is not an integer.
*/
@UnstableApi
public static void scaleLargeValuesInPlace(
long[] values, long multiplier, long divisor, RoundingMode roundingMode) {
if (multiplier == 0) {
Arrays.fill(values, 0);
return;
}
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = LongMath.divide(divisor, multiplier, RoundingMode.UNNECESSARY);
for (int i = 0; i < values.length; i++) {
values[i] = LongMath.divide(values[i], divisionFactor, roundingMode);
}
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = LongMath.divide(multiplier, divisor, RoundingMode.UNNECESSARY);
for (int i = 0; i < values.length; i++) {
values[i] = LongMath.saturatedMultiply(values[i], multiplicationFactor);
}
} else {
for (int i = 0; i < values.length; i++) {
if (values[i] == 0) {
continue;
}
if (divisor >= values[i] && (divisor % values[i]) == 0) {
long divisionFactor = LongMath.divide(divisor, values[i], RoundingMode.UNNECESSARY);
values[i] = LongMath.divide(multiplier, divisionFactor, roundingMode);
} else if (divisor < values[i] && (values[i] % divisor) == 0) {
long multiplicationFactor = LongMath.divide(values[i], divisor, RoundingMode.UNNECESSARY);
values[i] = LongMath.saturatedMultiply(multiplier, multiplicationFactor);
} else {
values[i] = scaleLargeValueFallback(values[i], multiplier, divisor, roundingMode);
}
}
}
}
/**
* Scales a large value by a multiplier and a divisor.
*
* <p>If naively multiplying {@code value} and {@code multiplier} will overflow a 64-bit long,
* this implementation uses {@link LongMath#gcd(long, long)} to try and simplify the fraction
* before computing the result. If simplifying is not possible (or the simplified result will
* still result in an overflow) then the implementation falls back to floating-point arithmetic.
*
* <p>If the mathematical result would overflow or underflow a 64-bit long, the result will be
* either {@link Long#MAX_VALUE} or {@link Long#MIN_VALUE}, respectively.
*
* <p>This implementation should be used after simpler simplifying efforts have failed (such as
* checking if {@code value} or {@code multiplier} are exact multiples of {@code divisor}).
*/
// LongMath.saturatedMultiply is @Beta in the version of Guava we currently depend on (31.1)
// but it is no longer @Beta from 32.0.0. This suppression is therefore safe because there's
// no version of Guava after 31.1 that doesn't contain this symbol.
// TODO(b/290045069): Remove this suppression when we depend on Guava 32+.
@SuppressWarnings("UnstableApiUsage")
private static long scaleLargeValueFallback(
long value, long multiplier, long divisor, RoundingMode roundingMode) {
long numerator = LongMath.saturatedMultiply(value, multiplier);
if (numerator != Long.MAX_VALUE && numerator != Long.MIN_VALUE) {
return LongMath.divide(numerator, divisor, roundingMode);
} else {
// Directly multiplying value and multiplier will overflow a long, so we try and cancel
// with GCD and try directly multiplying again below. If that still overflows we fall
// through to floating point arithmetic.
long gcdOfMultiplierAndDivisor = LongMath.gcd(multiplier, divisor);
long simplifiedMultiplier =
LongMath.divide(multiplier, gcdOfMultiplierAndDivisor, RoundingMode.UNNECESSARY);
long simplifiedDivisor =
LongMath.divide(divisor, gcdOfMultiplierAndDivisor, RoundingMode.UNNECESSARY);
long gcdOfValueAndSimplifiedDivisor = LongMath.gcd(value, simplifiedDivisor);
long simplifiedValue =
LongMath.divide(value, gcdOfValueAndSimplifiedDivisor, RoundingMode.UNNECESSARY);
simplifiedDivisor =
LongMath.divide(
simplifiedDivisor, gcdOfValueAndSimplifiedDivisor, RoundingMode.UNNECESSARY);
long simplifiedNumerator = LongMath.saturatedMultiply(simplifiedValue, simplifiedMultiplier);
if (simplifiedNumerator != Long.MAX_VALUE && simplifiedNumerator != Long.MIN_VALUE) {
return LongMath.divide(simplifiedNumerator, simplifiedDivisor, roundingMode);
} else {
double multiplicationFactor = (double) simplifiedMultiplier / simplifiedDivisor;
double result = simplifiedValue * multiplicationFactor;
// Clamp values that are too large to be represented by 64-bit signed long. If we don't
// explicitly clamp then DoubleMath.roundToLong will throw ArithmeticException.
if (result > Long.MAX_VALUE) {
return Long.MAX_VALUE;
} else if (result < Long.MIN_VALUE) {
return Long.MIN_VALUE;
} else {
return DoubleMath.roundToLong(result, roundingMode);
}
}
}
}
/**
* Scales a large timestamp.
*
* <p>Logically, scaling consists of a multiplication followed by a division. The actual
* operations performed are designed to minimize the probability of overflow.
* <p>Equivalent to {@link #scaleLargeValue(long, long, long, RoundingMode)} with {@link
* RoundingMode#FLOOR}.
*
* @param timestamp The timestamp to scale.
* @param multiplier The multiplier.
@ -1651,16 +1856,7 @@ public final class Util {
*/
@UnstableApi
public static long scaleLargeTimestamp(long timestamp, long multiplier, long divisor) {
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = divisor / multiplier;
return timestamp / divisionFactor;
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = multiplier / divisor;
return timestamp * multiplicationFactor;
} else {
double multiplicationFactor = (double) multiplier / divisor;
return (long) (timestamp * multiplicationFactor);
}
return scaleLargeValue(timestamp, multiplier, divisor, RoundingMode.FLOOR);
}
/**
@ -1673,24 +1869,7 @@ public final class Util {
*/
@UnstableApi
public static long[] scaleLargeTimestamps(List<Long> timestamps, long multiplier, long divisor) {
long[] scaledTimestamps = new long[timestamps.size()];
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = divisor / multiplier;
for (int i = 0; i < scaledTimestamps.length; i++) {
scaledTimestamps[i] = timestamps.get(i) / divisionFactor;
}
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = multiplier / divisor;
for (int i = 0; i < scaledTimestamps.length; i++) {
scaledTimestamps[i] = timestamps.get(i) * multiplicationFactor;
}
} else {
double multiplicationFactor = (double) multiplier / divisor;
for (int i = 0; i < scaledTimestamps.length; i++) {
scaledTimestamps[i] = (long) (timestamps.get(i) * multiplicationFactor);
}
}
return scaledTimestamps;
return scaleLargeValues(timestamps, multiplier, divisor, RoundingMode.FLOOR);
}
/**
@ -1702,22 +1881,7 @@ public final class Util {
*/
@UnstableApi
public static void scaleLargeTimestampsInPlace(long[] timestamps, long multiplier, long divisor) {
if (divisor >= multiplier && (divisor % multiplier) == 0) {
long divisionFactor = divisor / multiplier;
for (int i = 0; i < timestamps.length; i++) {
timestamps[i] /= divisionFactor;
}
} else if (divisor < multiplier && (multiplier % divisor) == 0) {
long multiplicationFactor = multiplier / divisor;
for (int i = 0; i < timestamps.length; i++) {
timestamps[i] *= multiplicationFactor;
}
} else {
double multiplicationFactor = (double) multiplier / divisor;
for (int i = 0; i < timestamps.length; i++) {
timestamps[i] = (long) (timestamps[i] * multiplicationFactor);
}
}
scaleLargeValuesInPlace(timestamps, multiplier, divisor, RoundingMode.FLOOR);
}
/**

View File

@ -1,204 +0,0 @@
/*
* Copyright 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
*
* https://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.common.util;
import static androidx.media3.common.util.Assertions.checkState;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableList;
import com.google.common.math.LongMath;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/**
* Parameterized tests for:
*
* <ul>
* <li>{@link Util#scaleLargeTimestamp}
* <li>{@link Util#scaleLargeTimestamps}
* <li>{@link Util#scaleLargeTimestampsInPlace}
* </ul>
*/
@RunWith(ParameterizedRobolectricTestRunner.class)
public class UtilScaleLargeTimestampParameterizedTest {
@Parameters(name = "{0}")
public static ImmutableList<Object[]> implementations() {
return ImmutableList.of(
new Object[] {"single-timestamp", (ScaleLargeTimestampFn) Util::scaleLargeTimestamp},
new Object[] {
"timestamp-list",
(ScaleLargeTimestampFn)
(timestamp, multiplier, divisor) ->
Util.scaleLargeTimestamps(ImmutableList.of(timestamp), multiplier, divisor)[0]
},
new Object[] {
"timestamp-array-in-place",
(ScaleLargeTimestampFn)
(timestamp, multiplier, divisor) -> {
long[] timestamps = new long[] {timestamp};
Util.scaleLargeTimestampsInPlace(timestamps, multiplier, divisor);
return timestamps[0];
}
});
}
// Every parameter has to be assigned to a field, even if it's only used to name the test.
@SuppressWarnings("unused")
@ParameterizedRobolectricTestRunner.Parameter(0)
public String name;
@ParameterizedRobolectricTestRunner.Parameter(1)
public ScaleLargeTimestampFn implementation;
@Test
public void zeroValue() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 0, /* multiplier= */ 10, /* divisor= */ 2);
assertThat(result).isEqualTo(0);
}
@Test
public void zeroDivisor_throwsException() {
assertThrows(
ArithmeticException.class,
() ->
implementation.scaleLargeTimestamp(
/* timestamp= */ 2, /* multiplier= */ 10, /* divisor= */ 0));
}
@Test
public void divisorMultipleOfMultiplier() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 7, /* multiplier= */ 2, /* divisor= */ 4);
assertThat(result).isEqualTo(3);
}
@Test
public void multiplierMultipleOfDivisor() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 7, /* multiplier= */ 4, /* divisor= */ 2);
assertThat(result).isEqualTo(14);
}
@Test
public void multiplierMultipleOfDivisor_resultOverflowsLong() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 7, /* multiplier= */ 1L << 62, /* divisor= */ 2);
assertThat(result).isEqualTo(-2305843009213693952L);
}
@Test
public void multiplierMultipleOfDivisor_resultUnderflowsLong() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ -7, /* multiplier= */ 1L << 62, /* divisor= */ 2);
assertThat(result).isEqualTo(2305843009213693952L);
}
@Test
public void divisorMultipleOfValue() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 2, /* multiplier= */ 7, /* divisor= */ 4);
assertThat(result).isEqualTo(3);
}
@Test
public void valueMultipleOfDivisor() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 4, /* multiplier= */ 7, /* divisor= */ 2);
assertThat(result).isEqualTo(14);
}
@Test
public void valueMultipleOfDivisor_resultOverflowsLong_clampedToMaxLong() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 1L << 62, /* multiplier= */ 7, /* divisor= */ 2);
assertThat(result).isEqualTo(Long.MAX_VALUE);
}
@Test
public void valueMultipleOfDivisor_resultUnderflowsLong_clampedToMinLong() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 1L << 62, /* multiplier= */ -7, /* divisor= */ 2);
assertThat(result).isEqualTo(Long.MIN_VALUE);
}
@Test
public void numeratorDoesntOverflow() {
// Deliberately choose value, multiplier and divisor so no pair trivially cancels to 1.
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ 12, /* multiplier= */ 15, /* divisor= */ 20);
assertThat(result).isEqualTo(9);
}
@Test
public void numeratorWouldOverflowIfNotCancelled() {
// Deliberately choose value, multiplier and divisor so that both value/divisor and
// multiplier/divisor have to be cancelled otherwise multiplier*value will overflow a long.
long value = LongMath.checkedMultiply(3, 1L << 61);
long multiplier = LongMath.checkedMultiply(5, 1L << 60);
long divisor = LongMath.checkedMultiply(15, 1L << 59);
long result = implementation.scaleLargeTimestamp(value, multiplier, divisor);
assertThat(result).isEqualTo(1L << 62);
}
// TODO(b/290045069): Remove this suppression when we depend on Guava 32+.
@SuppressWarnings("UnstableApiUsage")
@Test
public void numeratorOverflowsAndCantBeCancelled() {
// Use three Mersenne primes so nothing can cancel, and the numerator will (just) overflow 64
// bits - forcing the implementation down the floating-point branch.
long value = (1L << 61) - 1;
long multiplier = (1L << 5) - 1;
// Confirm that naively multiplying value and multiplier overflows.
checkState(LongMath.saturatedMultiply(value, multiplier) == Long.MAX_VALUE);
long result =
implementation.scaleLargeTimestamp(value, multiplier, /* divisor= */ (1L << 31) - 1);
assertThat(result).isEqualTo(33285996559L);
}
@Test
public void resultOverflows_truncatedToMaxLong() {
long result =
implementation.scaleLargeTimestamp(
/* timestamp= */ (1L << 61) - 1,
/* multiplier= */ (1L << 61) - 1,
/* divisor= */ (1L << 31) - 1);
assertThat(result).isEqualTo(Long.MAX_VALUE);
}
private interface ScaleLargeTimestampFn {
long scaleLargeTimestamp(long timestamp, long multiplier, long divisor);
}
}

View File

@ -0,0 +1,283 @@
/*
* Copyright 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
*
* https://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.common.util;
import static androidx.media3.common.util.Assertions.checkState;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import static org.junit.Assume.assumeTrue;
import com.google.common.collect.ImmutableList;
import com.google.common.math.LongMath;
import java.math.RoundingMode;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.ParameterizedRobolectricTestRunner;
import org.robolectric.ParameterizedRobolectricTestRunner.Parameters;
/**
* Parameterized tests for:
*
* <ul>
* <li>{@link Util#scaleLargeValue}
* <li>{@link Util#scaleLargeValues}
* <li>{@link Util#scaleLargeValuesInPlace}
* <li>{@link Util#scaleLargeTimestamp}
* <li>{@link Util#scaleLargeTimestamps}
* <li>{@link Util#scaleLargeTimestampsInPlace}
* </ul>
*/
@RunWith(ParameterizedRobolectricTestRunner.class)
public class UtilScaleLargeValueParameterizedTest {
@Parameters(name = "{0}")
public static ImmutableList<Object[]> implementations() {
return ImmutableList.of(
new Object[] {"single-value", (ScaleLargeValueFn) Util::scaleLargeValue},
new Object[] {
"list",
(ScaleLargeValueFn)
(value, multiplier, divisor, roundingMode) ->
Util.scaleLargeValues(ImmutableList.of(value), multiplier, divisor, roundingMode)[
0]
},
new Object[] {
"array-in-place",
(ScaleLargeValueFn)
(value, multiplier, divisor, roundingMode) -> {
long[] values = new long[] {value};
Util.scaleLargeValuesInPlace(values, multiplier, divisor, roundingMode);
return values[0];
}
},
new Object[] {
"single-timestamp",
(ScaleLargeValueFn)
(long timestamp, long multiplier, long divisor, RoundingMode roundingMode) -> {
assumeTrue(
roundingMode == RoundingMode.UNNECESSARY || roundingMode == RoundingMode.FLOOR);
return Util.scaleLargeTimestamp(timestamp, multiplier, divisor);
}
},
new Object[] {
"timestamp-list",
(ScaleLargeValueFn)
(timestamp, multiplier, divisor, roundingMode) -> {
assumeTrue(
roundingMode == RoundingMode.UNNECESSARY || roundingMode == RoundingMode.FLOOR);
return Util.scaleLargeTimestamps(ImmutableList.of(timestamp), multiplier, divisor)[
0];
}
},
new Object[] {
"timestamp-array-in-place",
(ScaleLargeValueFn)
(timestamp, multiplier, divisor, roundingMode) -> {
assumeTrue(
roundingMode == RoundingMode.UNNECESSARY || roundingMode == RoundingMode.FLOOR);
long[] timestamps = new long[] {timestamp};
Util.scaleLargeTimestampsInPlace(timestamps, multiplier, divisor);
return timestamps[0];
}
});
}
// Every parameter has to be assigned to a field, even if it's only used to name the test.
@SuppressWarnings("unused")
@ParameterizedRobolectricTestRunner.Parameter(0)
public String name;
@ParameterizedRobolectricTestRunner.Parameter(1)
public ScaleLargeValueFn implementation;
@Test
public void zeroValue() {
// Deliberately use prime multiplier and divisor so they can't cancel.
long result =
implementation.scaleLargeValue(
/* value= */ 0, /* multiplier= */ 5, /* divisor= */ 2, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(0);
}
@Test
public void zeroMultiplier() {
long result =
implementation.scaleLargeValue(
/* value= */ 10, /* multiplier= */ 0, /* divisor= */ 2, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(0);
}
@Test
public void zeroDivisor_throwsException() {
assertThrows(
ArithmeticException.class,
() ->
implementation.scaleLargeValue(
/* value= */ 2, /* multiplier= */ 10, /* divisor= */ 0, RoundingMode.UNNECESSARY));
}
@Test
public void divisorMultipleOfMultiplier() {
long result =
implementation.scaleLargeValue(
/* value= */ 7, /* multiplier= */ 2, /* divisor= */ 4, RoundingMode.FLOOR);
assertThat(result).isEqualTo(3);
}
@Test
public void multiplierMultipleOfDivisor() {
long result =
implementation.scaleLargeValue(
/* value= */ 7, /* multiplier= */ 4, /* divisor= */ 2, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(14);
}
@Test
public void multiplierMultipleOfDivisor_resultOverflowsLong_clampedToMaxLong() {
long result =
implementation.scaleLargeValue(
/* value= */ 7, /* multiplier= */ 1L << 62, /* divisor= */ 2, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(Long.MAX_VALUE);
}
@Test
public void multiplierMultipleOfDivisor_resultUnderflowsLong_clampedToMinLong() {
long result =
implementation.scaleLargeValue(
/* value= */ -7,
/* multiplier= */ 1L << 62,
/* divisor= */ 2,
RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(Long.MIN_VALUE);
}
@Test
public void divisorMultipleOfValue() {
long result =
implementation.scaleLargeValue(
/* value= */ 2, /* multiplier= */ 7, /* divisor= */ 4, RoundingMode.FLOOR);
assertThat(result).isEqualTo(3);
}
@Test
public void valueMultipleOfDivisor() {
long result =
implementation.scaleLargeValue(
/* value= */ 4, /* multiplier= */ 7, /* divisor= */ 2, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(14);
}
@Test
public void valueMultipleOfDivisor_resultOverflowsLong_clampedToMaxLong() {
long result =
implementation.scaleLargeValue(
/* value= */ 1L << 62, /* multiplier= */ 7, /* divisor= */ 2, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(Long.MAX_VALUE);
}
@Test
public void valueMultipleOfDivisor_resultUnderflowsLong_clampedToMinLong() {
long result =
implementation.scaleLargeValue(
/* value= */ 1L << 62,
/* multiplier= */ -7,
/* divisor= */ 2,
RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(Long.MIN_VALUE);
}
@Test
public void numeratorDoesntOverflow() {
// Deliberately choose value, multiplier and divisor so no pair trivially cancels to 1.
long result =
implementation.scaleLargeValue(
/* value= */ 12, /* multiplier= */ 15, /* divisor= */ 20, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(9);
}
@Test
public void numeratorWouldOverflowIfNotCancelled() {
// Deliberately choose value, multiplier and divisor so that both value/divisor and
// multiplier/divisor have to be cancelled otherwise multiplier*value will overflow a long.
long value = LongMath.checkedMultiply(3, 1L << 61);
long multiplier = LongMath.checkedMultiply(5, 1L << 60);
long divisor = LongMath.checkedMultiply(15, 1L << 59);
long result =
implementation.scaleLargeValue(value, multiplier, divisor, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(1L << 62);
}
/**
* This test uses real values from sample_ac4.mp4 that have an exact integer result, but the
* floating-point branch of {@link Util#scaleLargeValue} produces an incorrect fractional result.
*
* <p>Here we scale them up to ensure multiplier*value would overflow a long, so the
* implementation needs to simplify the fraction first to avoid falling through to the
* floating-point branch (which will cause this test to fail because passing
* RoundingMode.UNNECESSARY won't be allowed).
*/
// TODO(b/290045069): Remove this suppression when we depend on Guava 32+.
@SuppressWarnings("UnstableApiUsage")
@Test
public void cancelsRatherThanFallThroughToFloatingPoint() {
long value = 24960;
long multiplier = 1_000_000_000_000_000_000L;
// Confirm that naively multiplying value and multiplier overflows.
checkState(LongMath.saturatedMultiply(value, multiplier) == Long.MAX_VALUE);
long result =
implementation.scaleLargeValue(
value, multiplier, /* divisor= */ 48_000_000_000_000_000L, RoundingMode.UNNECESSARY);
assertThat(result).isEqualTo(520000);
}
// TODO(b/290045069): Remove this suppression when we depend on Guava 32+.
@SuppressWarnings("UnstableApiUsage")
@Test
public void numeratorOverflowsAndCantBeCancelled() {
// Use three Mersenne primes so nothing can cancel, and the numerator will (just) overflow 64
// bits - forcing the implementation down the floating-point branch.
long value = (1L << 61) - 1;
long multiplier = (1L << 5) - 1;
// Confirm that naively multiplying value and multiplier overflows.
checkState(LongMath.saturatedMultiply(value, multiplier) == Long.MAX_VALUE);
long result =
implementation.scaleLargeValue(
value, multiplier, /* divisor= */ (1L << 31) - 1, RoundingMode.FLOOR);
assertThat(result).isEqualTo(33285996559L);
}
@Test
public void resultOverflows_truncatedToMaxLong() {
long result =
implementation.scaleLargeValue(
/* value= */ (1L << 61) - 1,
/* multiplier= */ (1L << 61) - 1,
/* divisor= */ (1L << 31) - 1,
RoundingMode.FLOOR);
assertThat(result).isEqualTo(Long.MAX_VALUE);
}
private interface ScaleLargeValueFn {
long scaleLargeValue(long value, long multiplier, long divisor, RoundingMode roundingMode);
}
}

View File

@ -617,7 +617,11 @@ import java.util.List;
long durationUs = Util.scaleLargeTimestamp(duration, C.MICROS_PER_SECOND, track.timescale);
if (track.editListDurations == null) {
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
try {
Util.scaleLargeTimestampsInPlace(timestamps, C.MICROS_PER_SECOND, track.timescale);
} catch (ArithmeticException e) {
throw new RuntimeException("track.timescale=" + track.timescale, e);
}
return new TrackSampleTable(
track, offsets, sizes, maximumSize, timestamps, flags, durationUs);
}

View File

@ -70,7 +70,7 @@ track 0:
flags = 0
data = length 520, hash FEE56928
sample 13:
time = 519999
time = 520000
flags = 0
data = length 599, hash 41F496C5
sample 14:

View File

@ -70,7 +70,7 @@ track 0:
flags = 0
data = length 520, hash FEE56928
sample 13:
time = 519999
time = 520000
flags = 0
data = length 599, hash 41F496C5
sample 14:

View File

@ -70,7 +70,7 @@ track 0:
flags = 0
data = length 520, hash FEE56928
sample 13:
time = 519999
time = 520000
flags = 0
data = length 599, hash 41F496C5
sample 14:

View File

@ -70,7 +70,7 @@ track 0:
flags = 0
data = length 520, hash FEE56928
sample 13:
time = 519999
time = 520000
flags = 0
data = length 599, hash 41F496C5
sample 14:

View File

@ -70,7 +70,7 @@ track 0:
flags = 0
data = length 520, hash FEE56928
sample 13:
time = 519999
time = 520000
flags = 0
data = length 599, hash 41F496C5
sample 14:

View File

@ -68,7 +68,7 @@ track 0:
flags = 1
data = length 520, hash FEE56928
sample 13:
time = 519999
time = 520000
flags = 1
data = length 599, hash 41F496C5
sample 14:

View File

@ -44,7 +44,7 @@ track 0:
flags = 1
data = length 520, hash FEE56928
sample 7:
time = 519999
time = 520000
flags = 1
data = length 599, hash 41F496C5
sample 8:

View File

@ -20,7 +20,7 @@ track 0:
flags = 1
data = length 520, hash FEE56928
sample 1:
time = 519999
time = 520000
flags = 1
data = length 599, hash 41F496C5
sample 2:

View File

@ -68,7 +68,7 @@ track 0:
flags = 1
data = length 520, hash FEE56928
sample 13:
time = 519999
time = 520000
flags = 1
data = length 599, hash 41F496C5
sample 14:

View File

@ -95,7 +95,7 @@ track 0:
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 13:
time = 519999
time = 520000
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1

View File

@ -59,7 +59,7 @@ track 0:
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 7:
time = 519999
time = 520000
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1

View File

@ -23,7 +23,7 @@ track 0:
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 1:
time = 519999
time = 520000
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1

View File

@ -95,7 +95,7 @@ track 0:
crypto mode = 1
encryption key = length 16, hash 9FDDEA52
sample 13:
time = 519999
time = 520000
flags = 1073741825
data = length 616, hash 3F657E23
crypto mode = 1

View File

@ -230,11 +230,11 @@ track 0:
flags = 1
data = length 3, hash 4732
sample 52:
time = 513499
time = 513500
flags = 1
data = length 3, hash 4732
sample 53:
time = 523499
time = 523500
flags = 1
data = length 3, hash 4732
sample 54:

View File

@ -34,11 +34,11 @@ track 0:
flags = 1
data = length 3, hash 4732
sample 3:
time = 513499
time = 513500
flags = 1
data = length 3, hash 4732
sample 4:
time = 523499
time = 523500
flags = 1
data = length 3, hash 4732
sample 5:

View File

@ -230,11 +230,11 @@ track 0:
flags = 1
data = length 3, hash 4732
sample 52:
time = 513499
time = 513500
flags = 1
data = length 3, hash 4732
sample 53:
time = 523499
time = 523500
flags = 1
data = length 3, hash 4732
sample 54:

View File

@ -738,7 +738,7 @@ track 1:
flags = 1
data = length 685, hash 564F62A
sample 47:
time = 1045624
time = 1045625
flags = 1
data = length 691, hash 71BBA88D
sample 48:
@ -930,7 +930,7 @@ track 1:
flags = 1
data = length 664, hash A9D0717
sample 95:
time = 2069624
time = 2069625
flags = 1
data = length 672, hash 6F2663EA
sample 96:
@ -1310,7 +1310,7 @@ track 1:
flags = 1
data = length 822, hash 2A045560
sample 190:
time = 4096249
time = 4096250
flags = 1
data = length 643, hash 551E7C72
sample 191:
@ -1322,7 +1322,7 @@ track 1:
flags = 1
data = length 640, hash E714454F
sample 193:
time = 4160249
time = 4160250
flags = 1
data = length 646, hash 6DD5E81B
sample 194:

View File

@ -606,7 +606,7 @@ track 1:
flags = 1
data = length 685, hash 564F62A
sample 47:
time = 1045624
time = 1045625
flags = 1
data = length 691, hash 71BBA88D
sample 48:
@ -798,7 +798,7 @@ track 1:
flags = 1
data = length 664, hash A9D0717
sample 95:
time = 2069624
time = 2069625
flags = 1
data = length 672, hash 6F2663EA
sample 96:
@ -1178,7 +1178,7 @@ track 1:
flags = 1
data = length 822, hash 2A045560
sample 190:
time = 4096249
time = 4096250
flags = 1
data = length 643, hash 551E7C72
sample 191:

View File

@ -41,7 +41,7 @@ MediaCodecAdapter (exotest.audio.ac4):
timeUs = 1000000480000
contents = length 520, hash FEE56928
input buffer #13:
timeUs = 1000000519999
timeUs = 1000000520000
contents = length 599, hash 41F496C5
input buffer #14:
timeUs = 1000000560000
@ -117,7 +117,7 @@ MediaCodecAdapter (exotest.audio.ac4):
size = 0
rendered = false
output buffer #13:
timeUs = 1000000519999
timeUs = 1000000520000
size = 0
rendered = false
output buffer #14:
@ -186,7 +186,7 @@ AudioSink:
time = 1000000480000
data = 1
buffer #13:
time = 1000000519999
time = 1000000520000
data = 1
buffer #14:
time = 1000000560000

View File

@ -41,7 +41,7 @@ MediaCodecAdapter (exotest.audio.ac4):
timeUs = 1000000480000
contents = length 520, hash FEE56928
input buffer #13:
timeUs = 1000000519999
timeUs = 1000000520000
contents = length 599, hash 41F496C5
input buffer #14:
timeUs = 1000000560000
@ -117,7 +117,7 @@ MediaCodecAdapter (exotest.audio.ac4):
size = 0
rendered = false
output buffer #13:
timeUs = 1000000519999
timeUs = 1000000520000
size = 0
rendered = false
output buffer #14:
@ -186,7 +186,7 @@ AudioSink:
time = 1000000480000
data = 1
buffer #13:
time = 1000000519999
time = 1000000520000
data = 1
buffer #14:
time = 1000000560000

View File

@ -158,10 +158,10 @@ MediaCodecAdapter (exotest.audio.opus):
timeUs = 1000000503500
contents = length 3, hash 4732
input buffer #52:
timeUs = 1000000513499
timeUs = 1000000513500
contents = length 3, hash 4732
input buffer #53:
timeUs = 1000000523499
timeUs = 1000000523500
contents = length 3, hash 4732
input buffer #54:
timeUs = 1000000533500
@ -519,11 +519,11 @@ MediaCodecAdapter (exotest.audio.opus):
size = 0
rendered = false
output buffer #52:
timeUs = 1000000513499
timeUs = 1000000513500
size = 0
rendered = false
output buffer #53:
timeUs = 1000000523499
timeUs = 1000000523500
size = 0
rendered = false
output buffer #54:
@ -875,10 +875,10 @@ AudioSink:
time = 1000000503500
data = 1
buffer #51:
time = 1000000513499
time = 1000000513500
data = 1
buffer #52:
time = 1000000523499
time = 1000000523500
data = 1
buffer #53:
time = 1000000533500

View File

@ -146,7 +146,7 @@ MediaCodecAdapter (exotest.audio.aac):
timeUs = 1000001024000
contents = length 30, hash E7FD80C4
input buffer #48:
timeUs = 1000001044999
timeUs = 1000001045000
contents = length 42, hash 79A5EBAC
input buffer #49:
timeUs = 1000001066000
@ -290,10 +290,10 @@ MediaCodecAdapter (exotest.audio.aac):
timeUs = 1000002048000
contents = length 34, hash 16D99E00
input buffer #96:
timeUs = 1000002068999
timeUs = 1000002069000
contents = length 35, hash 1CE3D79C
input buffer #97:
timeUs = 1000002089999
timeUs = 1000002090000
contents = length 35, hash 552D9CF8
input buffer #98:
timeUs = 1000002112000
@ -578,16 +578,16 @@ MediaCodecAdapter (exotest.audio.aac):
timeUs = 1000004096000
contents = length 942, hash E4FA1D93
input buffer #192:
timeUs = 1000004116999
timeUs = 1000004117000
contents = length 896, hash 3AD1120
input buffer #193:
timeUs = 1000004137999
timeUs = 1000004138000
contents = length 903, hash 5841ACE
input buffer #194:
timeUs = 1000004159999
timeUs = 1000004160000
contents = length 883, hash FCD9B32A
input buffer #195:
timeUs = 1000004180999
timeUs = 1000004181000
contents = length 945, hash A5D31FA1
input buffer #196:
timeUs = 1000004202000
@ -1049,7 +1049,7 @@ MediaCodecAdapter (exotest.audio.aac):
size = 0
rendered = false
output buffer #48:
timeUs = 1000001044999
timeUs = 1000001045000
size = 0
rendered = false
output buffer #49:
@ -1241,11 +1241,11 @@ MediaCodecAdapter (exotest.audio.aac):
size = 0
rendered = false
output buffer #96:
timeUs = 1000002068999
timeUs = 1000002069000
size = 0
rendered = false
output buffer #97:
timeUs = 1000002089999
timeUs = 1000002090000
size = 0
rendered = false
output buffer #98:
@ -1625,19 +1625,19 @@ MediaCodecAdapter (exotest.audio.aac):
size = 0
rendered = false
output buffer #192:
timeUs = 1000004116999
timeUs = 1000004117000
size = 0
rendered = false
output buffer #193:
timeUs = 1000004137999
timeUs = 1000004138000
size = 0
rendered = false
output buffer #194:
timeUs = 1000004159999
timeUs = 1000004160000
size = 0
rendered = false
output buffer #195:
timeUs = 1000004180999
timeUs = 1000004181000
size = 0
rendered = false
output buffer #196:
@ -2139,7 +2139,7 @@ AudioSink:
time = 1000001024000
data = 1
buffer #48:
time = 1000001044999
time = 1000001045000
data = 1
buffer #49:
time = 1000001066000
@ -2283,10 +2283,10 @@ AudioSink:
time = 1000002048000
data = 1
buffer #96:
time = 1000002068999
time = 1000002069000
data = 1
buffer #97:
time = 1000002089999
time = 1000002090000
data = 1
buffer #98:
time = 1000002112000
@ -2571,16 +2571,16 @@ AudioSink:
time = 1000004096000
data = 1
buffer #192:
time = 1000004116999
time = 1000004117000
data = 1
buffer #193:
time = 1000004137999
time = 1000004138000
data = 1
buffer #194:
time = 1000004159999
time = 1000004160000
data = 1
buffer #195:
time = 1000004180999
time = 1000004181000
data = 1
buffer #196:
time = 1000004202000