From 6e18cb00533b9d66f75d36aa0a9b3c873e6f3b26 Mon Sep 17 00:00:00 2001 From: Googler Date: Mon, 8 Jul 2024 05:10:39 -0700 Subject: [PATCH] Add support for amr-wb audio codec. Implement damrBox to provide support for amr-wb audio codec. Add unit test and an Android end to end test. PiperOrigin-RevId: 650210732 --- ...MuxerEndToEndParameterizedAndroidTest.java | 4 +- .../java/androidx/media3/muxer/Boxes.java | 20 + .../java/androidx/media3/muxer/BoxesTest.java | 21 + .../mp4/bbb_mono_16kHz_23.05kbps_amrwb.3gp | Bin 0 -> 9443 bytes .../audio_sample_entry_box_sawb.dump | 2 + .../bbb_mono_16kHz_23.05kbps_amrwb.3gp.dump | 620 ++++++++++++++++++ 6 files changed, 666 insertions(+), 1 deletion(-) create mode 100644 libraries/test_data/src/test/assets/media/mp4/bbb_mono_16kHz_23.05kbps_amrwb.3gp create mode 100644 libraries/test_data/src/test/assets/muxerdumps/audio_sample_entry_box_sawb.dump create mode 100644 libraries/test_data/src/test/assets/muxerdumps/bbb_mono_16kHz_23.05kbps_amrwb.3gp.dump diff --git a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java index cdea32d3ed..ecc171bcec 100644 --- a/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java +++ b/libraries/muxer/src/androidTest/java/androidx/media3/muxer/Mp4MuxerEndToEndParameterizedAndroidTest.java @@ -51,6 +51,7 @@ public class Mp4MuxerEndToEndParameterizedAndroidTest { private static final String H265_HDR10_MP4 = "hdr10-720p.mp4"; private static final String H265_WITH_METADATA_TRACK_MP4 = "h265_with_metadata_track.mp4"; private static final String AV1_MP4 = "sample_av1.mp4"; + private static final String AMR_WB = "bbb_mono_16kHz_23.05kbps_amrwb.3gp"; @Parameters(name = "{0}") public static ImmutableList mediaSamples() { @@ -60,7 +61,8 @@ public class Mp4MuxerEndToEndParameterizedAndroidTest { H264_WITH_PYRAMID_B_FRAMES_MP4, H265_HDR10_MP4, H265_WITH_METADATA_TRACK_MP4, - AV1_MP4); + AV1_MP4, + AMR_WB); } @Parameter public @MonotonicNonNull String inputFile; diff --git a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java index 6c13832f10..ca4f1087d4 100644 --- a/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java +++ b/libraries/muxer/src/main/java/androidx/media3/muxer/Boxes.java @@ -21,6 +21,7 @@ import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.muxer.ColorUtils.MEDIAFORMAT_STANDARD_TO_PRIMARIES_AND_MATRIX; import static androidx.media3.muxer.ColorUtils.MEDIAFORMAT_TRANSFER_TO_MP4_TRANSFER; import static androidx.media3.muxer.Mp4Utils.UNSIGNED_INT_MAX_VALUE; +import static java.nio.charset.StandardCharsets.UTF_8; import android.media.MediaCodec; import android.media.MediaCodec.BufferInfo; @@ -541,6 +542,8 @@ import java.util.List; switch (mimeType) { case MimeTypes.AUDIO_AAC: return esdsBox(format); + case MimeTypes.AUDIO_AMR_WB: + return damrBox(/* mode= */ (short) 0x83FF); // mode set: all enabled case MimeTypes.VIDEO_H264: return avcCBox(format); case MimeTypes.VIDEO_H265: @@ -1317,6 +1320,8 @@ import java.util.List; switch (mimeType) { case MimeTypes.AUDIO_AAC: return "mp4a"; + case MimeTypes.AUDIO_AMR_WB: + return "sawb"; case MimeTypes.VIDEO_H264: return "avc1"; case MimeTypes.VIDEO_H265: @@ -1380,6 +1385,21 @@ import java.util.List; return BoxUtils.wrapIntoBox("esds", contents); } + /** Returns the audio damr box. */ + private static ByteBuffer damrBox(short mode) { + + ByteBuffer contents = ByteBuffer.allocate(MAX_FIXED_LEAF_BOX_SIZE); + + contents.put(" ".getBytes(UTF_8)); // vendor: 4 bytes + contents.put((byte) 0); // decoder version + contents.putShort(mode); + contents.put((byte) 0); // mode change period + contents.put((byte) 1); // frames per sample + + contents.flip(); + return BoxUtils.wrapIntoBox("damr", contents); + } + /** Packs a three-letter language code into a short, packing 3x5 bits. */ private static short languageCodeFromString(@Nullable String code) { if (code == null) { diff --git a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java index 44e74aac17..1b74bc912a 100644 --- a/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java +++ b/libraries/muxer/src/test/java/androidx/media3/muxer/BoxesTest.java @@ -253,6 +253,27 @@ public class BoxesTest { context, dumpableBox, getExpectedDumpFilePath("audio_sample_entry_box_mp4a")); } + @Test + public void createAudioSampleEntryBox_forSawb_matchesExpected() throws Exception { + Format format = + new Format.Builder() + .setPeakBitrate(128000) + .setSampleRate(48000) + .setId(3) + .setSampleMimeType("audio/amr-wb") + .setChannelCount(2) + .setAverageBitrate(128000) + .setLanguage("```") + .setMaxInputSize(502) + .build(); + + ByteBuffer audioSampleEntryBox = Boxes.audioSampleEntry(format); + + DumpableMp4Box dumpableBox = new DumpableMp4Box(audioSampleEntryBox); + DumpFileAsserts.assertOutput( + context, dumpableBox, getExpectedDumpFilePath("audio_sample_entry_box_sawb")); + } + @Test public void createAudioSampleEntryBox_withUnknownAudioFormat_throws() { // The audio format contains an unknown MIME type. diff --git a/libraries/test_data/src/test/assets/media/mp4/bbb_mono_16kHz_23.05kbps_amrwb.3gp b/libraries/test_data/src/test/assets/media/mp4/bbb_mono_16kHz_23.05kbps_amrwb.3gp new file mode 100644 index 0000000000000000000000000000000000000000..6722f13270c981305f31f058c295aaf0fa63d057 GIT binary patch literal 9443 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 |O$9w<>dFtD`!ksvof&Ji$Jzy3;*Qs%% zt5368Frn{ZNQ^8Un5XunsHI0|fG_Tc8knL$=Ju&q8ezkO{)sQE2%eiYROO6}4H1OS z+T1Sf$Mino&}7I|_W|avJEbz^)k)rvwO?L?;WD%x9KsFP_uU^T+EGRym7)2n{1IY) zd-W0t_GB5qd(0r@+Pmn-jj45wO}qz-N}J``qp zGw`K6m-{r>q;Y@2*NFJ4Kk)@LB#ay{f*+RZqQRV=>xpmu;fW5V8BhkDbI$Qt|IUq2 zX2-+u9wS@lhW;71plRN2Su=C;6-5O;53_LNnEJ*gyTN@YWK;3PUepy;1^`^ zNznCZuX?O_PiPIYSsN$f@i)!AG}Nh~JVhL=&SROTjo;@rqz66%B9k%SGGew*?Lh}O zD*f9)yE}{;XvDJNBm*J>x2;((fuU^EON z^1{i@T*Cm4(v0TD*x=lBhIa!=)wR{A(5)?JVX z{}4gnm=%i0u=W&oq|PZ2F>elu#v%avO5tK7i~$orqluoeV4PT)JZrgZ=}h}M@w};p zl+*9|JtG_EWs~2sw3--bZM72tK6^SFj3_RP4|zC}hx@20cg1lvDL;B7?v41i+R&R* zpzBJ9j$@Pj((VZHR-bgspMbCM3lN;xL11&ALntb~6r(TM2lztaAOa^-6aKCU6+y7= z&DHAs8h@-`9ooDDxpO-w9y#md?Er_gU@|{a*fMVmL6t1q^(q|Axzq09j}-=MTU%!k z2xMjLY-aR;{|BILgFrZqAQaHw>p$?nm@e{80#&23B`i@#uD&c-{u%h`lo$TmjVD&)rIyyQ2cW3;ydEuz