diff --git a/.travis.yml b/.travis.yml index 0e4ab13b..90266e02 100644 --- a/.travis.yml +++ b/.travis.yml @@ -14,6 +14,7 @@ matrix: - libvorbis-dev - libflac-dev - libasound2-dev + - libopus-dev - os: linux dist: xenial compiler: gcc @@ -25,6 +26,7 @@ matrix: - libvorbis-dev - libflac-dev - libasound2-dev + - libopus-dev - os: osx compiler: clang @@ -33,7 +35,7 @@ before_install: - | if [[ "${TRAVIS_OS_NAME}" == "osx" ]]; then brew update - brew install autogen flac libogg libvorbis + brew install autogen flac libogg libvorbis opus fi install: diff --git a/CMakeLists.txt b/CMakeLists.txt index 8ef045db..e4f92176 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,7 +55,7 @@ include(SndFileChecks) cmake_dependent_option (BUILD_REGTEST "Build regtest" ON "SQLITE3_FOUND" OFF) -cmake_dependent_option (ENABLE_EXTERNAL_LIBS "Enable FLAC and Vorbis codecs" ON "VORBISENC_FOUND;FLAC_FOUND" OFF) +cmake_dependent_option (ENABLE_EXTERNAL_LIBS "Enable FLAC, Vorbis, and Opus codecs" ON "VORBISENC_FOUND;FLAC_FOUND;OPUS_FOUND" OFF) cmake_dependent_option (ENABLE_CPU_CLIP "Enable tricky cpu specific clipper" ON "CPU_CLIPS_POSITIVE;CPU_CLIPS_NEGATIVE" OFF) if (NOT ENABLE_CPU_CLIP) set (CPU_CLIPS_POSITIVE FALSE) @@ -70,9 +70,10 @@ set (HAVE_SNDIO_H ${SNDIO_FOUND}) set (ENABLE_EXPERIMENTAL_CODE ${ENABLE_EXPERIMENTAL}) set (HAVE_SPEEX ${ENABLE_EXPERIMENTAL}) +set (HAVE_OPUS ${ENABLE_EXPERIMENTAL}) add_feature_info (BUILD_SHARED_LIBS BUILD_SHARED_LIBS "build shared libraries") -add_feature_info (ENABLE_EXTERNAL_LIBS ENABLE_EXTERNAL_LIBS "enable FLAC and Vorbis codecs") +add_feature_info (ENABLE_EXTERNAL_LIBS ENABLE_EXTERNAL_LIBS "enable FLAC, Vorbis, and Opus codecs") add_feature_info (ENABLE_EXPERIMENTAL ENABLE_EXPERIMENTAL "enable experimental code") add_feature_info (BUILD_TESTING BUILD_TESTING "build tests") add_feature_info (BUILD_REGTEST BUILD_REGTEST "build regtest") @@ -92,7 +93,7 @@ set_package_properties (Ogg PROPERTIES TYPE RECOMMENDED URL "www.xiph.org/ogg/" DESCRIPTION "library for manipulating ogg bitstreams" - PURPOSE "Required to enable Vorbis, Speex and Opus support" + PURPOSE "Required to enable Vorbis, Speex, and Opus support" ) set_package_properties (VorbisEnc PROPERTIES TYPE RECOMMENDED @@ -106,6 +107,12 @@ set_package_properties (FLAC PROPERTIES DESCRIPTION "Free Lossless Audio Codec Library" PURPOSE "Enables FLAC support" ) +set_package_properties(Opus PROPERTIES + TYPE RECOMMENDED + URL "www.opus-codec.org/" + DESCRIPTION "Standardized open source low-latency fullband codec" + PURPOSE "Enables experimental Opus support" + ) set_package_properties(Speex PROPERTIES TYPE OPTIONAL URL "www.speex.org/" DESCRIPTION "an audio codec tuned for speech" @@ -230,6 +237,7 @@ add_library (sndfile src/ogg_speex.c src/ogg_pcm.c src/ogg_opus.c + src/ogg_vcomment.c src/nms_adpcm.c src/GSM610/config.h src/GSM610/gsm.h @@ -288,6 +296,7 @@ target_link_libraries (sndfile $<$:Vorbis::VorbisEnc> $<$:FLAC::FLAC> $<$,$,$>:Speex::Speex> + $<$:Opus::Opus> ) set_target_properties (sndfile PROPERTIES PUBLIC_HEADER "${sndfile_HDRS}" @@ -1022,6 +1031,14 @@ if (BUILD_TESTING) $<$:m> ) + add_executable (ogg_opus_test tests/ogg_opus_test.c) + target_link_libraries (ogg_opus_test + PRIVATE + sndfile + test_utils + $<$:m> + ) + add_executable (stdin_test tests/stdin_test.c) target_link_libraries (stdin_test PRIVATE @@ -1242,6 +1259,12 @@ if (BUILD_TESTING) add_test (string_test_ogg string_test ogg) add_test (misc_test_ogg misc_test ogg) + ### opus-tests ### + add_test (ogg_opus_test ogg_opus_test) + add_test (compression_size_test_opus compression_size_test opus) + add_test (lossy_comp_test_ogg_opus lossy_comp_test ogg_opus) + add_test (string_test_opus string_test opus) + ### io-tests add_test (stdio_test stdio_test) add_test (pipe_test pipe_test) diff --git a/Makefile.am b/Makefile.am index 2744aef3..dff2ad32 100644 --- a/Makefile.am +++ b/Makefile.am @@ -21,7 +21,8 @@ cmake_files = cmake/ClipMode.cmake cmake/FindFLAC.cmake \ cmake/FindOgg.cmake cmake/FindVorbis.cmake cmake/FindSndio.cmake \ cmake/FindSpeex.cmake cmake/FindSQLite3.cmake cmake/FindVorbisEnc.cmake \ cmake/SndFileChecks.cmake cmake/TestInline.cmake \ - cmake/TestLargeFiles.cmake cmake/TestInline.c.in + cmake/TestLargeFiles.cmake cmake/TestInline.c.in \ + cmake/FindOpus.cmake pkgconfig_DATA = sndfile.pc @@ -64,8 +65,8 @@ src_libsndfile_la_LDFLAGS = -no-undefined -version-info $(SHARED_VERSION_INFO) $ src_libsndfile_la_SOURCES = src/sndfile.c src/aiff.c src/au.c src/avr.c src/caf.c src/dwd.c src/flac.c src/g72x.c src/htk.c src/ircam.c \ src/macos.c src/mat4.c src/mat5.c src/nist.c src/paf.c src/pvf.c src/raw.c src/rx2.c src/sd2.c \ src/sds.c src/svx.c src/txw.c src/voc.c src/wve.c src/w64.c src/wavlike.c src/wav.c src/xi.c src/mpc2k.c src/rf64.c \ - src/ogg_vorbis.c src/ogg_speex.c src/ogg_pcm.c src/ogg_opus.c src/common.h src/sfconfig.h src/sfendian.h src/wavlike.h \ - src/sf_unistd.h src/ogg.h src/chanmap.h + src/ogg_vorbis.c src/ogg_speex.c src/ogg_pcm.c src/ogg_opus.c src/ogg_vcomment.c \ + src/common.h src/sfconfig.h src/sfendian.h src/wavlike.h src/sf_unistd.h src/ogg.h src/chanmap.h src/ogg_vcomment.h nodist_src_libsndfile_la_SOURCES = $(nodist_include_HEADERS) src_libsndfile_la_LIBADD = src/GSM610/libgsm.la src/G72x/libg72x.la src/ALAC/libalac.la \ src/libcommon.la $(EXTERNAL_XIPH_LIBS) -lm @@ -217,7 +218,7 @@ check_PROGRAMS += tests/sfversion tests/floating_point_test tests/write_read_tes tests/locale_test tests/win32_ordinal_test tests/ogg_test tests/compression_size_test \ tests/checksum_test tests/external_libs_test tests/rdwr_test tests/format_check_test $(CPP_TEST) \ tests/channel_test tests/long_read_write_test tests/stdin_test tests/stdout_test \ - tests/dither_test tests/fix_this tests/largefile_test tests/benchmark + tests/dither_test tests/fix_this tests/largefile_test tests/benchmark tests/ogg_opus_test BUILT_SOURCES += \ tests/write_read_test.c \ @@ -339,6 +340,9 @@ tests_virtual_io_test_LDADD = src/libsndfile.la tests_ogg_test_SOURCES = tests/ogg_test.c tests/utils.c tests/utils.h tests_ogg_test_LDADD = src/libsndfile.la +tests_ogg_opus_test_SOURCES = tests/ogg_opus_test.c tests/utils.c tests/utils.h +tests_ogg_opus_test_LDADD = src/libsndfile.la + tests_compression_size_test_SOURCES = tests/compression_size_test.c tests/utils.c tests/utils.h tests/dft_cmp.h tests_compression_size_test_LDADD = src/libsndfile.la diff --git a/appveyor.yml b/appveyor.yml index 5cd185d5..10e5b84b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ platform: install: - if %platform%==Win32 set VCPKG_TRIPLET=x86-windows - if %platform%==x64 set VCPKG_TRIPLET=x64-windows - - vcpkg install libogg:%VCPKG_TRIPLET% libvorbis:%VCPKG_TRIPLET% libflac:%VCPKG_TRIPLET% sqlite3:%VCPKG_TRIPLET% + - vcpkg install libogg:%VCPKG_TRIPLET% libvorbis:%VCPKG_TRIPLET% libflac:%VCPKG_TRIPLET% sqlite3:%VCPKG_TRIPLET% opus:%VCPKG_TRIPLET% before_build: - mkdir CMakeBuild - cd CMakeBuild diff --git a/cmake/FindOgg.cmake b/cmake/FindOgg.cmake index 1e9c72d7..988f936d 100644 --- a/cmake/FindOgg.cmake +++ b/cmake/FindOgg.cmake @@ -11,7 +11,7 @@ if (OGG_INCLUDE_DIR) endif () find_package (PkgConfig QUIET) -pkg_check_modules (PC_OGG QUIET ogg) +pkg_check_modules (PC_OGG QUIET ogg>=1.3.0) set (OGG_VERSION ${PC_OGG_VERSION}) diff --git a/cmake/FindOpus.cmake b/cmake/FindOpus.cmake new file mode 100644 index 00000000..2b29d52d --- /dev/null +++ b/cmake/FindOpus.cmake @@ -0,0 +1,67 @@ +# - Find opus +# Find the native opus includes and libraries +# +# OPUS_INCLUDE_DIRS - where to find opus.h, etc. +# OPUS_LIBRARIES - List of libraries when using opus. +# OPUS_FOUND - True if Opus found. + +if (OPUS_INCLUDE_DIR) + # Already in cache, be silent + set(OPUS_FIND_QUIETLY TRUE) +endif () + +find_package (Ogg QUIET) + +find_package (PkgConfig QUIET) +pkg_check_modules(PC_OPUS QUIET opus>=1.1) + +set (OPUS_VERSION ${PC_OPUS_VERSION}) + +find_path (OPUS_INCLUDE_DIR opus/opus.h + HINTS + ${PC_OPUS_INCLUDEDIR} + ${PC_OPUS_INCLUDE_DIRS} + ${OPUS_ROOT} + ) + +# MSVC built opus may be named opus_static. +# The provided project files name the library with the lib prefix. + +find_library (OPUS_LIBRARY + NAMES + opus + opus_static + libopus + libopus_static + HINTS + ${PC_OPUS_LIBDIR} + ${PC_OPUS_LIBRARY_DIRS} + ${OPUS_ROOT} + ) + +# Handle the QUIETLY and REQUIRED arguments and set OPUS_FOUND +# to TRUE if all listed variables are TRUE. +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args (Opus + REQUIRED_VARS + OPUS_LIBRARY + OPUS_INCLUDE_DIR + OGG_FOUND + VERSION_VAR + OPUS_VERSION + ) + +if (OPUS_FOUND) + set (OPUS_LIBRARIES ${OPUS_LIBRARY}) + set (OPUS_INCLUDE_DIRS ${OPUS_INCLUDE_DIR}) + + if (NOT TARGET Opus::Opus) + add_library (Opus::Opus UNKNOWN IMPORTED) + set_target_properties (Opus::Opus PROPERTIES + INTERFACE_INCLUDE_DIRECTORIES "${OPUS_INCLUDE_DIRS}" + IMPORTED_LOCATION "${OPUS_LIBRARIES}" + ) + endif () +endif () + +mark_as_advanced(OPUS_INCLUDE_DIR OPUS_LIBRARY) diff --git a/cmake/SndFileChecks.cmake b/cmake/SndFileChecks.cmake index 5414859c..89fc7d8f 100644 --- a/cmake/SndFileChecks.cmake +++ b/cmake/SndFileChecks.cmake @@ -29,7 +29,8 @@ endif () find_package (VorbisEnc) find_package (FLAC) -if (VORBIS_FOUND AND FLAC_FOUND) +find_package (Opus) +if (VORBIS_FOUND AND FLAC_FOUND AND OPUS_FOUND) set (HAVE_EXTERNAL_XIPH_LIBS 1) else () set (HAVE_EXTERNAL_XIPH_LIBS 0) diff --git a/configure.ac b/configure.ac index bd5f72d9..eb35f991 100644 --- a/configure.ac +++ b/configure.ac @@ -336,7 +336,7 @@ AS_IF([test -n "$PKG_CONFIG"], [ dnl Make sure the FLAC_CFLAGS value is sane. FLAC_CFLAGS=`echo $FLAC_CFLAGS | $SED "s|include/FLAC|include|"` - PKG_CHECK_MOD_VERSION(OGG, ogg >= 1.1.3, ac_cv_ogg=yes, ac_cv_ogg=no) + PKG_CHECK_MOD_VERSION(OGG, ogg >= 1.3.0, ac_cv_ogg=yes, ac_cv_ogg=no) AS_IF([test "x$enable_experimental" = "xyes"], [ PKG_CHECK_MOD_VERSION(SPEEX, speex >= 1.2, ac_cv_speex=yes, ac_cv_speex=no) @@ -350,15 +350,17 @@ AS_IF([test -n "$PKG_CONFIG"], [ dnl See: http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=549899 PKG_CHECK_MOD_VERSION(VORBIS, vorbis >= 1.2.3, ac_cv_vorbis=yes, ac_cv_vorbis=no) PKG_CHECK_MOD_VERSION(VORBISENC, vorbisenc >= 1.2.3, ac_cv_vorbisenc=yes, ac_cv_vorbisenc=no) + + PKG_CHECK_MOD_VERSION(OPUS, opus >= 1.1, ac_cv_opus=yes, ac_cv_opus=no) enable_external_libs=yes ]) - AS_IF([test "x$ac_cv_flac$ac_cv_ogg$ac_cv_vorbis$ac_cv_vorbisenc" = "xyesyesyesyes"], [ + AS_IF([test "x$ac_cv_flac$ac_cv_ogg$ac_cv_vorbis$ac_cv_vorbisenc$ac_cv_opus" = "xyesyesyesyesyes"], [ HAVE_EXTERNAL_XIPH_LIBS=1 enable_external_libs=yes - EXTERNAL_XIPH_CFLAGS="$FLAC_CFLAGS $OGG_CFLAGS $VORBIS_CFLAGS $VORBISENC_CFLAGS $SPEEX_CFLAGS" - EXTERNAL_XIPH_LIBS="$FLAC_LIBS $OGG_LIBS $VORBIS_LIBS $VORBISENC_LIBS $SPEEX_LIBS " + EXTERNAL_XIPH_CFLAGS="$FLAC_CFLAGS $OGG_CFLAGS $VORBIS_CFLAGS $VORBISENC_CFLAGS $SPEEX_CFLAGS $OPUS_CFLAGS" + EXTERNAL_XIPH_LIBS="$FLAC_LIBS $OGG_LIBS $VORBIS_LIBS $VORBISENC_LIBS $SPEEX_LIBS $OPUS_LIBS " ], [ AS_ECHO([""]) AC_MSG_WARN([[*** One or more of the external libraries (ie libflac, libogg and]]) @@ -372,7 +374,7 @@ AS_IF([test -n "$PKG_CONFIG"], [ ]) ]) -AC_DEFINE_UNQUOTED([HAVE_EXTERNAL_XIPH_LIBS], [$HAVE_EXTERNAL_XIPH_LIBS], [Will be set to 1 if flac, ogg and vorbis are available.]) +AC_DEFINE_UNQUOTED([HAVE_EXTERNAL_XIPH_LIBS], [$HAVE_EXTERNAL_XIPH_LIBS], [Will be set to 1 if flac, ogg, vorbis, and opus are available.]) dnl ==================================================================================== dnl Check for libsqlite3 (only used in regtest). @@ -702,7 +704,7 @@ AC_MSG_RESULT([ Experimental code : ................... ${enable_experimental:-no} Using ALSA in example programs : ...... ${enable_alsa:-no} - External FLAC/Ogg/Vorbis : ............ ${enable_external_libs:-no} + External FLAC/Ogg/Vorbis/Opus : ....... ${enable_external_libs:-no} Building Octave interface : ........... ${OCTAVE_BUILD} Tools : diff --git a/doc/command.html b/doc/command.html index 47b3abae..14e38e97 100644 --- a/doc/command.html +++ b/doc/command.html @@ -269,7 +269,14 @@ SFC_RF64_AUTO_DOWNGRADE Enable auto downgrade from RF64 to WAV - + + SFC_GET_ORIGINAL_SAMPLERATE + Get original samplerate + + + SFC_SET_ORIGINAL_SAMPLERATE + Set original samplerate + + +


SFC_GET_ORIGINAL_SAMPLERATE

+

+Get original samplerate metadata. +

+

+The Opus audio codec stores audio data independent of samplerate, but only +supports encoding or decoding at 8000Hz, 12000Hz, 16000Hz, 24000HZ or 48000Hz. +Opus includes a header field to record the original source input samplerate, and +a samplerate converter may be used if needed. +

+

+This command gets the original samplerate header field. It does not enable any +(non-existent) samplerate conversion, nor change the current decoder samplerate. +

+

+Parameters: +

+
+         sndfile  : A valid SNDFILE* pointer
+         cmd      : SFC_GET_ORIGINAL_SAMPLERATE
+         data     : pointer to an integer
+         datasize : sizeof (int)
+
+

+Example: +

+
+         /* Get the original sample rate */
+         int original_samplerate ;
+         sf_command (sndfile, SFC_GET_ORIGINAL_SAMPLERATE, &original_samplerate, sizeof (original_samplerate)) ;
+
+
+
Return value:
+
Returns SF_TRUE on success, SF_FALSE otherwise. +
The passed integer is set to the value of the original samplerate. +
+ + + + +


SFC_SET_ORIGINAL_SAMPLERATE

+

+Set original samplerate metadata. +

+

+The Opus audio codec stores audio data independent of samplerate, but only +supports encoding or decoding at 8000Hz, 12000Hz, 16000Hz, 24000HZ or 48000Hz. +Opus includes a header field to record the original source input samplerate, and +a samplerate converter may be used if needed. +

+

+When writing an Opus file this command sets the original samplerate header field +to the provided value, which is then stored in the file. This has no effect on +the current encoder samplerate. +

+

+When reading an Opus file this command overrides the original samplerate value +as read from the file. libsndfile uses this value to choose what samplerate +to decode at, rounding up to the nearest valid Opus samplerate. After a +successful call, the file samplerate and frames count may have changed. +

+

+Note: This command should be issued before the first bit of audio data has been +read from or written to the file. +

+

+Parameters: +

+
+         sndfile  : A valid SNDFILE* pointer
+         cmd      : SFC_SET_ORIGINAL_SAMPLERATE
+         data     : pointer to an integer
+         datasize : sizeof (int)
+
+

+Example: +

+
+         /* Store the original sample rate as 44100 */
+         int original_samplerate 44100;
+         sf_command (sndfile, SFC_SET_ORIGINAL_SAMPLERATE, &original_samplerate, sizeof (input_samplerate)) ;
+
+
+
Return value:
+
Returns SF_TRUE on success, SF_FALSE otherwise. +
On write, can only succeed if no data has been written. +
On read, if successful, SFC_GET_CURRENT_SF_INFO + should be called to determine the new frames count and samplerate +
+ + diff --git a/src/command.c b/src/command.c index 175a5c56..bab5ca3e 100644 --- a/src/command.c +++ b/src/command.c @@ -66,6 +66,10 @@ static SF_FORMAT_INFO const simple_formats [] = }, #if HAVE_EXTERNAL_XIPH_LIBS + { SF_FORMAT_OGG | SF_FORMAT_OPUS, + "Ogg Opus (Xiph Foundation)", "opus" + }, + { SF_FORMAT_OGG | SF_FORMAT_VORBIS, "Ogg Vorbis (Xiph Foundation)", "oga" }, @@ -208,6 +212,7 @@ static SF_FORMAT_INFO subtype_formats [] = #if HAVE_EXTERNAL_XIPH_LIBS { SF_FORMAT_VORBIS, "Vorbis", NULL }, + { SF_FORMAT_OPUS, "Opus", NULL }, #endif { SF_FORMAT_ALAC_16, "16 bit ALAC", NULL }, diff --git a/src/common.h b/src/common.h index 32932f36..6694c137 100644 --- a/src/common.h +++ b/src/common.h @@ -758,6 +758,8 @@ enum SFE_FILENAME_TOO_LONG, SFE_NEGATIVE_RW_LEN, + SFE_OPUS_BAD_SAMPLERATE, + SFE_MAX_ERROR /* This must be last in list. */ } ; diff --git a/src/ogg.c b/src/ogg.c index d1279515..e5b002a3 100644 --- a/src/ogg.c +++ b/src/ogg.c @@ -1,6 +1,7 @@ /* ** Copyright (C) 2002-2016 Erik de Castro Lopo ** Copyright (C) 2007 John ffitch +** Copyright (C) 2018 Arthur Taylor ** ** This program is free software ; you can redistribute it and/or modify ** it under the terms of the GNU Lesser General Public License as published by @@ -17,6 +18,40 @@ ** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +/* +** This file contains code based on OpusFile and Opus-Tools, both by +** Xiph.Org. COPYING from each is identical and is as follows: +** +** Copyright (c) 1994-2013 Xiph.Org Foundation and contributors +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** - Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** +** - Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** +** - Neither the name of the Xiph.Org Foundation nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +** OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + #include "sfconfig.h" #include @@ -42,13 +77,78 @@ #include "ogg.h" -static int ogg_close (SF_PRIVATE *psf) ; -static int ogg_stream_classify (SF_PRIVATE *psf, OGG_PRIVATE * odata) ; -static int ogg_page_classify (SF_PRIVATE * psf, const ogg_page * og) ; +#define OGG_SYNC_READ_SIZE (2048) +#define OGG_PAGE_SIZE_MAX (65307) +#define OGG_CHUNK_SIZE (65536) +#define OGG_CHUNK_SIZE_MAX (1024*1024) -int ogg_read_first_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) -{ char *buffer ; - int bytes ; +/* + * The Ogg container may seem overly complicated, particularly when used for a + * on-disk audio file format. This is probably because Ogg is designed with + * streaming rather than storage as a priority, and can handle multiple codec + * payloads multiplexed together, then possibly chained on top of that. + * Ogg achieves its goals well, but it does lend to a bit of a learning curve, + * with many internal structures to push data around in compared to most sound + * file formats which only have a header and raw data. + * + * See + * - [https://xiph.org/ogg/doc/oggstream.html] + * - [https://xiph.org/ogg/doc/framing.html] + * + * libogg Memory Management + * =========================================================================== + * + * libOgg's memory management is documented in code, not in headers or external + * documentation. What follows is not an attempt to completely document it, but + * an explanation of the basics. + * + * libOgg has two data structures which allocate and manage data buffers: The + * ogg_sync_state structure and the ogg_stream_state structure. The remaining + * structures of ogg_page and ogg_packet are views into the buffers managed by + * the previous structures. + * + * ogg_sync_state is used for reading purposes. It takes a physical bitstream + * and searches for, validates, and returns complete Ogg Pages. The + * ogg_sync_state buffers the returned page data, holding at most one + * complete page at a time. A returned Ogg page remains valid until any + * operation other than ogg_sync_check() is called. + * + * ogg_stream_state is used for both reading and writing. For reading, the + * contents of an ogg_page is copied into the stream state. This data is + * buffered to be split or joined as necessary into complete ogg_packets. If, + * after copying an ogg_page into an ogg_stream_state, packets are available to + * be read, then all of those packets remain in memory and valid until either + * the ogg_stream_state is reset, destroyed, or a new ogg_page is read into it. + * As the maximum number of packets an Ogg Page may contain is 255, at most 255 + * packets may be available from an ogg_stream_state at one time. + * + * For writing, the life cycle of a buffer pointed to by a ogg_packet is the + * responsibility of the caller. Packets written into an ogg_stream_state are + * buffered until a complete page is ready for writing. Pages for writing out + * remain in the ogg_stream_state's buffer and valid until either the + * ogg_stream_state is reset, cleared, destroyed. Writing another packet into + * the ogg_stream_state might also invalidate such pages, but writing in + * packets when a page is ready to be written out is a caller bug anyways. + */ + +/*----------------------------------------------------------------------------------------------- +** Private function prototypes. +*/ + +static int ogg_close (SF_PRIVATE *psf) ; +static int ogg_stream_classify (SF_PRIVATE *psf, OGG_PRIVATE * odata) ; +static int ogg_page_classify (SF_PRIVATE * psf, const ogg_page * og) ; +static uint64_t ogg_page_search_do_rescale (uint64_t x, uint64_t from, uint64_t to) ; +static void ogg_page_search_continued_data (OGG_PRIVATE *odata, ogg_page *page) ; + +/*----------------------------------------------------------------------------------------------- +** Exported functions. +*/ + +int +ogg_read_first_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) +{ int ret ; + char *buffer ; /* ** The ogg standard requires that the first pages of a physical ogg @@ -60,47 +160,28 @@ int ogg_read_first_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) ** beyond the scope of this library. */ - /* Weird stuff happens if these aren't called. */ - ogg_stream_reset (&odata->ostream) ; - ogg_sync_reset (&odata->osync) ; + ret = ogg_sync_fseek (psf, psf->header.indx, SEEK_SET) ; + if (ret < 0) + return SFE_NOT_SEEKABLE ; - /* Expose the buffer */ - buffer = ogg_sync_buffer (&odata->osync, 4096L) ; + buffer = ogg_sync_buffer (&odata->osync, psf->header.indx) ; + memcpy (buffer, psf->header.ptr, psf->header.indx) ; + ogg_sync_wrote (&odata->osync, psf->header.indx) ; - /* - ** Grab some data. Beginning-of-stream Ogg pages are guarenteed to be - ** small. 4096 bytes ought to be enough. - */ + ret = ogg_sync_next_page (psf, &odata->opage, SF_MAX (0l, 4096 - psf->header.indx), NULL) ; - /* Avoid seeking if the file has just been opened. */ - if (psf_ftell (psf) == psf->header.indx) - { /* Grab the part of the header that has already been read. */ - memcpy (buffer, psf->header.ptr, psf->header.indx) ; - bytes = psf->header.indx ; - bytes += psf_fread (buffer + psf->header.indx, 1, 4096 - psf->header.indx, psf) ; - } - else - { if (psf_fseek (psf, 0, SEEK_SET) != 0) - return SFE_NOT_SEEKABLE ; - bytes = psf_fread (buffer, 1, 4096, psf) ; - } + /* Have we simply run out of data? If so, we're done. */ + if (ret == 0) + return 0 ; + if (ret < 0) + return psf->error ; - ogg_sync_wrote (&odata->osync, bytes) ; - - /* Get the first page. Check for Beginning-of-stream bit */ - if (ogg_sync_pageout (&odata->osync, &odata->opage) != 1 || - ogg_page_bos (&odata->opage) == 0) - { - /* Have we simply run out of data? If so, we're done. */ - if (bytes < 4096) - return 0 ; - - /* + if (!ogg_page_bos (&odata->opage)) + { /* ** Error case. Either must not be an Ogg bitstream, or is in the ** middle of a bitstream (live capture), or in the middle of a ** bitstream and no complete page was in the buffer. */ - psf_log_printf (psf, "Input does not appear to be the start of an Ogg bitstream.\n") ; return SFE_MALFORMED_FILE ; } ; @@ -109,8 +190,7 @@ int ogg_read_first_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) ** Get the serial number and set up the rest of decode. ** Serialno first ; use it to set up a logical stream. */ - ogg_stream_clear (&odata->ostream) ; - ogg_stream_init (&odata->ostream, ogg_page_serialno (&odata->opage)) ; + ogg_stream_reset_serialno (&odata->ostream, ogg_page_serialno (&odata->opage)) ; if (ogg_stream_pagein (&odata->ostream, &odata->opage) < 0) { /* Error ; stream version mismatch perhaps. */ @@ -125,7 +205,502 @@ int ogg_read_first_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) } ; return 0 ; -} +} /* ogg_read_first_page */ + +int +ogg_write_page (SF_PRIVATE *psf, ogg_page *page) +{ int bytes ; + + bytes = psf_fwrite (page->header, 1, page->header_len, psf) ; + bytes += psf_fwrite (page->body, 1, page->body_len, psf) ; + + return bytes == page->header_len + page->body_len ; +} /* ogg_write_page */ + +sf_count_t +ogg_sync_ftell (SF_PRIVATE *psf) +{ OGG_PRIVATE* odata = (OGG_PRIVATE *) psf->container_data ; + sf_count_t position ; + + position = psf_ftell (psf) ; + if (position >= 0) + { /* success */ + if (position < odata->osync.fill) + { /* Really, this should be an assert. */ + psf->error = SFE_INTERNAL ; + return -1 ; + } + position += (sf_count_t) (odata->osync.returned - odata->osync.fill) ; + } + + return position ; +} /* ogg_sync_ftell */ + +sf_count_t +ogg_sync_fseek (SF_PRIVATE *psf, sf_count_t offset, int whence) +{ OGG_PRIVATE* odata = (OGG_PRIVATE *) psf->container_data ; + sf_count_t ret ; + + ret = psf_fseek (psf, offset, whence) ; + if (ret >= 0) + { /* success */ + odata->eos = 0 ; + ogg_sync_reset (&odata->osync) ; + } + + return ret ; +} /* ogg_sync_fseek */ + +int +ogg_sync_next_page (SF_PRIVATE * psf, ogg_page *og, sf_count_t readmax, sf_count_t *offset) +{ OGG_PRIVATE* odata = (OGG_PRIVATE *) psf->container_data ; + sf_count_t position, nb_read, read_ret ; + unsigned char *buffer ; + int synced ; + int report_hole = 0 ; + + for (position = 0 ; readmax <= 0 || readmax > position ; ) + { synced = ogg_sync_pageseek (&odata->osync, og) ; + if (synced < 0) + { /* + ** Skipped -synced bytes before finding the start of a page. + ** If seeking, we have just landed in the middle of a page. + ** Otherwise, warn about junk in the bitstream. + ** Page might not yet be ready, hence the continue. + */ + if (!offset) + report_hole = 1 ; + position -= synced ; + continue ; + } ; + + if (report_hole) + { psf_log_printf (psf, "Ogg : Skipped %d bytes looking for the next page. Corrupted bitstream?!\n", position) ; + report_hole = 0 ; + } ; + + if (synced > 0) + { /* Have a page */ + if (offset) + *offset += position ; + return og->header_len + og->body_len ; + } ; + + /* + ** Else readmax == 0, Out of data. Try to read more in without + ** invalidating our boundary (readmax) constraint. + */ + if (readmax == 0) + return 0 ; + if (readmax > 0) + nb_read = SF_MIN ((sf_count_t) OGG_SYNC_READ_SIZE, readmax - position) ; + else + nb_read = OGG_SYNC_READ_SIZE ; + buffer = (unsigned char *) ogg_sync_buffer (&odata->osync, nb_read) ; + read_ret = psf_fread (buffer, 1, nb_read, psf) ; + if (read_ret == 0) + return psf->error ? -1 : 0 ; + ogg_sync_wrote (&odata->osync, read_ret) ; + } ; + return 0 ; +} /* ogg_sync_next_page */ + +int +ogg_stream_next_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) +{ int nn ; + + if (odata->eos) + return 0 ; + + for ( ; ; ) + { nn = ogg_sync_next_page (psf, &odata->opage, -1, NULL) ; + if (nn == 0) + { psf_log_printf (psf, "Ogg : File ended unexpectedly without an End-Of-Stream flag set.\n") ; + odata->eos = 1 ; + } + if (nn <= 0) + return nn ; + + if (ogg_page_serialno (&odata->opage) == odata->ostream.serialno) + break ; + } ; + + if (ogg_page_eos (&odata->opage)) + odata->eos = 1 ; + + if (ogg_stream_pagein (&odata->ostream, &odata->opage) < 0) + { psf->error = SFE_INTERNAL ; + return -1 ; + } + + return 1 ; +} /* ogg_stream_next_page */ + +int +ogg_stream_unpack_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) +{ int nn ; + int i ; + int found_hole = 0 ; + ogg_packet *ppkt = odata->pkt ; + + odata->pkt_indx = 0 ; + nn = ogg_stream_packetout (&odata->ostream, ppkt) ; + if (nn == 0) + { /* + ** Steam is out of packets. Read in more pages until there is one, or + ** the stream ends, or an error occurs. + */ + for ( ; nn == 0 ; nn = ogg_stream_packetout (&odata->ostream, ppkt)) + { nn = ogg_stream_next_page (psf, odata) ; + if (nn <= 0) + { odata->pkt_len = 0 ; + return nn ; + } + } + /* + ** In the case of the for loop exiting because + ** ogg_stream_packetout() == -1, fall-through. + */ + } + + if (nn == -1) + { /* + ** libOgg found a hole. That is, the next packet found was out of + ** sequence. As such, "flush" the hole marker by removing the invalid + ** packet, as the valid packets are queued behind it. + */ + psf_log_printf (psf, "Ogg : Warning, libogg reports a hole at %d bytes.\n", ogg_sync_ftell (psf)) ; + nn = ogg_stream_packetout (&odata->ostream, ppkt) ; + found_hole = 1 ; + } + + /* + ** Unpack all the packets on the page. It is undocumented (like much of + ** libOgg behavior) but all packets from a page read into the stream are + ** guarenteed to remain valid in memory until a new page is read into the + ** stream. + */ + for (i = 1 ; ; i++) + { /* Not an off-by-one, there are 255 not 256 packets max. */ + if (i == 255) + { if (ogg_stream_packetpeek (&odata->ostream, NULL) == 1) + { psf->error = SFE_INTERNAL ; + return -1 ; + } + break ; + } + if (ogg_stream_packetout (&odata->ostream, ++ ppkt) != 1) + break ; + } + odata->pkt_len = i ; + + /* 1 = ok, 2 = ok, and found a hole. */ + return 1 + found_hole ; +} /* ogg_stream_unpack_page */ + +sf_count_t +ogg_sync_last_page_before (SF_PRIVATE *psf, OGG_PRIVATE *odata, uint64_t *gp_out, sf_count_t offset, int32_t serialno) +{ sf_count_t begin, end, original_end, chunk_size, ret ; + sf_count_t position = 0 ; + uint64_t gp = -1 ; + int left_link ; + + /* Based on code from Xiph.org's Opusfile */ + + original_end = end = begin = offset ; + offset = -1 ; + chunk_size = OGG_CHUNK_SIZE ; + do + { begin = SF_MAX (begin - chunk_size, (sf_count_t) 0) ; + position = ogg_sync_fseek (psf, begin, SEEK_SET) ; + if (position < 0) + return position ; + left_link = 0 ; + while (position < end) + { ret = ogg_sync_next_page (psf, &odata->opage, end - position, &position) ; + if (ret < 0) + return -1 ; + if (ret == 0) + break ; + if (ogg_page_serialno (&odata->opage) == serialno) + { uint64_t page_gp = ogg_page_granulepos (&odata->opage) ; + if (page_gp != (uint64_t) -1) + { offset = position ; + gp = page_gp ; + } + } + else + left_link = 1 ; + position += ret ; + } + + if ((left_link || !begin) && offset < 0) + { psf->error = SFE_MALFORMED_FILE ; + return -1 ; + } + + chunk_size = SF_MIN (2 * chunk_size, (sf_count_t) OGG_CHUNK_SIZE_MAX) ; + end = SF_MIN (begin + OGG_PAGE_SIZE_MAX - 1, original_end) ; + } + while (offset < 0) ; + + *gp_out = gp ; + return offset ; +} /* ogg_sync_last_page_before */ + +int +ogg_stream_seek_page_search (SF_PRIVATE *psf, OGG_PRIVATE *odata, uint64_t target_gp, uint64_t pcm_start, uint64_t pcm_end, uint64_t *best_gp, sf_count_t begin, sf_count_t end) +{ ogg_page page ; + uint64_t gp ; + sf_count_t d0, d1, d2 ; + sf_count_t best ; + sf_count_t best_start ; + sf_count_t boundary ; + sf_count_t next_boundary ; + sf_count_t page_offset = -1 ; + sf_count_t seek_pos = -1 ; + sf_count_t bisect ; + sf_count_t chunk_size ; + int buffering = SF_FALSE ; + int force_bisect = SF_FALSE ; + int ret ; + int has_packets ; + + *best_gp = pcm_start ; + best = best_start = begin ; + boundary = end ; + + ogg_stream_reset_serialno (&odata->ostream, odata->ostream.serialno) ; + + /* + ** This code is based on op_pcm_seek_page() from Opusfile, which is in turn + ** based on "new search algorithm by Nicholas Vinen" from libvorbisfile. + */ + + d2 = d1 = d0 = end - begin ; + while (begin < end) + { /* + ** Figure out if and where to try and seek in the file. + */ + if (end - begin < OGG_CHUNK_SIZE) + bisect = begin ; + else + { /* Update the interval size history */ + d0 = d1 >> 1 ; + d1 = d2 >> 1 ; + d2 = (end - begin) >> 1 ; + if (force_bisect == SF_TRUE) + bisect = begin + ((end - begin) >> 1) ; + else + { /* Take a decent guess. */ + bisect = begin + ogg_page_search_do_rescale (target_gp - pcm_start, pcm_end - pcm_start, end - begin) ; + } + if (bisect - OGG_CHUNK_SIZE < begin) + bisect = begin ; + else + bisect -= OGG_CHUNK_SIZE ; + force_bisect = SF_FALSE ; + } + + /* + ** Avoid an actual fseek if we can (common for final iterations.) + */ + if (seek_pos != bisect) + { if (buffering == SF_TRUE) + ogg_stream_reset (&odata->ostream) ; + buffering = SF_FALSE ; + page_offset = -1 ; + seek_pos = ogg_sync_fseek (psf, bisect, SEEK_SET) ; + if (seek_pos < 0) + return seek_pos ; + } + + chunk_size = OGG_CHUNK_SIZE ; + next_boundary = boundary ; + + /* + ** Scan forward, figure out where we landed. + ** The ideal case is we see a page that ends before our target followed + ** by a page that ends after our target. + ** If we are too far before or after, breaking out will bisect what we + ** have found so far. + */ + while (begin < end) + { ret = ogg_sync_next_page (psf, &page, boundary - seek_pos, &seek_pos) ; + if (ret < 0) + return ret ; + page_offset = seek_pos ; + if (ret == 0) + { /* + ** There are no more pages in this interval from our stream + ** with a granulepos less than our target. + */ + if (bisect <= begin + 1) + { /* Scanned the whole interval, so we are done. */ + end = begin ; + } + else + { /* + ** Otherwise, back up one chunk. First discard any data + ** from a continued packet. + */ + if (buffering) + ogg_stream_reset (&odata->ostream) ; + buffering = SF_FALSE ; + bisect = SF_MAX (bisect - chunk_size, begin) ; + seek_pos = ogg_sync_fseek (psf, bisect, SEEK_SET) ; + if (seek_pos < 0) + return seek_pos ; + /* Bump up the chunk size. */ + chunk_size = SF_MIN (2 * chunk_size, (sf_count_t) OGG_CHUNK_SIZE_MAX) ; + /* + ** If we did find a page from another stream or without a + ** timestamp, don't read past it. + */ + boundary = next_boundary ; + } + continue ; + } + + /* Found a page. Advance seek_pos past it */ + seek_pos += page.header_len + page.body_len ; + /* + ** Save the offset of the first page we found after the seek, + ** regardless of the stream it came from or whether or not it has a + ** timestamp. + */ + next_boundary = SF_MIN (page_offset, next_boundary) ; + + /* If not from our stream, continue. */ + if (odata->ostream.serialno != (uint32_t) ogg_page_serialno (&page)) + continue ; + + /* + ** The Ogg spec says that a page with a granule pos of -1 must not + ** contain and packets which complete, but the lack of biconditional + ** wording means that /technically/ a packet which does not complete + ** any packets can have a granule pos other than -1. To make matters + ** worse, older versions of libogg did just that. + */ + has_packets = ogg_page_packets (&page) > 0 ; + gp = has_packets ? ogg_page_granulepos (&page) : -1 ; + if (gp == (uint64_t) -1) + { if (buffering == SF_TRUE) + { if (!has_packets) + ogg_stream_pagein (&odata->ostream, &page) ; + else + { /* + ** If packets did end on this page, but we still didn't + ** have a valid granule position (in violation of the + ** spec!), stop buffering continued packet data. + ** Otherwise we might continue past the packet we + ** actually wanted. + */ + ogg_stream_reset (&odata->ostream) ; + buffering = SF_FALSE ; + } + } + continue ; + } + + if (gp < target_gp) + { /* + ** We found a page that ends before our target. Advance to + ** the raw offset of the next page. + */ + begin = seek_pos ; + if (pcm_start > gp || pcm_end < gp) + break ; + /* Save the byte offset of after this page. */ + best = best_start = begin ; + if (buffering) + ogg_stream_reset (&odata->ostream) ; + /* Check to see if the last packet continues. */ + if (page.header [27 + page.header [26] - 1] == 255) + { ogg_page_search_continued_data (odata, &page) ; + /* + ** If we have a continued packet, remember the offset of + ** this page's start, so that if we do wind up having to + ** seek back here later, we can prime the stream with the + ** continued packet data. With no continued packet, we + ** remember the end of the page. + */ + best_start = page_offset ; + } ; + /* + ** Then force buffering on, so that if a packet starts (but + ** does not end) on the next page, we still avoid the extra + ** seek back. + */ + buffering = SF_TRUE ; + *best_gp = pcm_start = gp ; + if (target_gp - gp > 48000) + { /* Out by over a second. Try another bisection. */ + break ; + } + /* Otherwise, keep scanning forward (do NOT use begin+1). */ + bisect = begin ; + } + else + { /* + ** Found a page that ends after our target. If we had just + ** scanned the whole interval before we found it, we're good. + */ + if (bisect <= begin + 1) + end = begin ; + else + { end = bisect ; + /* + ** In later iterations, don't read past the first page we + ** found. + */ + boundary = next_boundary ; + /* + ** If we're not making much progress shrinking the interval + ** size, start forcing straight bisection to limit the + ** worst case. + */ + force_bisect = end - begin > d0 * 2 ? SF_TRUE : SF_FALSE ; + /* + ** Don't let pcm_end get out of range! That could happen + ** with an invalid timestamp. + */ + if (pcm_end > gp && pcm_start <= gp) + pcm_end = gp ; + } + break ; + } + } + } + + /* + ** If we are buffering, the page we want is currently buffered in the + ** Ogg stream structure, or in the Ogg page which has not been submitted. + ** If not, we need to seek back and load it again. + */ + if (buffering == SF_FALSE) + { if (best_start != page_offset) + { page_offset = -1 ; + seek_pos = ogg_sync_fseek (psf, best_start, SEEK_SET) ; + if (seek_pos < 0) + return seek_pos ; + } + if (best_start < best) + { if (page_offset < 0) + { ret = ogg_sync_next_page (psf, &page, -1, &seek_pos) ; + if (seek_pos != best_start) + return -1 ; + } + ogg_page_search_continued_data (odata, &page) ; + page_offset = -1 ; + } + } ; + + if (page_offset >= 0) + ogg_stream_pagein (&odata->ostream, &page) ; + + return 0 ; +} /* ogg_stream_seek_page_search */ int ogg_open (SF_PRIVATE *psf) @@ -160,6 +735,9 @@ ogg_open (SF_PRIVATE *psf) psf->container_close = NULL ; return flac_open (psf) ; + case SF_FORMAT_OGG | SF_FORMAT_OPUS : + return ogg_opus_open (psf) ; + #if ENABLE_EXPERIMENTAL_CODE case SF_FORMAT_OGG | SF_FORMAT_SPEEX : return ogg_speex_open (psf) ; @@ -177,6 +755,9 @@ ogg_open (SF_PRIVATE *psf) return SFE_INTERNAL ; } /* ogg_open */ +/*============================================================================== +** Private functions. +*/ static int ogg_close (SF_PRIVATE *psf) @@ -194,6 +775,7 @@ ogg_stream_classify (SF_PRIVATE *psf, OGG_PRIVATE* odata) /* Call this here so it only gets called once, so no memory is leaked. */ ogg_sync_init (&odata->osync) ; + ogg_stream_init (&odata->ostream, 0) ; /* Load the first page in the physical bitstream. */ if ((error = ogg_read_first_page (psf, odata)) != 0) @@ -215,6 +797,10 @@ ogg_stream_classify (SF_PRIVATE *psf, OGG_PRIVATE* odata) psf->sf.format = SF_FORMAT_OGG | SF_FORMAT_SPEEX ; return 0 ; + case OGG_OPUS : + psf->sf.format = SF_FORMAT_OGG | SF_FORMAT_OPUS ; + return 0 ; + case OGG_PCM : psf_log_printf (psf, "Detected Ogg/PCM data. This is not supported yet.\n") ; return SFE_UNIMPLEMENTED ; @@ -241,6 +827,7 @@ static struct { "PCM ", "PCM", 8, OGG_PCM }, { "Speex", "Speex", 5, OGG_SPEEX }, { "\001vorbis", "Vorbis", 7, OGG_VORBIS }, + { "OpusHead", "Opus", 8, OGG_OPUS }, } ; static int @@ -271,6 +858,47 @@ ogg_page_classify (SF_PRIVATE * psf, const ogg_page * og) return 0 ; } /* ogg_page_classify */ +/* +** Scale x from the range [0, from] to the range [0, to] +*/ +static uint64_t +ogg_page_search_do_rescale (uint64_t x, uint64_t from, uint64_t to) +{ uint64_t frac ; + uint64_t ret ; + int i ; + + /* I should have paid more attention in CSc 349A: Numerical Analysis */ + if (x >= from) + return to ; + if (x == 0) + return 0 ; + frac = 0 ; + for (i = 0 ; i < 63 ; i++) + { frac <<= 1 ; + if (x >= from >> 1) + { x -= from - x ; + frac |= 1 ; + } + else + x <<= 1 ; + } + ret = 0 ; + for (i = 0 ; i < 63 ; i++) + { if (frac & 1) + ret = (ret & to & 1) + (ret >> 1) + (to >> 1) ; + else + ret >>= 1 ; + frac >>= 1 ; + } + return ret ; +} /* ogg_page_search_do_rescale */ + +static void +ogg_page_search_continued_data (OGG_PRIVATE *odata, ogg_page *page) +{ ogg_stream_pagein (&odata->ostream, page) ; + while (ogg_stream_packetout (&odata->ostream, &odata->opacket)) ; +} /* ogg_page_search_continued_data */ + #else /* HAVE_EXTERNAL_XIPH_LIBS */ int diff --git a/src/ogg.h b/src/ogg.h index 4e53d6a7..5c837a8e 100644 --- a/src/ogg.h +++ b/src/ogg.h @@ -1,5 +1,6 @@ /* ** Copyright (C) 2008-2011 Erik de Castro Lopo +** Copyright (C) 2018 Arthur Taylor ** ** This program is free software ; you can redistribute it and/or modify ** it under the terms of the GNU Lesser General Public License as published by @@ -17,6 +18,7 @@ */ #ifndef SF_SRC_OGG_H +#define SF_SRC_OGG_H enum { OGG_ANNODEX = 300, @@ -26,6 +28,7 @@ enum OGG_PCM, OGG_SPEEX, OGG_VORBIS, + OGG_OPUS, } ; typedef struct @@ -37,6 +40,14 @@ typedef struct ogg_page opage ; /* One raw packet of data for decode */ ogg_packet opacket ; + + /* Unpacked packets. 255 is max there can ever be in one page. */ + ogg_packet pkt [255] ; + /* How many packets */ + int pkt_len ; + /* Current packet */ + int pkt_indx ; + int eos ; int codec ; } OGG_PRIVATE ; @@ -49,4 +60,75 @@ typedef struct int ogg_read_first_page (SF_PRIVATE *, OGG_PRIVATE *) ; +/* +** Write the whole Ogg page out. Convenience function as the ogg_page struct +** splits header and body data into separate buffers. +*/ +int ogg_write_page (SF_PRIVATE *, ogg_page *) ; + +/* +** Wrapper around psf_ftell() that returns the current offset in the file after +** the most recent page that has been returned by ogg_sync_pageout(). +*/ +sf_count_t ogg_sync_ftell (SF_PRIVATE *) ; + +/* +** Wrapper around psf_fseek() that on success resets the ogg_sync_state struct +** so that it doesn't get corrupted. +*/ +sf_count_t ogg_sync_fseek (SF_PRIVATE *, sf_count_t offset, int whence) ; + +/* +** Get the next page from the physical bitstream, reading in data as necessary. +** Pays no attention to Ogg BOS/EOS markers or stream serial numbers. +** The page is buffered in the ogg_sync_state struct, (replacing any other +** buffered there) and also returned in *og. readmax sets a boundary for how +** many bytes more may be read from the file, use already buffered only, or +** unlimited reading in the case of a positive, zero or negative argument +** respectively. If a pointer to a sf_count_t is passed in offset, then it will +** be incremented by how many bytes were skipped to find the next page header. +** (Useful for seeking, normally zero.) Returns the page size in bytes on +** success, 0 on out-of-data (be it end of file or readmax reached) and -1 on +** error with psf->error set appropriately. +*/ +int ogg_sync_next_page (SF_PRIVATE * psf, ogg_page *og, sf_count_t readmax, sf_count_t *offset) ; + +/* +** Load the last page of a stream before the provided file offset. Searches the +** physical bitstream, and selects a page of the passed serialno. The page +** found is loaded in the sync buffer and exposed in odata->opage, and not +** loaded into the ogg_stream_state. If found, the granulepos is returned in +** *gp_out. Returns the file offset *before* the last page on success, or -1 on +** error, setting psf->error as appropriate. +*/ +sf_count_t ogg_sync_last_page_before (SF_PRIVATE *psf, OGG_PRIVATE *odata, uint64_t *gp_out, sf_count_t offset, int32_t serialno) ; + +/* +** Load the next page from the virtual bitstream, reading data as necessary. +** Reads in pages from the physical bitstream, skipping pages until one of the +** virtual bitstream of interest is found, and then feeds it into the +** ogg_stream_state of odata->ostream, where it is buffered. Heeds EOS markers. +** Returns 1 on success, 0 on end of stream, and -1 on fatal error. +*/ +int ogg_stream_next_page (SF_PRIVATE * psf, OGG_PRIVATE *odata) ; + +/* +** Loads the next page using ogg_stream_next_page() and unpacks all packets +** into the array odata->pkt, updating odata->pkt_len and setting +** odata->pkt_indx to 0. Returns 1 if okay, 2 if okay but a hole was found +** in the bitstream, 0 if on end of stream, and -1 on fatal error. +*/ +int ogg_stream_unpack_page (SF_PRIVATE *psf, OGG_PRIVATE *odata) ; + +/* +** Seek within the Ogg virtual bitstream for a page containing target_gp. +** Preforms a bisection search. If not found exactly, the best result is +** returned in *best_gp. Found page is loaded into the virtual bitstream, +** ready for unpacking. Arguments pcm_start and pcm_end are the highest and +** lowest granule positions of the file. begin and end are the file offsets. +*/ +int ogg_stream_seek_page_search (SF_PRIVATE *psf, OGG_PRIVATE *odata, + uint64_t target_gp, uint64_t pcm_start, uint64_t pcm_end, + uint64_t *best_gp, sf_count_t begin, sf_count_t end) ; + #endif /* SF_SRC_OGG_H */ diff --git a/src/ogg_opus.c b/src/ogg_opus.c index 4a0ca1fd..5251f6d3 100644 --- a/src/ogg_opus.c +++ b/src/ogg_opus.c @@ -1,5 +1,6 @@ /* ** Copyright (C) 2013-2016 Erik de Castro Lopo +** Copyright (C) 2018 Arthur Taylor ** ** This program is free software ; you can redistribute it and/or modify ** it under the terms of the GNU Lesser General Public License as published by @@ -16,6 +17,128 @@ ** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ +/* +** This file contains code based on OpusFile and Opus-Tools, both by +** Xiph.Org. COPYING from each is identical and is as follows: +** +** Copyright (c) 1994-2013 Xiph.Org Foundation and contributors +** +** Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions +** are met: +** +** - Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** +** - Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in the +** documentation and/or other materials provided with the distribution. +** +** - Neither the name of the Xiph.Org Foundation nor the names of its +** contributors may be used to endorse or promote products derived from +** this software without specific prior written permission. +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE FOUNDATION +** OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +*/ + +/* +** TODO: +** - Channel mapping modification / reporting +** - connect psf->channel_map and Opus channel mapping somehow? +** - Gain parameters and their mappings +*/ + +/* +** Opus Sample, Frame, and Samples/Channel Terminology +** +** libsndfile refers to one PCM value as a 'sample,' and a group of samples of +** the same sample time, one for each channel, as a 'frame.' This differs from +** Opus, which has no corresponding name for sample, and refers to a group of +** PCM values, one per channel (aka libsndfile frames) as 'samples.' +** Further, Opus has an object called a 'frame' that is made up of multiple +** Opus-samples. +** All this means that one has to be careful with what is meant by each term. +** In an attempt to avoid ambiguity, this file adopts the following terms: +** - Samples shall refer to discrete PCM values, regardless of any channel +** considerations. This is the same as what libsndfile calls samples. +** - Samples/channel shall refer to groups of samples, one for each channel. +** This is what Opus calles samples, and what libsndfile calles frames. It +** has the advantage that its name is also the formula to calculate it. +** +** +** Opus vs OggOpus +** +** In this file a distinction is made between Opus and OggOpus. Opus refers to +** the codec alone, support for which is by libopus. OggOpus refers to an Opus +** payload encapsulated in an Ogg stream. This is also know as an "Opus file." +** The OggOpus spec includes information on header and granule position +** interpretation, which is outside of the scope of the Opus spec. As such, an +** attempt here is made to refer to either Opus or OggOpus depending on which +** spec is being referenced. See https://wiki.xiph.org/OggOpus +** +** +** Opus Sample Rates +** +** Opus only supports a fixed number of sample rates: 48kHz, 24kHz, 16kHz, +** 12kHz, 8kHz. Audio may be decoded or encoded at any of these rates, +** independent of the rate it was encoded at or to be decoded at respectively. +** Other sample rates must be converted to one of these rates. +** +** As 44.1kHz (CD sample rate) and 22.5kHz are popular sample rates, and to +** support any other sample rate there may be, the Opus header includes a field +** to save the input (original) sample rate before converting it to a supported +** one. Implementations are recommended by the Opus spec to do a sample rate +** conversion at encode, but decode at 48kHz if outputting to hardware, or do +** the reverse sample rate conversion if outputting to file. +** +** Heretofore libsndfile does not contain a sample rate converter, so doing the +** sample rate conversion is not supported. Instead audio must be provided by +** the user at a supported rate. However, the input sample rate field can be +** set and retrieved by the user using sf_command(). At decode we choose to +** decode at the lowest valid rate that is greater than or equal to the input +** sample rate. +** +** +** OggOpus Granule Positions +** +** Ogg streams include a strictly increasing granule position value. The +** interpretation of this value is dependent on the payload type. For Opus +** streams the granule position is the count of samples in the stream when +** encoding/decoding at 48kHz. Note that the actual position of the output +** sample relative to the granule position is offset by the preskip amount. +** That is, if a packet ends with a granule position of x, the last sample +** output when decoding is actually sample (x - preskip). +** +** Further, to allow for clipping off of the front of a stream without +** rewriting all following granule positions, an Opus stream granule position +** may be offset by a constant amount. This amount is evident by comparing the +** granule position of the first page of an Opus stream on which an audio +** packet completes is greater than the sum of the samples of all audio +** packets completed on the page. Only the first such page is allows to have an +** 'excessive' granule position, and only if it is not also the last page of +** the stream (e_o_s bit is not set.) +** +** The granule position is an unsigned 64-bit integer, with the special value +** of UINT64_MAX/-1 being treated as invalid. However, as not all platforms +** support unsigned 64-bit integers, libOgg uses signed 64-bit integers for the +** granule position. +** +** Remembering that signed integer overflow/underflow is explicitly undefined +** in C, and as we already assume support for unsigned 64-bit integers, the +** easiest way to deal with this problem is to modify granule positions as +** unsigned integers. +*/ + #include "sfconfig.h" @@ -36,21 +159,154 @@ #include "sfendian.h" #include "common.h" -#if (ENABLE_EXPERIMENTAL_CODE && HAVE_EXTERNAL_XIPH_LIBS) +#if HAVE_EXTERNAL_XIPH_LIBS #include +#include +#include #include "ogg.h" +#include "ogg_vcomment.h" + +#define OGG_OPUS_COMMENT_PAD (512) /* Same as oggenc default */ +#define OGG_OPUS_PAGE_LATENCY (1000 * 48) /* 1 second */ + +/* +** Opus packets can be any multiple of 2.5ms (at 48kHz). We use the recommended +** default for non-realtime of 20ms. While longer packets reduce the overhead +** data somewhat, it also decreases the quality. +*/ +#define OGG_OPUS_ENCODE_PACKET_LEN(samplerate) ((20 * (samplerate)) / 1000) + +/* +** How long does it take for a decoder to converge (avoiding flush on seek. +*/ +#define OGG_OPUS_PREROLL (80 * 48) /* 80 milliseconds */ typedef struct -{ int32_t serialno ; +{ int version ; + /* Number of channels, 1...255 */ + int channels ; - void * state ; + /* Encoder latency, the amount to skip before valid data comes out. */ + int preskip ; + + /* The sample rate of a the encoded source, as it may have been converted. */ + int input_samplerate ; + + /* 'baked-in' gain to apply, dB S7.8 format. Should be zero when possible. */ + int16_t gain ; + + /* Channel mapping type. See OggOpus spec */ + int channel_mapping ; + + /* The rest is only used if channel_mapping != 0 */ + /* How many streams are there? */ + int nb_streams ; + + /* How man of those streams are coupled? (aka stereo) */ + int nb_coupled ; + + /* Mapping of opus streams to output channels */ + unsigned char stream_map [255] ; +} OpusHeader ; + +typedef struct +{ uint32_t serialno ; + OpusHeader header ; + + /* Granule position before the current packet */ + uint64_t pkt_pos ; + + /* Granule position at the end of the current page (encode: last completed) */ + uint64_t pg_pos ; + + /* integer coefficient of (current sample rate) / 48000Hz */ + int sr_factor ; + + /* Current position in buffer expressed as samples/channel */ + int loc ; + + /* Current data fill (decode) or target (encode) of buffer expressed in samples/channel */ + int len ; + + /* Size of the buffer storage, in sizeof (float) * channels */ + int buffersize ; + + /* Samples, either decoded from a packet, or assembling for encode. */ + float *buffer ; + + union { + /* decode only members */ + struct { + OpusMSDecoder *state ; + uint64_t gp_start ; + uint64_t gp_end ; + sf_count_t last_offset ; + } decode ; + + /* encode only members */ + struct { + OpusMSEncoder *state ; + + /* How many Ogg page segments are in Ogg page currently being assembled. */ + int last_segments ; + + int bitrate ; + + /* Least significant bit of the source (aka bitwidth) */ + int lsb ; + int lsb_last ; + } encode ; + } u ; } OPUS_PRIVATE ; -static int ogg_opus_read_header (SF_PRIVATE * psf) ; -static int ogg_opus_close (SF_PRIVATE *psf) ; +/*----------------------------------------------------------------------------------------------- +** Private function prototypes. +*/ + +static int ogg_opus_close (SF_PRIVATE *psf) ; +static void opus_print_header (SF_PRIVATE *psf, OpusHeader *h) ; +static int opus_read_header_packet (SF_PRIVATE *psf, OpusHeader *h, ogg_packet *opacket) ; +static int ogg_opus_read_header (SF_PRIVATE * psf) ; +static int ogg_opus_setup_decoder (SF_PRIVATE *psf, int input_samplerate) ; + +static int ogg_opus_setup_encoder (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) ; +static int ogg_opus_write_header (SF_PRIVATE * psf, int calc_length) ; +static void ogg_opus_flush (SF_PRIVATE *psf) ; +static int ogg_opus_unpack_next_page (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) ; +static int ogg_opus_calculate_page_duration (OGG_PRIVATE *odata) ; +static int ogg_opus_read_refill (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) ; +static int ogg_opus_write_out (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) ; + +static sf_count_t ogg_opus_read_s (SF_PRIVATE *psf, short *ptr, sf_count_t len) ; +static sf_count_t ogg_opus_read_i (SF_PRIVATE *psf, int *ptr, sf_count_t len) ; +static sf_count_t ogg_opus_read_f (SF_PRIVATE *psf, float *ptr, sf_count_t len) ; +static sf_count_t ogg_opus_read_d (SF_PRIVATE *psf, double *ptr, sf_count_t len) ; + +static sf_count_t ogg_opus_write_s (SF_PRIVATE *psf, const short *ptr, sf_count_t len) ; +static sf_count_t ogg_opus_write_i (SF_PRIVATE *psf, const int *ptr, sf_count_t len) ; +static sf_count_t ogg_opus_write_f (SF_PRIVATE *psf, const float *ptr, sf_count_t len) ; +static sf_count_t ogg_opus_write_d (SF_PRIVATE *psf, const double *ptr, sf_count_t len) ; + +static sf_count_t ogg_opus_seek (SF_PRIVATE *psf, int mode, sf_count_t offset) ; +static sf_count_t ogg_opus_seek_null_read (SF_PRIVATE *psf, sf_count_t offset) ; +static sf_count_t ogg_opus_seek_manual (SF_PRIVATE *psf, uint64_t target_gp) ; +static int ogg_opus_seek_page_search (SF_PRIVATE *psf, uint64_t target_gp) ; + +static int ogg_opus_analyze_file (SF_PRIVATE *psf) ; +static int ogg_opus_command (SF_PRIVATE *psf, int command, void *data, int datasize) ; +static int ogg_opus_byterate (SF_PRIVATE *psf) ; + +/*----------------------------------------------------------------------------------------------- +*/ + +static vorbiscomment_ident opustags_ident = { "OpusTags", 8 } ; + +/*----------------------------------------------------------------------------------------------- +** Exported functions. +*/ int ogg_opus_open (SF_PRIVATE *psf) @@ -70,76 +326,1394 @@ ogg_opus_open (SF_PRIVATE *psf) if (psf->file.mode == SFM_RDWR) return SFE_BAD_MODE_RW ; - if (psf->file.mode == SFM_READ) - { /* Call this here so it only gets called once, so no memory is leaked. */ - ogg_sync_init (&odata->osync) ; + psf_log_printf (psf, "Opus library version: %s\n", opus_get_version_string ()) ; - if ((error = ogg_opus_read_header (psf))) + psf->codec_close = ogg_opus_close ; + if (psf->file.mode == SFM_READ) + { if ((error = ogg_opus_read_header (psf))) + return error ; + if ((error = ogg_opus_analyze_file (psf))) return error ; -#if 0 psf->read_short = ogg_opus_read_s ; psf->read_int = ogg_opus_read_i ; psf->read_float = ogg_opus_read_f ; psf->read_double = ogg_opus_read_d ; - psf->sf.frames = ogg_opus_length (psf) ; -#endif } ; - psf->codec_close = ogg_opus_close ; - if (psf->file.mode == SFM_WRITE) - { -#if 0 - /* Set the default oopus quality here. */ - vdata->quality = 0.4 ; + { if ((error = ogg_opus_setup_encoder (psf, odata, oopus))) + return error ; psf->write_header = ogg_opus_write_header ; psf->write_short = ogg_opus_write_s ; psf->write_int = ogg_opus_write_i ; psf->write_float = ogg_opus_write_f ; psf->write_double = ogg_opus_write_d ; -#endif psf->sf.frames = SF_COUNT_MAX ; /* Unknown really */ psf->strings.flags = SF_STR_ALLOW_START ; + psf->datalength = 0 ; + psf->dataoffset = 0 ; /* will be updated */ } ; - psf->bytewidth = 1 ; - psf->blockwidth = psf->bytewidth * psf->sf.channels ; - -#if 0 psf->seek = ogg_opus_seek ; psf->command = ogg_opus_command ; -#endif - - /* FIXME, FIXME, FIXME : Hack these here for now and correct later. */ - psf->sf.format = SF_FORMAT_OGG | SF_FORMAT_SPEEX ; - psf->sf.sections = 1 ; - - psf->datalength = 1 ; - psf->dataoffset = 0 ; - /* End FIXME. */ + psf->byterate = ogg_opus_byterate ; + psf->sf.format = SF_FORMAT_OGG | SF_FORMAT_OPUS ; return error ; } /* ogg_opus_open */ -static int -ogg_opus_read_header (SF_PRIVATE * UNUSED (psf)) -{ - return 0 ; -} /* ogg_opus_read_header */ +/*============================================================================== +** Private functions. +*/ static int -ogg_opus_close (SF_PRIVATE * UNUSED (psf)) -{ +ogg_opus_close (SF_PRIVATE *psf) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + if (!oopus) + return 0 ; + + if (psf->file.mode == SFM_WRITE) + { if (psf->have_written) + ogg_opus_flush (psf) ; + else { + /* Write a header... it is expected. */ + ogg_opus_write_header (psf, 0) ; + } ; + ogg_packet_clear (&odata->opacket) ; + if (oopus->u.encode.state) + { opus_multistream_encoder_destroy (oopus->u.encode.state) ; + oopus->u.encode.state = NULL ; + } ; + } + else if (psf->file.mode == SFM_READ) + { if (oopus->u.decode.state) + { opus_multistream_decoder_destroy (oopus->u.decode.state) ; + oopus->u.decode.state = NULL ; + } ; + } ; + + psf->codec_data = NULL ; + if (oopus->buffer) + free (oopus->buffer) ; + free (oopus) ; return 0 ; } /* ogg_opus_close */ +static void +opus_print_header (SF_PRIVATE *psf, OpusHeader *h) +{ psf_log_printf (psf, "Opus Header Metadata\n") ; + psf_log_printf (psf, " OggOpus version : %d\n", h->version) ; + psf_log_printf (psf, " Channels : %d\n", h->channels) ; + psf_log_printf (psf, " Preskip : %d samples @48kHz\n", h->preskip) ; + psf_log_printf (psf, " Input Samplerate : %d Hz\n", h->input_samplerate) ; + psf_log_printf (psf, " Gain : %d.%d\n", arith_shift_right (h->gain & 0xF0, 8), h->gain & 0x0F) ; + psf_log_printf (psf, " Channel Mapping : ") ; + switch (h->channel_mapping) + { case 0 : psf_log_printf (psf, "0 (mono or stereo)\n") ; break ; + case 1 : psf_log_printf (psf, "1 (surround, AC3 channel order)\n") ; break ; + case 255 : psf_log_printf (psf, "255 (no channel order)\n") ; break ; + default : psf_log_printf (psf, "%d (unknown or unsupported)\n", h->channel_mapping) ; break ; + } ; -#else /* ENABLE_EXPERIMENTAL_CODE && HAVE_EXTERNAL_XIPH_LIBS */ + if (h->channel_mapping > 0) + { int i ; + psf_log_printf (psf, " streams total : %d\n", h->nb_streams) ; + psf_log_printf (psf, " streams coupled : %d\n", h->nb_coupled) ; + psf_log_printf (psf, " stream mapping : [") ; + for (i = 0 ; i < h->channels - 1 ; i++) + psf_log_printf (psf, "%d,", h->stream_map [i]) ; + psf_log_printf (psf, "%d]\n", h->stream_map [i]) ; + } ; +} /* opus_print_header */ + +static int +opus_read_header_packet (SF_PRIVATE *psf, OpusHeader *h, ogg_packet *opacket) +{ int count, i ; + + /* + ** Opus headers are 19 bytes, in the case of type 0 channel mapping, + ** or 19 + 2 + (1 * channel count) bytes for other channel mappings, to a + ** maximum of 276 (255 channels). + */ + + if (opacket->bytes < 19 || opacket->bytes > 276) + return SFE_MALFORMED_FILE ; + + if (memcmp (opacket->packet, "OpusHead", 8) != 0) + return SFE_MALFORMED_FILE ; + + /* + ** Copy the header page into the binheader so we can use binheader + ** functions to safely unpack it. + */ + count = psf_binheader_writef (psf, "ob", BHWo (0), BHWv (opacket->packet), BHWz (opacket->bytes)) ; + psf->header.end = count ; + + count = psf_binheader_readf (psf, "ep1", 8, &h->version) ; + if (! (h->version == 1 || h->version == 0)) + { psf_log_printf (psf, "Opus : Unknown / unsupported embedding scheme version: %d.\n", h->version) ; + return SFE_UNIMPLEMENTED ; + } ; + + count += psf_binheader_readf (psf, "e12421", &h->channels, &h->preskip, + &h->input_samplerate, &h->gain, &h->channel_mapping) ; + + if (h->channel_mapping == 0) + { if (h->channels > 2) + return SFE_MALFORMED_FILE ; + + /* + ** Setup the stream mapping, so we can use the multistream decoder, + ** rather than have to deal with two decoder pointer types + */ + h->nb_streams = 1 ; + h->nb_coupled = h->channels - 1 ; + h->stream_map [0] = 0 ; + h->stream_map [1] = 1 ; + } + else + { if (opacket->bytes < 19 + 2 + h->channels) + return SFE_MALFORMED_FILE ; + + if (h->channel_mapping == 1 && h->channels > 8) + return SFE_MALFORMED_FILE ; + + count += psf_binheader_readf (psf, "11", &h->nb_streams, &h->nb_coupled) ; + + if (h->nb_streams < 1 || + h->nb_coupled > h->nb_streams || + h->nb_coupled + h->nb_streams > 255) + return SFE_MALFORMED_FILE ; + + for (i = 0 ; i < h->channels ; i++) + { count += psf_binheader_readf (psf, "1", &(h->stream_map [i])) ; + if (h->stream_map [i] > h->nb_streams + h->nb_coupled && h->stream_map [i] != 255) + return SFE_MALFORMED_FILE ; + } ; + } ; + + if (count != opacket->bytes) + { /* OggOpus spec mandates that this is a hard error. */ + psf_log_printf (psf, "Opus : Error, extra data in Ogg Opus header.\n") ; + return SFE_MALFORMED_FILE ; + } ; + + opus_print_header (psf, h) ; + + return 0 ; +} /* ogg_opus_read_header_packet */ + +static int +ogg_opus_read_header (SF_PRIVATE *psf) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + int error ; + + /* + ** First page is already loaded by the ogg container code when it + ** classified the stream, no need to re-load it now. + */ + + if (ogg_page_packets (&odata->opage) != 1 || !ogg_page_bos (&odata->opage)) + return SFE_MALFORMED_FILE ; + + oopus->serialno = ogg_page_serialno (&odata->opage) ; + if ((error = opus_read_header_packet (psf, &oopus->header, &odata->opacket))) + return error ; + + /* + ** The comment header MUST be next. It is one packet, that packet MUST begin + ** on the second page of the stream, but it MAY span multiple pages. + */ + + while (ogg_stream_packetout (&odata->ostream, &odata->opacket) != 1) + { if (ogg_stream_next_page (psf, odata) != 1) + { /* out of data... technically that's malformed. */ + return psf->error ? psf->error : SFE_MALFORMED_FILE ; + } ; + } ; + + if ((error = vorbiscomment_read_tags (psf, &odata->opacket, &opustags_ident))) + return error ; + + return ogg_opus_setup_decoder (psf, oopus->header.input_samplerate) ; +} /* ogg_opus_read_header */ + +static int +ogg_opus_setup_decoder (SF_PRIVATE *psf, int input_samplerate) +{ OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + OpusMSDecoder *decoder ; + int sr_factor ; + int error ; + + /* + ** Decide what sample rate to decode at. We choose the lowest valid rate + ** that is greater or equal to the original rate. + ** + ** Opus documentation recommends always decoding at 48000Hz if the file is + ** being decoded for playback, since most hardware will resample it back to + ** 48000Hz anyways. We don't know if that's true, maybe the user is + ** decoding for editing or transcoding purposes. + */ + if (input_samplerate > 24000) + sr_factor = 1 ; + else if (input_samplerate > 16000) + sr_factor = 2 ; + else if (input_samplerate > 12000) + sr_factor = 3 ; + else if (input_samplerate > 8000) + sr_factor = 4 ; + else + sr_factor = 6 ; + + decoder = opus_multistream_decoder_create ( + 48000 / sr_factor, + oopus->header.channels, + oopus->header.nb_streams, + oopus->header.nb_coupled, + oopus->header.stream_map, + &error) ; + + if (error != OPUS_OK) + { psf_log_printf (psf, "Opus : Failed to create multistream decoder: %s\n", + opus_strerror (error)) ; + return SFE_INTERNAL ; + } + + /* + ** Replace the decoder, if one was already initialized (see + ** SFC_GET_ORIGINAL_SAMPLERATE) + */ + if (oopus->u.decode.state) + opus_multistream_decoder_destroy (oopus->u.decode.state) ; + oopus->u.decode.state = decoder ; + + oopus->sr_factor = sr_factor ; + psf->sf.samplerate = 48000 / sr_factor ; + psf->sf.channels = oopus->header.channels ; + oopus->loc = oopus->len = 0 ; + + /* + ** The Opus decoder can do our gain for us. The OggOpus header contains a + ** gain field. This field, unlike various gain-related tags, is intended to + ** be a perminent baked-in gain applied before any user-configurable gain + ** (eg replay-gain.) This is so the gain of track can be set without having + ** to re-encode. + ** + ** Both the header.gain field and the parameter are in the Q7.8 format. + ** + ** TODO: Make this configurable? Include other gain sources too? + */ + opus_multistream_decoder_ctl (oopus->u.decode.state, OPUS_SET_GAIN (oopus->header.gain)) ; + + /* + ** Opus packets can vary in length, with the legal values being 2.5, 5, 10, + ** 20, 40 or 60ms. The recommended default for non-realtime is 20ms. As + ** such, allocate a buffer of that size now, we'll realloc later if a + ** larger one is needed. + ** + ** buffersize is expressed in samples/channel, as that is what opus_decode + ** expects. + */ + if (oopus->buffer) + free (oopus->buffer) ; + oopus->buffersize = (20 * psf->sf.samplerate / 1000) ; + oopus->buffer = (float *) malloc (sizeof (float) * psf->sf.channels * oopus->buffersize) ; + if (oopus->buffer == NULL) + return SFE_MALLOC_FAILED ; + + return 0 ; +} /* ogg_opus_setup_decoder */ + +static int +ogg_opus_setup_encoder (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) +{ int error ; + + switch (psf->sf.samplerate) + { case 8000 : + case 12000 : + case 16000 : + case 24000 : + case 48000 : + oopus->sr_factor = 48000 / psf->sf.samplerate ; + break ; + default : + return SFE_OPUS_BAD_SAMPLERATE ; + } ; + + if (psf->sf.channels <= 2) + { oopus->header.channel_mapping = 0 ; + oopus->header.nb_streams = 1 ; + oopus->header.nb_coupled = psf->sf.channels - 1 ; + oopus->header.stream_map [0] = 0 ; + oopus->header.stream_map [1] = 1 ; + + oopus->u.encode.state = opus_multistream_encoder_create ( + psf->sf.samplerate, + psf->sf.channels, + oopus->header.nb_streams, + oopus->header.nb_coupled, + oopus->header.stream_map, + OPUS_APPLICATION_AUDIO, + &error) ; + } + else + { if (psf->sf.channels <= 8) + { /* Use Vorbis/AC3 channel mappings for surround. */ + oopus->header.channel_mapping = 1 ; + } + else + { /* There is no channel mapping, just audio, in parallel, good luck */ + oopus->header.channel_mapping = 255 ; + } + + oopus->u.encode.state = opus_multistream_surround_encoder_create ( + psf->sf.samplerate, + psf->sf.channels, + oopus->header.channel_mapping, + &oopus->header.nb_streams, + &oopus->header.nb_coupled, + oopus->header.stream_map, + OPUS_APPLICATION_AUDIO, + &error) ; + } + + if (error != OPUS_OK) + { psf_log_printf (psf, "Opus : Error, opus_multistream_encoder_create returned %s\n", opus_strerror (error)) ; + return SFE_BAD_OPEN_FORMAT ; + } ; + + opus_multistream_encoder_ctl (oopus->u.encode.state, OPUS_GET_BITRATE (&oopus->u.encode.bitrate)) ; + psf_log_printf (psf, "Encoding at target bitrate of %dbps\n", oopus->u.encode.bitrate) ; + + /* TODO: Make configurable? */ + error = opus_multistream_encoder_ctl (oopus->u.encode.state, OPUS_SET_COMPLEXITY (10)) ; + if (error != OPUS_OK) + { /* Non-fatal */ + psf_log_printf (psf, "Opus : OPUS_SET_COMPLEXITY returned: %s\n", opus_strerror (error)) ; + } + + /* + ** Get the encoder delay. This can vary depending on implementation and + ** encoder configuration. + ** GOTCHA: This returns the preskip at the encoder samplerate, not the + ** granulepos rate of 48000Hz needed for header.preskip. + */ + error = opus_multistream_encoder_ctl (oopus->u.encode.state, OPUS_GET_LOOKAHEAD (&oopus->header.preskip)) ; + if (error != OPUS_OK) + { psf_log_printf (psf, "Opus : OPUS_GET_LOOKAHEAD returned: %s\n", opus_strerror (error)) ; + return SFE_BAD_OPEN_FORMAT ; + } ; + oopus->header.preskip *= oopus->sr_factor ; + + oopus->len = OGG_OPUS_ENCODE_PACKET_LEN (psf->sf.samplerate) ; + oopus->buffer = (float *) malloc (sizeof (float) * psf->sf.channels * oopus->len) ; + if (oopus->buffer == NULL) + return SFE_MALLOC_FAILED ; + + /* + ** Set up the resident ogg packet structure, ready for writing into. + ** 1275 * 3 + 7 bytes of packet per stream is from opusenc from opus-tools + */ + ogg_packet_clear (&odata->opacket) ; + oopus->buffersize = (1275 * 3 + 7) * oopus->header.nb_streams ; + odata->opacket.packet = malloc (oopus->buffersize) ; + odata->opacket.packetno = 2 ; + if (odata->opacket.packet == NULL) + return SFE_MALLOC_FAILED ; + + oopus->serialno = psf_rand_int32 () ; + ogg_stream_init (&odata->ostream, oopus->serialno) ; + + return 0 ; +} /* ogg_opus_setup_encoder */ + +static int +ogg_opus_write_header (SF_PRIVATE *psf, int UNUSED (calc_length)) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + int nn ; + ogg_packet op ; + + oopus->header.version = 1 ; + oopus->header.channels = psf->sf.channels ; + + /* FIXME: Allow the user to set this ?! */ + oopus->header.gain = 0 ; + + if (psf->dataoffset > 0) + { if (psf->have_written) + { /* + ** Might be possible to deal with this, but it's difficult as we + ** have to take Ogg Page header sizes in to account, not just + ** packet sizes. + */ + return SFE_UNIMPLEMENTED ; + } + if (psf_is_pipe (psf)) + return SFE_NOT_SEEKABLE ; + if (psf_fseek (psf, 0, SEEK_SET) < 0) + return SFE_SEEK_FAILED ; + ogg_stream_reset_serialno (&odata->ostream, oopus->serialno) ; + psf->dataoffset = 0 ; + } + else + opus_print_header (psf, &oopus->header) ; + + psf->header.ptr [0] = 0 ; + psf->header.indx = 0 ; + + /* Opus Header Marker */ + psf_binheader_writef (psf, "eb", BHWv ("OpusHead"), BHWz (8)) ; + + /* Ogg Embedding scheme version, Channel Count, Preskip Samples */ + psf_binheader_writef (psf, "e112", BHW1 (oopus->header.version), BHW1 (psf->sf.channels), BHW2 (oopus->header.preskip)) ; + + /* + ** If an original samplerate has not been set by the user command + ** SFC_SET_ORIGINAL_SAMPLERATE, write the current samplerate. + */ + if (oopus->header.input_samplerate) + psf_binheader_writef (psf, "e4", BHW4 (oopus->header.input_samplerate)) ; + else + psf_binheader_writef (psf, "e4", BHW4 (psf->sf.samplerate)) ; + + /* Input Sample Rate, Gain (S7.8 format), Channel Mapping Type */ + psf_binheader_writef (psf, "e21", BHW2 (oopus->header.gain), BHW1 (oopus->header.channel_mapping)) ; + + /* Channel mappings, required if not using type 0 (mono/stereo) */ + if (oopus->header.channel_mapping > 0) + { psf_binheader_writef (psf, "11", BHW1 (oopus->header.nb_streams), BHW1 (oopus->header.nb_coupled)) ; + for (nn = 0 ; nn < oopus->header.channels ; nn++) + psf_binheader_writef (psf, "1", BHW1 (oopus->header.stream_map [nn])) ; + } ; + + op.packet = psf->header.ptr ; + op.bytes = psf->header.indx ; + op.b_o_s = 1 ; + op.e_o_s = 0 ; + op.granulepos = 0 ; + op.packetno = 1 ; + + /* The first page MUST only contain the header, so flush it out now */ + ogg_stream_packetin (&odata->ostream, &op) ; + for ( ; (nn = ogg_stream_flush (&odata->ostream, &odata->opage)) ; ) + { if (! (nn = ogg_write_page (psf, &odata->opage))) + { psf_log_printf (psf, "Opus : Failed to write header!\n") ; + if (psf->error) + return psf->error ; + return SFE_INTERNAL ; + } ; + psf->dataoffset += nn ; + } + + /* + ** Metadata Tags (manditory) + ** + ** All tags must be in one packet, which may span pages, and these pages + ** must not contain any other packets, so flush. The vendor string should + ** be the libopus library version, as it is doing the actual encoding. We + ** put the libsndfile identifier in the ENCODER tag. + ** + ** See: https://wiki.xiph.org/VorbisComment#ENCODER + */ + vorbiscomment_write_tags (psf, &op, &opustags_ident, opus_get_version_string (), - (OGG_OPUS_COMMENT_PAD)) ; + op.packetno = 2 ; + ogg_stream_packetin (&odata->ostream, &op) ; + for ( ; (nn = ogg_stream_flush (&odata->ostream, &odata->opage)) ; ) + { if (! (nn = ogg_write_page (psf, &odata->opage))) + { psf_log_printf (psf, "Opus : Failed to write comments!\n") ; + if (psf->error) + return psf->error ; + return SFE_INTERNAL ; + } ; + psf->dataoffset += nn ; + } + + return 0 ; +} /* ogg_opus_write_header */ + +static void +ogg_opus_flush (SF_PRIVATE *psf) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + uint64_t last_granulepos ; + int nbytes ; + int len ; + int last_packet ; + + /* + ** Need to flush both samples waiting for a complete packet and samples + ** currently 'inside' the encoder because of its latency. In the case of + ** the latter, we need to encode an equivalent amount of silence to push + ** them out. + ** + ** Note that the last packet's granule position might be less than the + ** total number of samples completed in it. This is how Ogg embedded Opus + ** encodes the amount of appended padding to truncate for gapless playback. + */ + + last_granulepos = oopus->pkt_pos + (oopus->sr_factor * oopus->loc) + oopus->header.preskip ; + last_packet = SF_FALSE ; + memset (&(oopus->buffer [oopus->loc * psf->sf.channels]), 0, sizeof (float) * psf->sf.channels * (oopus->len - oopus->loc)) ; + + for (last_packet = SF_FALSE ; last_packet == SF_FALSE ; ) + { oopus->pkt_pos += oopus->len * oopus->sr_factor ; + if (oopus->pkt_pos >= last_granulepos) + { last_packet = SF_TRUE ; + /* + ** Try to shorten the last packet to the smallest valid packet size + ** to minimize padding samples. + */ + len = (oopus->len * oopus->sr_factor) - (oopus->pkt_pos - last_granulepos) ; + if (len <= 120) /* 2.5 ms */ + len = 120 / oopus->sr_factor ; + else if (len <= 240) /* 5 ms */ + len = 240 / oopus->sr_factor ; + else if (len <= 480) /* 10 ms */ + len = 480 / oopus->sr_factor ; + else + len = oopus->len ; + } + else + len = oopus->len ; + + nbytes = opus_multistream_encode_float (oopus->u.encode.state, oopus->buffer, + len, odata->opacket.packet, oopus->buffersize) ; + + if (nbytes < 0) + { psf_log_printf (psf, "Opus : opus_multistream_encode_float returned: %s\n", opus_strerror (nbytes)) ; + break ; + } + + odata->opacket.bytes = nbytes ; + odata->opacket.packetno++ ; + if (last_packet) + { odata->opacket.granulepos = (ogg_int64_t) last_granulepos ; + odata->opacket.e_o_s = 1 ; + } + else + odata->opacket.granulepos = (ogg_int64_t) oopus->pkt_pos ; + + ogg_stream_packetin (&odata->ostream, &odata->opacket) ; + while (ogg_stream_pageout (&odata->ostream, &odata->opage)) + ogg_write_page (psf, &odata->opage) ; + } ; + + while (ogg_stream_flush (&odata->ostream, &odata->opage)) + ogg_write_page (psf, &odata->opage) ; +} /* ogg_opus_flush */ + +static int +ogg_opus_calculate_page_duration (OGG_PRIVATE *odata) +{ int i, samples, duration ; + ogg_packet *ppkt ; + + duration = 0 ; + for (i = 0 , ppkt = odata->pkt ; i < odata->pkt_len ; i++, ppkt++) + { /* Use 48kHz to get the sample count for use with granule positions. */ + samples = opus_packet_get_nb_samples (ppkt->packet, ppkt->bytes, 48000) ; + if (samples > 0) + duration += samples ; + } ; + return duration ; +} /* ogg_opus_calculate_page_duration */ + +static int +ogg_opus_unpack_next_page (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) +{ int nn ; + + nn = ogg_stream_unpack_page (psf, odata) ; + + if (nn == 1) + { oopus->pkt_pos = oopus->pg_pos ; + oopus->pg_pos = odata->pkt [odata->pkt_len - 1].granulepos ; + } + else if (nn == 2) + { uint64_t gp, last_page ; + + /* Found a hole. Need to recalculated pkt_pos from pg_pos */ + last_page = oopus->pg_pos ; + oopus->pg_pos = odata->pkt [odata->pkt_len - 1].granulepos ; + gp = ogg_opus_calculate_page_duration (odata) ; + oopus->pkt_pos = oopus->pg_pos - gp ; + psf_log_printf (psf, "Opus : Hole found appears to be of length %d samples.\n", + (oopus->pkt_pos - last_page) / oopus->sr_factor) ; + /* + ** Could save the hole size here, and have ogg_opus_read_refill() + ** do packet loss concealment until the hole is gone, but libopus does + ** PLC by generating white-noise for the duration of the hole. That is + ** the correct thing for use in telephony, but it isn't generally + ** appropriate here. It actually sounds better with no PLC, as the + ** lapped nature of full-width Opus means the two edges of the hole + ** will be blended together. + */ + return 1 ; + } + + return nn ; +} /* ogg_opus_unpack_next_page */ + +static int +ogg_opus_read_refill (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) +{ uint64_t pkt_granulepos ; + int nn, nsamp ; + ogg_packet *ppkt ; + + if (odata->pkt_indx == odata->pkt_len) + { nn = ogg_opus_unpack_next_page (psf, odata, oopus) ; + if (nn <= 0) + return nn ; + } + + if (odata->pkt_indx == odata->pkt_len) + return 0 ; + + ppkt = &odata->pkt [odata->pkt_indx] ; + nsamp = opus_multistream_decode_float (oopus->u.decode.state, + ppkt->packet, ppkt->bytes, oopus->buffer, oopus->buffersize, 0) ; + + if (nsamp == OPUS_BUFFER_TOO_SMALL) + { nsamp = opus_packet_get_nb_samples (ppkt->packet, ppkt->bytes, psf->sf.samplerate) ; + psf_log_printf (psf, "Growing decode buffer to hold %d samples from %d\n", + nsamp, oopus->buffersize) ; + if (nsamp > 5760) + { psf_log_printf (psf, "Packet is larger than maximum allowable of 120ms!? Skipping.\n") ; + return 0 ; + } ; + oopus->buffersize = nsamp ; + + free (oopus->buffer) ; + oopus->buffer = malloc (sizeof (float) * oopus->buffersize * psf->sf.channels) ; + if (oopus->buffer == NULL) + { psf->error = SFE_MALLOC_FAILED ; + oopus->buffersize = 0 ; + return -1 ; + } ; + + nsamp = opus_multistream_decode_float (oopus->u.decode.state, + ppkt->packet, ppkt->bytes, oopus->buffer, oopus->buffersize, 0) ; + } ; + odata->pkt_indx ++ ; + + if (nsamp < 0) + { psf_log_printf (psf, "Opus : opus_multistream_decode returned: %s\n", + opus_strerror (nsamp)) ; + psf->error = SFE_INTERNAL ; + return nsamp ; + } ; + + /* + ** Check for if this decoded packet is the last of the stream, in + ** which case a page granule position which is shorter than the + ** sample count of all packets in the page indicates that the last + ** samples are padding and should be dropped. + */ + pkt_granulepos = oopus->pkt_pos + (nsamp * oopus->sr_factor) ; + if (pkt_granulepos <= oopus->pg_pos) + { oopus->len = nsamp ; + } + else + { if (ogg_page_eos (&odata->opage)) + { /* + ** Possible for pg_pos < pkt_pos if there is a trailing + ** packet. It's not supposed to happen, but could. + */ + oopus->len = SF_MAX ((int) (oopus->pg_pos - oopus->pkt_pos) / oopus->sr_factor, 0) ; + } + else + { /* + ** From https://wiki.xiph.org/OggOpus#Granule_Position + ** A decoder MUST reject as invalid any stream where the granule + ** position is smaller than the number of samples contained in + ** packets that complete on the first page with a completed + ** packet, unless that page has the 'end of stream' flag set. It + ** MAY defer this action until it decodes the last packet + ** completed on that page. + */ + psf_log_printf (psf, "Opus : Mid-strem page's granule position %d is less than total samples of %d\n", oopus->pg_pos, pkt_granulepos) ; + psf->error = SFE_MALFORMED_FILE ; + return -1 ; + } ; + } ; + + /* + ** Check for if this decoded packet contains samples from before the pre- + ** skip point, indicating that these samples are padding to get the decoder + ** to converge and should be dropped. + */ + if (oopus->pkt_pos < (unsigned) oopus->header.preskip) + oopus->loc = SF_MIN ((oopus->header.preskip - (int) oopus->pkt_pos) / oopus->sr_factor, oopus->len) ; + else + oopus->loc = 0 ; + + oopus->pkt_pos = pkt_granulepos ; + return nsamp ; +} /* ogg_opus_read_refill */ + +static int +ogg_opus_write_out (SF_PRIVATE *psf, OGG_PRIVATE *odata, OPUS_PRIVATE *oopus) +{ int nbytes ; + + if (oopus->u.encode.lsb != oopus->u.encode.lsb_last) + opus_multistream_encoder_ctl (oopus->u.encode.state, OPUS_SET_LSB_DEPTH (oopus->u.encode.lsb)) ; + + nbytes = opus_multistream_encode_float (oopus->u.encode.state, + oopus->buffer, oopus->len, + odata->opacket.packet, oopus->buffersize) ; + + if (nbytes < 0) + { psf_log_printf (psf, "Opus : Error, opus_multistream_encode_float returned: %s\n", opus_strerror (nbytes)) ; + psf->error = SFE_INTERNAL ; + return nbytes ; + } ; + + oopus->u.encode.last_segments += (nbytes + 255) / 255 ; + oopus->pkt_pos += oopus->len * oopus->sr_factor ; + odata->opacket.bytes = nbytes ; + odata->opacket.granulepos = oopus->pkt_pos ; + odata->opacket.packetno++ ; + + /* + ** Decide whether to flush the Ogg page *before* adding the new packet to + ** it. Check both for if there is more than 1 second of audio (our default + ** Ogg page latency) or if adding the packet would cause a continued page, + ** in which case we might as well make a new page anyways. + */ + for ( ; ; ) + { if (oopus->pkt_pos - oopus->pg_pos >= OGG_OPUS_PAGE_LATENCY || oopus->u.encode.last_segments >= 255) + nbytes = ogg_stream_flush_fill (&odata->ostream, &odata->opage, 255 * 255) ; + else + nbytes = ogg_stream_pageout_fill (&odata->ostream, &odata->opage, 255 * 255) ; + if (nbytes > 0) + { /* + ** LibOgg documentation is noted as being bad by it's author. Ogg + ** page header byte 26 is the segment count. + */ + oopus->u.encode.last_segments -= odata->opage.header [26] ; + oopus->pg_pos = oopus->pkt_pos ; + ogg_write_page (psf, &odata->opage) ; + } + else + break ; + } ; + + ogg_stream_packetin (&odata->ostream, &odata->opacket) ; + oopus->loc = 0 ; + oopus->u.encode.lsb_last = oopus->u.encode.lsb ; + oopus->u.encode.lsb = 0 ; + + return 1 ; +} /* ogg_opus_write_out */ + +static sf_count_t +ogg_opus_read_s (SF_PRIVATE *psf, short *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total = 0 ; + sf_count_t readlen, i ; + float *iptr ; + + for ( ; total < len ; ) + { if (oopus->loc == oopus->len) + { if (ogg_opus_read_refill (psf, odata, oopus) <= 0) + return total ; + } ; + + readlen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (readlen > 0) + { iptr = &(oopus->buffer [oopus->loc * psf->sf.channels]) ; + i = total ; + total += readlen ; + + if (psf->float_int_mult) + { float inverse = 1.0 / psf->float_max ; + for ( ; i < total ; i++) + { ptr [i] = lrintf (((*(iptr++)) * inverse) * 32767.0f) ; + } ; + } + else + { for ( ; i < total ; i++) + { ptr [i] = lrintf ((*(iptr++)) * 32767.0f) ; + } ; + } ; + oopus->loc += (readlen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_read_s */ + +static sf_count_t +ogg_opus_read_i (SF_PRIVATE *psf, int *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total = 0 ; + sf_count_t readlen, i ; + float *iptr ; + + for ( ; total < len ; ) + { if (oopus->loc == oopus->len) + { if (ogg_opus_read_refill (psf, odata, oopus) <= 0) + return total ; + } ; + + readlen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (readlen > 0) + { iptr = &(oopus->buffer [oopus->loc * psf->sf.channels]) ; + i = total ; + total += readlen ; + + if (psf->float_int_mult) + { float inverse = 1.0 / psf->float_max ; + for ( ; i < total ; i++) + { ptr [i] = lrintf (((*(iptr++)) * inverse) * 2147483647.0f) ; + } + } + else + { for ( ; i < total ; i++) + { ptr [i] = lrintf ((*(iptr++)) * 2147483647.0f) ; + } + } ; + oopus->loc += (readlen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_read_i */ + +static sf_count_t +ogg_opus_read_f (SF_PRIVATE *psf, float *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total = 0 ; + sf_count_t readlen ; + + for ( ; total < len ; ) + { if (oopus->loc == oopus->len) + { if (ogg_opus_read_refill (psf, odata, oopus) <= 0) + return total ; + } ; + + readlen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (readlen > 0) + { memcpy (&(ptr [total]), &(oopus->buffer [oopus->loc * psf->sf.channels]), sizeof (float) * readlen) ; + total += readlen ; + oopus->loc += (readlen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_read_f */ + +static sf_count_t +ogg_opus_read_d (SF_PRIVATE *psf, double *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total = 0 ; + sf_count_t readlen, i ; + float *iptr ; + + for ( ; total < len ; ) + { if (oopus->loc == oopus->len) + { if (ogg_opus_read_refill (psf, odata, oopus) <= 0) + return total ; + } ; + + readlen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (readlen > 0) + { iptr = &(oopus->buffer [oopus->loc * psf->sf.channels]) ; + i = total ; + total += readlen ; + for ( ; i < total ; i++) + { ptr [i] = *(iptr++) ; + } ; + oopus->loc += (readlen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_read_d */ + +static sf_count_t +ogg_opus_write_s (SF_PRIVATE *psf, const short *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total, i ; + int writelen ; + float *optr ; + + if (oopus->u.encode.lsb < 16) + oopus->u.encode.lsb = 16 ; + + for (total = 0 ; total < len ; ) + { if (oopus->loc == oopus->len) + { /* Need to encode the buffer */ + if (ogg_opus_write_out (psf, odata, oopus) <= 0) + return total ; + } ; + + writelen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (writelen) + { optr = &(oopus->buffer [oopus->loc * psf->sf.channels]) ; + i = total ; + total += writelen ; + for ( ; i < total ; i++) + { *(optr++) = (float) (ptr [i]) / 32767.0f ; + } + oopus->loc += (writelen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_write_s */ + +static sf_count_t +ogg_opus_write_i (SF_PRIVATE *psf, const int *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total, i ; + int writelen ; + float *optr ; + + if (oopus->u.encode.lsb < 24) + oopus->u.encode.lsb = 24 ; + + for (total = 0 ; total < len ; ) + { if (oopus->loc == oopus->len) + { /* Need to encode the buffer */ + if (ogg_opus_write_out (psf, odata, oopus) <= 0) + return total ; + } ; + + writelen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (writelen) + { optr = &(oopus->buffer [oopus->loc * psf->sf.channels]) ; + i = total ; + total += writelen ; + for ( ; i < total ; i++) + { *(optr++) = (float) (ptr [i]) / 2147483647.0f ; + } ; + oopus->loc += (writelen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_write_i */ + +static sf_count_t +ogg_opus_write_f (SF_PRIVATE *psf, const float *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total ; + int writelen ; + + if (oopus->u.encode.lsb < 24) + oopus->u.encode.lsb = 24 ; + + for (total = 0 ; total < len ; ) + { if (oopus->loc == oopus->len) + { /* Need to encode the buffer */ + if (ogg_opus_write_out (psf, odata, oopus) <= 0) + return total ; + } ; + + writelen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (writelen) + { memcpy (&(oopus->buffer [oopus->loc * psf->sf.channels]), &(ptr [total]), sizeof (float) * writelen) ; + total += writelen ; + oopus->loc += (writelen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_write_f */ + +static sf_count_t +ogg_opus_write_d (SF_PRIVATE *psf, const double *ptr, sf_count_t len) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total, i ; + int writelen ; + float *optr ; + + if (oopus->u.encode.lsb < 24) + oopus->u.encode.lsb = 24 ; + + for (total = 0 ; total < len ; ) + { if (oopus->loc == oopus->len) + { /* Need to encode the buffer */ + if (ogg_opus_write_out (psf, odata, oopus) <= 0) + return total ; + } ; + + writelen = SF_MIN ((int) (len - total), (oopus->len - oopus->loc) * psf->sf.channels) ; + if (writelen) + { optr = &(oopus->buffer [oopus->loc * psf->sf.channels]) ; + i = total ; + total += writelen ; + for ( ; i < total ; i++) + { *(optr++) = (float) (ptr [i]) ; + } ; + oopus->loc += (writelen / psf->sf.channels) ; + } ; + } ; + return total ; +} /* ogg_opus_write_d */ + +static int +ogg_opus_analyze_file (SF_PRIVATE *psf) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + uint64_t gp ; + sf_count_t saved_offset, last_page ; + int error ; + + psf->sf.sections = 1 ; + psf->sf.frames = SF_COUNT_MAX ; + oopus->u.decode.gp_end = (uint64_t) -1 ; + oopus->u.decode.last_offset = SF_COUNT_MAX ; + + psf->dataoffset = ogg_sync_ftell (psf) ; + if (psf->filelength != SF_COUNT_MAX) + psf->datalength = psf->filelength - psf->dataoffset ; + else + psf->datalength = SF_COUNT_MAX ; + + /* + ** Calculate the start granule position offset + ** + ** OggOpus streams are allowed to start with a granule position other than + ** zero. This allows for cutting the beginning off of streams without + ** having to modify all following granule positions, or for recording/ + ** joining a live stream in the middle. To figure out the offset, we need + ** to sum up how many samples are in all the packets that complete in the + ** page and subtract it from the page granule position. + ** + ** If this is the last page of the steam (EOS set), this is not possible, + ** as the granule position may be /less/ than the number of samples, to + ** indicate how many samples are end-padding. In this case the granule + ** position offset of the file must be 0, as otherwise it is considered + ** malformed. + */ + error = ogg_opus_unpack_next_page (psf, odata, oopus) ; + if (error < 0 && psf->error) + return psf->error ; + gp = ogg_opus_calculate_page_duration (odata) ; + if (!ogg_page_eos (&odata->opage)) + { if (gp > oopus->pg_pos) + { psf_log_printf (psf, "Opus : First data page's granule position is less than total number of samples on the page!\n") ; + return SFE_MALFORMED_FILE ; + } + oopus->pkt_pos = oopus->pg_pos - gp ; + } + else if (gp < oopus->pg_pos) + { psf_log_printf (psf, "Opus : First data page is also the last, and granule position has an (ambigious) offset.\n") ; + return SFE_MALFORMED_FILE ; + } ; + oopus->u.decode.gp_start = oopus->pkt_pos ; + + if (!psf->sf.seekable) + return 0 ; + + /* + ** Find the last page and fetch the last granule position. + ** First, save were we are now. + */ + saved_offset = ogg_sync_ftell (psf) ; + + /* This uses the sync page buffer, the stream page buffer is untouched. */ + last_page = ogg_sync_last_page_before (psf, odata, &oopus->u.decode.gp_end, psf->filelength, oopus->serialno) ; + if (last_page > 0) + { if (!ogg_page_eos (&odata->opage)) + psf_log_printf (psf, "Ogg : Last page lacks an end-of-stream bit.\n") ; + if (last_page + odata->opage.header_len + odata->opage.body_len < psf->filelength) + psf_log_printf (psf, "Ogg : Junk after the last page.\n") ; + oopus->u.decode.last_offset = last_page ; + + if (oopus->u.decode.gp_end != (uint64_t) -1) + { psf->sf.frames = (oopus->u.decode.gp_end - oopus->u.decode.gp_start + - oopus->header.preskip) / oopus->sr_factor ; + } ; + } + + /* Go back to where we left off. */ + ogg_sync_fseek (psf, saved_offset, SEEK_SET) ; + return 0 ; +} /* ogg_opus_analyze_file */ + +/* +** ogg_opus_seek_null_read +** +** Decode samples, doing nothing with them, until the desired granule position +** is reached. +*/ +static sf_count_t +ogg_opus_seek_null_read (SF_PRIVATE *psf, sf_count_t offset) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t total ; + sf_count_t readlen ; + + total = oopus->pkt_pos / oopus->sr_factor ; + total += oopus->loc ; + + for ( ; total < offset ; ) + { if (oopus->loc == oopus->len) + { if (ogg_opus_read_refill (psf, odata, oopus) <= 0) + return total ; + /* + ** Ignore pre-skip skipping. The preskip was accounted for in the + ** arugment to offset, so we need to count it. + */ + oopus->loc = 0 ; + } ; + + readlen = SF_MIN ((int) (offset - total), (oopus->len - oopus->loc)) ; + if (readlen > 0) + { total += readlen ; + oopus->loc += readlen ; + } ; + } ; + return total ; +} /* ogg_opus_seek_null_read */ + +/* +** Search within the file for the page with the highest granule position at or +** before our target. +*/ +static int +ogg_opus_seek_page_search (SF_PRIVATE *psf, uint64_t target_gp) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + uint64_t pcm_start ; + uint64_t pcm_end ; + uint64_t best_gp ; + sf_count_t begin ; + sf_count_t end ; + int ret ; + + best_gp = pcm_start = oopus->u.decode.gp_start ; + pcm_end = oopus->u.decode.gp_end ; + begin = psf->dataoffset ; + + /* Adjust the target to give time to converge. */ + if (target_gp >= OGG_OPUS_PREROLL) + target_gp -= OGG_OPUS_PREROLL ; + if (target_gp < pcm_start) + target_gp = pcm_start ; + + /* Seek to beginning special case */ + if (target_gp < pcm_start + (uint64_t) oopus->header.preskip) + end = begin ; + else + end = oopus->u.decode.last_offset ; + + ogg_stream_seek_page_search (psf, odata, target_gp, pcm_start, pcm_end, &best_gp, begin, end) ; + + oopus->loc = 0 ; + oopus->len = 0 ; + if ((ret = ogg_opus_unpack_next_page (psf, odata, oopus)) != 1) + return ret ; + oopus->pkt_pos = best_gp ; + opus_multistream_decoder_ctl (oopus->u.decode.state, OPUS_RESET_STATE) ; + /* Gain decoder settings survive resets. */ + + return 0 ; +} /* ogg_opus_seek_page_search */ + +static sf_count_t +ogg_opus_seek_manual (SF_PRIVATE *psf, uint64_t target_gp) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + sf_count_t pos ; + int nn ; + + if (target_gp > OGG_OPUS_PREROLL) + target_gp -= OGG_OPUS_PREROLL ; + if (target_gp < oopus->pg_pos) + target_gp = oopus->pg_pos ; + + if (oopus->pg_pos > target_gp) + { ogg_stream_reset (&odata->ostream) ; + pos = ogg_sync_fseek (psf, psf->dataoffset, SEEK_SET) ; + if (pos < 0) + return pos ; + oopus->pg_pos = oopus->u.decode.gp_start ; + opus_multistream_decoder_ctl (oopus->u.decode.state, OPUS_RESET_STATE) ; + } ; + + while (oopus->pg_pos < target_gp) + { nn = ogg_opus_unpack_next_page (psf, odata, oopus) ; + if (nn <= 0) + return nn ; + } ; + + return 1 ; +} /* ogg_opus_seek_manual */ + +static sf_count_t +ogg_opus_seek (SF_PRIVATE *psf, int mode, sf_count_t offset) +{ OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + uint64_t target_gp ; + uint64_t current ; + int ret ; + + /* Only support seeking in read mode. */ + if (mode != SFM_READ || psf->file.mode != SFM_READ) + { psf->error = SFE_BAD_SEEK ; + return PSF_SEEK_ERROR ; + } ; + + current = oopus->pkt_pos + oopus->loc * oopus->sr_factor ; + /* + ** Remember, there are preskip granulepos worth of samples at the front of + ** the stream which are bunk. Also, granule positions can be offset. + */ + target_gp = offset * oopus->sr_factor + oopus->u.decode.gp_start + oopus->header.preskip ; + + if (oopus->u.decode.gp_end == (uint64_t) -1) + { /* + ** Don't know the end of the file. Could be a chained file we don't yet + ** support. Oh well, just do it manually. + */ + ogg_opus_seek_manual (psf, target_gp) ; + } + else + { /* + ** Avoid seeking in the file if where we want is just ahead or exactly + ** were we are. To avoid needing to flush the decoder we choose pre- + ** roll plus 10ms. + */ + if (target_gp < current || target_gp - current > OGG_OPUS_PREROLL + 10 * 48) + { ret = ogg_opus_seek_page_search (psf, target_gp) ; + if (ret < 0) + { /* + ** Page seek failed, what to do? Could be bad data. We can + ** either fall-back to manual seeking or bail. Manaul seeking + ** from the beginning has the advantage of finding where the + ** file goes bad. + */ + ret = ogg_opus_seek_manual (psf, target_gp) ; + if (ret < 0) + { /* + ** If were here, and there is no error, we can be pretty + ** sure that it's the file that is to blame. + */ + if (!psf->error) + psf->error = SFE_MALFORMED_FILE ; + return ret ; + } ; + } ; + } ; + } ; + + /* + ** We've seeked or skipped through pages until just before our target, + ** now decode until we hit it. + */ + offset = ogg_opus_seek_null_read (psf, target_gp / oopus->sr_factor) ; + return offset - ((oopus->header.preskip + oopus->u.decode.gp_start) / oopus->sr_factor) ; + +} /* ogg_opus_seek */ + +static int +ogg_opus_command (SF_PRIVATE *psf, int command, void *data, int datasize) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + double quality ; + int error ; + + switch (command) + { case SFC_SET_CHANNEL_MAP_INFO : + /* TODO: figure this out */ + break ; + + case SFC_SET_COMPRESSION_LEVEL : + /* + ** Argument: double, range 0.0 (lest compressed, best quality) to + ** 1.0 (most compressed, worst quality) + */ + if (data == NULL || datasize != SIGNED_SIZEOF (double)) + return SFE_BAD_COMMAND_PARAM ; + + /* Usable bitrate range is [6, 256] kbps per channel. */ + quality = *((double *) data) ; + oopus->u.encode.bitrate = (int) (((1.0 - quality) * (250000.0)) + 6000.0) * psf->sf.channels ; + if (opus_multistream_encoder_ctl (oopus->u.encode.state, OPUS_SET_BITRATE (oopus->u.encode.bitrate)) == OPUS_OK) + { psf_log_printf (psf, "User changed encoding target bitrate to %dbps\n", oopus->u.encode.bitrate) ; + return SF_TRUE ; + } + psf_log_printf (psf, "Failed to set user encoding target bitrate of %dbps\n", oopus->u.encode.bitrate) ; + return SF_FALSE ; + break ; + + case SFC_SET_ORIGINAL_SAMPLERATE : + if (data == NULL || datasize != SIGNED_SIZEOF (int)) + return SFE_BAD_COMMAND_PARAM ; + /* + ** Only allow changing the input samplerate if at the beginning + ** of the stream, because while it might be possible to change + ** samplerate mid-decode, or to re-write the header for encode, + ** ain't nobody got time to implement and test that. + */ + if (psf->file.mode == SFM_WRITE) + { if (psf->have_written) + return SF_FALSE ; + oopus->header.input_samplerate = *((int *) data) ; + } + else { + if (oopus->pkt_pos > oopus->u.decode.gp_start || oopus->loc > 0) + return SF_FALSE ; + if ((error = ogg_opus_setup_decoder (psf, *((int *) data)))) + return error ; + odata->pkt_indx = 0 ; + /* Adjust file frames count. */ + if (oopus->u.decode.gp_end != (uint64_t) -1) + psf->sf.frames = (oopus->u.decode.gp_end - oopus->u.decode.gp_start + - oopus->header.preskip) / oopus->sr_factor ; + } ; + return SF_TRUE ; + + case SFC_GET_ORIGINAL_SAMPLERATE : + if (data == NULL || datasize != SIGNED_SIZEOF (int)) + return SFE_BAD_COMMAND_PARAM ; + *((int *) data) = oopus->header.input_samplerate ; + return SF_TRUE ; + + default : + break ; + } + + return SF_FALSE ; +} /* ogg_opus_command */ + +static int +ogg_opus_byterate (SF_PRIVATE *psf) +{ OGG_PRIVATE *odata = (OGG_PRIVATE *) psf->container_data ; + OPUS_PRIVATE *oopus = (OPUS_PRIVATE *) psf->codec_data ; + + if (psf->file.mode == SFM_READ) + { if (odata->pkt_indx == odata->pkt_len) + { if (ogg_opus_unpack_next_page (psf, odata, oopus) < 0) + return -1 ; + } ; + + if (odata->pkt_indx < odata->pkt_len) + { ogg_packet *ppkt = &odata->pkt [odata->pkt_indx] ; + return (ppkt->bytes * 8000) / opus_packet_get_nb_samples (ppkt->packet, ppkt->bytes, 8000) ; + } ; + + if (psf->datalength != SF_COUNT_MAX) + return (psf->datalength * psf->sf.samplerate) / psf->sf.frames ; + } ; + + if (psf->file.mode == SFM_WRITE && oopus->u.encode.state != NULL) + return (oopus->u.encode.bitrate + 7) / 8 ; + + return -1 ; +} /* ogg_opus_byterate */ + +#else /* HAVE_EXTERNAL_XIPH_LIBS */ int ogg_opus_open (SF_PRIVATE *psf) diff --git a/src/ogg_vcomment.c b/src/ogg_vcomment.c new file mode 100644 index 00000000..edde5bdd --- /dev/null +++ b/src/ogg_vcomment.c @@ -0,0 +1,269 @@ +/* +** Copyright (C) 2008-2018 Erik de Castro Lopo +** Copyright (C) 2018 Arthur Taylor +** +** This program is free software ; you can redistribute it and/or modify +** it under the terms of the GNU Lesser General Public License as published by +** the Free Software Foundation ; either version 2.1 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY ; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public License +** along with this program ; if not, write to the Free Software +** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "sfconfig.h" + +#include +#include +#include + +#include "sndfile.h" +#include "sfendian.h" +#include "common.h" + +#if HAVE_EXTERNAL_XIPH_LIBS + +#include + +#include "ogg_vcomment.h" + +typedef struct +{ int id ; + const char *name ; +} STR_PAIR ; + +/* See https://xiph.org/vorbis/doc/v-comment.html */ +static STR_PAIR vorbiscomment_mapping [] = +{ { SF_STR_TITLE, "TITLE" }, + { SF_STR_COPYRIGHT, "COPYRIGHT", }, + { SF_STR_SOFTWARE, "ENCODER", }, + { SF_STR_ARTIST, "ARTIST" }, + { SF_STR_COMMENT, "COMMENT" }, + { SF_STR_DATE, "DATE", }, + { SF_STR_ALBUM, "ALBUM" }, + { SF_STR_LICENSE, "LICENSE", }, + { SF_STR_TRACKNUMBER, "TRACKNUMBER", }, + { SF_STR_GENRE, "GENRE", }, + { 0, NULL, }, +} ; + +/*----------------------------------------------------------------------------------------------- +** Private function prototypes. +*/ + +static int vorbiscomment_lookup_id (const char *name) ; +static const char * vorbiscomment_lookup_name (int id) ; + +/*----------------------------------------------------------------------------------------------- +** Exported functions. +*/ + +int +vorbiscomment_read_tags (SF_PRIVATE *psf, ogg_packet *packet, vorbiscomment_ident *ident) +{ unsigned char *p, *ep ; + char *tag, *c ; + size_t tag_size, tag_len = 0 ; + unsigned int ntags, i = 0 ; + int id, ret = 0 ; + + /* + ** The smallest possible header is the ident string length plus two 4-byte + ** integers, (vender string length, tags count.) + */ + if (packet->bytes < (ident ? ident->length : 0) + 4 + 4) + return SFE_MALFORMED_FILE ; + + /* Our working pointer. */ + p = packet->packet ; + /* Our end pointer for bound checking. */ + ep = p + packet->bytes ; + + if (ident) + { if (memcmp (p, ident->ident, ident->length) != 0) + { psf_log_printf (psf, "Expected comment packet identifier missing.\n") ; + return SFE_MALFORMED_FILE ; + } ; + p += ident->length ; + } ; + + tag_size = 1024 ; + tag = malloc (tag_size) ; + /* Unlikely */ + if (!tag) + return SFE_MALLOC_FAILED ; + + psf_log_printf (psf, "VorbisComment Metadata\n") ; + + /* + ** Vendor tag, manditory, no field name. + */ + tag_len = LE2H_32_PTR (p) ; p += 4 ; + if (tag_len > 0) + { /* Bound checking. 4 bytes for remaining manditory fields. */ + if (p + tag_len + 4 > ep) + { ret = SFE_MALFORMED_FILE ; + goto free_tag_out ; + } ; + if (tag_len > tag_size - 1) + { free (tag) ; + tag_size = tag_len + 1 ; + tag = malloc (tag_size) ; + /* Unlikely */ + if (!tag) + return SFE_MALLOC_FAILED ; + } ; + memcpy (tag, p, tag_len) ; p += tag_len ; + tag [tag_len] = '\0' ; + psf_log_printf (psf, " Vendor: %s\n", tag) ; + } ; + + /* + ** List of tags of the form NAME=value + ** Allowable characters for NAME are the same as shell variable names. + */ + ntags = LE2H_32_PTR (p) ; p += 4 ; + for (i = 0 ; i < ntags ; i++) + { if (p + 4 > ep) + { ret = SFE_MALFORMED_FILE ; + goto free_tag_out ; + } ; + tag_len = LE2H_32_PTR (p) ; p += 4 ; + if (p + tag_len > ep) + { ret = SFE_MALFORMED_FILE ; + goto free_tag_out ; + } ; + if (tag_len > tag_size - 1) + { free (tag) ; + tag_size = tag_len + 1 ; + tag = malloc (tag_size) ; + /* Unlikely */ + if (!tag) + return SFE_MALLOC_FAILED ; + } ; + memcpy (tag, p, tag_len) ; p += tag_len ; + tag [tag_len] = '\0' ; + psf_log_printf (psf, " %s\n", tag) ; + for (c = tag ; *c ; c++) + { if (*c == '=') + break ; + *c = toupper (*c) ; + } ; + if (!c) + { psf_log_printf (psf, "Malformed Vorbis comment, no '=' found.\n") ; + continue ; + } ; + *c = '\0' ; + if ((id = vorbiscomment_lookup_id (tag)) != 0) + psf_store_string (psf, id, c + 1) ; + } ; + +free_tag_out: + if (tag != NULL) + free (tag) ; + return ret ; +} /* vorbiscomment_read_tags */ + +int +vorbiscomment_write_tags (SF_PRIVATE *psf, ogg_packet *packet, vorbiscomment_ident *ident, const char *vendor, int targetsize) +{ int i, ntags ; + int tags_start ; + const char *tag_name ; + int tag_name_len, tag_body_len ; + + psf->header.ptr [0] = 0 ; + psf->header.indx = 0 ; + + /* Packet identifier */ + if (ident) + psf_binheader_writef (psf, "eb", BHWv (ident->ident), BHWz (ident->length)) ; + + /* Manditory Vendor Tag */ + tag_name_len = vendor ? strlen (vendor) : 0 ; + psf_binheader_writef (psf, "e4b", BHW4 (tag_name_len), BHWv (vendor), BHWz (tag_name_len)) ; + + /* Tags Count. Skip for now, write after. */ + tags_start = psf->header.indx ; + psf_binheader_writef (psf, "j", BHWj (4)) ; + + ntags = 0 ; + /* Write each tag */ + for (i = 0 ; i < SF_MAX_STRINGS ; i++) + { if (psf->strings.data [i].type == 0) + continue ; + + tag_name = vorbiscomment_lookup_name (psf->strings.data [i].type) ; + if (tag_name == NULL) + continue ; + + tag_name_len = strlen (tag_name) ; + tag_body_len = strlen (psf->strings.storage + psf->strings.data [i].offset) ; + if (targetsize > 0 && tag_name_len + tag_body_len + psf->header.indx > targetsize) + { /* If we are out of space, stop now. */ + return SFE_STR_MAX_DATA ; + } + psf_binheader_writef (psf, "e4b1b", + BHW4 (tag_name_len + 1 + tag_body_len), + BHWv (tag_name), BHWz (tag_name_len), + BHW1 ('='), + BHWv (psf->strings.storage + psf->strings.data [i].offset), BHWz (tag_body_len)) ; + ntags++ ; + } ; + + if (targetsize < 0) + { /* + ** Padding. + ** + ** Pad to a minimum of -targetsize, but make sure length % 255 + ** = 254 so that we get the most out of the ogg segment lacing. + */ + psf_binheader_writef (psf, "z", BHWz ((psf->header.indx + -targetsize + 255) / 255 * 255 - 1)) ; + } + else if (targetsize > 0) + psf_binheader_writef (psf, "z", BHWz (targetsize - psf->header.indx)) ; + + packet->packet = psf->header.ptr ; + packet->bytes = psf->header.indx ; + packet->b_o_s = 0 ; + packet->e_o_s = 0 ; + + /* Seek back and write the tag count. */ + psf_binheader_writef (psf, "eo4", BHWo (tags_start), BHW4 (ntags)) ; + + return 0 ; +} /* vorbiscomment_write_tags */ + +/*============================================================================== +** Private functions. +*/ + +static int +vorbiscomment_lookup_id (const char * name) +{ STR_PAIR *p ; + + for (p = vorbiscomment_mapping ; p->id ; p++) + { if (!strcmp (name, p->name)) + return p->id ; + } ; + + return 0 ; +} /* vorbiscomment_lookup_id */ + +static const char * +vorbiscomment_lookup_name (int id) +{ STR_PAIR *p ; + + for (p = vorbiscomment_mapping ; p->id ; p++) + { if (p->id == id) + return p->name ; + } ; + + return NULL ; +} /* vorbiscomment_lookup_name */ + +#endif /* HAVE_EXTERNAL_XIPH_LIBS */ diff --git a/src/ogg_vcomment.h b/src/ogg_vcomment.h new file mode 100644 index 00000000..c25d900f --- /dev/null +++ b/src/ogg_vcomment.h @@ -0,0 +1,45 @@ +/* +** Copyright (C) 2008-2018 Erik de Castro Lopo +** Copyright (C) 2018 Arthur Taylor +** +** This program is free software ; you can redistribute it and/or modify +** it under the terms of the GNU Lesser General Public License as published by +** the Free Software Foundation ; either version 2.1 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY ; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU Lesser General Public License for more details. +** +** You should have received a copy of the GNU Lesser General Public License +** along with this program ; if not, write to the Free Software +** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#ifndef SF_SRC_OGG_VCOMMENT_H +#define SF_SRC_OGG_VCOMMENT_H + +/* +** Voriscomment identifier. Some Ogg stream embedding schemes require it. +*/ +typedef struct +{ const char *ident ; + int length ; +} vorbiscomment_ident ; + +/* +** Read all vorbiscomment tags from *packet. Tags which match ones used +** by libsndfile strings are loaded into *psf. Ogg streams which require an +** identifier for the tags packet should pass it in *ident. +*/ +int vorbiscomment_read_tags (SF_PRIVATE *psf, ogg_packet *packet, vorbiscomment_ident *ident) ; + +/* +** Write metadata strings stored in *psf to *packet. The packet is optionally +** prefixed with *ident. The always-present vendor field should be the library +** used for encoding the audio data. +*/ +int vorbiscomment_write_tags (SF_PRIVATE *psf, ogg_packet *packet, vorbiscomment_ident *ident, const char *vendor, int targetsize) ; + +#endif /* SF_SRC_OGG_VCOMMENT_H */ diff --git a/src/sfendian.h b/src/sfendian.h index 50bad3a1..8a720e9d 100644 --- a/src/sfendian.h +++ b/src/sfendian.h @@ -156,6 +156,8 @@ ENDSWAP_64 (uint64_t x) #error "Target CPU endian-ness unknown. May need to hand edit src/sfconfig.h" #endif +#define LE2H_32_PTR(x) (((x) [0]) + ((x) [1] << 8) + ((x) [2] << 16) + ((x) [3] << 24)) + #define LET2H_16_PTR(x) ((x) [1] + ((x) [2] << 8)) #define LET2H_32_PTR(x) (((x) [0] << 8) + ((x) [1] << 16) + ((x) [2] << 24)) diff --git a/src/sndfile.c b/src/sndfile.c index bad36a79..85317294 100644 --- a/src/sndfile.c +++ b/src/sndfile.c @@ -271,6 +271,8 @@ ErrorStruct SndfileErrors [] = { SFE_FILENAME_TOO_LONG , "Error : Supplied filename too long." }, { SFE_NEGATIVE_RW_LEN , "Error : Length parameter passed to read/write is negative." }, + { SFE_OPUS_BAD_SAMPLERATE , "Error : Opus only supports sample rates of 8000, 12000, 16000, 24000 and 48000." }, + { SFE_MAX_ERROR , "Maximum error number." }, { SFE_MAX_ERROR + 1 , NULL } } ; @@ -842,6 +844,8 @@ sf_format_check (const SF_INFO *info) return 0 ; if (subformat == SF_FORMAT_VORBIS) return 1 ; + if (subformat == SF_FORMAT_OPUS) + return 1 ; break ; case SF_FORMAT_MPC2K : diff --git a/src/sndfile.h.in b/src/sndfile.h.in index c392a1f8..a5019c1e 100644 --- a/src/sndfile.h.in +++ b/src/sndfile.h.in @@ -109,6 +109,7 @@ enum SF_FORMAT_DPCM_16 = 0x0051, /* 16 bit differential PCM (XI only) */ SF_FORMAT_VORBIS = 0x0060, /* Xiph Vorbis encoding. */ + SF_FORMAT_OPUS = 0x0064, /* Xiph/Skype Opus encoding. */ SF_FORMAT_ALAC_16 = 0x0070, /* Apple Lossless Audio Codec (16 bit). */ SF_FORMAT_ALAC_20 = 0x0071, /* Apple Lossless Audio Codec (20 bit). */ @@ -218,6 +219,10 @@ enum SFC_SET_CART_INFO = 0x1400, SFC_GET_CART_INFO = 0x1401, + /* Opus files original samplerate metadata */ + SFC_SET_ORIGINAL_SAMPLERATE = 0x1500, + SFC_GET_ORIGINAL_SAMPLERATE = 0x1501, + /* Following commands for testing only. */ SFC_TEST_IEEE_FLOAT_REPLACE = 0x6001, diff --git a/tests/compression_size_test.c b/tests/compression_size_test.c index aee17f28..c8f43c81 100644 --- a/tests/compression_size_test.c +++ b/tests/compression_size_test.c @@ -177,6 +177,7 @@ main (int argc, char *argv []) " Where is one of:\n" " vorbis - test Ogg/Vorbis\n" " flac - test FLAC\n" + " opus - test Opus\n" " all - perform all tests\n", argv [0]) ; exit (0) ; @@ -201,5 +202,10 @@ main (int argc, char *argv []) tests ++ ; } ; + if (all_tests || strcmp (argv [1], "opus") == 0) + { compression_size_test (SF_FORMAT_OGG | SF_FORMAT_OPUS, "opus.opus") ; + tests ++ ; + } ; + return 0 ; } /* main */ diff --git a/tests/external_libs_test.c b/tests/external_libs_test.c index d41075df..0c6fcf8e 100644 --- a/tests/external_libs_test.c +++ b/tests/external_libs_test.c @@ -84,7 +84,7 @@ major_format_test (void) static void subtype_format_test (void) { SF_FORMAT_INFO info ; - int have_vorbis = 0 ; + int have_vorbis = 0 , have_opus = 0 ; int s, subtype_count ; print_test_name (__func__, NULL) ; @@ -96,12 +96,17 @@ subtype_format_test (void) sf_command (NULL, SFC_GET_FORMAT_SUBTYPE, &info, sizeof (info)) ; have_vorbis = info.format == SF_FORMAT_VORBIS ? 1 : have_vorbis ; + have_opus = info.format == SF_FORMAT_OPUS ? 1 : have_opus ; } ; if (HAVE_EXTERNAL_XIPH_LIBS) - exit_if_true (have_vorbis == 0, "\n\nLine %d : Ogg/Vorbis should be available.\n\n", __LINE__) ; + { exit_if_true (have_vorbis == 0, "\n\nLine %d : Ogg/Vorbis should be available.\n\n", __LINE__) ; + exit_if_true (have_opus == 0, "\n\nLine %d : Ogg/Opus should be available.\n\n", __LINE__) ; + } else - exit_if_true (have_vorbis, "\n\nLine %d : Ogg/Vorbis should not be available.\n\n", __LINE__) ; + { exit_if_true (have_vorbis, "\n\nLine %d : Ogg/Vorbis should not be available.\n\n", __LINE__) ; + exit_if_true (have_opus, "\n\nLine %d : Ogg/Opus should not be available.\n\n", __LINE__) ; + } ; puts ("ok") ; } /* subtype_format_test */ @@ -109,7 +114,7 @@ subtype_format_test (void) static void simple_format_test (void) { SF_FORMAT_INFO info ; - int have_flac = 0, have_ogg = 0, have_vorbis = 0 ; + int have_flac = 0, have_ogg = 0, have_vorbis = 0, have_opus = 0 ; int s, simple_count ; print_test_name (__func__, NULL) ; @@ -138,6 +143,10 @@ simple_format_test (void) have_vorbis = 1 ; break ; + case SF_FORMAT_OPUS : + have_opus = 1 ; + break ; + default : break ; } ; @@ -148,11 +157,13 @@ simple_format_test (void) { exit_if_true (have_flac == 0, "\n\nLine %d : FLAC should be available.\n\n", __LINE__) ; exit_if_true (have_ogg == 0, "\n\nLine %d : Ogg/Vorbis should be available.\n\n", __LINE__) ; exit_if_true (have_vorbis == 0, "\n\nLine %d : Ogg/Vorbis should be available.\n\n", __LINE__) ; + exit_if_true (have_opus == 0, "\n\nLine %d : Ogg/Opus should be available.\n\n", __LINE__) ; } else { exit_if_true (have_flac, "\n\nLine %d : FLAC should not be available.\n\n", __LINE__) ; exit_if_true (have_ogg, "\n\nLine %d : Ogg/Vorbis should not be available.\n\n", __LINE__) ; exit_if_true (have_vorbis, "\n\nLine %d : Ogg/Vorbis should not be available.\n\n", __LINE__) ; + exit_if_true (have_opus, "\n\nLine %d : Ogg/Opus should not be available.\n\n", __LINE__) ; } ; puts ("ok") ; diff --git a/tests/floating_point_test.tpl b/tests/floating_point_test.tpl index 74b5a7e5..7258fa23 100644 --- a/tests/floating_point_test.tpl +++ b/tests/floating_point_test.tpl @@ -121,6 +121,8 @@ main (int argc, char *argv []) float_scaled_test ("flac_24.flac", allow_exit, SF_FALSE, SF_FORMAT_FLAC | SF_FORMAT_PCM_24, -138.0) ; float_scaled_test ("vorbis.oga", allow_exit, SF_FALSE, SF_FORMAT_OGG | SF_FORMAT_VORBIS, -31.0) ; + + float_scaled_test ("opus.opus", allow_exit, SF_FALSE, SF_FORMAT_OGG | SF_FORMAT_OPUS, -32.0) ; #endif float_scaled_test ("replace_float.raw", allow_exit, SF_TRUE, SF_ENDIAN_LITTLE | SF_FORMAT_RAW | SF_FORMAT_FLOAT, -163.0) ; @@ -178,6 +180,7 @@ main (int argc, char *argv []) double_scaled_test ("flac_24.flac", allow_exit, SF_FALSE, SF_FORMAT_FLAC | SF_FORMAT_PCM_24, -138.0) ; double_scaled_test ("vorbis.oga", allow_exit, SF_FALSE, SF_FORMAT_OGG | SF_FORMAT_VORBIS, -29.0) ; + double_scaled_test ("opus.opus", allow_exit, SF_FALSE, SF_FORMAT_OGG | SF_FORMAT_OPUS, -32.0) ; #endif double_scaled_test ("replace_double.raw", allow_exit, SF_TRUE, SF_FORMAT_RAW | SF_FORMAT_DOUBLE, -201.0) ; diff --git a/tests/format_check_test.c b/tests/format_check_test.c index 98bdbb64..7ab71c9b 100644 --- a/tests/format_check_test.c +++ b/tests/format_check_test.c @@ -107,7 +107,11 @@ format_combo_test (void) subtype_fmt_info.format = codec ; subtype_is_valid = sf_command (NULL, SFC_GET_FORMAT_SUBTYPE, &subtype_fmt_info, sizeof (subtype_fmt_info)) == 0 ; - sf_info_setup (&info, major_fmt_info.format | subtype_fmt_info.format, 22050, 1) ; + /* Opus only works with a fixed set of sample rates. */ + if (subtype_fmt_info.format == SF_FORMAT_OPUS) + sf_info_setup (&info, major_fmt_info.format | subtype_fmt_info.format, 24000, 1) ; + else + sf_info_setup (&info, major_fmt_info.format | subtype_fmt_info.format, 22050, 1) ; check_is_valid = sf_format_check (&info) ; diff --git a/tests/lossy_comp_test.c b/tests/lossy_comp_test.c index 07bbaf54..cca274cc 100644 --- a/tests/lossy_comp_test.c +++ b/tests/lossy_comp_test.c @@ -70,6 +70,8 @@ static void check_comment (SNDFILE * file, int format, int lineno) ; static int is_lossy (int filetype) ; +static int check_opus_version (SNDFILE *file) ; + /* ** Force the start of these buffers to be double aligned. Sparc-solaris will ** choke if they are not. @@ -463,6 +465,20 @@ main (int argc, char *argv []) test_count++ ; } ; + if (do_all || strcmp (argv [1], "ogg_opus") == 0) + { if (HAVE_EXTERNAL_XIPH_LIBS) + { /* Don't do lcomp_test_XXX as the errors are too big. */ + sdlcomp_test_short ("opus.opus", SF_FORMAT_OGG | SF_FORMAT_OPUS, 1, 0.57) ; + sdlcomp_test_int ("opus.opus", SF_FORMAT_OGG | SF_FORMAT_OPUS, 1, 0.54) ; + sdlcomp_test_float ("opus.opus", SF_FORMAT_OGG | SF_FORMAT_OPUS, 1, 0.55) ; + sdlcomp_test_double ("opus.opus", SF_FORMAT_OGG | SF_FORMAT_OPUS, 1, 0.55) ; + } + else + puts (" No Ogg/Opus tests because Ogg/Opus support was not compiled in.") ; + + test_count++ ; + } ; + /* Lite remove start */ if (do_all || strcmp (argv [1], "ircam_ulaw") == 0) { lcomp_test_short ("ulaw.ircam", SF_ENDIAN_LITTLE | SF_FORMAT_IRCAM | SF_FORMAT_ULAW, 2, 0.04) ; @@ -1428,23 +1444,35 @@ channels = 1 ; /* The Vorbis encoder has a bug on PowerPC and X86-64 with sample rates ** <= 22050. Increasing the sample rate to 32000 avoids triggering it. ** See https://trac.xiph.org/ticket/1229 + ** + ** Opus only supports discrete sample rates. Choose supported 12000. */ if ((file = sf_open (filename, SFM_WRITE, &sfinfo)) == NULL) { const char * errstr ; errstr = sf_strerror (NULL) ; - if (strstr (errstr, "Sample rate chosen is known to trigger a Vorbis") == NULL) + if (strstr (errstr, "Sample rate chosen is known to trigger a Vorbis") != NULL) + { printf ("\n Sample rate -> 32kHz ") ; + sfinfo.samplerate = 32000 ; + } + else if (strstr (errstr, "Opus only supports sample rates of") != NULL) + { printf ("\n Sample rate -> 12kHz ") ; + sfinfo.samplerate = 12000 ; + } + else { printf ("Line %d: sf_open_fd (SFM_WRITE) failed : %s\n", __LINE__, errstr) ; dump_log_buffer (NULL) ; exit (1) ; } ; - printf ("\n Sample rate -> 32kHz ") ; - sfinfo.samplerate = 32000 ; - file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_TRUE, __LINE__) ; } ; + if ((filetype & SF_FORMAT_SUBMASK) == SF_FORMAT_OPUS && !check_opus_version (file)) + { sf_close (file) ; + return ; + } ; + test_write_short_or_die (file, 0, orig, datalen, __LINE__) ; sf_set_string (file, SF_STR_COMMENT, long_comment) ; sf_close (file) ; @@ -1636,23 +1664,35 @@ channels = 1 ; /* The Vorbis encoder has a bug on PowerPC and X86-64 with sample rates ** <= 22050. Increasing the sample rate to 32000 avoids triggering it. ** See https://trac.xiph.org/ticket/1229 + ** + ** Opus only supports discrete sample rates. Choose supported 12000. */ if ((file = sf_open (filename, SFM_WRITE, &sfinfo)) == NULL) { const char * errstr ; errstr = sf_strerror (NULL) ; - if (strstr (errstr, "Sample rate chosen is known to trigger a Vorbis") == NULL) + if (strstr (errstr, "Sample rate chosen is known to trigger a Vorbis") != NULL) + { printf ("\n Sample rate -> 32kHz ") ; + sfinfo.samplerate = 32000 ; + } + else if (strstr (errstr, "Opus only supports sample rates of") != NULL) + { printf ("\n Sample rate -> 12kHz ") ; + sfinfo.samplerate = 12000 ; + } + else { printf ("Line %d: sf_open_fd (SFM_WRITE) failed : %s\n", __LINE__, errstr) ; dump_log_buffer (NULL) ; exit (1) ; } ; - printf ("\n Sample rate -> 32kHz ") ; - sfinfo.samplerate = 32000 ; - file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_TRUE, __LINE__) ; } ; + if ((filetype & SF_FORMAT_SUBMASK) == SF_FORMAT_OPUS && !check_opus_version (file)) + { sf_close (file) ; + return ; + } ; + test_writef_int_or_die (file, 0, orig, datalen, __LINE__) ; sf_set_string (file, SF_STR_COMMENT, long_comment) ; sf_close (file) ; @@ -1827,15 +1867,28 @@ channels = 1 ; print_test_name ("sdlcomp_test_float", filename) ; - if ((filetype & SF_FORMAT_SUBMASK) == SF_FORMAT_VORBIS) - /* Vorbis starts to loose fidelity with floating point values outside - ** the range of approximately [-2000.0, 2000.0] (Determined - ** experimentally, not know if it is a limitation of Vorbis or - ** libvorbis.) - */ - scale = 16.0 ; - else - scale = 1.0 ; + switch ((filetype & SF_FORMAT_SUBMASK)) + { case SF_FORMAT_VORBIS : + /* Vorbis starts to loose fidelity with floating point values outside + ** the range of approximately [-2000.0, 2000.0] (Determined + ** experimentally, not know if it is a limitation of Vorbis or + ** libvorbis.) + */ + scale = 16.0 ; /* 32000/16 = 2000 */ + break ; + + case SF_FORMAT_OPUS : + /* The Opus spec says that non-normalized floating point value + ** support (extended dynamic range in its terms) is optional and + ** cannot be relied upon. + */ + scale = 32000.0 ; /* 32000/32000 = 1 */ + break ; + + default : + scale = 1.0 ; + break ; + } ; datalen = BUFFER_SIZE ; @@ -1852,7 +1905,39 @@ channels = 1 ; sfinfo.channels = channels ; sfinfo.format = filetype ; - file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + + /* The Vorbis encoder has a bug on PowerPC and X86-64 with sample rates + ** <= 22050. Increasing the sample rate to 32000 avoids triggering it. + ** See https://trac.xiph.org/ticket/1229 + ** + ** Opus only supports discrete sample rates. Choose supported 12000. + */ + if ((file = sf_open (filename, SFM_WRITE, &sfinfo)) == NULL) + { const char * errstr ; + + errstr = sf_strerror (NULL) ; + if (strstr (errstr, "Sample rate chosen is known to trigger a Vorbis") != NULL) + { printf ("\n Sample rate -> 32kHz ") ; + sfinfo.samplerate = 32000 ; + } + else if (strstr (errstr, "Opus only supports sample rates of") != NULL) + { printf ("\n Sample rate -> 12kHz ") ; + sfinfo.samplerate = 12000 ; + } + else + { printf ("Line %d: sf_open_fd (SFM_WRITE) failed : %s\n", __LINE__, errstr) ; + dump_log_buffer (NULL) ; + exit (1) ; + } ; + + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_TRUE, __LINE__) ; + } ; + + if ((filetype & SF_FORMAT_SUBMASK) == SF_FORMAT_OPUS && !check_opus_version (file)) + { sf_close (file) ; + return ; + } ; + sf_command (file, SFC_SET_NORM_FLOAT, NULL, SF_FALSE) ; test_write_float_or_die (file, 0, orig, datalen, __LINE__) ; sf_set_string (file, SF_STR_COMMENT, long_comment) ; @@ -2025,15 +2110,28 @@ sdlcomp_test_double (const char *filename, int filetype, int channels, double ma channels = 1 ; print_test_name ("sdlcomp_test_double", filename) ; - if ((filetype & SF_FORMAT_SUBMASK) == SF_FORMAT_VORBIS) - /* Vorbis starts to loose fidelity with floating point values outside - ** the range of approximately [-2000.0, 2000.0] (Determined - ** experimentally, not know if it is a limitation of Vorbis or - ** libvorbis.) - */ - scale = 16.0 ; - else - scale = 1.0 ; + switch ((filetype & SF_FORMAT_SUBMASK)) + { case SF_FORMAT_VORBIS : + /* Vorbis starts to loose fidelity with floating point values outside + ** the range of approximately [-2000.0, 2000.0] (Determined + ** experimentally, not know if it is a limitation of Vorbis or + ** libvorbis.) + */ + scale = 16.0 ; /* 32000/16 = 2000 */ + break ; + + case SF_FORMAT_OPUS : + /* The Opus spec says that non-normalized floating point value + ** support (extended dynamic range in its terms) is optional and + ** cannot be relied upon. + */ + scale = 32000.0 ; /* 32000/32000 = 1 */ + break ; + + default : + scale = 1.0 ; + break ; + } ; datalen = BUFFER_SIZE ; @@ -2048,7 +2146,38 @@ channels = 1 ; sfinfo.channels = channels ; sfinfo.format = filetype ; - file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + /* The Vorbis encoder has a bug on PowerPC and X86-64 with sample rates + ** <= 22050. Increasing the sample rate to 32000 avoids triggering it. + ** See https://trac.xiph.org/ticket/1229 + ** + ** Opus only supports discrete sample rates. Choose supported 12000. + */ + if ((file = sf_open (filename, SFM_WRITE, &sfinfo)) == NULL) + { const char * errstr ; + + errstr = sf_strerror (NULL) ; + if (strstr (errstr, "Sample rate chosen is known to trigger a Vorbis") != NULL) + { printf ("\n Sample rate -> 32kHz ") ; + sfinfo.samplerate = 32000 ; + } + else if (strstr (errstr, "Opus only supports sample rates of") != NULL) + { printf ("\n Sample rate -> 12kHz ") ; + sfinfo.samplerate = 12000 ; + } + else + { printf ("Line %d: sf_open_fd (SFM_WRITE) failed : %s\n", __LINE__, errstr) ; + dump_log_buffer (NULL) ; + exit (1) ; + } ; + + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_TRUE, __LINE__) ; + } ; + + if ((filetype & SF_FORMAT_SUBMASK) == SF_FORMAT_OPUS && !check_opus_version (file)) + { sf_close (file) ; + return ; + } ; + sf_command (file, SFC_SET_NORM_DOUBLE, NULL, SF_FALSE) ; test_write_double_or_die (file, 0, orig, datalen, __LINE__) ; sf_set_string (file, SF_STR_COMMENT, long_comment) ; @@ -2455,3 +2584,34 @@ is_lossy (int filetype) return 1 ; } /* is_lossy */ + +static int +check_opus_version (SNDFILE *file) +{ char log_buf [256] ; + char *str, *p ; + const char *str_libopus = "Opus library version: " ; + int ver_major, ver_minor ; + + sf_command (file, SFC_GET_LOG_INFO, log_buf, sizeof (log_buf)) ; + str = strstr (log_buf, str_libopus) ; + if (str) + { str += strlen (str_libopus) ; + if ((p = strchr (str, '\n'))) + *p = '\0' ; + if (sscanf (str, "libopus %d.%d", &ver_major, &ver_minor) == 2) + { /* Reject versions prior to 1.3 */ + if (ver_major > 1 || (ver_major == 1 && ver_minor >= 3)) + { /* + ** Make sure that the libopus in use is not fixed-point, as it + ** sacrifices accuracy. libopus API documentation explicitly + ** allows checking for this suffix to determine if it is. + */ + if (!strstr (str, "-fixed")) + return 1 ; + } ; + } ; + } ; + + printf ("skipping (%s)\n", str ? str : "unknown libopus version") ; + return 0 ; +} /* check_opus_version */ diff --git a/tests/ogg_opus_test.c b/tests/ogg_opus_test.c new file mode 100644 index 00000000..37532bec --- /dev/null +++ b/tests/ogg_opus_test.c @@ -0,0 +1,424 @@ +/* +** Copyright (C) 2007-2018 Erik de Castro Lopo +** +** This program is free software; you can redistribute it and/or modify +** it under the terms of the GNU General Public License as published by +** the Free Software Foundation; either version 2 of the License, or +** (at your option) any later version. +** +** This program is distributed in the hope that it will be useful, +** but WITHOUT ANY WARRANTY; without even the implied warranty of +** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +** GNU General Public License for more details. +** +** You should have received a copy of the GNU General Public License +** along with this program; if not, write to the Free Software +** Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +*/ + +#include "sfconfig.h" + +#include +#include +#include +#if HAVE_UNISTD_H +#include +#else +#include "sf_unistd.h" +#endif + +#include +#include +#include + +#include "utils.h" + +#define SAMPLE_RATE 48000 +#define DATA_LENGTH (SAMPLE_RATE / 8) + +typedef union +{ double d [DATA_LENGTH] ; + float f [DATA_LENGTH] ; + int i [DATA_LENGTH] ; + short s [DATA_LENGTH] ; +} BUFFER ; + +static BUFFER data_out ; +static BUFFER data_in ; + +static void +ogg_opus_short_test (void) +{ const char * filename = "ogg_opus_short.opus" ; + + SNDFILE * file ; + SF_INFO sfinfo ; + short seek_data [10] ; + unsigned k ; + + print_test_name ("ogg_opus_short_test", filename) ; + + /* Generate float data. */ + gen_windowed_sine_float (data_out.f, ARRAY_LEN (data_out.f), 1.0 * 0x7F00) ; + + /* Convert to short. */ + for (k = 0 ; k < ARRAY_LEN (data_out.s) ; k++) + data_out.s [k] = lrintf (data_out.f [k]) ; + + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + /* Set up output file type. */ + sfinfo.format = SF_FORMAT_OGG | SF_FORMAT_OPUS ; + sfinfo.channels = 1 ; + sfinfo.samplerate = SAMPLE_RATE ; + + /* Write the output file. */ + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + test_write_short_or_die (file, 0, data_out.s, ARRAY_LEN (data_out.s), __LINE__) ; + sf_close (file) ; + + /* Read the file in again. */ + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + test_read_short_or_die (file, 0, data_in.s, ARRAY_LEN (data_in.s), __LINE__) ; + sf_close (file) ; + + puts ("ok") ; + + /* Test seeking. */ + print_test_name ("ogg_opus_seek_test", filename) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + + test_seek_or_die (file, 10, SEEK_SET, 10, sfinfo.channels, __LINE__) ; + test_read_short_or_die (file, 0, seek_data, ARRAY_LEN (seek_data), __LINE__) ; + compare_short_or_die (seek_data, data_in.s + 10, ARRAY_LEN (seek_data), __LINE__) ; + + /* Test seek to end of file. */ + test_seek_or_die (file, 0, SEEK_END, sfinfo.frames, sfinfo.channels, __LINE__) ; + + sf_close (file) ; + + puts ("ok") ; + + unlink (filename) ; +} /* ogg_opus_short_test */ + +static void +ogg_opus_int_test (void) +{ const char * filename = "ogg_opus_int.opus" ; + + SNDFILE * file ; + SF_INFO sfinfo ; + int seek_data [10] ; + unsigned k ; + + print_test_name ("ogg_opus_int_test", filename) ; + + /* Generate float data. */ + gen_windowed_sine_float (data_out.f, ARRAY_LEN (data_out.f), 1.0 * 0x7FFF0000) ; + + /* Convert to integer. */ + for (k = 0 ; k < ARRAY_LEN (data_out.i) ; k++) + data_out.i [k] = lrintf (data_out.f [k]) ; + + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + /* Set up output file type. */ + sfinfo.format = SF_FORMAT_OGG | SF_FORMAT_OPUS ; + sfinfo.channels = 1 ; + sfinfo.samplerate = SAMPLE_RATE ; + + /* Write the output file. */ + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + test_write_int_or_die (file, 0, data_out.i, ARRAY_LEN (data_out.i), __LINE__) ; + sf_close (file) ; + + /* Read the file in again. */ + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + test_read_int_or_die (file, 0, data_in.i, ARRAY_LEN (data_in.i), __LINE__) ; + sf_close (file) ; + + puts ("ok") ; + + /* Test seeking. */ + print_test_name ("ogg_opus_seek_test", filename) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + + test_seek_or_die (file, 10, SEEK_SET, 10, sfinfo.channels, __LINE__) ; + test_read_int_or_die (file, 0, seek_data, ARRAY_LEN (seek_data), __LINE__) ; + compare_int_or_die (seek_data, data_in.i + 10, ARRAY_LEN (seek_data), __LINE__) ; + + sf_close (file) ; + + puts ("ok") ; + + unlink (filename) ; +} /* ogg_opus_int_test */ + +static void +ogg_opus_float_test (void) +{ const char * filename = "ogg_opus_float.opus" ; + + SNDFILE * file ; + SF_INFO sfinfo ; + float seek_data [10] ; + + print_test_name ("ogg_opus_float_test", filename) ; + + gen_windowed_sine_float (data_out.f, ARRAY_LEN (data_out.f), 0.95) ; + + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + /* Set up output file type. */ + sfinfo.format = SF_FORMAT_OGG | SF_FORMAT_OPUS ; + sfinfo.channels = 1 ; + sfinfo.samplerate = SAMPLE_RATE ; + + /* Write the output file. */ + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + test_write_float_or_die (file, 0, data_out.f, ARRAY_LEN (data_out.f), __LINE__) ; + sf_close (file) ; + + /* Read the file in again. */ + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + test_read_float_or_die (file, 0, data_in.f, ARRAY_LEN (data_in.f), __LINE__) ; + sf_close (file) ; + + puts ("ok") ; + + /* Test seeking. */ + print_test_name ("ogg_opus_seek_test", filename) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + + test_seek_or_die (file, 10, SEEK_SET, 10, sfinfo.channels, __LINE__) ; + test_read_float_or_die (file, 0, seek_data, ARRAY_LEN (seek_data), __LINE__) ; + compare_float_or_die (seek_data, data_in.f + 10, ARRAY_LEN (seek_data), __LINE__) ; + + sf_close (file) ; + + puts ("ok") ; + + unlink (filename) ; +} /* ogg_opus_float_test */ + +static void +ogg_opus_double_test (void) +{ const char * filename = "ogg_opus_double.opus" ; + + SNDFILE * file ; + SF_INFO sfinfo ; + double seek_data [10] ; + + print_test_name ("ogg_opus_double_test", filename) ; + + gen_windowed_sine_double (data_out.d, ARRAY_LEN (data_out.d), 0.95) ; + + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + /* Set up output file type. */ + sfinfo.format = SF_FORMAT_OGG | SF_FORMAT_OPUS ; + sfinfo.channels = 1 ; + sfinfo.samplerate = SAMPLE_RATE ; + + /* Write the output file. */ + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + test_write_double_or_die (file, 0, data_out.d, ARRAY_LEN (data_out.d), __LINE__) ; + sf_close (file) ; + + /* Read the file in again. */ + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + test_read_double_or_die (file, 0, data_in.d, ARRAY_LEN (data_in.d), __LINE__) ; + sf_close (file) ; + + puts ("ok") ; + + /* Test seeking. */ + print_test_name ("ogg_opus_seek_test", filename) ; + + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + + test_seek_or_die (file, 10, SEEK_SET, 10, sfinfo.channels, __LINE__) ; + test_read_double_or_die (file, 0, seek_data, ARRAY_LEN (seek_data), __LINE__) ; + compare_double_or_die (seek_data, data_in.d + 10, ARRAY_LEN (seek_data), __LINE__) ; + + sf_close (file) ; + + puts ("ok") ; + + unlink (filename) ; +} /* ogg_opus_double_test */ + + +static void +ogg_opus_stereo_seek_test (const char * filename, int format) +{ static float data [SAMPLE_RATE] ; + static float stereo_out [SAMPLE_RATE * 2] ; + + SNDFILE * file ; + SF_INFO sfinfo ; + sf_count_t pos ; + unsigned k ; + + print_test_name (__func__, filename) ; + + gen_windowed_sine_float (data, ARRAY_LEN (data), 0.95) ; + for (k = 0 ; k < ARRAY_LEN (data) ; k++) + { stereo_out [2 * k] = data [k] ; + stereo_out [2 * k + 1] = data [ARRAY_LEN (data) - k - 1] ; + } ; + + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + /* Set up output file type. */ + sfinfo.format = format ; + sfinfo.channels = 2 ; + sfinfo.samplerate = SAMPLE_RATE ; + + /* Write the output file. */ + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + test_write_float_or_die (file, 0, stereo_out, ARRAY_LEN (stereo_out), __LINE__) ; + sf_close (file) ; + + /* Open file in again for reading. */ + memset (&sfinfo, 0, sizeof (sfinfo)) ; + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + + /* Read in the whole file. */ + test_read_float_or_die (file, 0, stereo_out, ARRAY_LEN (stereo_out), __LINE__) ; + + /* Now hammer seeking code. */ + test_seek_or_die (file, 234, SEEK_SET, 234, sfinfo.channels, __LINE__) ; + test_readf_float_or_die (file, 0, data, 10, __LINE__) ; + compare_float_or_die (data, stereo_out + (234 * sfinfo.channels), 10, __LINE__) ; + + test_seek_or_die (file, 442, SEEK_SET, 442, sfinfo.channels, __LINE__) ; + test_readf_float_or_die (file, 0, data, 10, __LINE__) ; + compare_float_or_die (data, stereo_out + (442 * sfinfo.channels), 10, __LINE__) ; + + test_seek_or_die (file, 12, SEEK_CUR, 442 + 10 + 12, sfinfo.channels, __LINE__) ; + test_readf_float_or_die (file, 0, data, 10, __LINE__) ; + compare_float_or_die (data, stereo_out + ((442 + 10 + 12) * sfinfo.channels), 10, __LINE__) ; + + test_seek_or_die (file, 12, SEEK_CUR, 442 + 20 + 24, sfinfo.channels, __LINE__) ; + test_readf_float_or_die (file, 0, data, 10, __LINE__) ; + compare_float_or_die (data, stereo_out + ((442 + 20 + 24) * sfinfo.channels), 10, __LINE__) ; + + pos = 500 - sfinfo.frames ; + test_seek_or_die (file, pos, SEEK_END, 500, sfinfo.channels, __LINE__) ; + test_readf_float_or_die (file, 0, data, 10, __LINE__) ; + compare_float_or_die (data, stereo_out + (500 * sfinfo.channels), 10, __LINE__) ; + + pos = 10 - sfinfo.frames ; + test_seek_or_die (file, pos, SEEK_END, 10, sfinfo.channels, __LINE__) ; + test_readf_float_or_die (file, 0, data, 10, __LINE__) ; + compare_float_or_die (data, stereo_out + (10 * sfinfo.channels), 10, __LINE__) ; + + sf_close (file) ; + + puts ("ok") ; + unlink (filename) ; +} /* ogg_opus_stereo_seek_test */ + + +static void +ogg_opus_original_samplerate_test (void) +{ const char * filename = "ogg_opus_original_samplerate.opus" ; + + SNDFILE * file ; + SF_INFO sfinfo ; + int original_samplerate = 54321 ; + sf_count_t frames ; + + print_test_name ("ogg_opus_original_samplerate_test", filename) ; + + gen_windowed_sine_double (data_out.d, ARRAY_LEN (data_out.d), 0.95) ; + + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + /* Set up output file type. */ + sfinfo.format = SF_FORMAT_OGG | SF_FORMAT_OPUS ; + sfinfo.channels = 1 ; + sfinfo.samplerate = SAMPLE_RATE ; + + /* Write the output file. */ + file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_FALSE, __LINE__) ; + if (sf_command (file, SFC_SET_ORIGINAL_SAMPLERATE, &original_samplerate, sizeof (original_samplerate)) != SF_TRUE) + { printf ("\nCommand SFC_SET_ORIGINAL_SAMPLERATE failed!\n") ; + exit (1) ; + } ; + test_write_double_or_die (file, 0, data_out.d, ARRAY_LEN (data_out.d), __LINE__) ; + if (sf_command (file, SFC_SET_ORIGINAL_SAMPLERATE, &original_samplerate, sizeof (original_samplerate)) != SF_FALSE) + { printf ("\nCommand SFC_SET_ORIGINAL_SAMPLERATE succeeded when it should have failed!") ; + exit (1) ; + } ; + sf_close (file) ; + + /* Read the file in again. */ + memset (&sfinfo, 0, sizeof (sfinfo)) ; + + original_samplerate = 0 ; + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + if (sf_command (file, SFC_GET_ORIGINAL_SAMPLERATE, &original_samplerate, sizeof (original_samplerate)) != SF_TRUE + || original_samplerate != 54321) + { printf ("\nCommand SFC_GET_ORIGINAL_SAMPLERATE failed!\n") ; + exit (1) ; + } ; + test_read_double_or_die (file, 0, data_in.d, 8, __LINE__) ; + if (sf_command (file, SFC_SET_ORIGINAL_SAMPLERATE, &original_samplerate, sizeof (original_samplerate)) == SF_TRUE) + { printf ("\nCommand SFC_SET_ORIGINAL_SAMPLERATE succeeded when it should have failed!\n") ; + exit (1) ; + } ; + sf_close (file) ; + + /* Test changing the decoder. */ + file = test_open_file_or_die (filename, SFM_READ, &sfinfo, SF_FALSE, __LINE__) ; + frames = sfinfo.frames ; + original_samplerate = 16000 ; + if (sf_command (file, SFC_SET_ORIGINAL_SAMPLERATE, &original_samplerate, sizeof (original_samplerate)) != SF_TRUE) + { printf ("\nCommand SFC_SET_ORIGINAL_SAMPLERATE failed!\n") ; + exit (1) ; + } ; + if (sf_command (file, SFC_GET_CURRENT_SF_INFO, &sfinfo, sizeof (sfinfo))) + { printf ("\nCommand SFC_GET_CURRENT_SF_INFO failed!\n") ; + exit (1) ; + } ; + if (frames / (48000 / 16000) != sfinfo.frames) + { printf ("\nIncorrect frame count! (%" PRId64 " vs %" PRId64")\n", frames / (48000 / 16000), sfinfo.frames) ; + exit (1) ; + } ; + test_read_double_or_die (file, 0, data_out.d, sfinfo.frames, __LINE__) ; + + sf_close (file) ; + + puts ("ok") ; + + unlink (filename) ; +} /* ogg_opus_original_samplerate_test */ + + +int +main (void) +{ + if (HAVE_EXTERNAL_XIPH_LIBS) + { ogg_opus_short_test () ; + ogg_opus_int_test () ; + ogg_opus_float_test () ; + ogg_opus_double_test () ; + + ogg_opus_stereo_seek_test ("ogg_opus_seek.opus", SF_FORMAT_OGG | SF_FORMAT_OPUS) ; + ogg_opus_original_samplerate_test () ; + } + else + puts (" No Ogg/Opus tests because Ogg/Opus support was not compiled in.") ; + + return 0 ; +} /* main */ diff --git a/tests/string_test.c b/tests/string_test.c index 96b0c547..70a0dfbf 100644 --- a/tests/string_test.c +++ b/tests/string_test.c @@ -61,6 +61,7 @@ main (int argc, char *argv []) printf (" aiff - test adding strings to AIFF files\n") ; printf (" flac - test adding strings to FLAC files\n") ; printf (" ogg - test adding strings to OGG files\n") ; + printf (" opus - test adding strings to OPUS files\n") ; printf (" all - perform all tests\n") ; exit (1) ; } ; @@ -114,12 +115,20 @@ main (int argc, char *argv []) if (do_all || ! strcmp (argv [1], "ogg")) { if (HAVE_EXTERNAL_XIPH_LIBS) - string_start_test ("vorbis.oga", SF_FORMAT_OGG) ; + string_start_test ("vorbis.oga", SF_FORMAT_OGG | SF_FORMAT_VORBIS) ; else puts (" No Ogg/Vorbis tests because Ogg/Vorbis support was not compiled in.") ; test_count++ ; } ; + if (do_all || ! strcmp (argv [1], "opus")) + { if (HAVE_EXTERNAL_XIPH_LIBS) + string_start_test ("opus.opus", SF_FORMAT_OGG | SF_FORMAT_OPUS) ; + else + puts (" No Ogg/Opus tests because Ogg/Opus support was not compiled in.") ; + test_count++ ; + } ; + if (do_all || ! strcmp (argv [1], "caf")) { string_start_test ("strings.caf", SF_FORMAT_CAF) ; string_start_end_test ("strings.caf", SF_FORMAT_CAF) ; @@ -340,11 +349,12 @@ string_start_end_test (const char *filename, int typemajor) } /* string_start_end_test */ static void -string_start_test (const char *filename, int typemajor) +string_start_test (const char *filename, int formattype) { const char *cptr ; SNDFILE *file ; SF_INFO sfinfo ; int errors = 0 ; + int typemajor = SF_FORMAT_TYPEMASK & formattype ; print_test_name ("string_start_test", filename) ; @@ -353,15 +363,20 @@ string_start_test (const char *filename, int typemajor) sfinfo.channels = 1 ; sfinfo.frames = 0 ; - switch (typemajor) - { case SF_FORMAT_OGG : - sfinfo.format = typemajor | SF_FORMAT_VORBIS ; + switch (formattype) + { case SF_FORMAT_OGG | SF_FORMAT_OPUS : + /* Opus only supports some discrete sample rates. */ + sfinfo.samplerate = 48000 ; + break ; + + case SF_FORMAT_OGG | SF_FORMAT_VORBIS : break ; default : - sfinfo.format = typemajor | SF_FORMAT_PCM_16 ; + formattype |= SF_FORMAT_PCM_16 ; break ; } ; + sfinfo.format = formattype ; file = test_open_file_or_die (filename, SFM_WRITE, &sfinfo, SF_TRUE, __LINE__) ; diff --git a/tests/test_wrapper.sh.in b/tests/test_wrapper.sh.in index c66211f7..b06a1788 100644 --- a/tests/test_wrapper.sh.in +++ b/tests/test_wrapper.sh.in @@ -342,6 +342,16 @@ echo "----------------------------------------------------------------------" echo " $sfversion passed tests on OGG/VORBIS files." echo "----------------------------------------------------------------------" +# opus-tests +./tests/ogg_opus_test@EXEEXT@ +./tests/compression_size_test@EXEEXT@ opus +./tests/lossy_comp_test@EXEEXT@ ogg_opus +./tests/string_test@EXEEXT@ opus + +echo "----------------------------------------------------------------------" +echo " $sfversion passed tests on OPUS files." +echo "----------------------------------------------------------------------" + # io-tests ./tests/stdio_test@EXEEXT@ ./tests/pipe_test@EXEEXT@