From 05466725af340b9b4a28657e310666e7e646518d Mon Sep 17 00:00:00 2001 From: Attila Uygun Date: Mon, 13 Apr 2020 13:24:53 +0200 Subject: [PATCH] Initial commit. --- .gitignore | 8 + LICENSE | 21 + README.md | 17 + assets/PixelCaps!.ttf | Bin 0 -> 13376 bytes assets/enemy_anims_01_frames_ok.png | Bin 0 -> 340296 bytes assets/enemy_anims_02_frames_ok.png | Bin 0 -> 56004 bytes assets/enemy_anims_blast_ok.png | Bin 0 -> 159456 bytes assets/enemy_anims_flare_ok.png | Bin 0 -> 224146 bytes assets/enemy_ray_ok.png | Bin 0 -> 17490 bytes assets/enemy_target_multi_ok.png | Bin 0 -> 11303 bytes assets/enemy_target_single_ok.png | Bin 0 -> 32354 bytes assets/engine/RobotoMono-Regular.ttf | Bin 0 -> 113500 bytes assets/engine/pass_through.glsl_fragment | 12 + assets/engine/pass_through.glsl_vertex | 26 + assets/engine/quad.mesh | 10 + assets/engine/solid.glsl_fragment | 9 + assets/engine/solid.glsl_vertex | 20 + assets/explosion.mp3 | Bin 0 -> 10368 bytes assets/sky.glsl_fragment | 61 + assets/sky.glsl_vertex | 17 + build/android/app/CMakeLists.txt | 130 + build/android/app/build.gradle | 40 + .../android/app/src/main/AndroidManifest.xml | 34 + .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3418 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2206 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4842 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 7718 bytes .../app/src/main/res/values/strings.xml | 4 + build/android/build.gradle | 21 + build/android/gradle.properties | 20 + .../android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 49896 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + build/android/gradlew | 164 + build/android/local.properties | 8 + build/android/settings.gradle | 2 + build/linux/Makefile | 174 + src/LICENSE | 21 + src/base/closure.h | 12 + src/base/collusion_test.cc | 51 + src/base/collusion_test.h | 23 + src/base/file.h | 25 + src/base/hash.h | 21 + src/base/interpolation.h | 43 + src/base/log.cc | 52 + src/base/log.h | 49 + src/base/mem.h | 52 + src/base/misc.h | 34 + src/base/random.cc | 26 + src/base/random.h | 27 + src/base/task_runner.cc | 30 + src/base/task_runner.h | 32 + src/base/timer.cc | 28 + src/base/timer.h | 29 + src/base/vecmath.cc | 15 + src/base/vecmath.h | 163 + src/base/worker.cc | 68 + src/base/worker.h | 40 + src/demo/credits.cc | 138 + src/demo/credits.h | 47 + src/demo/damage_type.h | 20 + src/demo/demo.cc | 251 + src/demo/demo.h | 82 + src/demo/enemy.cc | 476 + src/demo/enemy.h | 119 + src/demo/hud.cc | 164 + src/demo/hud.h | 54 + src/demo/menu.cc | 214 + src/demo/menu.h | 72 + src/demo/player.cc | 354 + src/demo/player.h | 79 + src/demo/sky_quad.cc | 64 + src/demo/sky_quad.h | 52 + src/engine/animatable.cc | 33 + src/engine/animatable.h | 66 + src/engine/animator.cc | 276 + src/engine/animator.h | 136 + src/engine/audio/audio.h | 20 + src/engine/audio/audio_alsa.cc | 187 + src/engine/audio/audio_alsa.h | 46 + src/engine/audio/audio_base.cc | 138 + src/engine/audio/audio_base.h | 41 + src/engine/audio/audio_forward.h | 16 + src/engine/audio/audio_oboe.cc | 78 + src/engine/audio/audio_oboe.h | 50 + src/engine/audio/audio_resource.cc | 72 + src/engine/audio/audio_resource.h | 41 + src/engine/audio/audio_sample.h | 32 + src/engine/engine.cc | 314 + src/engine/engine.h | 148 + src/engine/font.cc | 193 + src/engine/font.h | 54 + src/engine/game.h | 30 + src/engine/game_factory.h | 55 + src/engine/image.cc | 359 + src/engine/image.h | 59 + src/engine/image_quad.cc | 87 + src/engine/image_quad.h | 57 + src/engine/input_event.h | 49 + src/engine/mesh.cc | 169 + src/engine/mesh.h | 55 + src/engine/platform/asset_file.cc | 41 + src/engine/platform/asset_file.h | 43 + src/engine/platform/asset_file_android.cc | 74 + src/engine/platform/asset_file_linux.cc | 41 + src/engine/platform/platform.cc | 77 + src/engine/platform/platform.h | 88 + src/engine/platform/platform_android.cc | 288 + src/engine/platform/platform_linux.cc | 116 + src/engine/renderer/geometry.cc | 50 + src/engine/renderer/geometry.h | 38 + src/engine/renderer/opengl.h | 21 + src/engine/renderer/render_command.cc | 36 + src/engine/renderer/render_command.h | 135 + src/engine/renderer/render_resource.cc | 18 + src/engine/renderer/render_resource.h | 71 + src/engine/renderer/renderer.cc | 798 + src/engine/renderer/renderer.h | 230 + src/engine/renderer/renderer_android.cc | 57 + src/engine/renderer/renderer_linux.cc | 98 + src/engine/renderer/renderer_types.cc | 97 + src/engine/renderer/renderer_types.h | 47 + src/engine/renderer/shader.cc | 111 + src/engine/renderer/shader.h | 39 + src/engine/renderer/texture.cc | 48 + src/engine/renderer/texture.h | 38 + src/engine/shader_source.cc | 41 + src/engine/shader_source.h | 26 + src/engine/solid_quad.cc | 29 + src/engine/solid_quad.h | 28 + src/engine/sound.cc | 208 + src/engine/sound.h | 87 + src/engine/sound_player.cc | 57 + src/engine/sound_player.h | 51 + src/third_party/android/GLContext.cpp | 289 + src/third_party/android/GLContext.h | 117 + src/third_party/android/gestureDetector.cpp | 317 + src/third_party/android/gestureDetector.h | 145 + src/third_party/android/gl3stub.c | 421 + src/third_party/android/gl3stub.h | 661 + src/third_party/glew/glew.c | 16174 +++++++++++++++ src/third_party/glew/glew.h | 16287 ++++++++++++++++ src/third_party/glew/glxew.h | 1601 ++ src/third_party/jsoncpp/json.h | 2075 ++ src/third_party/jsoncpp/jsoncpp.cc | 5192 +++++ src/third_party/minimp3/minimp3.h | 1853 ++ src/third_party/minimp3/minimp3_ex.h | 1363 ++ src/third_party/minizip/ioapi.c | 247 + src/third_party/minizip/ioapi.h | 208 + src/third_party/minizip/unzip.c | 2125 ++ src/third_party/minizip/unzip.h | 437 + src/third_party/oboe/CMakeLists.txt | 92 + .../oboe/include/oboe/AudioStream.h | 543 + .../oboe/include/oboe/AudioStreamBase.h | 202 + .../oboe/include/oboe/AudioStreamBuilder.h | 438 + .../oboe/include/oboe/AudioStreamCallback.h | 123 + .../oboe/include/oboe/Definitions.h | 519 + .../oboe/include/oboe/LatencyTuner.h | 150 + src/third_party/oboe/include/oboe/Oboe.h | 37 + .../oboe/include/oboe/ResultWithValue.h | 155 + .../oboe/include/oboe/StabilizedCallback.h | 75 + src/third_party/oboe/include/oboe/Utilities.h | 87 + src/third_party/oboe/include/oboe/Version.h | 92 + .../oboe/src/aaudio/AAudioLoader.cpp | 348 + .../oboe/src/aaudio/AAudioLoader.h | 229 + .../oboe/src/aaudio/AudioStreamAAudio.cpp | 643 + .../oboe/src/aaudio/AudioStreamAAudio.h | 123 + src/third_party/oboe/src/common/AudioClock.h | 75 + .../oboe/src/common/AudioSourceCaller.cpp | 38 + .../oboe/src/common/AudioSourceCaller.h | 83 + .../oboe/src/common/AudioStream.cpp | 211 + .../oboe/src/common/AudioStreamBuilder.cpp | 201 + .../src/common/DataConversionFlowGraph.cpp | 215 + .../oboe/src/common/DataConversionFlowGraph.h | 84 + .../oboe/src/common/FilterAudioStream.cpp | 92 + .../oboe/src/common/FilterAudioStream.h | 214 + .../oboe/src/common/FixedBlockAdapter.cpp | 38 + .../oboe/src/common/FixedBlockAdapter.h | 67 + .../oboe/src/common/FixedBlockReader.cpp | 73 + .../oboe/src/common/FixedBlockReader.h | 60 + .../oboe/src/common/FixedBlockWriter.cpp | 73 + .../oboe/src/common/FixedBlockWriter.h | 54 + .../oboe/src/common/LatencyTuner.cpp | 105 + .../oboe/src/common/MonotonicCounter.h | 112 + src/third_party/oboe/src/common/OboeDebug.h | 41 + .../oboe/src/common/QuirksManager.cpp | 138 + .../oboe/src/common/QuirksManager.h | 110 + src/third_party/oboe/src/common/README.md | 33 + .../oboe/src/common/SourceFloatCaller.cpp | 30 + .../oboe/src/common/SourceFloatCaller.h | 44 + .../oboe/src/common/SourceI16Caller.cpp | 47 + .../oboe/src/common/SourceI16Caller.h | 48 + .../oboe/src/common/StabilizedCallback.cpp | 112 + src/third_party/oboe/src/common/Trace.cpp | 75 + src/third_party/oboe/src/common/Trace.h | 31 + src/third_party/oboe/src/common/Utilities.cpp | 305 + src/third_party/oboe/src/common/Version.cpp | 28 + src/third_party/oboe/src/fifo/FifoBuffer.cpp | 182 + src/third_party/oboe/src/fifo/FifoBuffer.h | 99 + .../oboe/src/fifo/FifoController.cpp | 31 + .../oboe/src/fifo/FifoController.h | 61 + .../oboe/src/fifo/FifoControllerBase.cpp | 71 + .../oboe/src/fifo/FifoControllerBase.h | 93 + .../oboe/src/fifo/FifoControllerIndirect.cpp | 31 + .../oboe/src/fifo/FifoControllerIndirect.h | 64 + .../oboe/src/flowgraph/ClipToRange.cpp | 38 + .../oboe/src/flowgraph/ClipToRange.h | 68 + .../oboe/src/flowgraph/FlowGraphNode.cpp | 111 + .../oboe/src/flowgraph/FlowGraphNode.h | 422 + .../src/flowgraph/ManyToMultiConverter.cpp | 47 + .../oboe/src/flowgraph/ManyToMultiConverter.h | 49 + .../src/flowgraph/MonoToMultiConverter.cpp | 43 + .../oboe/src/flowgraph/MonoToMultiConverter.h | 49 + .../oboe/src/flowgraph/RampLinear.cpp | 77 + .../oboe/src/flowgraph/RampLinear.h | 96 + .../src/flowgraph/SampleRateConverter.cpp | 64 + .../oboe/src/flowgraph/SampleRateConverter.h | 56 + .../oboe/src/flowgraph/SinkFloat.cpp | 50 + .../oboe/src/flowgraph/SinkFloat.h | 44 + .../oboe/src/flowgraph/SinkI16.cpp | 58 + src/third_party/oboe/src/flowgraph/SinkI16.h | 43 + .../oboe/src/flowgraph/SinkI24.cpp | 67 + src/third_party/oboe/src/flowgraph/SinkI24.h | 44 + .../oboe/src/flowgraph/SourceFloat.cpp | 43 + .../oboe/src/flowgraph/SourceFloat.h | 43 + .../oboe/src/flowgraph/SourceI16.cpp | 54 + .../oboe/src/flowgraph/SourceI16.h | 42 + .../oboe/src/flowgraph/SourceI24.cpp | 65 + .../oboe/src/flowgraph/SourceI24.h | 43 + .../resampler/HyperbolicCosineWindow.h | 68 + .../src/flowgraph/resampler/IntegerRatio.cpp | 50 + .../src/flowgraph/resampler/IntegerRatio.h | 52 + .../src/flowgraph/resampler/KaiserWindow.h | 87 + .../flowgraph/resampler/LinearResampler.cpp | 42 + .../src/flowgraph/resampler/LinearResampler.h | 44 + .../resampler/MultiChannelResampler.cpp | 171 + .../resampler/MultiChannelResampler.h | 271 + .../resampler/PolyphaseResampler.cpp | 61 + .../flowgraph/resampler/PolyphaseResampler.h | 51 + .../resampler/PolyphaseResamplerMono.cpp | 62 + .../resampler/PolyphaseResamplerMono.h | 39 + .../resampler/PolyphaseResamplerStereo.cpp | 78 + .../resampler/PolyphaseResamplerStereo.h | 39 + .../oboe/src/flowgraph/resampler/README.md | 93 + .../src/flowgraph/resampler/SincResampler.cpp | 76 + .../src/flowgraph/resampler/SincResampler.h | 47 + .../resampler/SincResamplerStereo.cpp | 80 + .../flowgraph/resampler/SincResamplerStereo.h | 39 + .../src/opensles/AudioInputStreamOpenSLES.cpp | 352 + .../src/opensles/AudioInputStreamOpenSLES.h | 65 + .../opensles/AudioOutputStreamOpenSLES.cpp | 448 + .../src/opensles/AudioOutputStreamOpenSLES.h | 77 + .../oboe/src/opensles/AudioStreamBuffered.cpp | 266 + .../oboe/src/opensles/AudioStreamBuffered.h | 92 + .../oboe/src/opensles/AudioStreamOpenSLES.cpp | 396 + .../oboe/src/opensles/AudioStreamOpenSLES.h | 132 + .../oboe/src/opensles/EngineOpenSLES.cpp | 101 + .../oboe/src/opensles/EngineOpenSLES.h | 65 + .../oboe/src/opensles/OpenSLESUtilities.cpp | 93 + .../oboe/src/opensles/OpenSLESUtilities.h | 44 + .../oboe/src/opensles/OutputMixerOpenSLES.cpp | 74 + .../oboe/src/opensles/OutputMixerOpenSLES.h | 58 + src/third_party/r8b/CDSPBlockConvolver.h | 646 + src/third_party/r8b/CDSPFIRFilter.h | 721 + src/third_party/r8b/CDSPFracInterpolator.h | 1019 + src/third_party/r8b/CDSPHBDownsampler.h | 393 + src/third_party/r8b/CDSPHBUpsampler.h | 857 + src/third_party/r8b/CDSPProcessor.h | 103 + src/third_party/r8b/CDSPRealFFT.h | 718 + src/third_party/r8b/CDSPResampler.h | 753 + src/third_party/r8b/CDSPSincFilterGen.h | 687 + src/third_party/r8b/pffft.cpp | 1891 ++ src/third_party/r8b/pffft.h | 178 + src/third_party/r8b/r8bbase.cpp | 39 + src/third_party/r8b/r8bbase.h | 1240 ++ src/third_party/r8b/r8bconf.h | 198 + src/third_party/stb/stb_image.h | 4673 +++++ src/third_party/stb/stb_truetype.h | 2066 ++ .../texture_compressor/dxt_encoder.cc | 760 + .../texture_compressor/dxt_encoder.h | 30 + .../dxt_encoder_implementation_autogen.h | 785 + .../dxt_encoder_internals.cc | 7 + .../dxt_encoder_internals.h | 85 + .../texture_compressor/dxt_encoder_neon.cc | 1268 ++ .../texture_compressor/dxt_encoder_neon.h | 30 + .../texture_compressor/texture_compressor.cc | 151 + .../texture_compressor/texture_compressor.h | 44 + .../texture_compressor_etc1.cc | 368 + .../texture_compressor_etc1.h | 186 + .../texture_compressor_etc1_neon.cc | 672 + .../texture_compressor_etc1_neon.h | 28 + 290 files changed, 92781 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 assets/PixelCaps!.ttf create mode 100644 assets/enemy_anims_01_frames_ok.png create mode 100644 assets/enemy_anims_02_frames_ok.png create mode 100644 assets/enemy_anims_blast_ok.png create mode 100644 assets/enemy_anims_flare_ok.png create mode 100644 assets/enemy_ray_ok.png create mode 100644 assets/enemy_target_multi_ok.png create mode 100644 assets/enemy_target_single_ok.png create mode 100755 assets/engine/RobotoMono-Regular.ttf create mode 100644 assets/engine/pass_through.glsl_fragment create mode 100644 assets/engine/pass_through.glsl_vertex create mode 100644 assets/engine/quad.mesh create mode 100644 assets/engine/solid.glsl_fragment create mode 100644 assets/engine/solid.glsl_vertex create mode 100644 assets/explosion.mp3 create mode 100644 assets/sky.glsl_fragment create mode 100644 assets/sky.glsl_vertex create mode 100644 build/android/app/CMakeLists.txt create mode 100644 build/android/app/build.gradle create mode 100644 build/android/app/src/main/AndroidManifest.xml create mode 100644 build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 build/android/app/src/main/res/values/strings.xml create mode 100644 build/android/build.gradle create mode 100644 build/android/gradle.properties create mode 100644 build/android/gradle/wrapper/gradle-wrapper.jar create mode 100644 build/android/gradle/wrapper/gradle-wrapper.properties create mode 100755 build/android/gradlew create mode 100644 build/android/local.properties create mode 100644 build/android/settings.gradle create mode 100644 build/linux/Makefile create mode 100644 src/LICENSE create mode 100644 src/base/closure.h create mode 100644 src/base/collusion_test.cc create mode 100644 src/base/collusion_test.h create mode 100644 src/base/file.h create mode 100644 src/base/hash.h create mode 100644 src/base/interpolation.h create mode 100644 src/base/log.cc create mode 100644 src/base/log.h create mode 100644 src/base/mem.h create mode 100644 src/base/misc.h create mode 100644 src/base/random.cc create mode 100644 src/base/random.h create mode 100644 src/base/task_runner.cc create mode 100644 src/base/task_runner.h create mode 100644 src/base/timer.cc create mode 100644 src/base/timer.h create mode 100644 src/base/vecmath.cc create mode 100644 src/base/vecmath.h create mode 100644 src/base/worker.cc create mode 100644 src/base/worker.h create mode 100644 src/demo/credits.cc create mode 100644 src/demo/credits.h create mode 100644 src/demo/damage_type.h create mode 100644 src/demo/demo.cc create mode 100644 src/demo/demo.h create mode 100644 src/demo/enemy.cc create mode 100644 src/demo/enemy.h create mode 100644 src/demo/hud.cc create mode 100644 src/demo/hud.h create mode 100644 src/demo/menu.cc create mode 100644 src/demo/menu.h create mode 100644 src/demo/player.cc create mode 100644 src/demo/player.h create mode 100644 src/demo/sky_quad.cc create mode 100644 src/demo/sky_quad.h create mode 100644 src/engine/animatable.cc create mode 100644 src/engine/animatable.h create mode 100644 src/engine/animator.cc create mode 100644 src/engine/animator.h create mode 100644 src/engine/audio/audio.h create mode 100644 src/engine/audio/audio_alsa.cc create mode 100644 src/engine/audio/audio_alsa.h create mode 100644 src/engine/audio/audio_base.cc create mode 100644 src/engine/audio/audio_base.h create mode 100644 src/engine/audio/audio_forward.h create mode 100644 src/engine/audio/audio_oboe.cc create mode 100644 src/engine/audio/audio_oboe.h create mode 100644 src/engine/audio/audio_resource.cc create mode 100644 src/engine/audio/audio_resource.h create mode 100644 src/engine/audio/audio_sample.h create mode 100644 src/engine/engine.cc create mode 100644 src/engine/engine.h create mode 100644 src/engine/font.cc create mode 100644 src/engine/font.h create mode 100644 src/engine/game.h create mode 100644 src/engine/game_factory.h create mode 100644 src/engine/image.cc create mode 100644 src/engine/image.h create mode 100644 src/engine/image_quad.cc create mode 100644 src/engine/image_quad.h create mode 100644 src/engine/input_event.h create mode 100644 src/engine/mesh.cc create mode 100644 src/engine/mesh.h create mode 100644 src/engine/platform/asset_file.cc create mode 100644 src/engine/platform/asset_file.h create mode 100644 src/engine/platform/asset_file_android.cc create mode 100644 src/engine/platform/asset_file_linux.cc create mode 100644 src/engine/platform/platform.cc create mode 100644 src/engine/platform/platform.h create mode 100644 src/engine/platform/platform_android.cc create mode 100644 src/engine/platform/platform_linux.cc create mode 100644 src/engine/renderer/geometry.cc create mode 100644 src/engine/renderer/geometry.h create mode 100644 src/engine/renderer/opengl.h create mode 100644 src/engine/renderer/render_command.cc create mode 100644 src/engine/renderer/render_command.h create mode 100644 src/engine/renderer/render_resource.cc create mode 100644 src/engine/renderer/render_resource.h create mode 100644 src/engine/renderer/renderer.cc create mode 100644 src/engine/renderer/renderer.h create mode 100644 src/engine/renderer/renderer_android.cc create mode 100644 src/engine/renderer/renderer_linux.cc create mode 100644 src/engine/renderer/renderer_types.cc create mode 100644 src/engine/renderer/renderer_types.h create mode 100644 src/engine/renderer/shader.cc create mode 100644 src/engine/renderer/shader.h create mode 100644 src/engine/renderer/texture.cc create mode 100644 src/engine/renderer/texture.h create mode 100644 src/engine/shader_source.cc create mode 100644 src/engine/shader_source.h create mode 100644 src/engine/solid_quad.cc create mode 100644 src/engine/solid_quad.h create mode 100644 src/engine/sound.cc create mode 100644 src/engine/sound.h create mode 100644 src/engine/sound_player.cc create mode 100644 src/engine/sound_player.h create mode 100644 src/third_party/android/GLContext.cpp create mode 100644 src/third_party/android/GLContext.h create mode 100644 src/third_party/android/gestureDetector.cpp create mode 100644 src/third_party/android/gestureDetector.h create mode 100644 src/third_party/android/gl3stub.c create mode 100644 src/third_party/android/gl3stub.h create mode 100644 src/third_party/glew/glew.c create mode 100644 src/third_party/glew/glew.h create mode 100644 src/third_party/glew/glxew.h create mode 100644 src/third_party/jsoncpp/json.h create mode 100644 src/third_party/jsoncpp/jsoncpp.cc create mode 100644 src/third_party/minimp3/minimp3.h create mode 100644 src/third_party/minimp3/minimp3_ex.h create mode 100644 src/third_party/minizip/ioapi.c create mode 100644 src/third_party/minizip/ioapi.h create mode 100644 src/third_party/minizip/unzip.c create mode 100644 src/third_party/minizip/unzip.h create mode 100644 src/third_party/oboe/CMakeLists.txt create mode 100644 src/third_party/oboe/include/oboe/AudioStream.h create mode 100644 src/third_party/oboe/include/oboe/AudioStreamBase.h create mode 100644 src/third_party/oboe/include/oboe/AudioStreamBuilder.h create mode 100644 src/third_party/oboe/include/oboe/AudioStreamCallback.h create mode 100644 src/third_party/oboe/include/oboe/Definitions.h create mode 100644 src/third_party/oboe/include/oboe/LatencyTuner.h create mode 100644 src/third_party/oboe/include/oboe/Oboe.h create mode 100644 src/third_party/oboe/include/oboe/ResultWithValue.h create mode 100644 src/third_party/oboe/include/oboe/StabilizedCallback.h create mode 100644 src/third_party/oboe/include/oboe/Utilities.h create mode 100644 src/third_party/oboe/include/oboe/Version.h create mode 100644 src/third_party/oboe/src/aaudio/AAudioLoader.cpp create mode 100644 src/third_party/oboe/src/aaudio/AAudioLoader.h create mode 100644 src/third_party/oboe/src/aaudio/AudioStreamAAudio.cpp create mode 100644 src/third_party/oboe/src/aaudio/AudioStreamAAudio.h create mode 100644 src/third_party/oboe/src/common/AudioClock.h create mode 100644 src/third_party/oboe/src/common/AudioSourceCaller.cpp create mode 100644 src/third_party/oboe/src/common/AudioSourceCaller.h create mode 100644 src/third_party/oboe/src/common/AudioStream.cpp create mode 100644 src/third_party/oboe/src/common/AudioStreamBuilder.cpp create mode 100644 src/third_party/oboe/src/common/DataConversionFlowGraph.cpp create mode 100644 src/third_party/oboe/src/common/DataConversionFlowGraph.h create mode 100644 src/third_party/oboe/src/common/FilterAudioStream.cpp create mode 100644 src/third_party/oboe/src/common/FilterAudioStream.h create mode 100644 src/third_party/oboe/src/common/FixedBlockAdapter.cpp create mode 100644 src/third_party/oboe/src/common/FixedBlockAdapter.h create mode 100644 src/third_party/oboe/src/common/FixedBlockReader.cpp create mode 100644 src/third_party/oboe/src/common/FixedBlockReader.h create mode 100644 src/third_party/oboe/src/common/FixedBlockWriter.cpp create mode 100644 src/third_party/oboe/src/common/FixedBlockWriter.h create mode 100644 src/third_party/oboe/src/common/LatencyTuner.cpp create mode 100644 src/third_party/oboe/src/common/MonotonicCounter.h create mode 100644 src/third_party/oboe/src/common/OboeDebug.h create mode 100644 src/third_party/oboe/src/common/QuirksManager.cpp create mode 100644 src/third_party/oboe/src/common/QuirksManager.h create mode 100644 src/third_party/oboe/src/common/README.md create mode 100644 src/third_party/oboe/src/common/SourceFloatCaller.cpp create mode 100644 src/third_party/oboe/src/common/SourceFloatCaller.h create mode 100644 src/third_party/oboe/src/common/SourceI16Caller.cpp create mode 100644 src/third_party/oboe/src/common/SourceI16Caller.h create mode 100644 src/third_party/oboe/src/common/StabilizedCallback.cpp create mode 100644 src/third_party/oboe/src/common/Trace.cpp create mode 100644 src/third_party/oboe/src/common/Trace.h create mode 100644 src/third_party/oboe/src/common/Utilities.cpp create mode 100644 src/third_party/oboe/src/common/Version.cpp create mode 100644 src/third_party/oboe/src/fifo/FifoBuffer.cpp create mode 100644 src/third_party/oboe/src/fifo/FifoBuffer.h create mode 100644 src/third_party/oboe/src/fifo/FifoController.cpp create mode 100644 src/third_party/oboe/src/fifo/FifoController.h create mode 100644 src/third_party/oboe/src/fifo/FifoControllerBase.cpp create mode 100644 src/third_party/oboe/src/fifo/FifoControllerBase.h create mode 100644 src/third_party/oboe/src/fifo/FifoControllerIndirect.cpp create mode 100644 src/third_party/oboe/src/fifo/FifoControllerIndirect.h create mode 100644 src/third_party/oboe/src/flowgraph/ClipToRange.cpp create mode 100644 src/third_party/oboe/src/flowgraph/ClipToRange.h create mode 100644 src/third_party/oboe/src/flowgraph/FlowGraphNode.cpp create mode 100644 src/third_party/oboe/src/flowgraph/FlowGraphNode.h create mode 100644 src/third_party/oboe/src/flowgraph/ManyToMultiConverter.cpp create mode 100644 src/third_party/oboe/src/flowgraph/ManyToMultiConverter.h create mode 100644 src/third_party/oboe/src/flowgraph/MonoToMultiConverter.cpp create mode 100644 src/third_party/oboe/src/flowgraph/MonoToMultiConverter.h create mode 100644 src/third_party/oboe/src/flowgraph/RampLinear.cpp create mode 100644 src/third_party/oboe/src/flowgraph/RampLinear.h create mode 100644 src/third_party/oboe/src/flowgraph/SampleRateConverter.cpp create mode 100644 src/third_party/oboe/src/flowgraph/SampleRateConverter.h create mode 100644 src/third_party/oboe/src/flowgraph/SinkFloat.cpp create mode 100644 src/third_party/oboe/src/flowgraph/SinkFloat.h create mode 100644 src/third_party/oboe/src/flowgraph/SinkI16.cpp create mode 100644 src/third_party/oboe/src/flowgraph/SinkI16.h create mode 100644 src/third_party/oboe/src/flowgraph/SinkI24.cpp create mode 100644 src/third_party/oboe/src/flowgraph/SinkI24.h create mode 100644 src/third_party/oboe/src/flowgraph/SourceFloat.cpp create mode 100644 src/third_party/oboe/src/flowgraph/SourceFloat.h create mode 100644 src/third_party/oboe/src/flowgraph/SourceI16.cpp create mode 100644 src/third_party/oboe/src/flowgraph/SourceI16.h create mode 100644 src/third_party/oboe/src/flowgraph/SourceI24.cpp create mode 100644 src/third_party/oboe/src/flowgraph/SourceI24.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/HyperbolicCosineWindow.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/KaiserWindow.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/LinearResampler.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/LinearResampler.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/README.md create mode 100644 src/third_party/oboe/src/flowgraph/resampler/SincResampler.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/SincResampler.h create mode 100644 src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.cpp create mode 100644 src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.h create mode 100644 src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.cpp create mode 100644 src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.h create mode 100644 src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.cpp create mode 100644 src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.h create mode 100644 src/third_party/oboe/src/opensles/AudioStreamBuffered.cpp create mode 100644 src/third_party/oboe/src/opensles/AudioStreamBuffered.h create mode 100644 src/third_party/oboe/src/opensles/AudioStreamOpenSLES.cpp create mode 100644 src/third_party/oboe/src/opensles/AudioStreamOpenSLES.h create mode 100644 src/third_party/oboe/src/opensles/EngineOpenSLES.cpp create mode 100644 src/third_party/oboe/src/opensles/EngineOpenSLES.h create mode 100644 src/third_party/oboe/src/opensles/OpenSLESUtilities.cpp create mode 100644 src/third_party/oboe/src/opensles/OpenSLESUtilities.h create mode 100644 src/third_party/oboe/src/opensles/OutputMixerOpenSLES.cpp create mode 100644 src/third_party/oboe/src/opensles/OutputMixerOpenSLES.h create mode 100644 src/third_party/r8b/CDSPBlockConvolver.h create mode 100644 src/third_party/r8b/CDSPFIRFilter.h create mode 100644 src/third_party/r8b/CDSPFracInterpolator.h create mode 100644 src/third_party/r8b/CDSPHBDownsampler.h create mode 100644 src/third_party/r8b/CDSPHBUpsampler.h create mode 100644 src/third_party/r8b/CDSPProcessor.h create mode 100644 src/third_party/r8b/CDSPRealFFT.h create mode 100644 src/third_party/r8b/CDSPResampler.h create mode 100644 src/third_party/r8b/CDSPSincFilterGen.h create mode 100644 src/third_party/r8b/pffft.cpp create mode 100644 src/third_party/r8b/pffft.h create mode 100644 src/third_party/r8b/r8bbase.cpp create mode 100644 src/third_party/r8b/r8bbase.h create mode 100644 src/third_party/r8b/r8bconf.h create mode 100644 src/third_party/stb/stb_image.h create mode 100644 src/third_party/stb/stb_truetype.h create mode 100644 src/third_party/texture_compressor/dxt_encoder.cc create mode 100644 src/third_party/texture_compressor/dxt_encoder.h create mode 100644 src/third_party/texture_compressor/dxt_encoder_implementation_autogen.h create mode 100644 src/third_party/texture_compressor/dxt_encoder_internals.cc create mode 100644 src/third_party/texture_compressor/dxt_encoder_internals.h create mode 100644 src/third_party/texture_compressor/dxt_encoder_neon.cc create mode 100644 src/third_party/texture_compressor/dxt_encoder_neon.h create mode 100644 src/third_party/texture_compressor/texture_compressor.cc create mode 100644 src/third_party/texture_compressor/texture_compressor.h create mode 100644 src/third_party/texture_compressor/texture_compressor_etc1.cc create mode 100644 src/third_party/texture_compressor/texture_compressor_etc1.h create mode 100644 src/third_party/texture_compressor/texture_compressor_etc1_neon.cc create mode 100644 src/third_party/texture_compressor/texture_compressor_etc1_neon.h diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9add192 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.vscode +build/android/.gradle +build/android/app/.cxx +build/android/app/build +build/android/build +build/linux/gltest_x86_64_debug +build/linux/gltest_x86_64_release +build/linux/obj diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e1f425d --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Attila Uygun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..10ab2d9 --- /dev/null +++ b/README.md @@ -0,0 +1,17 @@ +A simple, cross-platform, multi-threaded 2D game engine with OpenGL renderer written in modern +C++. Supports Linux and Android platforms. + +Build for Linux (gcc or clang): + +cd build/linux +make + +Build for Android: + +cd build/android +./gradlew :app:assembleRelease + +Build for Android and install (debug): + +cd build/android +./gradlew :app:installDebug diff --git a/assets/PixelCaps!.ttf b/assets/PixelCaps!.ttf new file mode 100644 index 0000000000000000000000000000000000000000..298733df1ef7fa41271e7f86c1725ade7bb350f4 GIT binary patch literal 13376 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 Gng0Q7{f(;A~!QBZV*dW0n1PLTaAh^Te?m9?t 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 z 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≪v{MA1LffsPxxp>x?`LYR0?@)UOnq*IUMv zBashjJ{XXAeErD)zmoV)awM|JSdIlipw6YEeJ$F{$86ZN*`bL1D3_qR{UN2;l5~G> zDt3%u$>RqX7U9xoY5gnQawH7W54F@VyYTXC-Z)GBM9S_d{mU!SGb?9rX~$s(Q<030 zE`1?TUKxGG{-ic$5~o~(-(={EHfOEf%oh)_K*vu-L)B|P<-dNzuA|4NR<;-7wF5YA zDJ2c*BBl4S;1J-iw4$aM;*eaAfb+HuG+})a4YrZDm%H^$1@p#Bj8aAM*?C30vO}w` zfNp4|!B*!!Td@B{j~LCbfvE3I2tm3tKza4VgwvvIOX#Y7v2@Z_dqx`PW*hg&QC#HB zAg+_jmflVv>&x=)O4m&UOY9t|iS1!}TxXH{_k)N|G6526_TtZ+z$FsZlF7`Z5K7Q=ZGJ1kd6_mKyEY)`U;ZA}9goF%pyFHfvmC5n|q;q5cV>CXw z?5R~_$koK0oPou`Z$|BWCC53~bFW^_{-b+b-ll!@b*xBALOSH=ri#o7Z%?Y!YAz+6 z4E!Ny&Le4bC~$H$AmPSZ_@a7I-xzx2Ps;}lVv9z8MR%MVLVk64fq8q;MoWj{ zNZ`g$H8s$29`~E9jlyxC7)X2fJ~X0DFts#5%||~>7?HHCWt#~!&hk<@_i-Itx-l0~ z;K`Y|ZCz*+nmSodR>D1tp|R)2nxq97;9J}S!|7jZl(uEgX!$;+#`rq5Ti`KV1`e^g zGfnoc6J<9#4s8NF*Ap+Ll`*21+rM~4llur&i|CZ*8+5iPqVXMi;9fa3j4hQ@)=G}r z*RN9^T{eq|88E=^d!Pxo?+WPPEA!ITY(IE$jvkRlDSbN}LWJ zQpC-~9UPAOwvah;fxTeAOs0eO#9;swxCG1-#)kIM|HOx;lYhYm(}wZV_n`)n4~l!+ z4>k6B;E~Yw4Vpoc5u+NL2k@XrUI`2Ww{K(-p7}3~J%Oa#-6rvhm9U*h0dBe_A_ITI zfiu_4G#3ocw4b1p_(YAfESHRZc0KAEq?xAfF!bkq1v-DPl!Ghrls~gr^ylZHpF?n6 z%s9|nys)znjvD=REBrbL9-F-4@;L>YEWi?OkuI*K81YNSFt+hZteNcPuU{|hcrey$ z_x#UYX<6ATs-%~idiHNf^hS%R>m~CTzqD$&6Yq)1(boiisz85U3z6r0uJnG~|Cx1u z>i$&hsXebMyu13*1$AP)+$d44ZYB%N2Mb!5_manw7x6ZL$G63&i9731T}#4x9>Hf6 zXM7wX+9f>?g;{mo_%YWkR?H0XiYQ7J{olBb<*gaI(33%*T>HZ^e883r@s3>^Sk10_ zOp!R4w(t|O1`e}~&9_+7ywm|BmZqHR01S^v*X0ol&jPud^vO_K=3JK_eW`~UvJyM- z(J@{t@v@T>!tQpii}MTL2ECqp1wGzdzT}eJy=!x^7WxbBntFh1x&>SV3ES@xxWqF@ zS}rsX)7O?Yz+*rpxmOY?raM0+F5KqhktOBENsB4XD?#jT}jOIYgc>$i>2)hKrCKk z+;+#5HZQiN7B6NPiRV<*Kkg&*)&FG?U8!aBn&?ICjJN$LJ^l>K-WfK56lo9@G>OGU zzIA>#L10hq_M$$m+;yg8Z2vg+{s!-zU)8SbSJu$4cX_D{^hb`UA_jGxAWe7Up|NVPJu z;&1x0gyK~K7dIs2_mZ++xXyx(9}#e|_}YjSRbw33mMtFKE?z1IRwB>P|XMZef}4o~K4ZW|hdZ+G4KHt6RDuOGUBUeCpXQX)WS@mv3Y2C0oS5T z?n9oS&pNILZs)aBnfGLe`Y+=)bjXsyiSP{TEsiY?VxZ)@3y zlF!24A5a0cine#N1ed~dOUq3zMq*o-+F%wj%`?FtB>hNI-N?=fOFza?~fkjM8e~Rv@CQm zU`9D}@#Ka+F3B695}tgc+9`3CenjEUzm9#%4)gEq_l#S-I2-c3r+B(@OQTiXA1zn6AAHJ9Ji&ROGUBM0as9R-hiBurEyxBukPK`pMlk6 zn>!xPX#-^?>0WX=UfIvg^#+{2r;IW;@M+G>VHmzh+I0$pWl))PB3aS`0WZ~P`B z<;VcZBNZ@oRC!|Q6F)(qL|5IinxBT_!#)dS5C;a=ljV3hZ!B28wKY+V@k0K{p(l{6 z$Q%mQluZbi7O*Fx*^|_|Yu=aHNR#(hoCsM{gb#pFXsirBO)A-b{({n*4mTO0=8VRp zb-(Kia;UP!XX*cgW=5OUZRkkwaTCff6NzmbfMqaoU~9( z3D1g?y`ZJ>XVs4HYQWa`sGJOZE0*03z1uGqUKxUxq543)N&S9d=k2nD&kB;cWCpBB z&%&>0qVvyx_(-ov55hWSB9iqKuN9e zLseSVtnlNu%duNU)&a?1kMBOWRX+G6wMopg=b^MGv%|vl=7=OC__2B>?_`l5r%A|r z30O!_u=5<4WBik3jjs$1*%JxtclsT>&AO*dPS{u`$u-fuwPjN)|JWF*-6JP?6UmF# z_M4+|Z?aTi$clFkA!g9p8#Lv8Cv88x0~dvb*xQJ?0hLQdD>!EnZ76EfdR(lIcaK;C zqy8%L%LxSy8ykdajJRE|_HJMaDP|tqud!vyG~}0=JTB-~yPd}$%THCCrAK!2zht6i zd}B=)?>=7#%{nkT96*?~>txJdif37_&>xG~o|8lSZ{KefleP=V*^yoMp|p^cgNA(S zzr0&*fG-T8!y0ObMKNL`v9WBB=H}~8uc%ZxVt7~LX*OmOG>qj99lf5hus~aC!4*t+ zG$g+r!c#PXcRI104=F7B#@u?&(8S=z*8uQKL9EMB+#)%$Ba0&AP=23t^JUUFt&ppr z5_|Hze|?xh958r!ozI!&3p1%h#$En^4hu@}^cW!8ypH=^^qlVVQ{cbB%d5k3Tmc@b zj9dDA5_67aoEVJ67amkS*EkDjH6`yL63Aq*FElT{{BHMQ9eGSW22cNMyE7|6WXzpB z{0MDG%|qxJnwzG~Q8j4&R{h8jU|CO9Ku=AlX(ONXLdowWARnt<=cP#q7&JhT`vo|A zi|l=P*3k&8NVQ3;EjHHW&Sli>T#}79LdVJYTotFsCj!0h*`S`Q`vS?==YJ1kHi%A` zEh>X81e2isba>HgX$w)}aa+0puo-kpEolqH6Z%{K$1dM113D$E1T6ttQn97;=h|B| zdl|)tb^Lb$Sj~&$9-lPV^C}s=)P@T+~`L7 z@s&=|)0i7^VZKy5#2vD5bh>tbC!mV`EyDe3RbZify9s9ja77Yzn0TqF{_L@6ZNo7P zuff*f`G}%KS2qC+s-@IDRB^AC=NK1bOL+G1YtOqG8pC>_82fgW?rH{fbq}>JEn{^R zr9|*je+R6yuNF>FGkc0bNlecqF7V>92Xl`E2g$baL~z4t#WMHQvTCM2Ylvo9yx027 z`5;9EmW#oL#06F4TL|Vp8&rdWm=1YDPI;0sR$eTUf+=zqMIU#H)*_AHz5}y2%q?=c zwbtzx5SEN@{$#!D7xgbO+*dXqb}r;Z>v@uY%#9Vk4^tKrwr*e}nasuJGf%Dx8}JCh zS%>WR`WRC4r{`!AHH7W&lFYT zSl#bTT&yQuG_KpN*fxYV`+P?v2t9n)S>Ef_PVC^rxK0hAtG$<+TQ~vY{QgSwJ9IeQ zSM&FnHp%OrNn?smw(IYt!0e{L&}j#eKjQu3NUs97%rKaRQb`vmVC~%hD~Xnc;`@%; z1wT}$c;3D&Y;enP@uq!OJ@`?^&-?pxvfq2`X%mHo@B?-SVx<_%``45V>D$}hHoYh7 zhuZb)dW>twIRwx4iNY5U3o+>pmboajj2|$T>c!WejsnBfIGT?CD}WE6?R4Ik$T0}8 zsfINvRjJ~&fHe_+6>{)Qnj$|!NUuTBW-}s~)zixUv$RZRB^w54j3f1wENqoX2&+dt zxsK{zr&Tsz@fRBfC6K7!6o0p#ac>fN2sjp#J8bCf%fU$e#Mn>-`*!J|LPVH;6!$5&{(@So&-~UE*B& z=KH8XCV!bGcW9%+L&ph^ci)ySTdgh}@Vt3Fqp#Y&dsg#G+Dd@m`cCA;?U4Fk5a0b` zdd;wEziPM@ER#PSdC%Hr*-iWG4Tsyg*?n+b!JYIBMkEaj88z$c>kr+9uE*!44W9UG z@sQc`ls_BKhg%=bzZ zL(&Xkl)&Pb85G-7rw6SE=-x#TR5PL8^b7swaq5JkC5YyE(c-|80m2skm4Dn-?(*tm z#{{!_5wQc@my{v1OoIRM@Ij{*F24K`JL&bY#hqD64qGf($W53hdnj<9V{|$mtk1-( z8SaX%YmOhfY%)K@dl0M_Ez#*{xBCb*A~n)SCRKeBD%z<0P3&aGaxHICq!IXi3}`Vd z-a3a2Sas?aba){37&^;!V`dY`nmVbswh%w5zj(bD1*9jYcy8i-KwYlj(Y)9S}_^l=2 z18m`lD*9_{GWh43&2+_(3NzRbJT!p#IEk#nz}^+H2=K5_j}R4t{IAP3xCE`sbeK$u6twLZ-+5`Ofyfp{UlH@_UcF zy=@!fUB>*Hu|kz&-MVj~miZl752emn#XRb}O>76GXg>vAtuv45Gi84G4gjw;8{_^+nIri_MCegHM~m36+a?Sc+%^mSgvA z^Q5~iKS)_8kPtUKuNr;;4BH2vu#fVHI_mb(R01qiC^jb9T-n`Q8j|i@pqMNMSJ9oE zY3Jm=JX7x1q>%+5(tyUl)7jDpELl9Au3j|-T5M_D`DZI>D??Z+1UEvR@_yh>{!Esx z?Wa^M>6QBiMY*cfpx~)M6-yKT9q}##b>hu=&I5rbt~qg^;JC*=ujqj5ViNvfM%#0r zrHEp$&#LoZAN^kMAE=;1YOdkWIDjpdT`UkIwZ<0%_4L|C@|vcnk3UD%j=0}2?!@0AY9sJ+vK2@rSiecy|3Hn#U? zPkA8p=ci#$IFH^7t7U6SA9HTEl`LeWA0ZkH_buRMq}+~<4ZP7={5dQHgg}0|po;x3 z9hhb2lhA_3=5(Ex{Z&{0H5f55|8!4JAg1KHUtkq{mC`!0Z8&X782 zeM62K<%?o|%ri^QR$LT{->_@JG!`dl`Y`w%HcT@%(q0kArpKdXFe{~*k2GMf6PtvO zSsWpK6$j}YUC+c`lbpalf-{boWa2?G|7o1-7xoFE#%o6{CT3OpieceUcRP7CS5&%b zX4mXZ_BLqB%Y(4ip+532Jz+x;#JLd5nomesg}uk&5mYFx4#IC{Twg@{h4K7e;Fgg4 zgJFgzVmVG|Y5toY@jKOnG$+SOT9~=(06{NQXoKdh1}Yijd{=||H4E?EH`)tJ*dpR> zmSQ7r?h1+vO#Rh1?eQ{0gcare=^%}YPEN;mgET`tj35A&4 zZ0Z|RX09p0y2aki_oo}n=%xYzz2@sK){)0<0?rRz4IL`U$GbXxK)R-y1^##EancFd z<=&41&T3z>6fvL4sTzON;uk)td_+l(B)V(j7djKPE<|epwj0g2IxuRMowf>r{PQER zYIf(@6ItxVDpa=UKYI=OEu82B)I$^OA$EM$yAZ`Y{#%4^o_6yuPkRaziaJkAjR7}3 z)sTUcvXu>x75w5u_i4T0tW$X378?%Ji~ETGdg@kakF%`6{^FA^C}aDp9n;G4md&R) zFCL>gbNn|3%}5eyPh?i4h)j1W{6bSMLY&}tFak4;k0~nGae0g+1ls$_U=^M~GD(%3 z*0)HFp(Q6ot6!!42Mq&Kf6l*ZyU(5~dwX*p_JyN-H!EyKR?sQOi?sS8v+owe&27=y z1}e%+9~I?Zr2_p6PeUneY*2rjb$p4wq)r*a!xghGNA6rUA1l1Bs28lmG78Mdwe&sn z(r(M|xu0~11Rb9HGA-Zwzf6{IPKvhg8dKksI!Sj70{xG&4I5l5^a`~djoPrr&^*Gq z{Xt)I=)d2QF@M`A+*_*Ccn6a~^h00f>|P0PzgR`i?g!p}X?FLr_x0$t+T_n}q37$J zpmi+eh4y14<-$*>77_q5KzzGSz0GQWIyK_c2ozA#JFJUBY{8OeXxq}34qfwZC4>{! znilTISZ0hYyqS<qLsU2jQ*@QmSR{>Nd;rG3V{}9=>HVe=jxMpe=L@zBxxy{(vKH%&QnRpozH;M+bpv843BGNar+1Qzjnpamc}UJ1#D86hoS4Y_l)y!VGMAreCCXkUT@`*#~m+pv=c1@$gJUC zI=Rg8Zc@1_-j?ucoBEHA(rBvORA<9FO+$so9LA%a)noDfMRISY@pZAd!m)YWF@tJ|6vPV$ERPXgOM2zT_Y&F0el&TP>2YG31td zXj!qeuHFF$M}U}fV~qiaec<^Fo%~YRc-%(qI>EM%ysUz8>lG4V*o5@PrH2m4AHjFR zXUO{GW~<_x^aI@~2W31*2$xoE+%)s>ty*f>P?XNw2pP7T&5gFGH%O^BKe^mzPH%%e zWB!$=$uh(KD!GSPQwrJUfJs<+#F@KLE$R=e!EMgwR7EU?jnVydn)n#R4=bB(B`GVp zE3ba_5u~ALl`-1jVY{um3c^0T&532q0cZAv(q|Iq@9ZTf&>Z8q_W?t$kF41Js1mNQxF<*l+tFEtBViW_emlF_`JCGuR(lslfWA4HZPF(95| zV+4n)d{!M;isv|rm-GJ#|*TC}nnuMJ5@!(kV)y&y1UkWhbl%|(4H zDrY}@X)t4I&=1GS;a3r3=*Qj@dB{3@LWSI05uEBSFmWStN<0%XLsYi5Hy`qlvndBA3Iv63M1j?!G1nUP8@2L$kcFVIN;N z!D?&skD#1grt!_VF!)(iMDNcK+gbOXe2_HMA*F`)$CF&3@Rp{I2Onm3t0Jm7Cq#u94IEri0O&yNz1pBYZHmEz$Kt z%yCcG2xET3ZdTvzB+`d27cGDE9lvuVTkDa=_IVGf1p;W?UB!52kjPA7P7mY0VEOe) zRovgt{KJ7X-RLmmAr}||E2X?~wt;^7Jj~Y8(vgJlOizrJS~1GaIG;BIY6Rj@oQx1U<6ZP{V19?FB9|qc&K0KShUN`v#zWnV*mwx*hY*ud2xw{5T|Rr@&4eIzUFG_pS%de+ zPFQS^;g1%lx}!~ABvTTQ*9Le4Wfy9=H)-A#(EOr;{p>0w&G=*gt1qPv8N>*Tqr;1m zLro-<2~?nGA!60Np`F^!M+p(?ZSo}{N`g_a^TT5nXC*8T5# zq{JVGOesz`IF9N{b^v-M#z&MJ--M=libZXl82Nj^vU6{wXI>U z1dev3s9#AAGECW(>0??t#z;7F{)&KxOLcmFLWk=dmG%!Huob#@BuD>@5Jygh`AwmU zPODjes^W%Qnw;ym2pK|F1E+)nk2!|S|UgOO)0iQfa($H zE1EcHZ5)1}%UX#m!+WzT(zFQ|q);@00z-Z{P9B0u~|pZp?dXhhi!NOFq* zU$VtVQl+wydil`rwCwYOyg+uh?O5?{s)eQJn1*%D#EVYSNySrc)jrM1+tF+}x^SUwO zl6nN#G=n8Hd@ASIWX(IFoaZaXHs(0uQn94dE@q;WHVAx;9v35@Nh<`C;5^M z&(W4#IP@6s&&@#e6e<#ROwtbqQFjdv$G*k4kB^Dv?-Ne>Z+MK#Yy{mXpsV$5qw8QL zHZ#v=N>dGaChsKaibM*P2D4{Wft(tY_CJT?>{dN(G<;EU2NpOX?#$X&KSy zB%8C)FsJG3q^l{xxTt02UJ~35j~n-Ocj*;k%W#^6duao*I;SL|=$+0tk1`GkKANgr zec$seJ2v5j4-L(3lYIIFs$tzI>uMxI>1zEW#?i+EA6hv&4cFn~<$}*FQm#@vF?kf> zjGm1nTPz}i`AYm=$_~RiqRL$aq)T6;&I5f0ZJvq(!?3_=&rkOzqif~VYHT<{BL51R z0^haIB7WLiz+s!TEGyAAb0CoCqGw&O;m%}WQ4onBV$USzp@FwOkB3QI@n%l9SbNu# zjPYFRQURfVy=2{V6~OHuuM8Q^o}p|(H}sd7Ta8szNTzBEb$yIBavzinEG4ac<6v{p zD$39?_h$%}CIVK%Ube`ftuq9JJQKRh_m*&RH<$Llw8Yi&AOfYNr)s5lm9gV(l!gaK z94%T6LT9{qpm~XYff?#sR7bK9L41EURJUlc23V3t|7e0lmJZVao<-T*9S4@t}v%3?w0@U7!gR@eC@6QBbtd&oX0_WT5p&@g7(dcOTqTz z5wfgG->Tq4u;;HE|BH%SMw}%pOJYxUJ>ocNN%X`*({M9LVT>e*&Qq*xKsXuoE}qkN}*;+*g^vVW1o z=MgAtQ>zO515}qxU0>dTLDio_ECef7?2L{xYW7bx!cZ459;~I6@hc))IlJrIy3-^Ini|D^%kYxuD-moHu4#2 zVe_mCN%v=2(xM?L4g4CCKyYeWxl(VVlLR@=i)>^6eoa6la3uIOG@<5RAeV^@cl`ZD zN@GjD(;D43^bDOd!j1)%v*9DXxY@RCxfe^)>oOT|!FT&Kiv9gc6K;r3g3@T%V22yT zU;Al5YEn;yHF&04x52FLt9+siy$U!32bXGxQns2%%JU4(LPVg9!i z;^X`Msqg*3G0riIsTR^HxgGDFVoJvC!7KXW5D_~aBxTb<3`p~wS&68_wXR$oyYirWe^|6TE+?v#Hg#y7jm0PBtC|gyZ#Ui2_1b5|R5c z%nl#r&~MSUUhrEx)5L$qf!g~ddTaHqt}KZN^xBlshAl>X@IQ?V=G|8ZGbHZ~iV?%rgwC-cR1UVlyEF(<|4BTX>iytuIO%_7-W$;#cbeB+D*|LI!r>-J7L3+Q(!3qXODojq;xvs0IQB2a zW{aQF7z`h!nWtz;SKXep0$FNj5LVo~`r#FcE&8q3F5{)XxIq^^6qh`LUOZYsk+A;X zvBNabZu{$%KC^!sH?CUTsn@Iz$t(I#8To5-(X;q5oE84!0GAcR;bz5;*Dp?6k#~2^ zb5V2KGV8!$?aJLI`W3LZVEp#?=To)S(> z_uzqoE^8g9>*IHdNp0HmZ@wJ>Ckfr_YKJK1j^)k9r-u8nj(CLS-64A)4a}}jr?aNf z>`B`P1e=81hc8yyL`n@UMhSZrHCCg{el?t~8U|+x$UZR`<~JT?&t>ypBY9otaY9(#J0GUOi=JpS zm`>fC{lN4MZ!Qg3NFCTipZsrmx{p98;jJUw6oIlS_ zxix9Ek`yWn$NT9vt<`7ow}M#)FY}VzCjS}Vr)wv?2<|<{;AyKdTFp}GXDN;7y*rg! z3|*5o^hpj@%OAMH_2sFGLr^j+L1qprSx?>Rc)6P%0W_FJI942CZV(~tUpMym0tL+b zpE$Fms@-=3U6&(pCYhy^Wz#@yz_0hL-W*nFow^bh(}zGpDRQZQ#8qJM{dWAKFe_{} z z-k@vac0QQ%acAcXUBiN1lAfwX+?kdeZWgn@uxRbK^`D)d+=X@2#I<>D>c6zD{x$DN z40`uAz5&E?ej`Dyh5v{JOfrrlZisIJ)Slm8x)_i_VMiNbSbgL`UwsOI|CuD1PKTz7 ztvE+kkOwzd0Dj9}ql@@F$u|Vh{6KN*-)g-|3B+}W87y#Lsl%VLdQki=0`3394t|!} z7-;JlCOzgWq077Z(oRS5#>clQG27|Cb}JXD*X6v0r7Q95Gw~{k1j7#3%=H8>mp zUm_e5F#nGT$G@Juig|Q~yTdr}wPkGxk?{S`7s9Z59C&c?i{S|V_tpO~$bbL%|Dk#Z z+5hu?|5t3{@1OtQPhkEZ2@!PapK{Okuf8sgwbAtv@HmukWCsbsvj5|~W%~+I(RR@G zvN|6Rl4#bEHb$@S?@y&Wg2q5y<5kYyF5^a}Vw5WD!_uY2 zR+!KNP8m(Djp`!M%l3k*C2nmGl|`(f8> zFtF-mD-(%;VYm;!$dHo7<53Jdl7I1VWqkH^HUG3<@PbAG3&YCLiT|(3dJmiZU!&+| zs1#5#`b1aDIPy$N>$y5kU~N;IFtdcIz;KzR1B^ z2XBaagGNj>$xoS+Q{2qwxBaePkxWU+Ocq{Kwa^u;^*am3M|R-^z|~^9*xg-Zd3o{* z{AK|B_-{mvPd2sZPiN?!j)*7y1~&dS=8Ns<)1(z+BmDtgj?UieI0n4(UiS?foo6#X z=;qIdXA`bqk|wa(-Ccgi$X6cHsQ$JA&&|$n_}VP!%O~*O4%1Q(+x4pd2aiB-zy6`U zreA;1Cs`+%WGs&9_e*~LKAPd>8uCc=6zdg$PPepqx&i4cu6Twpy-beW|Nm(xhO8Yd zVwe2)DSC_r?7pe>-t!(Bp#AjS1OU~6q4-g!CjgvRQvi79tOx+Vop{I-09*`%0I;hT zT#V6HjBQJ|*j~Ee)HEo*KP@S&5`R3wr2FkA78$|jed^U`&935RqOQ9 zprzxCYOFhDJ#1gKwp?+DaObAOSN$c^9RS$HcYMUIF?ntz?Oq*M#Pe$V4<4=7ZrKB% z-5zdNqXGZ`0001h`GqjVEh+gUb|(S=006+y3IHJP*0001xA(`gi0000002pWi zK;qM<{!lIH>6(7acfUKW*EsTPFR((w3zk_LAUmsy_YD7hVM8&tNRL zw(nLx_{yV>WiOA9rQ4@|;ScENXMcPJy+;^~vG+g!%_wQBF+TG6;%Wb7Z{PS2S9-RW zC5FZ)`Es{B&bsB|^z}o=tisrUbceg^5(_M?BfiEf{G=!NW(+g8)<6=U1e&-_+^ z#*dYo&k?cQmP93>Q?;J+iJLwrLZ<}uLoa#nNY6(r(`!d)f#jrTUf^w!`J>mqhW+tj zuSv}-*lC}CUSS?dM4a-TxfNI3@sZ!YB*$!^%ll6}=xpzK&wu3$(tkJ8wuTR%{j$dT zPp++k%9c$9OsDJP-L*Tve#^C+ezIfPdp`H4q;rf;k{)BgUJRUc3U zAoZ*0mrMfy0L&{Fh@7BB5rd@0yrr`HOb12W4~95&^XcFd|8|zh0{v=|mar&y4=)!6 zfLR(F64{t2j1RYBfCt5KBJzR&#s>fZvmHy;i#Kk$|5A3eZoK@BwP&$*I%j1S0p|-; z0nC#CaOBR<^;Y>?{`p_JKK|1`x={Sd5r4+Yn#q6cJ$i}JYs~k)d71s2bh1R7%Fg=t zt=qZz#y|fhE)E^|EAktAB+T8L`GVcvQXmODXtS&DK08Merf*W&k(#H(?}lT4KC*t< z`NHkQ%CYTi8PkjQ5KeHy3Vv~M>m45}Cw?ZtsmR=1ch{Nhh<#N@=5ubnTFA$7KE~ej z+}Dr%_~=%`nLh!*&RHVr5^QoODC%uqS;|OdwCTV1KKJOx_~Uh7HS0xxCCbtOXWep% zOkwKaCDvm@1^qUl$(24=5elA;jU-ouimkJeIJ{S@J1N;_`iKhcw#{Mcez#RLoa#vw8ui( zk$@6SI2KS|pJptO-R)cySB{M!{nl|wxATx2k-+>+D#k}h@2JM5Jd5G98O;ezI1PYG z&?zBSCqtP6yv~)NnNp4} zB0zz3AdA*F)c)p}s*`Nsug8N5_64gWFl53&Km#cx2PPEsBw{M{Yuob4LYrw&9lHi7w~KQCN+*AmU9&OgNL3Vpu_;KssiM?luVWQF)3uMhZ)no zR<2+B8Fu)p)6zM=ql3rT8yght+wr09iz{4H6a}RC((=ZKw0V$HOX)i~=LH;<8}-Ld zR2szi^W2oRX&7?ypFq{~tgP2^?U3_Qz)k0^~X_VnM;9 z{Bsc%lkv5{Ebhob@1fTvus81gIbU zr-4Rl=vYkI;6~(v8Y2>90aYkRL&<0hDIhX_%7Pvf?5F^WC+P?Z#?r@Q4pX9WCJ@U2 z-3I+$HceQP7t|e6I;(8A0;Fq8Y}O+#DA<(#e&OZ&(Tc(yc{xs>7h)Vy@E@c;iAntJ zI6_zK1^e>4t{NBa_^@k`6x))-A0J%JceHVa#s>s|S{J%r->pzwktL1F ztsesl_DnBN$$=zgCEJsr7!y|tDg_$ui2`om=?MaYdEzBde(h)Cq>2svRXa1KUpeN? zQ8ab}o8V$p#Wy9LW5s|-DAit80$hF986*OXItFTGlnYX;ZHAZ1oXMb{3q_*ij^4wW zi*Q6mke|5OX}M>eVK7#KIs~T^ne>5DIfauiQ$$)0zK{a@q(G*t2C5`$N>Sz2#3Wt8 zP`uF{s=6idv;3N9TQP8}&Gm<3p|?{`e4fd;oQV|4f`QK|N!|wIa(x zr?XxsUB4-KvR$LQz@xnEtX-G;ceMo=7(S(7*H^C4z}f74IF1o6l0 z^)9+Uq^wI!WTGlNdG&o4`P_|h8nmrk@NWp3s^$>%-qI)E|Ms_NFQ?hxEZ<*|lhGm8 zFG~F(e;&2Z-&jI$+WrwO`dM}@FUZ8+WGI|T)0B-0rz*n@h~z|h-Bo{2M7x#RUiu`a zK(^fGU;Np((*YH-cCd)O<5{m9nF&+wBb3*2eh;UsTuZw~P6^v}SDnl9+2>HtF|ilC zy2LJ|eYOc@msR64C6a|*{UKlXUC(~)Q2Qftui?}}KfUoMM6T$Vvr5{a<`+4K#Ci@h zdK|L}k`@`GR?}nS@HLPA&#)fFk z=|*5>=1k5pX16|7O_;#nt`$-0gQ$|wl?yw}67hh7N@v|kUtb;@D@nG@{k(p~YF+Xl z(t7f_-+s7s-5FNYxmKvpl2C#E;j2z3o3kWV>pN9tWaA^?tdwi@93LfPL{KF`tk)b> zj;qFps>U%}W4c+N#(T+JFs%6E&##x|c4T*^1^j|ILzS$F%{&`%##FcjxwXRB&{QaW z=UeAyVnF&D-aE+OO9bSm_K-0+{}(q3_h`)gaicNWN!WGx`m4^<0*o&jXG0;eTq@S{ zmwXLw$3PZ%(t53Xl?0-tI9qCG*_4i_^Wa&xwCPCSe^~Pg%aLz6?v9b*QNkJ5lIt6> zMa3R!>jhO0xdAHA>g)3B&sFzcXm7u1#mSz3m2YD06SZaR=#}pz|6FtLxilYTRR9Ui zcXC`@d&fuEk4D5Fb0PrrbwTL;;&QPvN{=WB`d#PC0mw@hbVDXwbtiF#CzU;?6CvfV zwFp4(AEF|_?wz%hU$*l}Xhqr!{E5&-g#|r!hHhYe5qN&o1v->ZlfLUDzF zwx%kVmI`coolIkQl(j1a0WIX{ijOc@Y%`sj!>H{|jT|>Tw|1EHRzmhrb5+M#C**oN zQMT$-ZiUG|&d2~tJkaDia9*F*FD1y3=)zfvk4J60S&{|+RQ+gz&{&n`^NN7K?x{_} z86Px6GNLZG;ZWJX9v_?wNA$0`^GEfw zA3x$;S92cW#05%ti5*)yl`uA@sNhzVx@_>BV zNAp@|8-VcZywwa+-MZ2Bi&}?UU3`)oBQjASKMxNdKOc>r%AW7-MHe*5CIQovVu&Va+5>_zj|uz6+RxcusmH8*hng7OERGKR z-7JnvB;0bH-Nts%1Cysd+01ysw`K;F*`mpGa$Mw0!ilZu7;RBTmyPFwf0nDnP{F?v z#^mi@BAO{Pq~XtFOCYjRH8Wn)>mpp#bqmMntz3E@k^Yf_32pH2Xc0v=B30It`_0Ta zF?%}QOiWo9bde`?B>pJI9O!Y%gRzmX@66jyI00xOxG4#2;u=AYtVE&!N(e95F@cT@ zazc3@1-%ni1W3{aCJ1CUPKwP_R2489K2gCx=Y|wVYCR}vT53pkD;*C}QtgZE@41p& z6367+Cv=lQBLsAG+s0POQGz41c4*<4)(toK*W)8EjCn!7E#t2_KFpk`$A}pp>OBJe z=Z?4j2+a$G7@hG^SU+UdV``(Rc%l%1nqWQ)W|C6T({2}_btU$;AhoE1QdY=xDC|#U zWo3F>CJRP!{V?VG()MPLd1hDlGz_8VuCn-pQ98?SklD$uc|8xw^+1k=Fts%a(%h7w zCp0dlAT!!AhpL7#94aw$P;LvW$Vo4|w70jvJuvlnZdkeVeVFmxX)jWq%US&0N4<1(9TQP$N%Ha>|OhO(7JQkvZu3ae)Bg@iwB`g zpkj8)z8~_+(@aLw4fvTIfLRw|p=Yy~?Q#SSy4&vhygymmL9WGygeeg~4$^Qb-M>EC zL;+{h=fxL)^JWBw=9Y$*-?ATfj6aXX|n$!7yRe5IOPHQTUQ)k`o6cVJ&%4a z58IZ&zisb#vo?{4SoaX(lqrw_=cWJiS0vm>lJJWY?&hO6L1vxEGf_okS%O?8h6yyw zGHTgzO~MgvS~f6Mwr}Ysf{Z;O$Aa7wr85k2VxflCMRaMTIYuwCX zZqOf^%w?Gnpi9V&a3G)OiUC5F{F||0y3D&G01-F zg>&4WQ&r}!7lv%R!GDmO{|bN4UGG%!N4Asxh-eO@_vVS7v&yIqAL?xYwXPM)x~o%O znA=Rw86O2<%nABK*4E=AE^k6-{6McATG2pzOc8FbEi7~uLOK>>QMRd5kT_A+B+#7; znz(*Q;i@C6#&TOSRIdiTRY@kfgeY#k(Y^}wJSMBpWDvShH^%0!k5b#yb|{*wM3(6s zsRWDZ&m)NQtdBO^KvbCd$eFjynw>wTL~^OPWNV5LMzj}dEbx|&ZMINMVtS96(*-2A zbNiKy3cmm6zvR!nWCcYSm)-fdB%~al#(mVEO|KQQ!uzHTS#H@TfhB3^!S_J_N&H{-(UfTVn%Fr z@SioNiOETF0CHLw)NP@Uu3^qTv#;KFnWGXZQ6MfUHMDlCDD-_FrQKA|&s&4em{!{C_3Utgr zbD#UT#Cg6E1WW@!CbG)FM9-Vq_?+JN8s_n#uI-Kz^Tm`6{ySzJw;4A*J`z>U;*E`} z`Naq?W}GlBG+0}!@u4a-_4sfa0jeNOZ#C3g+eU5uki4@agG0R~!vz0!e8~CBY^Bj_ zulrBUX?nh)IZBE8X6+Gf(n}|D+bm|W&0<@{|pVmV~CtJiua2r5k1Q^{_o5UB91^ujmw&5-NYlz%&@Ga!x0soZk;9NpV2wl~y6^ zuWZ%v8)|iFG8TFrp4C`1LV$Wft~00Y>@mTZz8R@AiPLc#lG@yK2e~tg2?S#sq%+2B z(4YCL-bx#nG&8MU+k~nF5Lq?T$j(u;r##Z7|GJ`5+Qb|f?~D(%jU^MrOb?j2&iJ4) zkz^AYxhu?sGU*E*)0`!22ku0lsNq~cvRPY`mwHFgSCtk@7?HwmM`xeax?%+5P8KXy z9x|GpC}VtYxRv5hv29c;G@qkpXhNUjM1(IGnQTCPMfQw1Qquc4|~(w=!id&lH93@bikyyP~Jlk>hntRVzlU8bnH}83pDva`)0y_KCatd1%M^2M7UL+(AnRfl5)V4_WJZDqU zlrFhSC&kn^f<8ewGTDJYzI=buA&=baFWBo>#xyC$@l(2LrIQOli`~e(%V6$wr$wG5 z_9o4&=R#!Uw{&9QY7*4h=T_L#CC{U?V9Cl85l?E{N3>-|*ly0Mai%Ks%eG>;sPgc%>Sp0rDNnW_SqoP3zd z)dN1prl!nxnP$ypssc#&g`u@1(pi2JVP@H9UocKeB{t*d>{^aX$ED$7Mg(9Y)N@io zpQ9Bnp;tcVXV(fIE^5RnGL_TTS zGW8@8*$DwjVvoX>TWurRJBCg(2~)234jNNu{cuct`|h(@D;)<-E;ncTbSY1`PkF8z z#CMX8C%$w)dB)w=${n8UPV=r15PSW6-n4Rc8v9>QIL(`x#=qZ`k`isvlYU_*cH+ zt?Z8vdj&iDI#q#u{qN48Z43|qrh!!rXwkdRoN>A|C2H6bkYejidfuH z%BTta6}c|&%*nc(NAF)SYA~wIXINOE1d+>#wn)jNM4EKWS(0SKZfZ(60c3}coUy8? zh;-2I2me8ih;c^*C>xM=P>Q(V56-d@G`a8?)msKV`OddZ^+(xts)%%A)yQlQXEFE} zrCGF?>>4`VnGI*yc)F;aa>z>?Ng$MRtyOAMS}b%n$Js(*nm1(&S4QZv>%Co^zwEdOZ1soS_aAa~ zuiFSvk?n}e8Rb5;z*$Zk>yKQgc|#&Zu1%1spva!rsm%B%qIYS&$_mB45I}p~!)*L< zwWpl2sr37p5=w10HXUpFq!}Y<4BNaUy7a8}_RrGKV(_0|Gh6{V5#8)>-orWBNB62} zhb{kDN9S6RnI3h3tU6Y$yqPd>L?`aeoA0K5-D8WN#%zXJi|Z!)-|zUS8XI}>S*|Ur z87;R$fV6jhd?@*i>ptGp!qF2gY0YZLa$)A+#5RV_)(=_wF9kx=-$!kMj7rw>#90f9 zYlYg2-m&Y6Fv6I=o=uX4YbjqPZp@K@oT>oSdKP-?M-%LBXD)sGqVxk*G3wJ6XwI<$ za^l1V*<2J0VG}Qz?E|{r-T6u#$Joim5Tbg_P@+<9y-lzFKyfWCPiVH#%q^BuWRme) zq zIxhl1>~xE@q&il?e>9b_sFSd%NCp3r?aJVPa$<@C|4tlvi?ndU$g&GU4br%hxVRnW z1IpM4r_hQwILj$?vlz!Ov;YYkAX2cRt0_#mzX|^H*_%2gmYEtI>N3R4A7;XpieFok zI3oghzSI7wL)aUkR#WMoE-lxU7>P(&))cOoX>+e_#xioCu5<1wp+b`e`52ig^Q$%z zX4+~d%p6@arn_saGu20`3LrKO|6+B_pQ(xwYTFXm6}4{Jb6M6Pb{4nD&Q08U#mq;- zA$}x^Q6)f2Z_(k(-$WYHIgIx9j;Tg-@^iMDTuc~OYXa6)Q#QCUdusa$Zp!Dcs6+?| zG=bN`8g=PTS!1-dK^coTgLPlZqEFbUw1{W^gkc`^OB%nc-hVhuM1ab4sGxg z>dLcLz*%-oVCIvUjs$G$>maCqwB0a|53|0C#90TjO0T&YA8}C#pd)FG2w=uX+cF2? zQZh<5jOqC=JMOWG>qjRVC;Q7pOsKbWG2QFJpX1EQSRLYG0S^l+O62sqik*;0IAzVM zzlK?(TDm!AtdCosN>T)rmi2tIZ~0fKq!GMabgM6n9rTt61_D>SUDwQzQQ@F zDtk`H*@_u@JWW;tWIZ0z^ !32##1H*d@vzWG=tOLQe(Qi$VNJdhLe^xD8e!|p}JIHvC3xF50p4p-oW~6{hn@!1%T)T`aE&5&vha~)` zpA%Dp=6Yd|J&CL`Z^%F*Sf+qV#nRE|IrGnvcWul|7I5PWwU8ywtN0lp6Y!YQDZw&- zs8XxuDsrMMA54=JPbWt=_>Xcye=+!Hth`C1u*K#)BM|ANruLP$DWjGJSBXA_L6bw` z*|Ll|_Qprh&`B}BkeDWzhCr#G9lfoH7XqJ5u}-paR`vSMcyPy)=~d#22WAV7puNs* zYM4o91B8nhV@ZR3ka+u<-D^529Z!i3E~(r#XrpBGOIf+H z%5n-lk0l-f74`_KLB$Qn{@j!FUU}uAf;p9TzZ!qA^m*^w_q48pq#OL3xxTo!Jn1nv z1~!=GxwW3iJJa)%D*~8CnlwKp-o9Rz8*i3`>i1c<`Y}F?_x%K$NZ3u^k^- zWs9kR32kqTL~yr2N~)4XW4F8tlPU4}QI5lA95;VtXcqS+xl@4DwV&A7qbLTb zV;jxZG5L&^K4HTozm)I7>^NZ)v&d?hq}k%ktO@`tmhUK5jAdsv9_Xu$i2=d?E9hg0!ahRV3BG{7s`pVIjfPZz_{>^kE-J0 zf1Jw7m*xKd`!7?u=G5SC+4DG-b+L8>PUZb>8gCd_cmX6ah`TwIofGB?fIEi*ovEda=%(<;{{xVJ~f z*cHpF*wTJ)UvYvBe7>BHttv#0ptUrJJu=R$SQc7g%!qi(A8lKe5$p2KCd^m29)Fkj zC7yfPnWEzmEYqZPT{P}wq2>9&OTFwSv&YOuP5_^gHFvDIL5C38_~BN#3<@HEd409t z*^s<8RRPGp4}q+jVQQ}@WnNoj^qyDUbtb!i>PhcAAG~A_ee1D7q-`}HC~5@%v3r7f z;Y~{WF606e!2(O9vw1Nu0{EXxH3TddM2GD-zylWVMn7*Kf56^%oO@yC#z$i3u6pAmAMmT@r2g@ardr0Wl#%0O=hiav>+d_+@qS;u`%C)xWhGU# z^=rwMC_fhr-P$2yXZ?t^Jf*5+l#nZ|UrK|2ZBNOaHVZ$fxh%(pmCZOiIbC}wsIkrI zGC_<6nm&S#o2^MHzaoB3M`$*O@2W=9YF|Ie^SXbG851Y&^-e3S%XQ=YyU%x`kigCl zQ7-AUm`9!<@7bS+9w&1vlH8sEvJ_s?;Z{~*`TyB_6974`>RkBVs_LE@jrP@&Y+1H! zu-TV{1iwdCNPa?M5_X8ewropgOCW6FWqElaCV{Yn7s)GNOb9kR;e`Zvfg}V7OB@nx zELpN-t)pEU%}jSy-M{Wyx3=!;nVy;M>hF`sbGxUyy8E2FednGf8g`d&d>#4X?i(w) zkJ8n?cz?cf)7#V@w;>+ivFpQeeyO>zdv?$+D%B>Lf><{%iA^BL8}BDjOn`YZwT1DX-*$ z7no9ez@>5ZL=pNXv3{z%~Q8kwnV?dRf)ni5MlWBSr7ysyMldhhAW|JSc zwHJW(oXEr;s1v9@s`zYuXFs(T+J+ZqmETLx;_b-4%yA-R%73UJKwEss8wMr`&m9|X9{;Tm}%P~-EY^(8+v$DiVY753l!oWsYHhjbw zRB3gpFeP%9FhRD(nh{LK^0rF72B~s?Nh3?yxiXHE=HW5%ZYnUw>`;Y-_+i5&fUewr z*OEO8{J8R%{T)@Ol*aXDF8NQ6Ft=@@-Msa}HUh*tEt-p}y_aI9!*NPCXXPxwa@ zd)Us#Lvo#Mm*9!y&fYNxu*(?nCCKTib{#HETKrK!`B*~r_8oHV0;3g zPjp;EM&&?qtc5M9gY-{+Vn&)+uxx{jowvb!PBn=KVYN^H6Vo6k3)Uo=H4KDFQm6yb zRpw>0c~(F6=t?hSwh_RFo7lAJ&mqeX@@-l(oKiZgc3uYlJy~eEol_;XOx$KN(V1xoqjf>WDSm#oVQmC(Y5qvkhKOdgu?$)Ds|vFmA0yjvC{NbHRH#5`P6cd2!Z*o9 zKRSuIHU#kXCwBGp6GYh%oR+5(Ooq@@7ICaO&)XBIxLQdpQ{>+qMwXjDv?0J==UGlQ z6uWATWln2JoU66L4icm~%rOcFRz;aaazK_X8{2YxgjwTSQOJNiab~$L&Q;!6^Z8h= zA5Bx5Pwq4A1et-QkEEAIe4v$FE2whoXNmj)IcrzoC#!JS*BdW@rsy|%J+Fq`eO2{K zd9v!mJ{3YFWRl2dpqT zS4CnZoia9&Fti2d$Xh1@HP>k)07>i^+*HXymfVY1e2x;bWiuT3&1UlTPhYl?X{!B- zjjttN-}g5np5|BAzJTb{GG-+VY6f4c!jW&|v{p(Z>_+SkJV<G*1cGCliaOexGKBk zQ-{8+{>qe*nzo$3T*Jl@cn_0RFq)*%JeC*-To#`__!-l~$Ae~mZj)|q zUX*O@O`RSqrme0R4|VJ&v5gEha+&8RE&})SA)C`;k|Iv2pLg&l94;TevP> zu;vA(+E2}`S^H$I2S<2>d@Fh#zv`FQzl?lx_swQp^~)NfWgmyjvAEPKIRhx(pGs3;|phGB({69EFdCuO0lX z;4Fvz6M{4Iz15F5_vf0S*KTI{0hgVdUb^OaIpcuD-W+AthSa!lt;f6~fId`_vUkbn z_I)bad&w2!|BL#An48QzCFiaGxBt_k4B%T|cuYKh!(~fJt=8X5$*g(+gekI^&h}_; z-U1l|U@bjC%S1H!2ojwnBw^B(o#m8NA$eJitA3gm7^2Bi-#L%`>xr-v@}GQ-GWJ-f zWTBa0$v}fyB^Yxxr3t3a=4_FFnXEC7{JVuZvdL&u#Q*6DIWV1pgyfQcX6?5EQanb&l-EG|8sjd@WFSnKdaXHp9t2M%mh79h z=L0L~qc-`E4`~{0Lm%_6$tNZ8ep??L3`pNsdz>P#DyJN}plwUc74P`ie-Y zW1WSlnwlA52H6cBX~v{5vGENuwc$Haekh~-OOejxL|qMKn~gKeH>lWdwLv1cnQb@; zTI#Re#@Z%0X@g9faFP_^tQz>ZaA|9!OfkVslK*7Ux$?%Dv=P8w2qoE?{$$?Yx{2p4 ze)^xDx6*pk#+mknvz)PvvfP!V33Ma^vo|nFoVJ zo{89JqY8wl?Aqz%kpE8iyg~k0%psmheS~JcORR%@A5Wb=l{+gTuWTaFH0f-w(I8_4 zV3uLUzF&~)`vGgghgj@cS(UkoS>fNsbCZ*hTl*?XjF6TYf`U|@9zS7l5m$s$$_S9o zbwdina4WUv?RE&j#>J~xasg(EO_H#`Sc!y;>P2EWs-AVbI2j}tD-nCc!h+63+fLzQ zTaWkE>a=$Ia+X}|?M|Nh&Z^9!aCsr!uM5j^P8fJafZ~a^diq$meFe*Up~QNaOP#QS z6Q5$8R8Jpi-J~}V_VUt7H{G1KV!dcWnKx%uUmtqzD%OQHNAU0zp@;6TA*IKQs5u|D zyhFd~#V>$-fMCS?1!9HsrGhh-@}l=%3mIcs*-5I$EnQu7ik z)PYVZq=(7A>v~%cS-F>#eM0_ZR>uXiWBv>DAgh_s)VH90J`Otg=yR}sCyfi)F;O%( zrS*XjfLUD5XZ;HgK<)B8sj@N=nIgMbdQ+gFIeLo%x;xO_5%pXLSUWm|uG5UR7VaZPGbgax?GMVtyLfXtIQ%a;|k> zJU0!3KD@cx#MviSw6FtodjRVDNkS_v;q6aqilHrB{d}@!HguWO%-LN)@^ryHKeDGb zwDj<+hCb}m|GD3EutW^|%sxX^#z+1lkUw5#Ypf*ZtZ8nhtEP{_`75=No15{1@lmv? znkxb_Q+@O^@=rf|oghjNP?;IO|4jnZ)u3A{$@tVO)?2Ysn3awGG^q4uA=u>jr>7{ki+1xn9;FZ8T0r?Fkb2XG9i8{+lBRC&~8R z+f6BsK-33r5RxzwNL;<{1*#R0x-(4TjLhkY*GVPTh)Zlaj;A`8L(Vi$iFQqtcJQo{ z5U#1$=|#`Ul0jagT*^&5|GMuK{Pri`LK@ApLT*7Y9{o&+#Xux#V3VdY!}dpRm))P2 zLuqlpq&{j^*3+y$h^TQMB=n(mQFCOh=!vOg30=t7x~goWBknI2O0`(Jj_$JksgFPY znY)SapCw^1Mf^w?LLbq1;E5{(QDR2Q>z3WyR3>;mHl&OM&^rlcIL=_MPt&x@a(&Qi&yu0&<*Ozu}e9xKgD{)gUN4 zKH7~JuAh^QxnF zw8m)oqV9WWl3OmVuS_J%;&V%x3QTzky!d*+lg{aN!HveoO^@8cHx?$wS(j?$MNDha zb$qtE?&3cZbC~Pa$tbzp$d-%V9?f5}T;0fmbz&~-b@|(}f6b;h8Cg7PCAcG9WnJAb-hk%>U7r3!gTw-zfBC|yks2O0z_&5T1mC#P1m8#ZgO3b2~!(L z%IYEeK0)H1lDLy-!<^IyH3PY3k;iC~eDt1NUDzj@RnUk0T4CGGb*w6yT4rWuH5U3% zn+4nIn%0LsKGLTJmD5KXU340np%m#X?YU`u(wZkvSP+t}&8pm?p zA~}EYx$W+Qf2Z`z+Ga&3B%5)15OmQelW|{5yp>nXc24>dYrSXnWlCPW>X%fXKOWCZ zsX1$DefhLLR}co#+%x$*Jh{ z)%7X>cRc5tk?fjc6I|p0K%&8R^RAmM4}juDqUgjqND)q|tUpNuK)jhwlg^Lr|Ln93 z0FPeRUs*XXylpj;r5NKhfY)E}T6tn=ZJPD+(F_|Ii`%iOgPDmj51&5rubpkWG8^d5 z4FLV1kBe?!E56w49`|lxm9Nzn3+tyJOh%7SM?aIo(#&Pg$HqkSv^gcv)=_TR{^a4W z&eBGTC3G7mN;h?80O+^zp>^|?pZ#PsU!9C*{DxSdxqeQb{;`^G0%N(>R%=1ZAN{1N zUP5M9FD-Rat@&!({r`{o%~K={e7QbE{oeP-$@tlys(Rw`WG8Wo5Xpw0=G=YYiwS}= z>!h)b0U#Nkk(|pkdff})-QT=fcw>mthS6N~{)hgG6jcG3>k0s|WaIUtb(Np~uL5A7 z{<9ndK%uK`jsajU$H%qjzCsuTY>zU}vEqKC&$f8+lf@|;KYiAva9{JZ=k|qe&b@U7 z)2E-E7yxEhAKCSTdxgk-os>C;=L`$Vzijc7i0chSdKxj;zwd{e#qS;09H6}_cQY0` zGyo*!Kf5}(;+)?QziYyanG;PpSB}({k(xUs=b~s1b;XJE5-|q6+CtLcd?xx_ANlXM z^sEkStmosPLp^k3EWCK-WjSk7@*sHm@Hy)4jyX8y>w&9u67s+BIZ0V)+-N62fQj)(g*!9fxP5!?Cu9i>{{LuEo(w0%V!-RZHZ2#9x+X<^gT=1rCicWwlWhB^-YjJKQNLk{J4z$6BYbHPo1hW0Kz!k5 z)Q+}H+-XTkW_A)^F*VV}`k;xiQ3ZYE>E;_hxj`CWBDwzDx|i?~L57Fc$#9uSFOqyV z)h;6tx^bd~!|wBp*j^RE9O;BsYQBo~Kq5W7P|Qc%KsFNTf``lqLs>8-bivz!3b7FG zo6$Tnr^J({1g><0xcPoU#dVb`&zPRC=qx5swAc61`eGT@BsSM7WQOm}q766yY&0T1 zlN0+y2ms%=ef!Z*Dg!`&h`Gti#Ga=rRix&tZmsQUy-GdEy%l$N4+Oud3F67y+~wSL z7mBdAwERb{P^W&7ds+GGl5?}UUf6nATwi8B19vT#>lP1!X_6q6UW5N~@XN%D#zdWm zVkq}@R~cz~T@M;|-l3%DWbruY#@w{1wnW`4PoI(p@_mC2%lP7&mA!o^#w1I zvqz<2fVW*`s2t(p0IEFZ!amVKC#X!#95$VWyoGy6I=NQm_60dsw)Wq+nEV6N>9j!! zxodKV@>YXGNl?OR5awHu>E6%``qf;2=p63^2Xo$>Zu}jJz1w`qofbXsK{#^+Zsel5 zW{JToQ#x*X4xH#^625Go{Gf`noiANr z^#Fj*@WEZ%a}QkJa>*Yj#(Ra%qqe5xgkjEqhEEgrnaIr;Ws%d+#Hz-tr5FkaJStyr>l=Dj1OBkd^3&Ba;~i14+VsW4D}pR&8J z#sQ_YxJ2nJt=)mBnB|(JfUVcw_GN`v^Lz}vT1j-jRrPhzGY?m4^_vxQE2s8e8<%C7 zsLCRDv*O|$CQ551lhd@{1klPT*(vqCv<_zTBseeAYVK!FCD5uGsG6h_R^L=zVD1*# znX5*#sBvlj#!K1t!!@O{ol1CM(84*F_0e}2u(2>lbCY9x(x&*P=BsY% z8mHnONnQ5<_~)ZP6=U$S6&EpkbIQ*#pSkE#k#U}<1w7C=LVQurIV4B@GvvJbN|D&_ z-1-oCaPqKu<#;0dbusQf@MZGKwZB@x0VcUDQw+!_}P!^6Ym zUr+tO)HC0$N?qk8N-?oWk-YBM_bGDK`WMYAi5!H@@>j3;K9$*io%ZF1zA@#RYQRq@R1z9lO}?^18y}B%0~fVE%Mds+3J!4WOU#H!uAMYU|;O zO>fT3<-IX?x1Ohx2F&c|>GkaYdEX~!Gc}9a=judrJ}6Y#vC_}5`;A=@pffqm-T27v zwOG#hNa7O@-9UbD^}~HTnWa559I@WB9$r7(O3A;Yz0MpOo-)Q_F;~j*@v+^vkdwir zG34u90RgMdez zl(PLi(A~AY)Ad8RP5VKm!Ktb zj|O}Gif1V$`s7Q;zi;ky=(+=@z2?@nx$k@D{?E72V6D^%XTNTyf9>w4TfMp|y&kBH z4u0v{mzclY_UOk=du;1*(_Xc0d1_F|-*@c0w^&wmq{GRuDx0oy$UCKZ^tz+=-_<~j z79gJVENXxF;TtoxQ`55S?na^g26<52&E?$Xh%zuY{q!BC)^^z{BQw=a#>KpZ`+pTY zmWuQs`djq%Zr<^(_IfZUYIC%qLl*6n?44Per)D}<%A7h6j*fb3eCRsc9Lu~>S&FMN z1hoCUoHokL_4;vRB!8!Z^0y5zt&C#xvA6nAw7;Y3GcFPt>8U73m*z|M{?Oh__M7)u zIf3be+L_Hwt@xiu^|7z=pfXT?pvdhFl(Rv)uw z05DOsG;d>2D zZPcp1b&E-L9AD3rp1A9nBWjSwdh6~+0fZ*GP&lHVZP_GSyi$hk`aE6TyNYY;Nevddu{)!Hlm3Vf2%96l=A9bZ0DddrhhO+QDWZ$C>J<8|%9s?*nI|ne+97 z74EABQ~pWfIa_-x+xdrHntu{TEd*X}$n5w_S@mC=UHtbJu%TT8chkRrvQ zxI3k|L($?6h2rk+6e|RGcPQ=@EAB4A-K7+FDDKJ4dCob{d++ymK9Jc-e(cHYwbm^C zuf``rSDWLcU||UJW5o9lcnI{eFY<}ljFYYHwOB^Dgq{Nf@M}S3-}e0$-^d~ieh*ro z>Se5m`?@Q75s?ME9d9|3>rj_mu;jiC7GWTMqf;W1IT+k{_?#V=^aAf1B>naDEo~K& zgo}Z32Kd*J=LnG0_9JkQmaW)V2)2wJp@Ew7TCoHn?!_b2Q^{CgTUw{;!W>-}!JIVW zAh=am1Ysd>Q#O}z_1ewHalzX+DPpd|0%=%&c-fm|*_K!S=FEWxVZH>VyV$WIkllxE{iotCW@rdEe^H?y_lD%QRz|*~gc27KQ`>+Sd11S#BHR8EmKwpuW zVO(JPuJzT`&H(Z$sdLS}qi8l5B}`hlM>=JC!ky>B`^_#JYf>^34!f&raG)~*kkr*u?TrLyzY zZPKFZ<-=jtZ_QhH%JMKNn#_^#9mu4 z^txLOr|mwodS5#lB7>?E*{k%({J#ohuISA28+aU{4Y*2&pTuG>5l0HQvmifYy~WJC zlKxy@>zABSXaF4fNYxrAB@lmw&s`|-aPBITGVP+U>j~rL>X^c_+YRnpk|LM6bLBn0 zm@kjWencdD5s7%+a=VA?bSM8$lI#`dtXzmP0yn@@m7wOpOu!z~a z%4I1YKcG`iet$c>N&f0hxfBSK7ZVaS7L|JXPGILJ&CS<|gKbHWJIU>;I%4fM_NLR8 z#jV~%i=MO1a{wuM*^5QndMKqe{7>Ehz0VRYfe4>-B^ceup2m<>lX7{MojUF12@tft z_Sxq@Gb;FHJK_n}E?J0SVx(JLdGC}g5$Ut-z4;v`+-iQY`Oe|pO~Hb9(ol(Ybh$-` zSLn?jh^x3sPk*A|r^2q+RC#bP*NbxoG4(8l@^*iJMvLF10x1rzqT6O_4$tcJ``l-! zb(vsm979eRR}}W#0_f{wn>Md4ST~Y;g#* zaAF{ehr!BA2sI7@B6T){=L;3x8Nw}u{2WW4il=mvrf*s(Gi+zQy&jJZBQ!r8XLjgP zT@r5AOUB`&F{jdm4_$d=W9@y}i+FX0){>;Y)U2-ofN>*t+kbSo2QE4F zO*I$?OjXb*p=wnL*@I7Y)X_u_#cub$wzW8HIF_6rmHAcs)O|=q1ObzFyM;FM`0uM& zI`}W%6=?2ypsldOa$4~jG~Ri+h?6Gd(2C11ln4xa*?Yr8@C40>&Y`HN3d7K=9{32T zmUIS*Gx86I>T1@O%Sa{3DcwrYe;u)9Z81nT-KCv8Zls+tN-zr@Q!vZLpiimOQ!Nnl zhUW|Xt(JnNPfxHLLDa}->rmB+7XRi=a6JRQI53Gg=jR?Ngf(J-)d&3URCJ{u06OIn}}K0%|r;xoNLqqoyX$93Dy@> zpo9R0@yn1GSQcaR5sy}~z6eiK+aKJ+{B+-kZQ4Q;nOaA9Q}>81BZuU>Y23Dry}HZ^ zYz;>viblWJHhn*ulmU96ax^H909B8=Z>(K)S^X8wr{g_o*9*F#OHFizg7ZQ zYD@})GZCruAjy{Zd-Hhr)#$sK?@^XZ=F3vcuRIq9L^>H)o_6^Y*w6#$PJZD1V*frKWU~S&8}kM8sjMv&)4z zkTw)u^XkY*uBGJp1u`?BiKD?mhlj}e8CJx4e>d@mn&pynm13%`JHb)4Ln&*%4b|Mp zsc_v1dWG8XrrLO|Kw45(@H0u)ORk}zO_#n+o!~x9*ZQ*Yo@g?+aJ8UTes6-z7z~^V zeX%W1WZj{My8}Lo<03f%kxDf@bL^jsN!}6c6f@yG>wTUInb4$ivht7Oq$fpZ=|^f0 zl}6lgd7#7X)-+Ki3}ai&Pm8uUU{YP(LdU@ zAYMGzv>!4TI3ls8TgC3!3Ui+yH|Xq8T)fMkRKVapdv&pi5z21e=RW8jJuQAPF0JsS zRHSW;>15ZL+qw?bkUG)mFgSA{IeES{QxXIOa|>|y+Ok^y;{L>lW5g#%+94jy)pTG* z?ayD;T*>yt|3hzhKcKRAkhnKYOX|7} z^3#D9{mdM{NIrk+T)^Z$6OSmC@7CgY5582U|NMXjkJ+lrD0bL{I*o*L`!a9c_cx;T zaWa!#>t$E10mayDA+t@ayCwK+HYT8rjJOyQDo!iY;He83>+ss`Rs#zW5R-lj|5a91 z?WfB7DlnUqB!faa4t)j4|GefK3G|?(DJLJrR3Nq=u|V$QY++cOb6ZT{Xd5ObUu{vY z>NFyg{aVKBM8rt+aYIK$XF?Tuie`y)T{Tgs-O{%6XKPqOr$Dl+=K}|Z{MU|eb?f(^ z&~^Q49I7_B7Mjn3-r4__iwD}!x40LHeZC11^&Dpti~F8(4sqo=`8;K~!Q^l!6eGG=MyWMpBj45-58)u-2{Q)E zbE3r9mWVUK`8U>PkI?0(DE)M1;hCj5$S6W_%fX`1?BZ{;X|(=O6({y*D4eJ4%72M{ zh=3o+ZauHgiA7J$LOMCzLj1i=W4B~>h_LHCQ<%r+gpNMF2`i83kyYAL3-o=KkqG7r zX-Z)!q3*<|p9Tw!QOQZzqWh|6rDCty1o31i zV&c{5%$U36HYQ+f9c|gSSjNo3hVe+L-B@gS`I5+dP8+WoTRj&eW`fOgPb}(tDEt90 zD0~?ubq=!>65FV467q-@m`WTR>#q8BN6<)$sNT4)18~KI&6xu9xe{5vokVt*ABim) zHl^PwQiD^}-MD%5Oh90j4*q>a6Lq;>)n{@#p*i_Pq2wWdSe?4ukGtC|Gx*#*3d$1! zT8QMW?|d-#V=2j`ugHUO$-|d8We6hrS&Z?Vemkd+owF0kfgwfYP5V<0BT3L(Q`9e|kOk?b z?mb>Vmmq6$``8RL%uPOMIjbdJ$gO&!tly89-veaZ6%!KFI@2Pp5Vh8O15HDdN-oRK z5p_0;2s`OO9#u|nnOA$hV<;F+vsaTBon_%?p*Pp*DI?+Qo6YxyHhpTc581i=s_?1*ry$-xPl!O=KJ&?v86nNb#8fvCjF@!qMGG5>z4PwXwewd ziS$_b+;X1eMU@^)&G6o0IZ)pItZ`Nt_9`DvB_Yr!MJ^DYqLV3L9CA~t6`-O$yLBl8 z;bf%DOoT{x<-1|UZ&^`JZ$8Zo8FLUFS;&AAv8eWc&?cPTpYTi2fTP^7Vy9*bwDo;o zcHnqvZt}X>m&B=cSZ!Vg=$^oo`!I2Mm!VR^LqG9j|K_` zzGMuwsOAu#t~u<^TgXqz3t-Jz18wkxabj-RZOG^aIT&?HW(un;5bX2ROSK0m&pH6X z4uSQYMC47v$BVu~w%5Xwb;MxqMyyMpHJ}|WK5CWbl$`REY)bgUQ|V2Z!BEqX>v^Vn z!I0goO=fnz4#70EWhQ<7V$C1CihedI*uL0#3&h>b*SC?|av}O9j?4U0gUFG1Yg$5Q zr^W85qmC3JZ@mWV*RVmW-IXJLP^&G-XQcHO_s3hfi(7++n)s6i6UPj%jhymM8;28p z(&QZ#W2LL*-9Z>)<&Cj|(oZFR`RLMFTAGSVP}H*K)2^|%C=_Py_6bM#YO38e$SWVs zTAL*%syGt%&qsV+qOIaw85L9dm2MfiJ+J3vZ59i*7);*fx*aQ4IbAmBIp`N~*v(b; zhy7t-!{fn~6WsxA`@VbhIHdR}EUV4GSXZc@Xfw)qRH1gDLnESp!SPG3Vf0WtTItvV zLubRQ@eFJ3JE@k~LSQf28)Mb4zqvA|PS)mxL(Jf6@O{SF#80(c&Qplrosq=u(mgH8mBcxrNG zi7nEfN)9)(-$lO)jf|@ixR8n9lYi)Z*6=I7*WkyUoHvNwKYtZGEO=vy846g7PvV@K zmT-~Q;aig-lR$oQZNy)L7Bd+C4aKw)yNn1 zI%4$6u1i_6J8VIAJy3mdmfe*|8V3dP^O$M%^;C3n1eh}O?y@d%*^xweY4|x9sE~D& z`&LOhNQ}rVR3d^5&)%)rXLoBqBG|7%-S)jl@f;;{7N9>eXTU9xT<`TK*0k%n-`kJ4 zSzM*#IgQ94=lLwgIaaM2yPbb{UcPTVgfn-$W@~-Pf%JOrMft{1|G<~mj&14h!|t#> z9uOSse4+&0cbyWqez*+l@*&^h=z0V>V#aBD_M3F_E$*AGWXVW*WM@iw4d$yOJoQ{% zY3+JjWaV(>QXg}d&(@jh#@^hXe{xutA?5`q*L0Zeh`aHW^sW zOTav2v+Gk@A$4_C;4YizA4Tz1G1pWn5=k}BI|acSMnf`$;u)nk7i+ZE@>mcvh}KaI z!BjeP91u89Mpb=~uq1fq^%HZPXFU_gKy4S`n1y-ETeyAkxuD&MR*m^aj?jFTmf6J#9SwE&g0JwzVW@@@B4<_cOBj} zmUQl9!bNo3cesB6TS$5k)gmL6%uv=7ta)eMJu+~59%mB|;(Wt#g6sMvlQT-Si{QxakwMvVTW0_FQLYYnH}@XTIbSIK|>q&hONCVqnhR8e9RfF zws{d6U_6*^FBjUPSq^}ZYMh8eG)2{1g%o2sSi<_%cAw^~$UOY#LBF^z`WGn?@`pR2 z=v)%SnSE{~x{+tHm-=x5N+E>0>)92RFOs=ie*JWitQPME>!D~Kz#pJxpM`~0*mg%D z1dXHWr^Rz-^0OD`jG;TSqK{)TuZB)c9LElgkcOQVoIf=xfE+UiDaZ(*1$8SvPi}s} z=yKsJ<*`9EEf=CZGt_1^<$QYJyo^sT>1rUGeZC4<3^Gv5zN7Vx>X?wSCP8Nj$ru;! z^MxU6u>b~AV^w@!$Ef+0-Lk1`$Z)U6tJjn~Z}Tc-K2dQY+fKQ7+>xLC@#-e@!Yy*` z3x3lWx;^_!NafmP_H=&(UZ!=TOdg^;azh4XWRhuW03B=AVY0}eLedNj>T_P5iR>77 zm5RoKu6p14Dwm9R?^E(&PuzEo*4V3|ptZfvzFP_rc#8q&9mPK+;N8j)Pn-8qNl9r+ zTn$xtJ?x#30u#Lj9)hVX?9R<|M!z~7nC7LQBDh?$F_ez?{LUvXvcP;FO)8Q@yjFfd z)hN3d+-W?T@znx}fYBMY=pOZ5QJs^$<(w9Eo8k+MQIqO!w$><$Y0kZ_sK>2I62uO9 zw)fZSS6>UrVge!uwtlmdxyf{jdpWbq#V#LdLoD;bxxorxZn@d zQCFk(+I{W))(>bN_2c3PTXh{Sd8MrX^+iwnIycX~l{Z=L zP9cO0?0Wrc^r%Nz)Q;QL1J2r^VDs6H0Iym)+l7Vz@MK;XsOGOtJPrkVWIpSo6X=XF z)!!6V2z$Yxxy1Sc6F9!gwqdunAz(a|xhcJq_N7#U4ge4uSN>hj9wdaymiNJJ)FxC6 zimPoj?I%UT6go$aVDeLmKn?&XyCDhlB17ZyRELWI01&{>I$kHN?9zSd z%?buomdAw80=;S>e`Wu<$JH%a7nzo(d2&QxmezrAfTql{^dbvd_4;(^{0+sdN%^$G z*UO8hUUlXnp@0hCNYLXNVGeT*;Nxasi(sjs#&aBK6^i~)f)SIeSI>I?2+fzUg*T#i zlG|_S*>|pXa~(N5(Q@3i1bA1(eU8p011NFXY4g$0zzi`^7=d!|&v<{-RJJ~P-}&*K zIbZ+gMzmT$jAnwQb(iKIEy=saH2G{dfYf4n>-W)_>q!p?`WvDEmd+2so#);dWq* z+D}+Cw*c#;a;Zd2!d_&-d^ZIG7tA{}YQ_M`b6$a)f)wHT7#Z!QJh4%DH3HucT=Bl7RJj#@KH*4TX8xr%xxd0O_50h(-9XT9sI1U=Awe>f6xg9V7z^ufG& zokBV{#gH7Za7yItz0kDtY9oGm~4Jj91 zZsq%ZEJExGpdBj^&g3<1M5|yDdy0BMnTF;I@;K~m1dqc|)XoyK=e%9Fg-OpxM>+6k zvy?L`TUqgu3r6zqlGsQfZjxF)J$~(T`Z4brfDd3Zoc67$WcR;6bBIY=G<~z>3%KLz zL4AZ%afb8XfZ1QsM?2rIk5V$_$;SWy_D3=E3^yM;5O`0h_`e3(rE(6V9%m9wKJsOZ zXh9hR(+y)w7R~`6qwCjZjFc`O2WvzmfZ**e5Q^jw&+%mpQ?SOTrBXHtpex8c?ob_QAlg5muM5mtM$G*vQO8^7fMu$e3+S8p+HCI z%02M$%5ICw$TO6RE%xf+LPu2l_Oq~)0~PQ^G(!oF^)DJSn)Js}2K1yur~c624;@c9 z^C75t7{h4L`Qe4R|HRKCo%$8{SE%FrqvFk;%1fQAYW$=R?dMdP}D|3tZ;w1 zQNel|H^g$FFqTo|AbjBb@T|_@!%Z;X2lq5mqd~eyq#H}j_!Bm#5 zb*qvVNA_fikKGu-4d#|Ow3VMTM)h~#3&N+$8KbZy{>eUV2nl5rXck*@b?Q;iU9W|M z((AD8HDz(Ly}L16cB<_VA7$8w>|riv8Z<(2q(g-5K`}I!sk@}DmLT2tM}dj_ z3!YLk6Pl@`OjzuASI)qJkVK7(>!FPFin@y2fku8sTfTZlmv)17c^q8fI#x zjwYsiDV*mG++Uv-3i@#Fl7T~~wdfVE3lynu^&0K@<;RrAs*&baeD*G5sk=zhFbsWK zu3VPP6GO&r!IFQH?pi8@u?3rk64fXtuC zU#h#RD=7i~!rr*%KS)H$T@kxfN5>S#@Ugp`n4P5uF+j-mEr5-o3!$A56QlK)o!jeU zaXTCtXr`{kKK?Xle6MML5zVd(#69hM=q_8Mc}IydO(;N_xomk`EzGtuUJv81tJQ$t zv$b#SJi2lu%J|vu$qRMhZrOK(V8H+Ydf{Yd&YK3U$HvuKePap|-p740lkkCwek=`M z`aFy2#ItI}%ywl@tCqDt#e%Ni9CkN!u#6#eA1{m`zdzDtnd>rHnlt(>R&2;^F;s&? z-zEW42)1^f&44x{E@E@S*ggU_BkjB0sbt;%3Ca=Udkqsd`inN9!p_9=q`rxUN739~nYo_e^S->c923f@k}M9V)*YhILeg89uod8oO~Y_Ay=W%&QBx{ML?Z#4G5ui*f}(20uqat<>0h7*!@$yz^)cFo$Dtyz*ojiD&Z z^qD)#u)v0}Wpd^GS#iHvz5~X>_Ol0^n&RyI8BWq}v;#Q_y|kMTM7z$g`97Pr-R)T` z>2&^V#Q${eEIxi`QJ1TVhw-0AAgF-<^HAXYy)FR{#c@J8Av82hPY~`n-w(*2yfEDl z#DO_24hv1@i{qem35GjZpc=$2bLSlf`XE#bD<>EvtuFGYxTazR8 zap~NzZL|?Hej*Mcl5k3#fJ%kNCUyRB(VQIn(Xi3Jbw-x=How-w=NrVG8{2mgLn6{y zc7ian4M5xc68>`v@_+3Y;gBn$2(eW*po8Avb?9fZ#t{{nv{Tz@G&0{!nf8hc_&s2hc;- z?!MtyOpSkZ=T*3i+~#}a;!^HlSeKYpvvyD&Ef1;E!Unyv>S3mVD;6TwCRk_e zFn2Uu_wDqs{~V6+k@plzZzuX=kWdo4J=24RZ8m;=RPw7_EA_1tZ-&D=zz(1OxjGC* zFaOVg6TSb_%j=uwgO)UcK)v}gTPoMNpAki-`_Mdg5y4w&=sDQ@k?#D$Z--a$`RoER zD95-8j@^8^5=r%**t-`@GQU;Xcjugw{)m1x$qP=hi;lTVL4P=C3c0L;JjXQEI5V8d zNLSssJ__MasIW3Uy)<+hJgM;hb9*)<(f{2O0~LS?8y`siTDWn$`^H}z&J-p%K15c{ zs{NIZ5;@uSm-cLKH zbp+>X?me-(YTBi>?baDW-dwLce%mIj+~mwy(>{P)#Z=Y!Zj&CcWjHN`A+Q0~Xz2l^8r>42s$f7H}`5+kE?pR*bc2Jwx0-DXImg~I@q`w9=0^xWHxu-B^o}T{MCl_8%TioEVVkg>+rXy?N2?SVap&2 zGJHK+FDIcN(I0vG20D8e|CO-_{$zkJyX;f~Toy?zF~h(CgLNWY!Q>^y3%Xts1(m3G zvwClB$Z9P@m2GZS_Ii8WsV-=c%eW9IW;{#(gye_NnSr90O&2fT@f9HYecm6k!hAoD zFJMw6^~}Q@UJEa|+0!z(wgR?qhU9L_?#O<$f)ln75(M(a| zs2vH*XCjF{nrHMgf)?-gl?S!}QL!LOB>B4};|+rst9640K8GdSMZxZmU63I9Z6((q zgw2J#bro(YVrtelw?VkWaB^kX-$1%M1u|2zYryFVh=UH=?VWjgRzhve%s#{+!?KM z>krwMf(uiI*%tnMLBRg^?EnC{9#mOW`lNsY2Fgd!6p(J4>JZ3a$3X$Fq*- zHpdoCc4LlD!?WmVqsCX6@V)n1yb(}1r=0@NlYIeCfNjS=k$Wwz-ZHv+w~*F5^~~^Ut0#(LozqVMuogBVK|V;8*%43as3;^WoI023!KY^6%RiHzp8B+m@ZLA!@6gd zgab@v$NM?J?FB5Pi>BZ?RdMfAND2J3ND(_Nz>C6PCYzDqT8Z)f<$1KB|1{xKC;EBC z4Jl7JG1ga>d$w85r6}?2b#^OaXK$o=XxH#FWzhBAbYzRZuJsohf)R2hw6O(< zT~hSP-Q?VEgM%{sZsh{vRQ${Pe_HAI!{pJxY?@zIk8Y63XuUgH;Bjo1R|vCpy6jrg%?1{3W8LM}%z-hj&+%;8j^w zm>ar333UJu65GcGl56TO65g&%zEMdR6i+8cnXuc!Q!d~VnU9tsOk9Puu`ZW|m%p3M zt&gf%llX>eqWV#&gjACI|6vaR0YG3t6_QMWM>#h+q!16>DstG4>qkYts+!v~)Om(= zxu;&kzIVSVw{+WBL@}Vwb+w6Hzv^~{-FZv3BXE1@=>FLC@|gVL8q)D_KZ2tSPP2jx z-WxC?owTjac4?f;Ld-@h6N?T9JdKv=Cu+xQgf8SReTteG#6F4C9(?`&iSE43eP?n0 zSq8ft2bN=Q&ukLl@G|2^zK-%6m;s?oq4^Lf!M4rpnFP2XP7$m{W z{jxpd3|PspPuDe++>W(-_FG9lWUWHROqe6=xJ_9={V0#R=T6pAe=CxZ(La5lL(Zix zl|-NpwX)_OZv>Gz@7ar}X7|768vvIBuzpp^T1QhGS0=?Kw*3G!OccRm@VI`tmM5e7 zb9K!_KTHo)0>yq*#|GxCWF;v&AjQ9*gB9~i=c7!CI3lnOzl&mdL`gz4il;E^<#I+H zfA5lTN_!a{0lsi(VVv#P@ALS*<8w|^v+m>`<$=Nk2gf@kGdrT3)0$(0RsNEVrVB0L zw!?Wk$OSojS%rO6d775;FX{~?dgN4-?ZrP!%l4ps-2K&VqO&P>2UdxZwC4H~QoC17 zTuqr9IfNJEMk5Y)Eb9ovF04H@X^2icq|@9Xr9c%4Vxf;)IHR|t7)lxZ52drVQcUJ_ z8#V6TbvN{!ZSqu&o8SU0jlW$;_zc)!9CUZljzBN35?IAdLXcR^KqPv08zH+?2! zWe-*{3>C-7=F}EKpsbjSo)U*R(21KBNk8bJk06N_QX7ybd&L;=7;!S^?fft4?GmfsD~2h*}O#^JY83ej;;m)9pW^=w9Gc7<8O;q{jF zZKO!~HvO?NnBAWkwgpBpns!tlVy_=kE38;1s=WVS+sLR34WOVZ>DxAPBvJ46DW34W zk39IwKZ5=dMZ0M?OHsci>gvM5GbIA6+jc5@n^)vRmq~xhY&vG=pNy%FoR%E%^~?`S zFDLG4^Pzl4ab>8$Us2~Og}d_i-=YN}eZ103^U#t+hR4-H@`}m=5XeK86?K111=YZd zl4@CT%za&%&?vMsgnw7$T}}H{#|NrMd9RJz;q2W=SHq0y6&!Mpl;QmgK9D>c&jzP^#>2XBQ%(nU%7BKK%>3t_i{-?a$)7lK5b;n@7DmryKSG_t2dSr+woB z@wX(U5TwJ03~AJSd|$l;$4`;U5dPy|1&6B^EE&8dDKeJp&yqW{AU&+jkpzy4rQ>~W zJ@WOIt03}UOncU}kR7Q|6+*(}#uO^Oq5NAJAT`G$g)%`|^}%aoy6SSS_rK}_w6G~$ z7x;Lg-4l3vL`O|{#i~U4Wra=ZZDM+a-EBa)q!OqR@%SR~ti{a6B+}D8u0@O?yyOoj z5+NbkCH!9vdT@BW$Y2fe$_{FZ4Q?AJBC@ALYC2fVK1*aM3o9SvJx)>Ptw;dbiPUfg zoNciJJg7{LF;|_2hlkme5h#W&4j;^HA{nlZGLmfVi!dDyKP?#)-%{=!(#!Z>-G~U0 z!Md7rSR2i%;2@-S6*TQg?U+Q093TbxiyyB8rQ&_hV~np{+2qU|{4<}zJ90f|xQ$~y zdS4P>4>uH3!IVUnSBCBWcJpWQivwpd2SW~!54?%08wL%(M(=>JQ2rTrdA?`T^s#j4 z^4xkx)y8>{)N$9!{!&8H*Jf!~B zr%?gS6gHFf14rG0V!IKOnaEdDICmj5BIx=riav;JQB0Q#dp2>E5V9G?ZC*1n03KU? zP_=6!PB7ME#Bk|U0a~XUyGKOyM~@8{Hh@(k!N_{dE*6{DXhlRRv$RqT9}K>ColRR9 zQ}?n-JP+m{ke?`#o;cQ4%|ht#z(CO-Buptp4eILB2-^v&w){gJ^rRdFL4}}%A9xN( z^K8~7cCcLU9jRTtI^FI)a7<|xQbumRrVekM(sR|q?mgd+$0 z`w?Y(t8lJTn(M9LP3z^NK`e&_Cbtk(BTP9x+eCukbtmjGj=M=DoU27LBJFKumuRjL zE#GsP9_44-U-1+bF+&m(4`EcW#|aS;{?Xy0u2bHf^go2>6-J~Ci7c#CynKgeX3u?w z=pk9*&jFjCc>^|ohDJy(WZV>7%IK4lj;#}}Q%f{8s17BOQA_L_V;a02uN`tBfjXD} z9zFiVfOLwREJeR8o zSllf7U=yD8R&L)q&^q@H@z{}k;~Cjy@F^|*0P<&zwN?SSw!GIb_T={hz8!8T0^v!` z+?cbk+SWv`$WclDLa^Fm&FNFPm(UU##Luy#66p5F)dSiQZtH_$c7)-P>3A+%op5uP zx~FA_S^t;vx2U5Z0tZ`(NU(C1C*F4stwGL;nMKmTD!M!!qXaM}XtJ!(0u2UHhK_e- zCkfYK8j&AbQ@<+2B;3E;lP6vu%P$CJuU`o1hX3kmX<~I6Vcan2j<4iB+cRr*sL`@- zeXVI)2Wx6MK%x$o&bm>ta|bhf&!!xJ^v~_996!xX$LyAlv{I8Oz2?17IJG$b7DrFS z^*+HMvuJ*0H@XoB{7eo^<_nG__)#P$LNE2YFtos_ZbZGy^*M|l4fV6b)YW5Ut*GUf z<|x$6NVsj&r`a_BFCPcEvkfU(*9*#s{Ou51ce`JFRBd&hPe^2%pJ(z=JoX04s zu8gtj6S6lfc;{bx<|=zN6{E9y(s!62#NqqYY03((9jaz6z7bpAfhCX(npNC?{+EK- zej)&r)HAo#C93VQ=||w@TZBu+ba50CwAjE{D^qANv-|KSw76!32Fb!Rc%?s$Rh${c zap30$UM#UEYacrJCBcNqu4o8Ph763T56sXukvqyB?JCKR$!5v#r{D}RRml+kyhK65 z5u=ki_-;zYD9ndBkRKCi3Fe-Lqp=dkl~4TlU*5$muo-*G^zjOCL=d6W{NvLBhl=&vrF5rKgeR^Rt){`2;%&D77|Ee{Fg>F3Zw?I5}9FIg4{RVAl zV3PAvg_W6TrQss5#XQYG1EFz^o9+-+M;_nqeuEY8nLe8?RAAiiFp`?4)}cX+PPo6h zCTe5idKh@rWptBJNSrFzs}#mCOp_2mQ5yX2zFbT^z9NW27JldR_pcAAoGcLIK72~0 zKEh2A-~W0HpphMlH#8!O+a??byNQsxf$m`>3deVtB4X*Q>vL36eZ+ZG8Ixm}mP6ZI z->h92Ob3YPzFkmg@~$KGgzLkyzT*e%23hRaf5Ea2igs8t4QAJss&b)fq0Pa-Hl^iZ zeApL)qdlR>%2WmZ>!HkR7F|Kj_)-Jd1vXv7gDgtJ@}YsdHt@l<^<_My1NUh{2e&;a*+ZEG~_0ZkM&$ z_SyTuplAhPPSXjs`ug3jiH1EQ-X8{e%t{P=9gR?57&kct_g^og5daPFSPC(J2<1d9 zDoH!HA%T7s>~GafT`GmdGm<3>*Hyvi-`opx zw8swzi7d;jNUF>={)4rOml&jlh`Hu3&&ikDoNMn^`bU3_ML56qlFG;Ix@R9)gYHJEC#n z!I}-0Oue@)t8*stIEN?%7O04YFAQm>eB24%Z$mL;HP~tEpP9n`T*FKg%loNgb$g!o zwYW>gN{3vhD;eLcL56e*Wl624Wn8?D;;Ab@i3eY3DcbE_XjjCwMag*wIz4)FR(E<~ z!dD~4uUU!A#MRgrs;=gmZrN|Bu!0zHhHE00qju&aKqr~4;xiX-W=tLUyuTWvaaH4k zg$#+0f=|RF^WP64lXD902rIYo>KT%`--IS`F9p#3QoI!evS98osSC6?>InRFK6^@g z@8x4Qyhls)$mfEJRz$@w#r(wN&aTm7P(nu@AW*}Zy^T= zr4fEr_JRGAH1uH+oN7g$OESkltR_fHF&4InnaFrV9|K$MO+uFlQpCt-7YN3c<GbpLl(W}JZIY^1hYgO10v0Rg>#h}MyL!rT}-!p z#Ta=$f3kE=aiM*nff3|!*vgQ3z3pRi4;jAL@LZYk`InYUa&%8qPyZ#4j1HW? z?=gM9UP#Pqy`KdQWM)(}eE~Z<`(`*-ilh@k&XHtv{tGUb9U*PerRh^S^OxuHso&a{JHm6V$ViXt;BH!XP!v~>`|tQS zlbHE4&zFD9;@S!(ZrxU%(xQat?8xHMvzG1!v|EhekY6IoE|eucIY=oqQYt~ntE~*taEZBaz6N;*^26Ve`chmS1oru-AEFQuZHuE?;15tiHGp5y1hJpuI zhlx7#jznfw4|<=krS|y(=>OS#z?`IV_HnR~OP#`pZ_AR_y&h^b z3=p|SZZsV>^0~K@=C+@srv-$8TbX>@YwS(d$$-`Ro^+Tfg+M1VPzb{8XCNczaz$Ss~22VL~3?t#}}h^Tte1^OcYSotw#h zc4yo=9Z2csPs;aSN-zGJ4rTY%zx%TS*oKYX)cs?*BEZ(~pD5w1fJ-$qD~T`8=(YsR6y~U6&V$>s;lH z+|~(?>Qz)YFXQ-7f;`Lh}*Xmba5V z)_y6!16PIHkhGnPZeK$44>Io@YdllgSX7P^ge*O<;}#{n>ftmLZcOp?incTK#RkYa z&&xc)>$;lZwjV&;$coeL#N=J7!8zCI5XwtmYEr10fIqyF$_{d5de$cF&beanJUs2k z3ve6ei)!?&=9D=8F|fY(!7sxzR$AEip`sv;WYuL{uNew4cin26;_@1I>pUlZK~>k1 zJ3szY4PVEoM5Q44B%j$d#8>~2A|$LpxQ2al+t@EwS2_BJq3)CV<#Po+xL=R6iF>j4 zhy57G5V7>`oYQdj$6xQ=hA!rn`TrB6hfuGev{T{#ws3Cp6>0%*o}7gfzSlE8^dTj3)7X^YVunFGPEAj@DrEryEFG@nBXygDqNk3U_BY%{+r z9b>6Va?JIbR!L2#P=%J&ZgSHkL3XMkL^a>8Vz%#@MG|>C!0)H3NpO5$OL(!!&d0oz zhUI6RkKU7hb6T>O%8})#cZEf57cXxg?H1zmqA{nAA34w}N`hqZ<=qg*gP6TtBpL+e zOUH2bAko5P_hj4N;=8Hji^ak@Pgz}yVdtSW?YpE{&fl?J+^Ov0#l#PT)2zNhVli}G zv@@(RSL-XW?x zjOtg+$Wq3_tTQN>38}RugIcCKTnCnosS{AIM*gmn_usi zr+n=N3wmLDYtaZhmR`7-)^y4$t4-@TXMDJDT8i4#;g zR{s^Z8{C0!mNUmkEtfeHT+uywo-6bvHP|A4JDpT%u`k>%QtgS2&KZwz3R~v86`p0` zR3b3dJDC>p^eVJRD*E=E#nWmboDL!!5Ph%AH;6>7sq@yFl;*YBV-76uC&M8a+zvI({_el{3L!Z>Ho=~ds zT4A_qjg0jo810ELf5FoYZOXq+U`d#_e#A~tkk(tn%z@`fdZhBu5|kd?B)RI72Maz0 zN!y$U9#Y>se(q@y2H)QsWK_gS#+oXn8+IPuW2bd&_1T&h432UBnD$qT!3pd^JZ@-# zg1{lsH_-@Z+-X<#xA7|INiyPlq70T_-5wZHgPV-3V&no3WbdzJ@MAflHV{Kqv=aE` zSu<*Ltb=lYw9yC>^xl@0dJM0_9Eul&r9+}H`T9$p4tX}$j4e8WZd zro61>+zj3pQG28_|Rfj-mR6ygGcPmit1mRQMWaSG3@iL zqYQ=L01_(-VgIZZ8c-BP-^9?xM)(1y7lgr<9-T$%ku7ktkCjpVE%1g{Wgskfe{BfR zJUPXxh~zJB;$dO`j)0HBm!@tC<_72 z`KP=|F8w2?)C7y|V#Gho!ZM@jYQsLFX@Nkm__aLmHR2xE08y}OeP&?s0}H(t?IUpp zU*w;qm}tAJGa5lQ8)Ppi=zFJAJZOiRE1#9lEp%2-$d(!nBZ?3{`=4hb`ei1i@Js3L zik5-UwfV14+exBXVC74`%R_@T58!-3PIP5>b;4OZSBBfx!5Qs&I2B)F{HZ<3RLh_1 zq`gcwMEGsUy6q(cqqFz{FE(X z3Rx!1kTOE6eH~jUH7R?RFoaAaJ4rLilGnavixE-QP+p8(W2tB?y<|u+M6Zy2otf`R zdcXI-zW(^0zn|YIVS&NQ zq!MtVa$*zk>BRUmX3!=ku!^Mj-TYk3wp1=1Y4!n*_SKl%vKtvi_lpeq0}Z*@M^xv_ z#ERcc7e(y~r-Hd!1DevkuPz~>+VfSlSe~E+^dZc*V0=zJMaXnxbe&nd*RK2yf9=b* zL@M4OcTatl{hVr=?z2WP<479ZKGa0a1Z7sx0m`di z$jSPM$QFLwzMbPu9@lP@7-n1SaWNe!)RbE=hS*|}%dWx5aH}mzw7{J{n?17u543k# zn6tEH9bP;{@;CAHV1AWSk&t%MpE2P%p$Dw{nTOg*R?()`H6MR{^S8YHO3!Ln0l(<| zx|s#{=(M%;k@-0G87V;PYYTk}T47!Vhl*!-Zw}uhSuFF<#p(cIW~9Yz-B#H-E+PWPpD7RX7!n|Fh>@Q@-2 z^-%N3Q7#y!Rqk6#J@L}WJaF%;-3Sd2>^EIkZu>OOiDl{GOY?yd3xc1ReaP>)<8`BZ zyG?}F$nyk_3R#TPMx%c-x+5$;+L26}-8I9}`8x3vM?l1I6t}(F82HmL&9pM~4+GEG zXjWxjR788^Fn2{mgVJd9BcCRpgL_^E%f0^kkaD8vgf0W7TA@{wV>}vkQ$>mIgIl@( zOn|sRfs2}UiZnv&sp>FF6?!;xh3a&=dGh!FrZCq!aV)?Woh|<7ek?G{ga2@*MA9ER#gj4#wi(?GHI)gGc4s$7@ED4X1Qp;W4_&vrZ+U`T@ zyB9pInDcLDo@}GMu8$hjO%gs2rsS}+SlnLge#7RtB#_r$-_5Bn8SW;+Fz*15As|#Y? zS^Yl7A)Y1&+Gy%_8_=wTh~)9a*L&ueuD&8VR`mE>;V$?u@xy)h1l0_F7nY(!h5qD;*bDFDxrwmYdx?E z*^3H)c490kr*}BB4Y*Pp6OY}@I!CuTN2tQjZ;`3uOq#WQ*Z#o^0N3D2U2)lp8S~R* zn->X{2->SPXVuSy+`eiCcI+YLsR7X!qz;nrD_?yc6kr%syu_a>!_YVUd@YybbAMLT#9w^kiZEs z^1B(rN6kBWRRmw|LC4ZT-Sb_q<%K#9f$XjV1S)vN!IaBFQ0S7#v)VL$9VCS78<`ZN zY>YsI_4L`sWN6_lBrIc}C6G+ck(t&*NE;U&Bq-?{bQv7ZEXob}_;XC^SLYt+9?sO1eW9ND@}+%gx*3~>N7 z3F5w{h;r$zLD9F^#>E2#X+^7~6Bq@Dl#K#e(mO*npOE728PmzQP-6Yfaz02|HwT+V zoVyVhyDQXr;sB?f>JVu4-_e)~3GMap?oS2ewd##W6(J zBDj_ zaVkF=N)eV2^4T0tHq*f8c?2}EL>kysP22iS_JzUQX$18P2Z`#whhheBO9sNLjHY;{ zdSha1{g zl50X7G1KMa@?RRQR`(BJtg`$w?-V!l9$4`6$YGevv*E#j_KP0 z2CDfMAO}U!#QMntHAYorRo%C;i>-$$&R3;C+9!6op+MJM6AT9)rbfM?i6~!)nO+(3 z0MGW-i=7$j3MhDM6!nV^!Ls)AVq; zJ=F5gXXiH&h?Sl~X-a>4x&eGk>xs%yalaVjX5GygXF53q+m`<1LQ9K4(8S%#8`Yx~ zc|j$K$c+KsaoNUs|F+ZzNb#b&*3b>n&y44}0_iSJ%L)Bmx8F3{65?sSl?^4_**fS| zy4X3hFDtuKiu-3mkDv13e|a1#KTzvfo$=9KN;Dty+Ht?RloBs#aFfhBURIOUI=WXZ z@+EVQ23lGpb#~HKE)M=lE!7s|WHyRp5n?d>TJ=*Z4Y`x$Wu2-eEp*k2M^>(D!sd{@?#G8{ zMgVZ~lMnz9Ft(>K{s_3s*~s%Tt=*{@YV6VYfZ(?GH4+8vuTJyS*%auvNdh{U0rEcex+1 z{to4TGYo9^{7b0+YVa$lKP+zd{yV6DwEaIB{~cfaHuB#w{x3fks6xU QIA6foGbr6sZM)$A00}%HqyPW_ literal 0 HcmV?d00001 diff --git a/assets/enemy_anims_02_frames_ok.png b/assets/enemy_anims_02_frames_ok.png new file mode 100644 index 0000000000000000000000000000000000000000..5a8870035b11899dd1d090fa8c6f942b374a5765 GIT binary patch literal 56004 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 z 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

>|6k2H;x)_n3Pqbh(>;`OZpp=h*%ubd7 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%0AR78*KAF2x9eCN^>YI|c4$Kz+HnSCHxb}23w#=l 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 Tzka(L19)kv>8aKzzX<<7iay$ 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

?YMPB&tSW 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 Pv;>2vtDnm{r-UW|44jQC literal 0 HcmV?d00001 diff --git a/assets/enemy_target_multi_ok.png b/assets/enemy_target_multi_ok.png new file mode 100644 index 0000000000000000000000000000000000000000..acc40ffd01ec04cafe3ba9cecf73ee7fe569f588 GIT binary patch literal 11303 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 literal 0 HcmV?d00001 diff --git a/assets/enemy_target_single_ok.png b/assets/enemy_target_single_ok.png new file mode 100644 index 0000000000000000000000000000000000000000..2ed17ac4cd739a1c8daaf0f5bdc4e1a06a4061de GIT binary patch literal 32354 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

2HsGO+W>G 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%0AU;n}ma{F~s z2VG3dm@RG?e%cs;J1h&BrbMddhfIrm+jLBsZbI4+DL*xUv^?DmX zgxxS*7UrmTJI2%7?do(o-O4@B!+f)IxA@VmrO6y3@}y+{B;&@dDm@=;h42}n1+;>> z?-Nq-Ywgh{)SqdVrnXor@mEaTa8D#qmGQII%%AYOvs1Fy+vW0hQlDk{k({WyJORH1 zdO_*S9hwmWw}oz@`CDh>81%7B-tX#6v(+rC=UWuaI9T(}&c{D{QMl9>A(2+~v^2-{s;cf;&1Lz5o4cQZDe;5gvnMii;3NH1 zf?LZS4cwoTfD2xdq=E{+_vx(PlA5akcWk6d!Qtf)`*gC*coTlk_Dim9xuLOULy-q4 z=<}*LzT>Gc&_`KLKa|$u`t@%$be~phJ!4vvZq0L*79m}bU)t+e5kE?d<-wRSFdJk! zSfu!MoOuM&(X9fM0(HayMSv>1=z^udJ-XWBV&|6_=XDgBL}2YBV$*Em!n-bszl>*q z2yMod5M_$D;2Trx3#Jx$orf>v?sQ}Qdzmw<6k|t2^y0eQ`T_)j9;3eXX;#NVjI_Vn zCvjxuP{#%R&t1{x7)rN3HiADjq;^`bI{R)bC_L!z5Yxhzn*VRcJ3Dg8jn7QS&6Hav=o&OuJu3iP{MO_cECdTeR=+^AH(zf4X|Anbd+Sk)B!t0z+a zFk8wip58JRs`F!?`Ww4BWc8z~-Xy(b z%%-b@hiGo?)5{$VS=PeCW$mvfpaTu_rNp1_Jo8szat(abO5OzbqM=*hI^`MGSasDHDhVGJhv%s_Q+q8yO3^8Fk^E-Ho{)qdTwKrf#Wd#m&&-)h9L68 zuI@u0>*+Hz`|+7kvRCOy<3b|;EAsme->Sk6pjE(ID5;O=jl9Tb_~)?|%0A>@O-mV) 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 + + + + + + + + + + + + + + + + + + + diff --git a/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..cde69bcccec65160d92116f20ffce4fce0b5245c GIT binary patch literal 3418 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 literal 0 HcmV?d00001 diff --git a/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..bfa42f0e7b91d006d22352c9ff2f134e504e3c1d GIT binary patch literal 4842 zcmZ{oXE5C1x5t0WvTCfdv7&7fy$d2l*k#q|U5FAbL??P!61}%ovaIM)mL!5G(V|6J zAtDH(OY|Du^}l!K&fFLG%sJ2JIp@rG=9y>Ci)Wq~U2RobsvA@Q0MM$dq4lq5{hy#9 zzgp+B{O(-=?1<7r0l>Q?>N6X%s~lmgrmqD6fjj_!c?AF`S0&6U06Z51fWOuNAe#jM z%pSN#J-Mp}`ICpL=qp~?u~Jj$6(~K_%)9}Bn(;pY0&;M00H9x2N23h=CpR7kr8A9X zU%oh4-E@i!Ac}P+&%vOPQ3warO9l!SCN)ixGW54Jsh!`>*aU)#&Mg7;#O_6xd5%I6 zneGSZL3Kn-4B^>#T7pVaIHs3^PY-N^v1!W=%gzfioIWosZ!BN?_M)OOux&6HCyyMf z3ToZ@_h75A33KyC!T)-zYC-bp`@^1n;w3~N+vQ0#4V7!f|JPMlWWJ@+Tg~8>1$GzLlHGuxS)w&NAF*&Y;ef`T^w4HP7GK%6UA8( z{&ALM(%!w2U7WFWwq8v4H3|0cOjdt7$JLh(;U8VcTG;R-vmR7?21nA?@@b+XPgJbD z*Y@v&dTqo5Bcp-dIQQ4@?-m{=7>`LZ{g4jvo$CE&(+7(rp#WShT9&9y>V#ikmXFau03*^{&d(AId0Jg9G;tc7K_{ivzBjqHuJx08cx<8U`z2JjtOK3( zvtuduBHha>D&iu#))5RKXm>(|$m=_;e?7ZveYy=J$3wjL>xPCte-MDcVW<;ng`nf= z9);CVVZjI-&UcSAlhDB{%0v$wPd=w6MBwsVEaV!hw~8G(rs`lw@|#AAHbyA&(I-7Y zFE&1iIGORsaskMqSYfX33U%&17oTszdHPjr&Sx(`IQzoccST*}!cU!ZnJ+~duBM6f z{Lf8PITt%uWZ zTY09Jm5t<2+Un~yC-%DYEP>c-7?=+|reXO4Cd^neCQ{&aP@yODLN8}TQAJ8ogsnkb zM~O>~3&n6d+ee`V_m@$6V`^ltL&?uwt|-afgd7BQ9Kz|g{B@K#qQ#$o4ut`9lQsYfHofccNoqE+`V zQ&UXP{X4=&Z16O_wCk9SFBQPKyu?<&B2zDVhI6%B$12c^SfcRYIIv!s1&r|8;xw5t zF~*-cE@V$vaB;*+91`CiN~1l8w${?~3Uy#c|D{S$I? zb!9y)DbLJ3pZ>!*+j=n@kOLTMr-T2>Hj^I~lml-a26UP1_?#!5S_a&v zeZ86(21wU0)4(h&W0iE*HaDlw+-LngX=}es#X$u*1v9>qR&qUGfADc7yz6$WN`cx9 zzB#!5&F%AK=ed|-eV6kb;R>Atp2Rk=g3lU6(IVEP3!;0YNAmqz=x|-mE&8u5W+zo7 z-QfwS6uzp9K4wC-Te-1~u?zPb{RjjIVoL1bQ=-HK_a_muB>&3I z*{e{sE_sI$CzyK-x>7abBc+uIZf?#e8;K_JtJexgpFEBMq92+Fm0j*DziUMras`o= zTzby8_XjyCYHeE@q&Q_7x?i|V9XY?MnSK;cLV?k>vf?!N87)gFPc9#XB?p)bEWGs$ zH>f$8?U7In{9@vsd%#sY5u!I$)g^%ZyutkNBBJ0eHQeiR5!DlQbYZJ-@09;c?IP7A zx>P=t*xm1rOqr@ec>|ziw@3e$ymK7YSXtafMk30i?>>1lC>LLK1~JV1n6EJUGJT{6 zWP4A(129xkvDP09j<3#1$T6j6$mZaZ@vqUBBM4Pi!H>U8xvy`bkdSNTGVcfkk&y8% z=2nfA@3kEaubZ{1nwTV1gUReza>QX%_d}x&2`jE*6JZN{HZtXSr{{6v6`r47MoA~R zejyMpeYbJ$F4*+?*=Fm7E`S_rUC0v+dHTlj{JnkW-_eRa#9V`9o!8yv_+|lB4*+p1 zUI-t)X$J{RRfSrvh80$OW_Wwp>`4*iBr|oodPt*&A9!SO(x|)UgtVvETLuLZ<-vRp z&zAubgm&J8Pt647V?Qxh;`f6E#Zgx5^2XV($YMV7;Jn2kx6aJn8T>bo?5&;GM4O~| zj>ksV0U}b}wDHW`pgO$L@Hjy2`a)T}s@(0#?y3n zj;yjD76HU&*s!+k5!G4<3{hKah#gBz8HZ6v`bmURyDi(wJ!C7+F%bKnRD4=q{(Fl0 zOp*r}F`6~6HHBtq$afFuXsGAk58!e?O(W$*+3?R|cDO88<$~pg^|GRHN}yml3WkbL zzSH*jmpY=`g#ZX?_XT`>-`INZ#d__BJ)Ho^&ww+h+3>y8Z&T*EI!mtgEqiofJ@5&E z6M6a}b255hCw6SFJ4q(==QN6CUE3GYnfjFNE+x8T(+J!C!?v~Sbh`Sl_0CJ;vvXsP z5oZRiPM-Vz{tK(sJM~GI&VRbBOd0JZmGzqDrr9|?iPT(qD#M*RYb$>gZi*i)xGMD`NbmZt;ky&FR_2+YqpmFb`8b`ry;}D+y&WpUNd%3cfuUsb8 z7)1$Zw?bm@O6J1CY9UMrle_BUM<$pL=YI^DCz~!@p25hE&g62n{j$?UsyYjf#LH~b z_n!l6Z(J9daalVYSlA?%=mfp(!e+Hk%%oh`t%0`F`KR*b-Zb=7SdtDS4`&&S@A)f>bKC7vmRWwT2 zH}k+2Hd7@>jiHwz^GrOeU8Y#h?YK8>a*vJ#s|8-uX_IYp*$9Y=W_Edf%$V4>w;C3h z&>ZDGavV7UA@0QIQV$&?Z_*)vj{Q%z&(IW!b-!MVDGytRb4DJJV)(@WG|MbhwCx!2 z6QJMkl^4ju9ou8Xjb*pv=Hm8DwYsw23wZqQFUI)4wCMjPB6o8yG7@Sn^5%fmaFnfD zSxp8R-L({J{p&cR7)lY+PA9#8Bx87;mB$zXCW8VDh0&g#@Z@lktyArvzgOn&-zerA zVEa9h{EYvWOukwVUGWUB5xr4{nh}a*$v^~OEasKj)~HyP`YqeLUdN~f!r;0dV7uho zX)iSYE&VG67^NbcP5F*SIE@T#=NVjJ1=!Mn!^oeCg1L z?lv_%(ZEe%z*pGM<(UG{eF1T(#PMw}$n0aihzGoJAP^UceQMiBuE8Y`lZ|sF2_h_6 zQw*b*=;2Ey_Flpfgsr4PimZ~8G~R(vU}^Zxmri5)l?N>M_dWyCsjZw<+a zqjmL0l*}PXNGUOh)YxP>;ENiJTd|S^%BARx9D~%7x?F6u4K(Bx0`KK2mianotlX^9 z3z?MW7Coqy^ol0pH)Z3+GwU|Lyuj#7HCrqs#01ZF&KqEg!olHc$O#Wn>Ok_k2`zoD z+LYbxxVMf<(d2OkPIm8Xn>bwFsF6m8@i7PA$sdK~ZA4|ic?k*q2j1YQ>&A zjPO%H@H(h`t+irQqx+e)ll9LGmdvr1zXV;WTi}KCa>K82n90s|K zi`X}C*Vb12p?C-sp5maVDP5{&5$E^k6~BuJ^UxZaM=o+@(LXBWChJUJ|KEckEJTZL zI2K&Nd$U65YoF3_J6+&YU4uKGMq2W6ZQ%BG>4HnIM?V;;Ohes{`Ucs56ue^7@D7;4 z+EsFB)a_(%K6jhxND}n!UBTuF3wfrvll|mp7)3wi&2?LW$+PJ>2)2C-6c@O&lKAn zOm=$x*dn&dI8!QCb(ul|t3oDY^MjHqxl~lp{p@#C%Od-U4y@NQ4=`U!YjK$7b=V}D z%?E40*f8DVrvV2nV>`Z3f5yuz^??$#3qR#q6F($w>kmKK`x21VmX=9kb^+cPdBY2l zGkIZSf%C+`2nj^)j zo}g}v;5{nk<>%xj-2OqDbJ3S`7|tQWqdvJdgiL{1=w0!qS9$A`w9Qm7>N0Y*Ma%P_ zr@fR4>5u{mKwgZ33Xs$RD6(tcVH~Mas-87Fd^6M6iuV^_o$~ql+!eBIw$U)lzl`q9 z=L6zVsZzi0IIW=DT&ES9HajKhb5lz4yQxT-NRBLv_=2sn7WFX&Wp6Y!&}P+%`!A;s zrCwXO3}jrdA7mB`h~N~HT64TM{R$lNj*~ekqSP^n9P~z;P zWPlRPz0h6za8-P>!ARb+A1-r>8VF*xhrGa8W6J$p*wy`ULrD$CmYV7Gt^scLydQWbo7XN-o9X1i7;l+J_8Ncu zc=EX&dg`GRo4==cz2d_Rz28oLS`Suf6OCp~f{0-aQ`t5YZ=!CAMc6-RZw#}A%;s44 znf2`6gcgm=0SezTH9h+JzeR3Lcm;8?*@+?FDfguK^9)z(Z`I!RKrSAI?H~4et6GTkz07Qgq4B6%Q*8Y0yPc4x z8(^YwtZjYIeOvVLey#>@$UzIciJ#x0pJLFg=8UaZv%-&?Yzp7gWNIo_x^(d75=x2c zv|LQ`HrKP(8TqFxTiP5gdT2>aTN0S7XW*pilASS$UkJ2*n+==D)0mgTGxv43t61fr z47GkfMnD-zSH@|mZ26r*d3WEtr+l-xH@L}BM)~ThoMvKqGw=Ifc}BdkL$^wC}=(XSf4YpG;sA9#OSJf)V=rs#Wq$?Wj+nTlu$YXn yn3SQon5>kvtkl(BT2@T#Mvca!|08g9w{vm``2PjZHg=b<1c17-HkzPl9sXa)&-Ts$ literal 0 HcmV?d00001 diff --git a/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/build/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..324e72cdd7480cb983fa1bcc7ce686e51ef87fe7 GIT binary patch literal 7718 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 literal 0 HcmV?d00001 diff --git a/build/android/app/src/main/res/values/strings.xml b/build/android/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..d8f5513 --- /dev/null +++ b/build/android/app/src/main/res/values/strings.xml @@ -0,0 +1,4 @@ + + + NativeActivity + diff --git a/build/android/build.gradle b/build/android/build.gradle new file mode 100644 index 0000000..b4f5ce0 --- /dev/null +++ b/build/android/build.gradle @@ -0,0 +1,21 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.5.2' + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/build/android/gradle.properties b/build/android/gradle.properties new file mode 100644 index 0000000..7bef3c2 --- /dev/null +++ b/build/android/gradle.properties @@ -0,0 +1,20 @@ +# Project-wide Gradle settings. + +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. + +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html + +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true +org.gradle.jvmargs=-Xmx1536m + +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true + diff --git a/build/android/gradle/wrapper/gradle-wrapper.jar b/build/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..8c0fb64a8698b08ecc4158d828ca593c4928e9dd GIT binary patch literal 49896 zcmagFb986H(k`5d^NVfUwr$(C?M#x1ZQHiZiEVpg+jrjgoQrerx!>1o_ul)D>ebz~ zs=Mmxr&>W81QY-S1PKWQ%N-;H^tS;2*XwVA`dej1RRn1z<;3VgfE4~kaG`A%QSPsR z#ovnZe+tS9%1MfeDyz`RirvdjPRK~p(#^q2(^5@O&NM19EHdvN-A&StN>0g6QA^VN z0Gx%Gq#PD$QMRFzmK+utjS^Y1F0e8&u&^=w5K<;4Rz|i3A=o|IKLY+g`iK6vfr9?+ z-`>gmU&i?FGSL5&F?TXFu`&Js6h;15QFkXp2M1H9|Eq~bpov-GU(uz%mH0n55wUl- zv#~ccAz`F5wlQ>e_KlJS3@{)B?^v*EQM=IxLa&76^y51a((wq|2-`qON>+4dLc{Oo z51}}o^Zen(oAjxDK7b++9_Yg`67p$bPo3~BCpGM7uAWmvIhWc5Gi+gQZ|Pwa-Gll@<1xmcPy z|NZmu6m)g5Ftu~BG&Xdxclw7Cij{xbBMBn-LMII#Slp`AElb&2^Hw+w>(3crLH!;I zN+Vk$D+wP1#^!MDCiad@vM>H#6+`Ct#~6VHL4lzmy;lSdk>`z6)=>Wh15Q2)dQtGqvn0vJU@+(B5{MUc*qs4!T+V=q=wy)<6$~ z!G>e_4dN@lGeF_$q9`Ju6Ncb*x?O7=l{anm7Eahuj_6lA{*#Gv*TaJclevPVbbVYu z(NY?5q+xxbO6%g1xF0r@Ix8fJ~u)VRUp`S%&rN$&e!Od`~s+64J z5*)*WSi*i{k%JjMSIN#X;jC{HG$-^iX+5f5BGOIHWAl*%15Z#!xntpk($-EGKCzKa zT7{siZ9;4TICsWQ$pu&wKZQTCvpI$Xvzwxoi+XkkpeE&&kFb!B?h2hi%^YlXt|-@5 zHJ~%AN!g_^tmn1?HSm^|gCE#!GRtK2(L{9pL#hp0xh zME}|DB>(5)`iE7CM)&_+S}-Bslc#@B5W4_+k4Cp$l>iVyg$KP>CN?SVGZ(&02>iZK zB<^HP$g$Lq*L$BWd?2(F?-MUbNWTJVQdW7$#8a|k_30#vHAD1Z{c#p;bETk0VnU5A zBgLe2HFJ3032$G<`m*OB!KM$*sdM20jm)It5OSru@tXpK5LT>#8)N!*skNu1$TpIw zufjjdp#lyH5bZ%|Iuo|iu9vG1HrIVWLH>278xo>aVBkPN3V$~!=KnlXQ4eDqS7%E% zQ!z^$Q$b^6Q)g#cLpwur(|<0gWHo6A6jc;n`t(V9T;LzTAU{IAu*uEQ%Ort1k+Kn+f_N`9|bxYC+~Z1 zCC1UCWv*Orx$_@ydv9mIe(liLfOr7mhbV@tKw{6)q^1DH1nmvZ0cj215R<~&I<4S| zgnr;9Cdjqpz#o8i0CQjtl`}{c*P)aSdH|abxGdrR)-3z+02-eX(k*B)Uqv6~^nh** z zGh0A%o~bd$iYvP!egRY{hObDIvy_vXAOkeTgl5o!33m!l4VLm@<-FwT0+k|yl~vUh z@RFcL4=b(QQQmwQ;>FS_e96dyIU`jmR%&&Amxcb8^&?wvpK{_V_IbmqHh);$hBa~S z;^ph!k~noKv{`Ix7Hi&;Hq%y3wpqUsYO%HhI3Oe~HPmjnSTEasoU;Q_UfYbzd?Vv@ zD6ztDG|W|%xq)xqSx%bU1f>fF#;p9g=Hnjph>Pp$ZHaHS@-DkHw#H&vb1gARf4A*zm3Z75QQ6l( z=-MPMjish$J$0I49EEg^Ykw8IqSY`XkCP&TC?!7zmO`ILgJ9R{56s-ZY$f> zU9GwXt`(^0LGOD9@WoNFK0owGKDC1)QACY_r#@IuE2<`tep4B#I^(PRQ_-Fw(5nws zpkX=rVeVXzR;+%UzoNa;jjx<&@ABmU5X926KsQsz40o*{@47S2 z)p9z@lt=9?A2~!G*QqJWYT5z^CTeckRwhSWiC3h8PQ0M9R}_#QC+lz>`?kgy2DZio zz&2Ozo=yTXVf-?&E;_t`qY{Oy>?+7+I= zWl!tZM_YCLmGXY1nKbIHc;*Mag{Nzx-#yA{ zTATrWj;Nn;NWm6_1#0zy9SQiQV=38f(`DRgD|RxwggL(!^`}lcDTuL4RtLB2F5)lt z=mNMJN|1gcui=?#{NfL{r^nQY+_|N|6Gp5L^vRgt5&tZjSRIk{_*y<3^NrX6PTkze zD|*8!08ZVN)-72TA4Wo3B=+Rg1sc>SX9*X>a!rR~ntLVYeWF5MrLl zA&1L8oli@9ERY|geFokJq^O$2hEpVpIW8G>PPH0;=|7|#AQChL2Hz)4XtpAk zNrN2@Ju^8y&42HCvGddK3)r8FM?oM!3oeQ??bjoYjl$2^3|T7~s}_^835Q(&b>~3} z2kybqM_%CIKk1KSOuXDo@Y=OG2o!SL{Eb4H0-QCc+BwE8x6{rq9j$6EQUYK5a7JL! z`#NqLkDC^u0$R1Wh@%&;yj?39HRipTeiy6#+?5OF%pWyN{0+dVIf*7@T&}{v%_aC8 zCCD1xJ+^*uRsDT%lLxEUuiFqSnBZu`0yIFSv*ajhO^DNoi35o1**16bg1JB z{jl8@msjlAn3`qW{1^SIklxN^q#w|#gqFgkAZ4xtaoJN*u z{YUf|`W)RJfq)@6F&LfUxoMQz%@3SuEJHU;-YXb7a$%W=2RWu5;j44cMjC0oYy|1! zed@H>VQ!7=f~DVYkWT0nfQfAp*<@FZh{^;wmhr|K(D)i?fq9r2FEIatP=^0(s{f8GBn<8T zVz_@sKhbLE&d91L-?o`13zv6PNeK}O5dv>f{-`!ms#4U+JtPV=fgQ5;iNPl9Hf&9( zsJSm5iXIqN7|;I5M08MjUJ{J2@M3 zYN9ft?xIjx&{$K_>S%;Wfwf9N>#|ArVF^shFb9vS)v9Gm00m_%^wcLxe;gIx$7^xR zz$-JDB|>2tnGG@Rrt@R>O40AreXSU|kB3Bm)NILHlrcQ&jak^+~b`)2;otjI(n8A_X~kvp4N$+4|{8IIIv zw*(i}tt+)Kife9&xo-TyoPffGYe;D0a%!Uk(Nd^m?SvaF-gdAz4~-DTm3|Qzf%Pfd zC&tA;D2b4F@d23KV)Csxg6fyOD2>pLy#n+rU&KaQU*txfUj&D3aryVj!Lnz*;xHvl zzo}=X>kl0mBeSRXoZ^SeF94hlCU*cg+b}8p#>JZvWj8gh#66A0ODJ`AX>rubFqbBw z-WR3Z5`33S;7D5J8nq%Z^JqvZj^l)wZUX#7^q&*R+XVPln{wtnJ~;_WQzO{BIFV55 zLRuAKXu+A|7*2L*<_P${>0VdVjlC|n^@lRi}r?wnzQQm z3&h~C3!4C`w<92{?Dpea@5nLP2RJrxvCCBh%Tjobl2FupWZfayq_U$Q@L%$uEB6#X zrm_1TZA8FEtkd`tg)a_jaqnv3BC_O*AUq-*RNLOT)$>2D!r>FZdH&$x5G_FiAPaw4 zgK*7>(qd6R?+M3s@h>Z|H%7eGPxJWn_U$w`fb(Mp+_IK2Kj37YT#Xe5e6KS-_~mW} z`NXEovDJh7n!#q4b+=ne<7uB7Y2(TAR<3@PS&o3P$h#cZ-xF$~JiH6_gsv9v(#ehK zhSB_#AI%lF#+!MB5DMUN+Zhf}=t~{B|Fn{rGM?dOaSvX!D{oGXfS*%~g`W84JJAy4 zMdS?9Bb$vx?`91$J`pD-MGCTHNxU+SxLg&QY+*b_pk0R=A`F}jw$pN*BNM8`6Y=cm zgRh#vab$N$0=XjH6vMyTHQg*+1~gwOO9yhnzZx#e!1H#|Mr<`jJGetsM;$TnciSPJ z5I-R0)$)0r8ABy-2y&`2$33xx#%1mp+@1Vr|q_e=#t7YjjWXH#3F|Fu<G#+-tE2K7 zOJkYxNa74@UT_K4CyJ%mR9Yfa$l=z}lB(6)tZ1Ksp2bv$^OUn3Oed@=Q0M}imYTwX zQoO^_H7SKzf_#kPgKcs%r4BFUyAK9MzfYReHCd=l)YJEgPKq-^z3C%4lq%{&8c{2CGQ3jo!iD|wSEhZ# zjJoH87Rt{4*M_1GdBnBU3trC*hn@KCFABd=Zu`hK;@!TW`hp~;4Aac@24m|GI)Ula z4y%}ClnEu;AL4XVQ6^*!()W#P>BYC@K5mw7c4X|Hk^(mS9ZtfMsVLoPIiwI?w_X0- z#vyiV5q9(xq~fS`_FiUZw->8Awktga>2SrWyvZ|h@LVFtnY#T z%OX30{yiSov4!43kFd(8)cPRMyrN z={af_ONd;m=`^wc7lL|b7V!;zmCI}&8qz=?-6t=uOV;X>G{8pAwf9UJ`Hm=ubIbgR zs6bw3pFeQHL`1P1m5fP~fL*s?rX_|8%tB`Phrij^Nkj{o0oCo*g|ELexQU+2gt66=7}w5A+Qr}mHXC%)(ODT# zK#XTuzqOmMsO~*wgoYjDcy)P7G`5x7mYVB?DOXV^D3nN89P#?cp?A~c%c$#;+|10O z8z(C>mwk#A*LDlpv2~JXY_y_OLZ*Mt)>@gqKf-Ym+cZ{8d%+!1xNm3_xMygTp-!A5 zUTpYFd=!lz&4IFq)Ni7kxLYWhd0o2)ngenV-QP@VCu;147_Lo9f~=+=Nw$6=xyZzp zn7zAe41Sac>O60(dgwPd5a^umFVSH;<7vN>o;}YlMYhBZFZ}-sz`P^3oAI>SCZy&zUtwKSewH;CYysPQN7H>&m215&e2J? zY}>5N-LhaDeRF~C0cB>M z7@y&xh9q??*EIKnh*;1)n-WuSl6HkrI?OUiS^lx$Sr2C-jUm6zhd{nd(>#O8k9*kF zPom7-%w1NjFpj7WP=^!>Vx^6SG^r`r+M&s7V(uh~!T7aE;_ubqNSy)<5(Vi)-^Mp9 zEH@8Vs-+FEeJK%M0z3FzqjkXz$n~BzrtjQv`LagAMo>=?dO8-(af?k@UpL5J#;18~ zHCnWuB(m6G6a2gDq2s`^^5km@A3Rqg-oHZ68v5NqVc zHX_Iw!OOMhzS=gfR7k;K1gkEwuFs|MYTeNhc0js>Wo#^=wX4T<`p zR2$8p6%A9ZTac;OvA4u#Oe3(OUep%&QgqpR8-&{0gjRE()!Ikc?ClygFmGa(7Z^9X zWzmV0$<8Uh)#qaH1`2YCV4Zu6@~*c*bhtHXw~1I6q4I>{92Eq+ZS@_nSQU43bZyidk@hd$j-_iL=^^2CwPcaXnBP;s;b zA4C!k+~rg4U)}=bZ2q*)c4BZ#a&o!uJo*6hK3JRBhOOUQ6fQI;dU#3v>_#yi62&Sp z-%9JJxwIfQ`@w(_qH0J0z~(lbh`P zHoyp2?Oppx^WXwD<~20v!lYm~n53G1w*Ej z9^B*j@lrd>XGW43ff)F;5k|HnGGRu=wmZG9c~#%vDWQHlOIA9(;&TBr#yza{(?k0> zcGF&nOI}JhuPl`kLViBEd)~p2nY9QLdX42u9C~EUWsl-@CE;05y@^V1^wM$ z&zemD1oZd$Z))kEw9)_Mf+X#nT?}n({(+aXHK2S@j$MDsdrw-iLb?#r{?Vud?I5+I zVQ8U?LXsQ}8-)JBGaoawyOsTTK_f8~gFFJ&lhDLs8@Rw$ey-wr&eqSEU^~1jtHmz6 z!D2g4Yh?3VE*W8=*r&G`?u?M~AdO;uTRPfE(@=Gkg z7gh=EGu!6VJJ?S_>|5ZwY?dGFBp3B9m4J1=7u=HcGjsCW+y6`W?OWxfH?S#X8&Zk& zvz6tWcnaS1@~3FTH}q_*$)AjYA_j;yl0H0{I(CW7Rq|;5Q2>Ngd(tmJDp+~qHe_8y zPU_fiCrn!SJ3x&>o6;WDnjUVEt`2fhc9+uLI>99(l$(>Tzwpbh>O775OA5i`jaBdp zXnCwUgomyF3K$0tXzgQhSAc!6nhyRh_$fP}Rd$|*Y7?ah(JrN=I7+)+Hp4BLJJ2P~ zFD!)H^uR2*m7GQZpLUVS#R3^?2wCd}(gcFcz!u5KN9ldNJdh@%onf06z9m~T0n;dqg6@?>G@S|rPO*Kj>{su+R|7bH>osA&uD4eqxtr**k($ii`uO? z7-&VkiL4Rp3S&e+T}2Z#;NtWHZco(v8O3QMvN0g7l8GV|U2>x-DbamkZo5)bjaSFR zr~Y9(EvF9{o*@|nBPj+e5o$_K`%TH1hD=|its}|qS^o6EQu_gOuDUH=Dtzik;P7G$ zq%_T<>9O}bGIB?;IQ*H`BJ5NWF6+XLv@G7aZwcy(&BoepG~u`aIcG>y+;J7+L=wTZ zB=%n@O}=+mjBO%1lMo6C0@1*+mhBqqY((%QMUBhyeC~r*5WVqzisOXFncr*5Lr0q6 zyPU&NOV}Vt2jl>&yig4I6j93?D>Ft=keRh=Y;3*^Z-I26nkZ#Jj5OJ89_?@#9lNjp z#gfAO6i937)~I|98P%xAWxwmk(F&@lTMx63*FZ~2b{NHU+}EV8+kMAB0bM*Zn#&7ubt98!PT^ZcMOfwMgkYz6+;?CKbvV zQ}Z@s_3JcMPhF&y1?}9uZFIBiPR3g7lf=+XEr9Bl%zRfGcaKb*ZQq5b35ZkR@=JEw zP#iqgh2^#@VA-h)>r`7R-$1_ddGr&oWWV$rx;pkG0Yohp9p@In_p)hKvMo@qIv zcN2t{23&^Nj=Y&gX;*vJ;kjM zHE2`jtjVRRn;=WqVAY&m$z=IoKa{>DgJ;To@OPqNbh=#jiS$WE+O4TZIOv?niWs47 zQfRBG&WGmU~>2O{}h17wXGEnigSIhCkg%N~|e?hG8a- zG!Wv&NMu5z!*80>;c^G9h3n#e>SBt5JpCm0o-03o2u=@v^n+#6Q^r#96J5Q=Dd=>s z(n0{v%yj)=j_Je2`DoyT#yykulwTB+@ejCB{dA7VUnG>4`oE?GFV4sx$5;%9&}yxfz<-wWk|IlA|g&! zN_Emw#w*2GT=f95(%Y1#Viop;Yro3SqUrW~2`Fl?Ten{jAt==a>hx$0$zXN`^7>V_ zG*o7iqeZV)txtHUU2#SDTyU#@paP;_yxp!SAG##cB= zr@LoQg4f~Uy5QM++W`WlbNrDa*U;54`3$T;^YVNSHX4?%z|`B~i7W+kl0wBB`8|(l zAyI6dXL&-Sei0=f#P^m`z=JJ`=W;PPX18HF;5AaB%Zlze`#pz;t#7Bzq0;k8IyvdK=R zBW+4GhjOv+oNq^~#!5(+pDz)Ku{u60bVjyym8Or8L;iqR|qTcxEKTRm^Y%QjFYU=ab+^a|!{!hYc+= z%Qc02=prKpzD+jiiOwzyb(dELO|-iyWzizeLugO!<1(j|3cbR!8Ty1$C|l@cWoi?v zLe<5+(Z-eH++=fX**O-I8^ceYZgiA!!dH+7zfoP-Q+@$>;ab&~cLFg!uOUX7h0r== z`@*QP9tnV1cu1!9pHc43C!{3?-GUBJEzI(&#~vY9MEUcRNR*61)mo!RG>_Yb^rNN7 zR9^bI45V?3Lq`^^BMD!GONuO4NH#v9OP3@s%6*Ha3#S*;f z6JEi)qW#Iq#5BtIXT9Gby|H?NJG}DN#Li82kZ_Rt1=T0Z@U6OAdyf}4OD|Sk^2%-1 zzgvqZ@b6~kL!^sZLO$r{s!3fQ5bHW}8r$uTVS*iw1u8^9{YlPp_^Xm5IN zF|@)ZOReX zB*#tEbWEX~@f)ST|s$oUKS@drycE1tYtdJ9b*(uFTxNZ{n3BI*kF7wXgT6+@PI@vwH7iQS{1T!Nauk>fm8gOLe`->Pi~ z8)3=UL_$OLl2n7QZlHt846nkYFu4V};3LpYA%5VaF#a2#d2g0&ZO~3WA%1XlerVpg zCAlM;(9OqH@`(>Tha{*@R%twB!}1ng4V=^+R`Q{#fkRk)C|suozf-uCXrkIH2SC^C z6wlxR`yS;-U#uu#`OnD%U<41%C4mp>LYLPIbgVO~WsT1if)Y)T*8nUB`2*(B;U_ha1NWv2`GqrZ z3MWWpT3tZ!*N@d*!j3=@K4>X*gX4A^@QPAz24?7u90AXaLiFq=Z$|5p$Ok2|YCX_Z zFgNPiY2r_Bg2BQE!0z=_N*G?%0cNITmAru*!Mws=F+F&Qw!&1?DBN{vSy%IvGRV@1 zS->PARgL^XS!-aZj zi@`~LhWfD!H-L0kNv=Jil9zR0>jZLqu)cLq?$yXVyk%EteKcWbe^qh#spHJPa#?92 za(N(Kw0se^$7nQUQZBet;C_Dj5(2_?TdrXFYwmebq}YGQbN5Ex7M zGSCX~Ey;5AqAzEDNr%p^!cuG?&wIeY&Bm5guVg>8F=!nT%7QZTGR(uGM&IZuMw0V_ zhPiIFWm?H?aw*(v6#uVT@NEzi2h5I$cZ-n0~m$tmwdMTjG*of^Y%1 zW?Y%o*-_iMqEJhXo^!Qo?tGFUn1Mb|urN4_;a)9bila2}5rBS#hZ5wV+t1xbyF1TW zj+~cdjbcMgY$zTOq6;ODaxzNA@PZIXX(-=cT8DBd;9ihfqqtbDr9#gXGtK24BPxjZ z9+Xp>W1(s)->-}VX~BoQv$I|-CBdO`gULrvNL>;@*HvTdh@wyNf}~IB5mFnTitX2i z;>W>tlQyc2)T4Mq+f!(i3#KuK-I8Kj3Wm(UYx?KWWt8DEPR_Jdb9CE~Fjc7Rkh#gh zowNv()KRO@##-C+ig0l!^*ol!Bj%d32_N*~d!|&>{t!k3lc?6VrdlCCb1?qyoR42m zv;4KdwCgvMT*{?tJKa(T?cl|b;k4P>c&O@~g71K5@}ys$)?}WSxD;<5%4wEz7h=+q ztLumn6>leWdDk#*@{=v9p)MsvuJMyf_VEs;pJh?i3z7_W@Q|3p$a}P@MQ-NpMtDUBgH!h4Ia#L&POr4Qw0Tqdw^}gCmQAB z8Dgkzn?V!_@04(cx0~-pqJOpeP1_}@Ml3pCb45EJoghLows9ET13J8kt0;m$6-jO( z4F|p+JFD1NT%4bpn4?&)d+~<360$z5on`eS6{H`S>t`VS$>(D`#mC*XK6zULj1Da# zpV$gw$2Ui{07NiYJQQNK;rOepRxA>soNK~B2;>z;{Ovx`k}(dlOHHuNHfeR}7tmIp zcM}q4*Fq8vSNJYi@4-;}`@bC?nrUy`3jR%HXhs79qWI5;hyTpH5%n-NcKu&j(aGwT z1~{geeq?Jd>>HL+?2`0K8dB2pvTS=LO~tb~vx_<=iN8^rW!y@~lBTAaxHmvVQJSeJ z!cb9ffMdP1lgI=>QJN{XpM4{reRrdIt|v|0-8!p}M*Qw^uV1@Ho-YsNd0!a(os$F* zT0tGHA#0%u0j*%S>kL*73@~7|iP;;!JbWSTA@`#VHv_l_%Z7CgX@>dhg_ zgn0|U)SY~U-E5{QiT@(uPp#1jaz!(_3^Cbz2 z4ZgWWz=PdGCiGznk{^4TBfx_;ZjAHQ>dB4YI}zfEnTbf60lR%=@VWt0yc=fd38Ig* z)Q38#e9^+tA7K}IDG5Z~>JE?J+n%0_-|i2{E*$jb4h?|_^$HRHjVkiyX6@Y+)0C2a zA+eegpT1dUpqQFIwx;!ayQcWQBQTj1n5&h<%Lggt@&tE19Rm~Rijtqw6nmYip_xg0 zO_IYpU304embcWP+**H|Z5~%R*mqq+y{KbTVqugkb)JFSgjVljsR{-c>u+{?moCCl zTL)?85;LXk0HIDC3v*|bB-r_z%zvL6Dp__L*A~Z*o?$rm>cYux&)W=6#+Cb}TF&Kd zdCgz3(ZrNA>-V>$C{a^Y^2F!l_%3lFe$s(IOfLBLEJ4Mcd!y&Ah9r)7q?oc z5L(+S8{AhZ)@3bw0*8(}Xw{94Vmz6FrK&VFrJN;xB96QmqYEibFz|yHgUluA-=+yS}I-+#_Pk zN67-#8W(R^e7f!;i0tXbJgMmJZH%yEwn*-}5ew13D<_FYWnt?{Mv1+MI~u;FN~?~m z{hUnlD1|RkN}c1HQ6l@^WYbHAXPJ^m0te1woe;LDJ}XEJqh1tPf=sD0%b+OuR1aCoP>I>GBn4C24Zu$D)qg=gq;D??5 zUSj%;-Hvk_ffj-+SI{ZCp`gZcNu=L@_N}kCcs?TyMr-37fhy$?a<7lt1`fZw<%$8@B6(Wgo!#!z9z{ab|x`+&;kP!(gfdY}A-GP&4Cbh-S< z1(kmgnMyB2z3ipEj5;4<{(=&<7a>A_Jl`ujUKYV@%k(oD=cD7W@8~5O=R*zdjM_y; zXwme~0wo0aDa~9rDnjF=B}Bbj|DHRQjN|?@(F^=bVFdr!#mwr|c0843k>%~5J|7|v zSY=T)iPU6rEAwrM(xTZwPio%D4y9Z4kL0bMLKvu4yd)0ZJA3<;>a2q~rEfcREn}~1 zCJ~3c?Afvx?3^@+!lnf(kB6YwfsJ*u^y7kZA?VmM%nBmaMspWu?WXq4)jQsq`9EbT zlF2zJ)wXuAF*2u|yd5hNrG>~|i}R&ZyeetTQ!?Hz6xGZZb3W6|vR>Hq=}*m=V=Lsp zUOMxh;ZfP4za~C{Ppn^%rhitvpnu^G{Z#o-r?TdEgSbtK_+~_iD49xM;$}X*mJF02|WBL{SDqK9}p4N!G$3m=x#@T+4QcapM{4j|Q zwO!(hldpuSW#by!zHEP@tzIC|KdD z%BJzQ7Ho1(HemWm`Z8m_D#*`PZ-(R%sZmPrS$aHS#WPjH3EDitxN|DY+ zYC|3S?PQ3NNYau$Qk8f>{w}~xCX;;CE=7;Kp4^xXR8#&^L+y-jep7oO^wnQ840tg1 zuN17QKsfdqZPlB8OzwF+)q#IsmenEmIbRAJHJ$JjxzawKpk8^sBm3iy=*kB%LppNb zhSdk`^n?01FKQ;=iU+McN7Mk0^`KE>mMe1CQ2a_R26_}^$bogFm=2vqJake7x)KN( zYz;gRPL+r4*KD>1U+DU+1jh{mT8#P#(z9^(aDljpeN{mRmx{AZX&hXKXNuxj3x*RrpjvOaZ#`1EqK!$+8=0yv8}=;>f=E?5tGbRUd4%?QL zy$kq6mZeF%k6E1&8nwAYMd!-lRkhQTob$7s`*XqcHs;l~mHV}fx&0I&i!CHaPVSM{ zHdRh7a>hP)t@YTrWm9y zl-ENWSVzlKVvTdWK>)enmGCEw(WYS=FtY{srdE{Z(3~4svwd)ct;`6Y{^qiW+9E@A ztzd?lj5F#k`=E1U-n*1JJc0{x{0q!_tkD<_S6bGsW)^RxGu%Rj^Mvw|R0WP1SqvAI zs(MiAd@Y5x!UKu376&|quQNxir;{Iz(+}3k-GNb29HaQh?K30u=6sXpIc?j0hF{VY zM$Do*>pN)eRljAOgpx7fMfSrnZ7>fi@@>Jh;qxj1#-Vj}JC3E^GCbC(r55_AG>6cq z4ru34FtVuBt)bkX4>ZFWjToyu)VA>IE6hXc+^(3ruUaKRqHnx3z)(GXetm;^0D95s zQ&drwfjhM4*|q=;i5Io0eDf?I{p}qo@7i7abHX5qLu~VDwYf4bmV~-^M_U?DL(+cG z{AyE^a|*73Ft)o5k-p)+GLXj#q01VlJ9#ZJkf|+c%6qfRgVp&6NsU3~F?!uh}HJm73xq>v$h zYoW3wJE6n9P|;{8U<^%UE2wjR4x^G_Nc$J(i)!>;g4`CCh2z^Dth#ah#<`#axDR?F z4>~hnN2%B2ZUuU6j>m1Qjj~5jQSdA&Q#7hOky#=Ue)}7LPJ!8nbZO_0Sw{G>>M7&E zb1dy|0Zi$(ubk`4^XkVI%4WIpe?Bh!D~IjvZs14yHw=aQ8-`N-=P*?Kzi&eRGZ_6Z zT>eis`!Dy3eT3=vt#Lbc+;}i5XJf7zM3QneL{t?w=U<1rk7+z2Cu^|~=~54tAeSYF zsXHsU;nM0dpK>+71yo(NFLV-^Lf7%U?Q$*q{^j04Gl71ya2)^j`nmJ$cmI9eFMjp+ z#)jKmi4lZc<;l>!={@jTm%?!5jS;6;c*Ml55~r6Y?22B^K3bPhKQ(ICc&z%w<4W1= zjTTtz_}IA$%kCqU)h#$!Yq>>2mVG}qYL}!avmCWYV}x4!YEeq)pgTp| zR;+skHuc7YXRLrcbYXt>?@pa{l^2pL>RrZ!22zMmi1ZR?nkaWF*`@XFK4jGh&Em3vn(l z3~^Q9&tM^eV=f^lccCUc9v02z%^n5VV6s$~k0uq5B#Ipd6`M1Kptg^v<2jiNdlAWQ z_MmtNEaeYIHaiuaFQdG&df7miiB5lZkSbg&kxY*Eh|KTW`Tk~VwKC~+-GoYE+pvwc{+nIEizq6!xP>7ZQ(S2%48l$Y98L zvs7s<&0ArXqOb*GdLH0>Yq-f!{I~e~Z@FUIPm?jzqFZvz9VeZLYNGO}>Vh<=!Er7W zS!X6RF^et7)IM1pq57z*^hP5w7HKSDd8jHX!*gkKrGc-GssrNu5H%7-cNE{h$!aEQK3g*qy;= z)}pxO8;}nLVYm_24@iEs8)R7i;Th0n4->&$8m6(LKCRd(yn7KY%QHu_f=*#e`H^U( z{u!`9JaRD?Z?23fEXrjx>A@+a!y-_oaDB)o@2s{2%A97-ctFfrN0cXQ@6aGH`X~Nr z144?qk;MzDU-cgQOLfT3-ZR#hKmYtKG*iGf4ZJ`|`9!^SkBDUUSJCba)>mM!)k~(z zdjUqB`)~!UObMHB1b$UItM$<0kwlqHH;c z=)+~bkOcIT7vI0Iy(wD)vsg9|oi##%Rgrq`Ek;pN)}lbpz`iv{F4K*{ZZ?Zjixxxr zY|SPl2NsXH+5pimj+MvbZ_+HrfvdC13|9Zs)Y=nW$z<0mhl}%irBSm5T3ZrN#2AhY z_ZrTmS(L`U#y}VZ@~QL9wUS6AnU*7LWS02Xyz`b>%rTml#Wb0yr>@c(Ym*40g;P{V zjV1XSHdU>oY!&Jh7MzhzUV8(9E+yl5UJYga>=0Ldjwtc`5!1>LxaB-kVW;IlSPs+0 zUBx=m8OKVp<`frNvMK>WMO(iKY%PuvqD+PK*vP6f?_o!O)MCW5Ic zv(%f5PLHyOJ2h@Yn_to@54Yq;fdoy40&sbe3A$4uUXHsHP_~K}h#)p&TyOx(~JE?y(IBAQKl}~VQjVC-c6oZwmESL;`Xth?2)-b6ImNcJi z;w|`Q*k?`L(+Dp}t(FocvzWB(%~9$EAB6_J6CrA}hMj-Vy*6iA$FdV}!lvk%6}M)4 zTf<)EbXr9^hveAav1yA?>O0aNEpv0&rju{(Gt|dP=AP%)uQm~OE7@+wEhILrRLt&E zoEsF^nz>4yK1|EOU*kM+9317S;+bb7?TJM2UUpc!%sDp}7!<`i=W!ot8*C&fpj>mk#qt~GCeqcy)?W6sl>eUnR%yCBR&Ow-rc|q;lhnI+f-%`6Xf)% zIYZru;27%vA{Qi2=J`PQC<28;tFx(V^sgXf>)8WNxxQwT14M9I6- z+V0@tiCiDkv`7r-06sJS8@s|Lf>mV+8h}SPT4ZGPSMaFK7_SMXH$3KN7b2V?iV-jA zh1!Z>2tv^HVbHnNUAf-wQW#zMV(h8=3x2Swd|-%AczEIWLcm~EAu7rc3s%56b;7ME zj}$pe#fc^314Mb9i)xH^_#({)tTD4hsoz!7XcHUh9*G|}?k=D?9LBkTm2?fgaIG(%%$DL#}a-_990rQBU+M;jrf zCcvgM`+oyZmsUqc?lly9axZfO)02l$TMS#I+jHYY`Uk!gtDv|@GBQ||uaG^n*QR3Q z@tV?D;R;KmkxSDQh<2DkDC1?m?jTvf2i^T;+}aYhzL?ymNZmdns2e)}2V>tDCRw{= zTV3q3ZQDkdZQHi3?y{@8Y@1!SZQHi(y7|qSx$~Vl=iX<2`@y3eSYpsBV zI`Q-6;)B=p(ZbX55C*pu1C&yqS|@Pytis3$VDux0kxKK}2tO&GC;cH~759o?W2V)2 z)`;U(nCHBE!-maQz%z#zoRNpJR+GmJ!3N^@cA>0EGg?OtgM_h|j1X=!4N%!`g~%hdI3%yz&wq4rYChPIGnSg{H%i>96! z-(@qsCOfnz7ozXoUXzfzDmr>gg$5Z1DK$z#;wn9nnfJhy6T5-oi9fT^_CY%VrL?l} zGvnrMZP_P|XC$*}{V}b^|Hc38YaZQESOWqA1|tiXKtIxxiQ%Zthz?_wfx@<8I{XUW z+LH%eO9RxR_)8gia6-1>ZjZB2(=`?uuX|MkX082Dz*=ep%hMwK$TVTyr2*|gDy&QOWu zorR#*(SDS{S|DzOU$<-I#JTKxj#@0(__e&GRz4NuZZLUS8}$w+$QBgWMMaKge*2-) zrm62RUyB?YSUCWTiP_j-thgG>#(ZEN+~bMuqT~i3;Ri`l${s0OCvCM>sqtIX?Cy`8 zm)MRz-s^YOw>9`aR#J^tJz6$S-et%elmR2iuSqMd(gr6a#gA_+=N(I6%Cc+-mg$?_1>PlK zbgD2`hLZ?z4S~uhJf=rraLBL?H#c$cXyqt{u^?#2vX2sFb z^EU-9jmp{IZ~^ii@+7ogf!n_QawvItcLiC}w^$~vgEi(mX79UwDdBg`IlF42E5lWE zbSibqoIx*0>WWMT{Z_NadHkSg8{YW4*mZ@6!>VP>ey}2PuGwo%>W7FwVv7R!OD32n zW6ArEJX8g_aIxkbBl^YeTy5mhl1kFGI#n>%3hI>b(^`1uh}2+>kKJh0NUC|1&(l)D zh3Barl&yHRG+Le2#~u>KoY-#GSF>v)>xsEp%zgpq4;V6upzm3>V&yk^AD}uIF{vIn zRN-^d4(Sk6ioqcK@EObsAi#Z-u&Hh#kZdv1rjm4u=$2QF<6$mgJ4BE0yefFI zT7HWn?f668n!;x>!CrbdA~lDfjX?)315k1fMR~lG)|X_o()w|NX&iYUTKxI2TLl|r z{&TWcBxP>*;|XSZ1GkL&lSg?XL9rR4Ub&4&03kf};+6$F)%2rsI%9W_i_P|P%Z^b@ zDHH2LV*jB@Izq0~E4F^j04+C|SFiV8{!bth%bz(KfCg42^ zGz5P7xor$)I4VX}Cf6|DqZ$-hG7(}91tg#AknfMLFozF1-R~KS3&5I0GNb`P1+hIB z?OPmW8md3RB6v#N{4S5jm@$WTT{Sg{rVEs*)vA^CQLx?XrMKM@*gcB3mk@j#l0(~2 z9I=(Xh8)bcR(@8=&9sl1C?1}w(z+FA2`Z^NXw1t(!rpYH3(gf7&m=mm3+-sls8vRq z#E(Os4ZNSDdxRo&`NiRpo)Ai|7^GziBL6s@;1DZqlN@P_rfv4Ce1={V2BI~@(;N`A zMqjHDayBZ);7{j>)-eo~ZwBHz0eMGRu`43F`@I0g!%s~ANs>Vum~RicKT1sUXnL=gOG zDR`d=#>s?m+Af1fiaxYxSx{c5@u%@gvoHf#s6g>u57#@#a2~fNvb%uTYPfBoT_$~a^w96(}#d;-wELAoaiZCbM zxY4fKlS6-l1!b1!yra|`LOQoJB))=CxUAYqFcTDThhA?d}6FD$gYlk**!# zD=!KW>>tg1EtmSejwz{usaTPgyQm~o+NDg`MvNo)*2eWX*qAQ)4_I?Pl__?+UL>zU zvoT(dQ)pe9z1y}qa^fi-NawtuXXM>*o6Al~8~$6e>l*vX)3pB_2NFKR#2f&zqbDp7 z5aGX%gMYRH3R1Q3LS91k6-#2tzadzwbwGd{Z~z+fBD5iJ6bz4o1Rj#7cBL|x8k%jO z{cW0%iYUcCODdCIB(++gAsK(^OkY5tbWY;)>IeTp{{d~Y#hpaDa-5r#&Ha?+G{tn~ zb(#A1=WG1~q1*ReXb4CcR7gFcFK*I6Lr8bXLt9>9IybMR&%ZK15Pg4p_(v5Sya_70 ziuUYG@EBKKbKYLWbDZ)|jXpJJZ&bB|>%8bcJ7>l2>hXuf-h5Bm+ zHZ55e9(Sg>G@8a`P@3e2(YWbpKayoLQ}ar?bOh2hs89=v+ifONL~;q(d^X$7qfw=; zENCt`J*+G;dV_85dL3Tm5qz2K4m$dvUXh>H*6A@*)DSZ2og!!0GMoCPTbcd!h z@fRl3f;{F%##~e|?vw6>4VLOJXrgF2O{)k7={TiDIE=(Dq*Qy@oTM*zDr{&ElSiYM zp<=R4r36J69aTWU+R9Hfd$H5gWmJ?V){KU3!FGyE(^@i!wFjeZHzi@5dLM387u=ld zDuI1Y9aR$wW>s#I{2!yLDaVkbP0&*0Rw%6bi(LtieJQ4(1V!z!ec zxPd)Ro0iU%RP#L|_l?KE=8&DRHK>jyVOYvhGeH+Dg_E%lgA(HtS6e$v%D7I;JSA2x zJyAuin-tvpN9g7>R_VAk2y;z??3BAp?u`h-AVDA;hP#m+Ie`7qbROGh%_UTW#R8yfGp<`u zT0}L)#f%(XEE)^iXVkO8^cvjflS zqgCxM310)JQde*o>fUl#>ZVeKsgO|j#uKGi)nF_ur&_f+8#C0&TfHnfsLOL|l(2qn zzdv^wdTi|o>$q(G;+tkTKrC4rE)BY?U`NHrct*gVx&Fq2&`!3htkZEOfODxftr4Te zoseFuag=IL1Nmq45nu|G#!^@0vYG5IueVyabw#q#aMxI9byjs99WGL*y)AKSaV(zx z_`(}GNM*1y<}4H9wYYSFJyg9J)H?v((!TfFaWx(sU*fU823wPgN}sS|an>&UvI;9B(IW(V)zPBm!iHD} z#^w74Lpmu7Q-GzlVS%*T-z*?q9;ZE1rs0ART4jnba~>D}G#opcQ=0H)af6HcoRn+b z<2rB{evcd1C9+1D2J<8wZ*NxIgjZtv5GLmCgt?t)h#_#ke{c+R6mv6))J@*}Y25ef z&~LoA&qL-#o=tcfhjH{wqDJ;~-TG^?2bCf~s0k4Rr!xwz%Aef_LeAklxE=Yzv|3jf zgD0G~)e9wr@)BCjlY84wz?$NS8KC9I$wf(T&+79JjF#n?BTI)Oub%4wiOcqw+R`R_q<`dcuoF z%~hKeL&tDFFYqCY)LkC&5y(k7TTrD>35rIAx}tH4k!g9bwYVJ>Vdir4F$T*wC@$08 z9Vo*Q0>*RcvK##h>MGUhA9xix+?c1wc6xJhn)^9;@BE6i*Rl8VQdstnLOP1mq$2;!bfASHmiW7|=fA{k$rs^-8n{D6_ z!O0=_K}HvcZJLSOC6z-L^pl3Gg>8-rU#Sp1VHMqgXPE@9x&IHe;K3;!^SQLDP1Gk&szPtk| z!gP;D7|#y~yVQ?sOFiT*V(Z-}5w1H6Q_U5JM#iW16yZiFRP1Re z6d4#47#NzEm};1qRP9}1;S?AECZC5?6r)p;GIW%UGW3$tBN7WTlOy|7R1?%A<1!8Z zWcm5P6(|@=;*K&3_$9aiP>2C|H*~SEHl}qnF*32RcmCVYu#s!C?PGvhf1vgQ({MEQ z0-#j>--RMe{&5&$0wkE87$5Ic5_O3gm&0wuE-r3wCp?G1zA70H{;-u#8CM~=RwB~( zn~C`<6feUh$bdO1%&N3!qbu6nGRd5`MM1E_qrbKh-8UYp5Bn)+3H>W^BhAn;{BMii zQ6h=TvFrK)^wKK>Ii6gKj}shWFYof%+9iCj?ME4sR7F+EI)n8FL{{PKEFvB65==*@ ztYjjVTJCuAFf8I~yB-pN_PJtqH&j$`#<<`CruB zL=_u3WB~-;t3q)iNn0eU(mFTih<4nOAb>1#WtBpLi(I)^zeYIHtkMGXCMx+I zxn4BT0V=+JPzPeY=!gAL9H~Iu%!rH0-S@IcG%~=tB#6 z3?WE7GAfJ{>GE{?Cn3T!QE}GK9b*EdSJ02&x@t|}JrL{^wrM@w^&})o;&q816M5`} zv)GB;AU7`haa1_vGQ}a$!m-zkV(+M>q!vI0Swo18{;<>GYZw7-V-`G#FZ z;+`vsBihuCk1RFz1IPbPX8$W|nDk6yiU8Si40!zy{^nmv_P1=2H*j<^as01|W>BQS zU)H`NU*-*((5?rqp;kgu@+hDpJ;?p8CA1d65)bxtJikJal(bvzdGGk}O*hXz+<}J? zLcR+L2OeA7Hg4Ngrc@8htV!xzT1}8!;I6q4U&S$O9SdTrot<`XEF=(`1{T&NmQ>K7 zMhGtK9(g1p@`t)<)=eZjN8=Kn#0pC2gzXjXcadjHMc_pfV(@^3541)LC1fY~k2zn&2PdaW`RPEHoKW^(p_b=LxpW&kF?v&nzb z1`@60=JZj9zNXk(E6D5D}(@k4Oi@$e2^M%grhlEuRwVGjDDay$Qpj z`_X-Y_!4e-Y*GVgF==F0ow5MlTTAsnKR;h#b0TF>AyJe`6r|%==oiwd6xDy5ky6qQ z)}Rd0f)8xoNo)1jj59p;ChIv4Eo7z*{m2yXq6)lJrnziw9jn%Ez|A-2Xg4@1)ET2u zIX8`u5M4m=+-6?`S;?VDFJkEMf+=q?0D7?rRv)mH=gptBFJGuQo21rlIyP>%ymGWk z=PsJ>>q~i>EN~{zO0TklBIe(8i>xkd=+U@;C{SdQ`E03*KXmWm4v#DEJi_-F+3lrR z;0al0yXA&axWr)U%1VZ@(83WozZbaogIoGYpl!5vz@Tz5?u36m;N=*f0UY$ssXR!q zWj~U)qW9Q9Fg9UW?|XPnelikeqa9R^Gk77PgEyEqW$1j=P@L z*ndO!fwPeq_7J_H1Sx>#L$EO_;MfYj{lKuD8ZrUtgQLUUEhvaXA$)-<61v`C=qUhI zioV&KR#l50fn!-2VT`aMv|LycLOFPT{rRSRGTBMc)A`Cl%K&4KIgMf}G%Qpb2@cB* zw8obt-BI3q8Lab!O<#zeaz{P-lI2l`2@qrjD+Qy)^VKks5&SeT(I)i?&Kf59{F`Rw zuh7Q>SQNwqLO%cu2lzcJ7eR*3!g}U)9=EQ}js-q{d%h!wl6X3%H0Z2^8f&^H;yqti4z6TNWc& zDUU8YV(ZHA*34HHaj#C43PFZq7a>=PMmj4+?C4&l=Y-W1D#1VYvJ1~K%$&g-o*-heAgLXXIGRhU zufonwl1R<@Kc8dPKkb`i5P9VFT_NOiRA=#tM0WX2Zut)_ zLjAlJS1&nnrL8x8!o$G+*z|kmgv4DMjvfnvH)7s$X=-nQC3(eU!ioQwIkaXrl+58 z@v)uj$7>i`^#+Xu%21!F#AuX|6lD-uelN9ggShOX&ZIN+G#y5T0q+RL*(T(EP)(nP744-ML= z+Rs3|2`L4I;b=WHwvKX_AD56GU+z92_Q9D*P|HjPYa$yW0o|NO{>4B1Uvq!T;g_N- zAbNf%J0QBo1cL@iahigvWJ9~A4-glDJEK?>9*+GI6)I~UIWi>7ybj#%Po}yT6d6Li z^AGh(W{NJwz#a~Qs!IvGKjqYir%cY1+8(5lFgGvl(nhFHc7H2^A(P}yeOa_;%+bh` zcql{#E$kdu?yhRNS$iE@F8!9E5NISAlyeuOhRD)&xMf0gz^J927u5aK|P- z>B%*9vSHy?L_q)OD>4+P;^tz4T>d(rqGI7Qp@@@EQ-v9w-;n;7N05{)V4c7}&Y^!`kH3}Q z4RtMV6gAARY~y$hG7uSbU|4hRMn97Dv0$Le@1jDIq&DKy{D$FOjqw{NruxivljBGw zP4iM(4Nrz^^~;{QBD7TVrb6PB=B$<-e9!0QeE8lcZLdDeb?Gv$ePllO2jgy&FSbW* zSDjDUV^=`S(Oo0;k(Idvzh}aXkfO)F6AqB?wWqYJw-1wOn5!{-ghaHb^v|B^92LmQ9QZj zHA&X)fd%B$^+TQaM@FPXM$$DdW|Vl)4bM-#?Slb^qUX1`$Yh6Lhc4>9J$I4ba->f3 z9CeGO>T!W3w(){M{OJ+?9!MK68KovK#k9TSX#R?++W4A+N>W8nnk**6AB)e;rev=$ zN_+(?(YEX;vsZ{EkEGw%J#iJYgR8A}p+iW;c@V>Z1&K->wI>!x-+!0*pn|{f=XA7J zfjw88LeeJgs4YI?&dHkBL|PRX`ULOIZlnniTUgo-k`2O2RXx4FC76;K^|ZC6WOAEw zz~V0bZ29xe=!#Xk?*b{sjw+^8l0Koy+e7HjWXgmPa4sITz+$VP!YlJ$eyfi3^6gGx6jZLpbUzX;!Z6K}aoc!1CRi zB6Lhwt%-GMcUW;Yiy6Y7hX(2oksbsi;Z6k*=;y;1!taBcCNBXkhuVPTi+1N*z*}bf z`R=&hH*Ck5oWz>FR~>MO$3dbDSJ!y|wrff-H$y(5KadrA_PR|rR>jS=*9&J*ykWLr z-1Z^QOxE=!6I z%Bozo)mW7#2Hd$-`hzg=F@6*cNz^$#BbGlIf${ZV1ADc}sNl=B72g`41|F7JtZ^BT z+y}nqn3Ug`2scS_{MjykPW2~*k$i6PhvvxJCW;n!SK5B8Rpm41fCEdy=ea-4F`rN5 zF>ClKp#4?}pI7eR#6U|}t`DA!GQJB7nT$HVV*{qPjIRU1Ou3W;I^pCt54o|ZHvWaH zooFx9L%#yv)!P;^er5LCU$5@qXMhJ-*T5Ah8|}byGNU5oMp3V)yR;hWJKojJEregX z<1UPt%&~=5OuP(|B{ty);vLdoe7o^?`tkQa7zoXKAW6D@lc+FTzucotaOfJ!(Bm zHE8f8j@6||lH`y2<&hP}Q1wr(=6ze0D6NRL{7QaE1=nTAzqjIeD}Be&@#_d*dyurz z&L7xo-D9!dS`i>^GaIPArR@r=N#-ppIh!UBcb!N*?nLUO+*%C>_dCF1IH)q>5oT(t zjQo{AoDB;mWL;3&;vTt?;bvJSj>^Gq4Jrh}S}D>G)+b!>oRDWI?c_d77$kF5ms{Gx zak*>~*5AvaB-Xl)IgdZ^Cupv6HxQ0 zM(KPaDpPsPOd)e)aFw}|=tfzg@J1P8oJx2ZBY=g4>_G(Hkgld(u&~jN((eJ}5@b1} zI(P7j443AZj*I@%q!$JQ2?DZV47U!|Tt6_;tlb`mSP3 z74DE4#|1FMDqwYbT4P6#wSI%s?*wDc>)MR$4z9ZtJg04+CTUds>1JSDwI}=vpRoRR zLqx(Tvf34CvkTMOPkoH~$CG~fSZb;(2S4Q6Vpe9G83V={hwQ>acu+MCX)@0i>Vd`% z4I8Ye+7&Kcbh(*bN1etKmrpN)v|=eI+$oD=zzii6nP&w|kn2Y-f!(v<aE zKmOz#{6PZB(8zD={il`RO6D}v(@mN_66KXUAEefgg|;VmBfP?UrfB$&zaRw7oanna zkNmVGz4Vhd!vZSnp1(&_5^t;eSv6O771BloJAHi=Pnn+aa6y(e2iiE97uZ{evzQ^8 z*lN@ZYx<-hLXP^IuYLGf<01O*>nDp0fo;;Iyt`JADrxt7-jEF(vv_btyp6CT8=@5t zm`I0lW+2+_xj2CRL|40kcYysuyYeiGihGe&a)yilqP}5h+^)m8$=mzrUe`$(?BIY> zfF7-V10Gu0CkWF)wz04&hhI>es0NS7d`cnT`4y8K!wUAKv$H09fa>KeNQvwUNDT1zn}_*RHykC$CD%*h7vRCQ&Z z4&N-!L>(@8i?K$l5)13n0%VPPV`iG7Q$2{1T3JypLSvN%1kX73goBIOEmg=Uf$9e? zm}g>JFu}EQKH>|K!)m9teoCmTc`y2Ll}msZYyy0Pkqjeid66>DP_?C{KCw94lHvLW z-+X!2YSm70s833lH0o+|A%Xwsw`@8lE3ia0n_Dve;LC7@I+i~@%$lD|3fNf&R6ob6 z@iGfx^OC4s`$|vO!0jTWwVpX;X^EqJF{i324I>N=f@u+rTN+xJGGR0LsCQc;iFD=F zbZJrgOpS;04o^wP7HF5QBaJ$KJgS2V4u02ViWD=6+7rcu`uc&MOoyf%ZBU|gQZkUg z<}ax>*Fo?d*77Ia)+{(`X45{a8>Bi$u-0BWSteyp#GJnTs?&k&<0NeHA$Qb3;SAJK zl}H*~eyD-0qHI3SEcn`_7d zq@YRsFdBig+k490BZSQwW)j}~GvM7x>2ymO4zakaHZ!q6C2{fz^NvvD8+e%7?BQBH z-}%B{oROo2+|6g%#+XmyyIJrK_(uEbg%MHlBn3^!&hWi+9c0iqM69enep#5FvV_^r z?Yr(k*5FbG{==#CGI1zU0Wk{V?UGhBBfv9HP9A-AmcJmL^f4S zY3E2$WQa&n#WRQ5DOqty_Pu z-NWQGCR^Hnu^Vo2rm`-M>zzf|uMCUd1X0{wISJL2Pp=AO5 zF@(50!g|SYw3n<_VP0T~`WUjtY**6Npphr5bD%i3#*p7h8$#;XTLJAt5J-x~O1~`z z`2C~P4%XSI(JbrEmVMEwqdsa^aqXWg;A6KBn^jDxTl!}Q!^WhprL$kb(Iqq zUS`i$tIPs#hdE-zAaMGoxcG?Z;RO2L0Y|gcjV_)FFo|e)MtTl`msLTwq>po$`H6_U zhdWK97~M>idl9GE_WgobQkK_P85H_0jN?s3O)+m&68B`_;FnbZ3W*Qm++ghSs7|T4b7m~VVV%j0gl`Iw!?+-9#Lsb!j3O%fSTVuK z37V>qM81D+Atl};23`TqEAfEkQDpz$-1$e__>X2jN>xh@Sq)I6sj@< ziJ^66GSmW9c%F7eu6&_t$UaLXF4KweZecS1ZiHPWy-$e_7`jVk74OS*!z=l#(CQ^K zW-ke|g^&0o=hn+4uh-8lUh0>!VIXXnQXwKr>`94+2~<;+`k z$|}QZ>#pm2g}8k*;)`@EnM~ZQtci%_$ink9t6`HP{gn}P1==;WDAld3JX?k%^GcTU za>m|CH|UsyFhyJBwG5=`6562hkVRMQ=_ron-Vlm$4bG^GFz|Jh5mM{J1`!!hAr~8F^w> z^YhQ=c|bFn_6~9X$v(30v$5IX;#Nl-XXRPgs{g_~RS*znH^6Vhe}8>T?aMA|qfnWO zQpf(wr^PfygfM+m2u!9}F|frrZPBQ!dh(varsYo!tCV)WA(Wn^_t=WR_G7cQU`AGx zrK^B6<}9+$w;$vra)QWMKf_Tnqg93AMVZ6Qd=q6rdB{;ZhsoT zWy9QhnpEnc@Dauz4!8gq zqDanAX#$^vf-4~ZqUJtSe?SO+Hmb?)l2#}v(8}2+P{ZZuhlib0$3G0|a5?JR>QgUUP$HTE5hb`h>imq#7P+Y*-UVLm@9km|V# zoigziFt$bxgQMwqKKhd!c--&ciywIED>faY3zHLrA{V#IA)!mq!FXxf?1coGK~N(b zjwu*@2B1^(bzFVBJO`4EJ$=it!a0kbgUvPL;Er(0io{W4G7Bkqh)=g)uS|l0YfD}f zaCJwY7vR-D=P9M68`cmtmQ^!F-$lt@0S|9G7cHgT13A0xMv)HmH#Z<4{~iYo_VOD{ z5!kU+>mUOvHouw+-y?*cNlUlDwD#;6ZvAIc$YcwG&qKZFh>EtM(Eda+w)E$HcfZyB zG*$<*ae_ApE%gxWx%O^~XMnRSNLv!y`g99F(J_m)spJAc95P|_joOIoru%atbw z9PYgkcE*8x#)-W{>96KDl&74iW<#wrK)1s zxzU{`rW5af+dT6Z@_1dG<}CtDMT`EGVEXSL_5D9)Z;6UJe-TW7)M?bY%E;8G?Yc!$ zic;F5=#dba^P~7f#qvC}Nd#XEo2r_UlgfR_`B2^W0QjXU?RAi$>f&{G_Lu8Fp0qDp z?vAdm%z#3kcZmaJ@afooB=A@>8_N~O9Yzu=ZCEikM>UgU+{%>pPvmSNzGk@*jnc5~ z(Z#H4OL^gw>)gqZ!9X|3i4LAdp9vo)?F9QCR3##{BHoZ73Uk^Ha={2rc*TBijfKH- z=$cZQdc<5%*$kVo|{+bL3 zEoU&tq*YPR)^y-SISeQNQ)YZ9v>Hm4O=J)lf(y=Yu1ao&zj#5GVGxyj%V%vl9}dw< zO;@NRd4qe@Et}E@Q;SChBR2QPKll1{*5*jT*<$$5TywvC77vt=1=0xZ46>_17YzbiBoDffH(1_qFP7v2SVhZmA_7JDB50t#C39 z8V<9(E?bVWI<7d6MzcS^w!XmZ**{AO!~DZNU)pgr=yY1 zT@!AapE;yg&hmj*g{I3vd## zx+d%^O?d%%?Dba|l~X6ZOW|>FPsrjPjn-h4swysH!RNJUWofC?K(^0uHrBPrH5#W> zMn8^@USzjUucqo%+5&))Dnnw`5l1mp>roaA99Nkk4keZl2wAF7oa(!x?@8uGWzc5Q zM}g`}zf-D@B6lVFYWmmJ8a+_%z8g$C7Ww~PD9&jki08NY!b!fK288R;E?e3Z+Pk{is%HxQU`xu9+y5 zq?DWJD7kKp(B2J$t5Ij8-)?g!T9_n<&0L8F5-D0dp>9!Qnl#E{eDtkNo#lw6rMJG$ z9Gz_Z&a_6ie?;F1Y^6I$Mg9_sml@-z6t!YLr=ml<6{^U~UIbZUUa_zy>fBtR3Rpig zc1kLSJj!rEJILzL^uE1mQ}hjMCkA|ZlWVC9T-#=~ip%McP%6QscEGlYLuUxDUC=aX zCK@}@!_@~@z;70I+Hp5#Tq4h#d4r!$Np1KhXkAGlY$ap7IZ9DY})&(xoTyle8^dBXbQUhPE6ehWHrfMh&0=d<)E2+pxvWo=@`^ zIk@;-$}a4zJmK;rnaC)^a1_a_ie7OE*|hYEq1<6EG>r}!XI9+(j>oe!fVBG%7d}?U z#ja?T@`XO(;q~fe2CfFm-g8FbVD;O7y9c;J)k0>#q7z-%oMy4l+ zW>V~Y?s`NoXkBeHlXg&u*8B7)B%alfYcCriYwFQWeZ6Qre!4timF`d$=YN~_fPM5Kc8P;B-WIDrg^-j=|{Szq6(TC)oa!V7y zLmMFN1&0lM`+TC$7}on;!51{d^&M`UW ztI$U4S&}_R?G;2sI)g4)uS-t}sbnRoXVwM!&vi3GfYsU?fSI5Hn2GCOJ5IpPZ%Y#+ z=l@;;{XiY_r#^RJSr?s1) z4b@ve?p5(@YTD-<%79-%w)Iv@!Nf+6F4F1`&t~S{b4!B3fl-!~58a~Uj~d4-xRt`k zsmGHs$D~Wr&+DWK$cy07NH@_z(Ku8gdSN989efXqpreBSw$I%17RdxoE<5C^N&9sk!s2b9*#}#v@O@Hgm z2|U7Gs*@hu1JO$H(Mk)%buh~*>paY&Z|_AKf-?cz6jlT-v6 zF>l9?C6EBRpV2&c1~{1$VeSA|G7T(VqyzZr&G>vm87oBq2S%H0D+RbZm}Z`t5Hf$C zFn7X*;R_D^ z#Ug0tYczRP$s!6w<27;5Mw0QT3uNO5xY($|*-DoR1cq8H9l}_^O(=g5jLnbU5*SLx zGpjfy(NPyjL`^Oln_$uI6(aEh(iS4G=$%0;n39C(iw79RlXG>W&8;R1h;oVaODw2nw^v{~`j(1K8$ z5pHKrj2wJhMfw0Sos}kyOS48Dw_~=ka$0ZPb!9=_FhfOx9NpMxd80!a-$dKOmOGDW zi$G74Sd(-u8c!%35lL|GkyxZdlYUCML{V-Ovq{g}SXea9t`pYM^ioot&1_(85oVZ6 zUhCw#HkfCg7mRT3|>99{swr3FlA@_$RnE?714^o;vps4j4}u=PfUAd zMmV3j;Rogci^f!ms$Z;gqiy7>soQwo7clLNJ4=JAyrz;=*Yhe8q7*$Du970BXW89Xyq92M4GSkNS-6uVN~Y4r7iG>{OyW=R?@DmRoi9GS^QtbP zFy2DB`|uZTv8|ow|Jcz6?C=10U$*_l2oWiacRwyoLafS!EO%Lv8N-*U8V+2<_~eEA zgPG-klSM19k%(%;3YM|>F||hE4>7GMA(GaOvZBrE{$t|Hvg(C2^PEsi4+)w#P4jE2XDi2SBm1?6NiSkOp-IT<|r}L9)4tLI_KJ*GKhv16IV}An+Jyx z=Mk`vCXkt-qg|ah5=GD;g5gZQugsv!#)$@ zkE=6=6W9u9VWiGjr|MgyF<&XcKX&S3oN{c{jt-*1HHaQgY({yjZiWW97rha^TxZy< z2%-5X;0EBP>(Y9|x*603*Pz-eMF5*#4M;F`QjTBH>rrO$r3iz5 z?_nHysyjnizhZQMXo1gz7b{p`yZ8Q78^ zFJ3&CzM9fzAqb6ac}@00d*zjW`)TBzL=s$M`X*0{z8$pkd2@#4CGyKEhzqQR!7*Lo@mhw`yNEE6~+nF3p;Qp;x#-C)N5qQD)z#rmZ#)g*~Nk z)#HPdF_V$0wlJ4f3HFy&fTB#7Iq|HwGdd#P3k=p3dcpfCfn$O)C7;y;;J4Za_;+DEH%|8nKwnWcD zBgHX)JrDRqtn(hC+?fV5QVpv1^3=t2!q~AVwMBXohuW@6p`!h>>C58%sth4+Baw|u zh&>N1`t(FHKv(P+@nT$Mvcl){&d%Y5dx|&jkUxjpUO3ii1*^l$zCE*>59`AvAja%`Bfry-`?(Oo?5wY|b4YM0lC?*o7_G$QC~QwKslQTWac z#;%`sWIt8-mVa1|2KH=u!^ukn-3xyQcm4@|+Ra&~nNBi0F81BZT$XgH@$2h2wk2W% znpo1OZuQ1N>bX52II+lsnQ`WVUxmZ?4fR_f0243_m`mbc3`?iy*HBJI)p2 z`GQ{`uS;@;e1COn-vgE2D!>EheLBCF-+ok-x5X8Cu>4H}98dH^O(VlqQwE>jlLcs> zNG`aSgDNHnH8zWw?h!tye^aN|%>@k;h`Z_H6*py3hHO^6PE1-GSbkhG%wg;+vVo&dc)3~9&` zPtZtJyCqCdrFUIEt%Gs_?J``ycD16pKm^bZn>4xq3i>9{b`Ri6yH|K>kfC; zI5l&P)4NHPR)*R0DUcyB4!|2cir(Y1&Bsn3X8v4D(#QW8Dtv@D)CCO zadQC85Zy=Rkrhm9&csynbm>B_nwMTFah9ETdNcLU@J{haekA|9*DA2pY&A|FS*L!*O+>@Q$00FeL+2lg2NWLITxH5 z0l;yj=vQWI@q~jVn~+5MG!mV@Y`gE958tV#UcO#56hn>b69 zM;lq+P@MW=cIvIXkQmKS$*7l|}AW%6zETA2b`qD*cL z(=k4-4=t6FzQo#uMXVwF{4HvE%%tGbiOlO)Q3Y6D<5W$ z9pm>%TBUI99MC`N9S$crpOCr4sWJHP)$Zg#NXa~j?WeVo03P3}_w%##A@F|Bjo-nNxJZX%lbcyQtG8sO zWKHes>38e-!hu1$6VvY+W-z?<942r=i&i<88UGWdQHuMQjWC-rs$7xE<_-PNgC z_aIqBfG^4puRkogKc%I-rLIVF=M8jCh?C4!M|Q=_kO&3gwwjv$ay{FUDs?k7xr%jD zHreor1+#e1_;6|2wGPtz$``x}nzWQFj8V&Wm8Tu#oaqM<$BLh+Xis=Tt+bzEpC}w) z_c&qJ6u&eWHDb<>p;%F_>|`0p6kXYpw0B_3sIT@!=fWHH`M{FYdkF}*CxT|`v%pvx z#F#^4tdS0|O9M1#db%MF(5Opy;i( zL(Pc2aM4*f_Bme@o{xMrsO=)&>YKQw+)P-`FwEHR4vjU>#9~X7ElQ#sRMjR^Cd)wl zg^67Bgn9CK=WP%Ar>T4J!}DcLDe z=ehSmTp##KyQ78cmArL=IjOD6+n@jHCbOatm)#4l$t5YV?q-J86T&;>lEyK&9(XLh zr{kPuX+P8LN%rd%8&&Ia)iKX_%=j`Mr*)c)cO1`-B$XBvoT3yQCDKA>8F0KL$GpHL zPe?6dkE&T+VX=uJOjXyrq$BQ`a8H@wN1%0nw4qBI$2zBx)ID^6;Ux+? zu{?X$_1hoz9d^jkDJpT-N6+HDNo%^MQ2~yqsSBJj4@5;|1@w+BE04#@Jo4I63<~?O?ok%g%vQakTJKpMsk&oeVES1>cnaF7ZkFpqN6lx` zzD+YhR%wq2DP0fJCNC}CXK`g{AA6*}!O}%#0!Tdho4ooh&a5&{xtcFmjO4%Kj$f(1 zTk||{u|*?tAT{{<)?PmD_$JVA;dw;UF+x~|!q-EE*Oy?gFIlB*^``@ob2VL?rogtP z0M34@?2$;}n;^OAV2?o|zHg`+@Adk+&@Syd!rS zWvW$e5w{onua4sp+jHuJ&olMz#V53Z5y-FkcJDz>Wk%_J>COk5<0ya*aZLZl9LH}A zJhJ`Q-n9K+c8=0`FWE^x^xn4Fa7PDUc;v2+us(dSaoIUR4D#QQh91R!${|j{)=Zy1 zG;hqgdhSklM-VKL6HNC3&B(p1B)2Nshe7)F=-HBe=8o%OhK1MN*Gq6dBuPvqDRVJ{ z;zVNY?wSB%W0s^OMR_HL(Ws)va7eWGF*MWx<1wG7hZ}o=B62D?i|&0b14_7UG287YDr%?aYMMpeCkY1i`b+H!J9sqrvKc#Y6c8At@QiLSwj)@ifz~Z|c$lOMA@?cPqFRmZ%_>bz2X4(B=`^3;MDjsEeAO=? zSoD&+L>A|fGt7+6kF2@LqhL06sD%|~YsIe=EcWqy{e_61N_D(*CacnMvyXMjP87HI z4PT6!$fzxx{}=>jeqzkkoN+!r9e|@lZUN4pn(T28v`k=_vIhTn^i9O3qTqd)-%!QQ zYB6*6B@&b(!#X4C~59SLZuorNU_wWZA36{>O%iX)VS5NNZh49C_ppI>?)wwml}_0MLzOXT>lmo#&Ew6d?mu8~~I_^4VGBQtCAke;RQa5DL` z1PFDPsKb3CS$v;RhlQ1J@AHa1VRuuxp}NOIvrC>4$$A0Ix0VpAc0lfG%8{mR{TRQ( zbXM#1Tci3H*Wt>cVuMta^6^z`=^B@j+YhJqq9?>zZPxyg2U(wvod=uwJs{8gtpyab zXHQX<0FOGW6+dw&%c_qMUOI^+Rnb?&HB7Fee|33p4#8i>%_ev(aTm7N1f#6lV%28O zQ`tQh$VDjy8x(Lh#$rg1Kco$Bw%gULq+lc4$&HFGvLMO30QBSDvZ#*~hEHVZ`5=Kw z3y^9D512@P%d~s{x!lrHeL4!TzL`9(ITC97`Cwnn8PSdxPG@0_v{No|kfu3DbtF}K zuoP+88j4dP+Bn7hlGwU$BJy+LN6g&d3HJWMAd1P9xCXG-_P)raipYg5R{KQO$j;I9 z1y1cw#13K|&kfsRZ@qQC<>j=|OC?*v1|VrY$s=2!{}e33aQcZghqc@YsHKq^)kpkg z>B;CWNX+K=u|y#N)O>n5YuyvPl5cO6B^scmG?J zC8ix)E1PlhNaw8FpD+b|D$z`Id^4)rJe78MNiBga?Z- z0$L&MRTieSB1_E#KaN*H#Ns1}?zOA%Ybr{G+Sn3moXTVZj=L`nt?D&-MjOMz-Yq&@ z$P3h23d_F8Dcf*?txX7}p>nM*s+65t z1il8bHHsBynUK|aEXSjzY6sz1nZ%|%XeWTcGLRyRl@q4YAR)JovbdTTY&7u>@}28A zgV^Npp?}I!?3K7IXu9ml-Lw;w@9m zBYTeU+Seh8uJ-w?4e_6byq0f7>O3xm(hO}Y=fgU5^vW|>0yQ^0+?}LT55ei$i zzlU-iRbd8TRX9Ept%h%ariV=%u%F@@FA>U*XdAalcH%>#5_a&w)g`uW%3}m?vP- zc5}DkuF6ruKDwEYj+2YTSQ9=rkp19U5P@(zRm(nLod(sG9{~nw1BUoS2OFDXa{xfw zZ~UaZLFUZxfQ*9?_X?*~`d;nn-BbaefLJ`DT13KF6?T5Mnt;v5d>H}s)aAIzJcs#B z|CuXPJKww}hWBKsUfks#Kh$)ptp?5U1b@ttXFRbe_BZ&_R9XC6CA4WhWhMUE9Y2H4 z{w#CBCR<)Fd1M;mx*m?Z=L-^1kv1WKtqG(BjMiR4M^5yN4rlFM6oGUS2Wf~7Z@e*- ze84Vr`Bmi!(a1y}-m^HHMpbAiKPVEv|(7=|}D#Ihfk+-S5Hlkfch02z&$(zS3vrYz2g*ic{xBy~*gIp(eG}^gMc7 zPu2Eivnp@BH3SOgx!aJXttx*()!=2)%Bf$Gs^4cCs@)=(PJNxhH5lVY&qSZYaa?A^LhZW`B9(N?fx<^gCb(VE%3QpA*_Pohgp6vCB36iVaq zc1TI%L2Le?kuv?6Dq`H+W>AqnjyEzUBK948|DB|)U0_4DzWF#7L{agwo%y$hC>->r z4|_g_6ZC!n2=GF4RqVh6$$reQ(bG0K)i9(oC1t6kY)R@DNxicxGxejwL2sB<>l#w4 zE$QkyFI^(kZ#eE5srv*JDRIqRp2Totc8I%{jWhC$GrPWVc&gE1(8#?k!xDEQ)Tu~e zdU@aD8enALmN@%1FmWUz;4p}41)@c>Fg}1vv~q>xD}KC#sF|L&FU);^Ye|Q;1#^ps z)WmmdQI2;%?S%6i86-GD88>r|(nJackvJ#50vG6fm$1GWf*f6>oBiDKG0Kkwb17KPnS%7CKb zB7$V58cTd8x*NXg=uEX8Man_cDu;)4+P}BuCvYH6P|`x-#CMOp;%u$e z&BZNHgXz-KlbLp;j)si^~BI{!yNLWs5fK+!##G;yVWq|<>7TlosfaWN-;C@oag~V`3rZM_HN`kpF`u1p# ztNTl4`j*Lf>>3NIoiu{ZrM9&E5H~ozq-Qz@Lkbp-xdm>FbHQ2KCc8WD7kt?=R*kG# z!rQ178&ZoU(~U<;lsg@n216Ze3rB2FwqjbZ=u|J?nN%<4J9(Bl(90xevE|7ejUYm9 zg@E_xX}u2d%O1mpA2XzjRwWinvSeg)gHABeMH(2!A^g@~4l%8e0WWAkBvv60Cr>TR zQB1%EQ zUoZeUdqjh+1gFo6h~C~z#A57mf5ibmq$y_uVtA_kWv8X)CzfVEooDaY!#P?5$Y zGPKXbE<75nc%D-|w4OrP#;87oL@2^4+sxKah;a-5&z_&SUf~-z(1}bP=tM^GYtR3a z!x4zjSa^)KWG6jxfUI#{<26g$iAI;o_+B{LXY@WfWEdEl6%#8s3@b`?&Tm#aSK!~| z^%DdrXnijW`d!ajWuKApw&{L+WCPpFialo&^dZ9jC7A%BO`2ZF&YUDe;Yu|zFuv`2 z)BE*7Lkay)M7uohJ)446X``0x0%PzPTWY92`1Oq4a2D_7V0wypPnXFR)WM0IlFgg@ zqz#hv2xJEQL8eu}O;e(w4rSA?5|eZHbS6jENytJBq59?bOf>Wrl8ySZH36H(6fGR#vHM6q zn}!7!I@4$*+LFXs{x?|=q2*QtYT%Lw3+5(8uc0j8o3}TrG(zSV#>4wo6~)u|R+Yx# z?0$AspZDjv{dfv417~C17Oy%Fal{%+B6H(NX`$Bl>II-L3N3 zZc+sKZbqewU*&_Xt;9k=%4*aVYBvE1n&JZS7Uqjd%n8nOQmzh^x#vWK{;In~=QO)g zT-n3OU(1@3QfL|$g1d2xeBb@O15Rl01+hmpup2De7p%Yrd$E7(In!*R+;IJZh}v!svi z;7N~pq8KZDXXap0qd_D=Y^B)rz4S0^SF=&v6YYTAV$ad43#x!+n~-6< zK{8*vWoAdW(gGGt&URD}@g6tMoY(+Lw=vvxhfIIK9AjvNF_(W}1Rxn(mp;tJfDV<0 zbJN0t(@Xb8UeO{&T{$$uDrs7)j$}=?WsuDl+T2N5Y<4TMHGOMcocPr$%~(yvtKv(n z`U96d!D0cb9>Dx2zz$m&lAhazs%UeR^K*gb>d8CPs+?qlpfA;t{InXa)^2ryC(FU(Zc6Xbnnh`lg`K&g^JeS>}^c0MJKUCfV+~ zV(EN0Z5ztoN;hqcj!8V+VRbSltJ<~|y`U+9#wv|~H zNE!j9uXa=dec@JQSgJ6N6@Il&tzCBJv9#ldR`Lm*<)YwH4tdlAlG0Fl8Nfa(J~c%DQ2AA-}x8D=p(l#n1+hgx;N;1Aq?lq@{Lt9FKu89CjnnHD1G_@p;%Lp`+b@ttb33!E_Xt;QUD9~nRQl&xAro9-{+&6^ljK2f-d>&qy&d#0xwH z@slNv@ULKp!Cf*JHuS@#4c?F->WjPc)yiuSargAIEg>muRxzY?Hzdq@G5CS)U1*Et zE2SLh=@DI1J(guiy2Igq(?(xI9WL%g^f@{5Hmr|!Qz4`vn|LjrtO=b~I6~5EU5Fxy z;-#<)6w#w=DkpSthAu+E;OL?!?6C9Mwt*o(@68(Jhvs-eX4V z=d=>HI|`3J%H5X|gSrC8KH^IL?h5=3ID6svwHH@(wRbSG`Zsor^q4`3PCn#-(YX?< z_q8+T)51$E0xyKR{L!LN(G=+9K6$3#PDT^IAe|Igkx=!4#rqKWoXiZdh`&ocjp=Ok zemJe6*{it~>;sr(B0fSmp(S#*y5I0)OOz~Oe6Im+($S}e3tyx7Y6pA8vKCBmSEQDa zLfkm*;uMbTLpcR0)tF_v-lbK%`5>POyI2E(!)2=Rj0p;WKi=|UNt6HsQv0xR3QIK9 zsew(AFyzH!7Azxum{%VC^`cqhGdGbABGQ4cYdNBPTx+XpJ=NUEDeP^e^w^AOE1pQI zP{Us-sk!v$gj}@684E!uWjzvpoF|%v-6hwnitN1sCSg@(>RDCVgU8Ile_-xX`hL6u zzI4*Q)AVu(-ef8{#~P9STQ5t|qIMRoh&S?7Oq+cL6vxG?{NUr@k(~7^%w)P6nPbDa~4Jw}*p-|cT4p1?)!c0FoB(^DNJ+FDg+LoP6=RgB7Or673WD5MG&C!4< zerd6q$ODkBvFoy*%cpHGKSt z3uDC6Sc=xvv@kDzRD)aIO`x}BaWLycA%(w-D`Pd+uL*rL|etagQ;U&xt_9?7#}=}5HI)cU-0 z%pMA`>Xb7s)|Y)4HKSZOu;{lg=KjeIyXb0{@EM`FTDkLRH`!W%z*lQJ74P%Ka76)H zblrSIzf+dMWbO`g;=(b@{pS)zUcO&GrIFe%&?YeX4r8B2bBArB%-5ZrQ+vonr%AYy z1+u0*K{UVUmV>h5vD!F;6}a%KdMZQLs04oGkpiaC)zI( zT2U9qta5o|6Y+It1)sE8>u&0)W~l$NX@ZQ8UZfB=`($EW6?FT%{EoRhOrb9)z@3r8y?Z99FNLDE;7V=Q zotj&igu*Rh^VQn3MQKBq!T{yTwGhn1YL6k*?j?{_ek5xe8#i#GG4S-a_Re2lssG!} z`Y-d0BcOdB@!m?4y&hMN68}#0-IIlm_xO)d#}ugX{q^OZe{-@LeJyv`cY&ze4t2~! zKb{qX-j;kt{?gC(vW%}X4pm@1F?~LH{^Q8d@X$dy@5ff~p!J3zmA>H`A)y+6RB_h* zZfIO+bd=*LiymRw{asW%xxaVl33_xtdVrrqIPn zc@y8oMJvNtgcO~4i0`f)GCFkWY8EF?4duLVjHTdb6oYLnO9}Q-pe{CKQJL)hV8)JI z$mVA0Dq&7Z1TbYdSC(WbJ+IBjXngZTu&I+vHF|>Zo$757{8lL;8Zr-Exkf?3jzN5k z_d9I>{>^J?!l)< zNd$7E9FVrta}3qy3L7Ys$^fRWNuu^hs^{*eXvazd&+Q*?lTfc>2+EdP(o0P_Z05HX zVKsfFAQ{t^CRu~Dw(CuJ>tvx*p$5@flA>QRl455b&{*U?xU8`)nF2T$uu_(l8VNtq z?pBiRQIckGzk8W&SFSB=g6eG`ZC;6v9w`?eF*S}3E@N`2ropeHP)E}o?qJkyVEI;K$!)bWY zt9>4WmDVJh7U~m$|K`T#hF!v|znj^=M;69uXrFys#51XT;DbMr4H)>7UQ1e2(cuQf z4kr~Tt1tpBB2GaJ(|j~lHgW40EgMMVqR6eJoJig1SBg|2=$~4I3P0eP$q%_`sS&4~ z26=&a&tLjQbch1`cVXa-2fTl1y8}->|Nqu?uVrNTov!=VKh)g89wUPTgAzkSKZ57_ zr=B^mcldE3K04t4{;RaG53&9yovq;@aR#VHx+R1^^*kr-vEEd!uea68Z<{R%_DD6fn&T4 zu;fDj07L-(_fLSJGdkeh&c&7A(ZLj`7iwnkAcqUexU;WjUkqeg1m1-IUZTIZA(4dtr2Gr`e{BIejlCgS<33MB=1!8?a74!F%=Uo7N`F@k} ze+1C_eU4Y_$mvdjci zwEtCIphA2PBzBhng5=M#e4r%)RW5rVD|_`PvY$7BK`}w~d>%0O9sY#*LUAq=^OjMF^PY5m<7!=s5jyRfosCQAo#hL`h5vN-M}6Q z0Li}){5?wi8)GVHNkF|U9*8V5ej)nhb^TLw1KqiPK(@{P1^L&P=`ZNt?_+}&0(8Uh zfyyZFPgMV7ECt;Jdw|`|{}b$w4&x77VxR>8wUs|GQ5FBf1UlvasqX$qfk5rI4>Wfr zztH>y`=daAef**C12yJ7;LDf&3;h3X+5@dGPy@vS(RSs3CWimbTp=g \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/build/android/local.properties b/build/android/local.properties new file mode 100644 index 0000000..b03a2b4 --- /dev/null +++ b/build/android/local.properties @@ -0,0 +1,8 @@ +## This file must *NOT* be checked into Version Control Systems, +# as it contains information specific to your local configuration. +# +# Location of the SDK. This is only used by Gradle. +# For customization when using a Version Control System, please read the +# header note. +#Thu Apr 09 18:23:32 CEST 2020 +sdk.dir=/home/auygun/Android/Sdk diff --git a/build/android/settings.gradle b/build/android/settings.gradle new file mode 100644 index 0000000..573abcb --- /dev/null +++ b/build/android/settings.gradle @@ -0,0 +1,2 @@ +include ':app' + diff --git a/build/linux/Makefile b/build/linux/Makefile new file mode 100644 index 0000000..6e1dfe3 --- /dev/null +++ b/build/linux/Makefile @@ -0,0 +1,174 @@ +.DEFAULT_GOAL := all + +# --- Input variables --- +BUILD ?= release +ifeq ($(findstring $(BUILD),debug release),) + $(error BUILD must be set to debug or release) +endif + +# Build all executables by default. +APPS ?= gltest + +# If the VERBOSE flag isn't set, then mute superfluous output. +ifeq ($(VERBOSE),) + HUSH_COMPILE = @echo "Compiling $<"; + HUSH_LINK = @echo "Linking $@"; + HUSH_GENERATE = @echo "Generating $@"; + HUSH_CLEAN = @ +endif + +# --- Internal variables --- +ARCH := $(shell uname -p) +SRC_ROOT := $(abspath ../../src) +OUTPUT_DIR := $(abspath .) +INTERMEDIATE_DIR := $(OUTPUT_DIR)/obj +BUILD_DIR := $(INTERMEDIATE_DIR)/$(BUILD) + +ARFLAGS = r +LDFLAGS = -lX11 -lGL -lz -pthread -lasound + +# Always enable debug information. +CFLAGS += -g + +# Flags to generate dependency information. +CFLAGS += -MD -MP -MT $@ + +# Predefined flags. +ifeq ($(BUILD), debug) + CFLAGS += -D_DEBUG +endif + +# Enable compiler optimizations for everything except debug. +# Note that a very aggresssive optimization level is used and it may not be +# valid for all standard compliant programs. Reduce this level on individual +# files or modules as needed. +ifneq ($(BUILD), debug) + CFLAGS += -Ofast +endif + +# Flag to turn on extended instruction sets for the compiler. +CFLAGS += -msse2 + +# Let C++ inherit all C flags. +CXXFLAGS = $(CFLAGS) + +# Enable C++17 +CXXFLAGS += -std=c++17 + +# --- Internal functions --- +app_exe = $(OUTPUT_DIR)/$(1)_$(ARCH)_$(BUILD) +objs_from_src = $(patsubst $(SRC_ROOT)/%, $(BUILD_DIR)/%.o, $(basename $(1))) +objs_from_src_in = $(call objs_from_src, $(shell find $(1) -name "*.cc" -o -name "*.cpp" -o -name "*.c")) + +# --- gltest application --- +ifneq ($(filter gltest,$(APPS)),) + +GLTEST_SRC := \ + $(SRC_ROOT)/base/collusion_test.cc \ + $(SRC_ROOT)/base/log.cc \ + $(SRC_ROOT)/base/random.cc \ + $(SRC_ROOT)/base/task_runner.cc \ + $(SRC_ROOT)/base/timer.cc \ + $(SRC_ROOT)/base/vecmath.cc \ + $(SRC_ROOT)/base/worker.cc \ + $(SRC_ROOT)/demo/credits.cc \ + $(SRC_ROOT)/demo/demo.cc \ + $(SRC_ROOT)/demo/enemy.cc \ + $(SRC_ROOT)/demo/hud.cc \ + $(SRC_ROOT)/demo/menu.cc \ + $(SRC_ROOT)/demo/player.cc \ + $(SRC_ROOT)/demo/sky_quad.cc \ + $(SRC_ROOT)/engine/animatable.cc \ + $(SRC_ROOT)/engine/animator.cc \ + $(SRC_ROOT)/engine/audio/audio_alsa.cc \ + $(SRC_ROOT)/engine/audio/audio_base.cc \ + $(SRC_ROOT)/engine/audio/audio_resource.cc \ + $(SRC_ROOT)/engine/engine.cc \ + $(SRC_ROOT)/engine/font.cc \ + $(SRC_ROOT)/engine/image_quad.cc \ + $(SRC_ROOT)/engine/image.cc \ + $(SRC_ROOT)/engine/mesh.cc \ + $(SRC_ROOT)/engine/platform/asset_file_linux.cc \ + $(SRC_ROOT)/engine/platform/asset_file.cc \ + $(SRC_ROOT)/engine/platform/platform_linux.cc \ + $(SRC_ROOT)/engine/platform/platform.cc \ + $(SRC_ROOT)/engine/renderer/geometry.cc \ + $(SRC_ROOT)/engine/renderer/render_command.cc \ + $(SRC_ROOT)/engine/renderer/render_resource.cc \ + $(SRC_ROOT)/engine/renderer/renderer_linux.cc \ + $(SRC_ROOT)/engine/renderer/renderer_types.cc \ + $(SRC_ROOT)/engine/renderer/renderer.cc \ + $(SRC_ROOT)/engine/renderer/shader.cc \ + $(SRC_ROOT)/engine/renderer/texture.cc \ + $(SRC_ROOT)/engine/shader_source.cc \ + $(SRC_ROOT)/engine/solid_quad.cc \ + $(SRC_ROOT)/engine/sound_player.cc \ + $(SRC_ROOT)/engine/sound.cc \ + $(SRC_ROOT)/third_party/glew/glew.c \ + $(SRC_ROOT)/third_party/jsoncpp/jsoncpp.cc \ + $(SRC_ROOT)/third_party/minizip/ioapi.c \ + $(SRC_ROOT)/third_party/minizip/unzip.c \ + $(SRC_ROOT)/third_party/r8b/pffft.cpp \ + $(SRC_ROOT)/third_party/r8b/r8bbase.cpp \ + $(SRC_ROOT)/third_party/texture_compressor/dxt_encoder_internals.cc \ + $(SRC_ROOT)/third_party/texture_compressor/dxt_encoder.cc \ + $(SRC_ROOT)/third_party/texture_compressor/texture_compressor_etc1.cc \ + $(SRC_ROOT)/third_party/texture_compressor/texture_compressor.cc + +GLTEST_EXE := $(call app_exe,gltest) +GLTEST_OBJS := $(call objs_from_src, $(GLTEST_SRC)) +EXES += $(GLTEST_EXE) +OBJS += $(GLTEST_OBJS) + +$(GLTEST_EXE): $(GLTEST_OBJS) $(LIBS) + +endif + + +# --- Build rules --- + +# Dependencies. +DEPS = $(OBJS:.o=.d) +-include $(DEPS) + +.PHONY: all clean cleanall help + +all: $(EXES) + +clean: + @echo "Cleaning..." + $(HUSH_CLEAN) $(RM) -r $(BUILD_DIR) + +cleanall: + @echo "Cleaning all..." + $(HUSH_CLEAN) $(RM) -r $(INTERMEDIATE_DIR) + +help: + @echo "BUILD = Build mode. One of:" + @echo " debug (no optimizations)" + @echo " release (optimizations, the default)" + @echo "APPS = Applications to build. Defaults to all." + @echo "VERBOSE = Full output from commands if set." + +# It's important that libraries are specified last as Ubuntu uses "ld --as-needed" by default. +# Only the static libraries referenced by the object files will be linked into the executable. +# Beware that circular dependencies doesn't work with this flag. +$(EXES): + @mkdir -p $(@D) + $(HUSH_LINK) $(CXX) -o $@ $^ $(LDFLAGS) + +$(BUILD_DIR)/%.a: + @mkdir -p $(@D) + $(HUSH_GENERATE) $(AR) $(ARFLAGS) $@ $^ + +$(BUILD_DIR)/%.o: $(SRC_ROOT)/%.c + @mkdir -p $(@D) + $(HUSH_COMPILE) $(CC) -c $(CFLAGS) -o $@ $< + +$(BUILD_DIR)/%.o: $(SRC_ROOT)/%.cc + @mkdir -p $(@D) + $(HUSH_COMPILE) $(CXX) -c $(CXXFLAGS) -o $@ $< + +$(BUILD_DIR)/%.o: $(SRC_ROOT)/%.cpp + @mkdir -p $(@D) + $(HUSH_COMPILE) $(CXX) -c $(CXXFLAGS) -o $@ $< diff --git a/src/LICENSE b/src/LICENSE new file mode 100644 index 0000000..e1f425d --- /dev/null +++ b/src/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Attila Uygun + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/src/base/closure.h b/src/base/closure.h new file mode 100644 index 0000000..0c4036d --- /dev/null +++ b/src/base/closure.h @@ -0,0 +1,12 @@ +#ifndef CLOSURE_H +#define CLOSURE_H + +#include + +namespace base { + +using Closure = std::function; + +} // namespace base + +#endif // CLOSURE_H diff --git a/src/base/collusion_test.cc b/src/base/collusion_test.cc new file mode 100644 index 0000000..f783913 --- /dev/null +++ b/src/base/collusion_test.cc @@ -0,0 +1,51 @@ +#include "collusion_test.h" + +#include +#include +#include + +namespace base { + +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& point) { + float dx = point.x - center.x; + float px = size.x / 2 - fabs(dx); + if (px <= 0) + return false; + + float dy = point.y - center.y; + float py = size.y / 2 - fabs(dy); + return py > 0; +} + +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& origin, + const Vector2& dir) { + Vector2 min = center - size / 2; + Vector2 max = center + size / 2; + + float tmin = std::numeric_limits::min(); + float tmax = std::numeric_limits::max(); + + if (dir.x != 0.0) { + float tx1 = (min.x - origin.x) / dir.x; + float tx2 = (max.x - origin.x) / dir.x; + + tmin = std::max(tmin, std::min(tx1, tx2)); + tmax = std::min(tmax, std::max(tx1, tx2)); + } + + if (dir.y != 0.0) { + float ty1 = (min.y - origin.y) / dir.y; + float ty2 = (max.y - origin.y) / dir.y; + + tmin = std::max(tmin, std::min(ty1, ty2)); + tmax = std::min(tmax, std::max(ty1, ty2)); + } + + return tmax >= tmin; +} + +} // namespace base diff --git a/src/base/collusion_test.h b/src/base/collusion_test.h new file mode 100644 index 0000000..a62b896 --- /dev/null +++ b/src/base/collusion_test.h @@ -0,0 +1,23 @@ +#ifndef COLLUSION_TEST_H +#define COLLUSION_TEST_H + +#include "vecmath.h" + +namespace base { + +// AABB vs point. +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& point); + +// Ray-AABB intersection test. +// center, size: Center and size of the box. +// origin, dir: Origin and direction of the ray. +bool Intersection(const Vector2& center, + const Vector2& size, + const Vector2& origin, + const Vector2& dir); + +} // namespace base + +#endif // COLLUSION_TEST_H diff --git a/src/base/file.h b/src/base/file.h new file mode 100644 index 0000000..154c499 --- /dev/null +++ b/src/base/file.h @@ -0,0 +1,25 @@ +#ifndef FILE_H +#define FILE_H + +#include +#include + +namespace internal { + +struct ScopedFILECloser { + inline void operator()(FILE* x) const { + if (x) + fclose(x); + } +}; + +} // namespace internal + +namespace base { + +// Automatically closes file. +using ScopedFILE = std::unique_ptr; + +} // namespace base + +#endif // FILE_H diff --git a/src/base/hash.h b/src/base/hash.h new file mode 100644 index 0000000..06b7020 --- /dev/null +++ b/src/base/hash.h @@ -0,0 +1,21 @@ +#ifndef HASH_H +#define HASH_H + +#include + +#define HHASH(x) base::HornerHash(31, x) + +namespace base { + +// Compile time string hashing function. +template +constexpr inline size_t HornerHash(size_t prime, + const char (&str)[N], + size_t Len = N - 1) { + return (Len <= 1) ? str[0] + : (prime * HornerHash(prime, str, Len - 1) + str[Len - 1]); +} + +} // namespace base + +#endif // HASH_H diff --git a/src/base/interpolation.h b/src/base/interpolation.h new file mode 100644 index 0000000..bfe7f44 --- /dev/null +++ b/src/base/interpolation.h @@ -0,0 +1,43 @@ +#ifndef INTERPOLATION_H +#define INTERPOLATION_H + +namespace base { + +// Round a float to int. +inline int Round(float f) { + return int(f + 0.5f); +} + +// Linearly interpolate between a and b, by fraction t. +template +inline T Lerp(const T& a, const T& b, float t) { + return a + (b - a) * t; +} + +template <> +inline int Lerp(const int& a, const int& b, float t) { + return Round(a + (b - a) * t); +} + +inline float SmoothStep(float t) { + return t * t * (3 - 2 * t); +} + +inline float SmootherStep(float t) { + return t * t * t * (t * (t * 6 - 15) + 10); +} + +// Interpolating spline defined by four control points with the curve drawn only +// from 0 to 1 which are p1 and p2 respectively. +inline float CatmullRom(float t, float p0, float p3) { + return 0.5f * ((-p0 + 1) * t + (2 * p0 + 4 * 1 - p3) * t * t + + (-p0 - 3 * 1 + p3) * t * t * t); +} + +inline float Acceleration(float t, float w) { + return w * t * t + (1 - w) * t; +} + +} // namespace base + +#endif // INTERPOLATION_H diff --git a/src/base/log.cc b/src/base/log.cc new file mode 100644 index 0000000..8eeeba6 --- /dev/null +++ b/src/base/log.cc @@ -0,0 +1,52 @@ +#include "log.h" + +#if defined(__ANDROID__) +#include +#else +#include +#endif + +namespace base { + +// This is never instantiated, it's just used for EAT_STREAM_PARAMETERS to have +// an object of the correct type on the LHS of the unused part of the ternary +// operator. +Log* Log::swallow_stream; + +Log::Log(const char* file, int line) : file_(file), line_(line) {} + +Log::~Log() { + stream_ << std::endl; + std::string text(stream_.str()); + std::string filename(file_); + size_t last_slash_pos = filename.find_last_of("\\/"); + if (last_slash_pos != std::string::npos) + filename = filename.substr(last_slash_pos + 1); +#if defined(__ANDROID__) + __android_log_print(ANDROID_LOG_ERROR, "kaliber", "[%s:%d] %s", + filename.c_str(), line_, text.c_str()); +#else + printf("[%s:%d] %s", filename.c_str(), line_, text.c_str()); +#endif +} + +template <> +Log& Log::operator<<(const bool& arg) { + stream_ << (arg ? "true" : "false"); + return *this; +} + +template <> +Log& Log::operator<<(const Vector2& arg) { + stream_ << "(" << arg.x << ", " << arg.y << ")"; + return *this; +} + +template <> +Log& Log::operator<<(const Vector4& arg) { + stream_ << "(" << arg.x << ", " << arg.y << ", " << arg.z << ", " << arg.w + << ")"; + return *this; +} + +} // namespace base diff --git a/src/base/log.h b/src/base/log.h new file mode 100644 index 0000000..a83505f --- /dev/null +++ b/src/base/log.h @@ -0,0 +1,49 @@ +#ifndef LOG_H +#define LOG_H + +#include +#include "vecmath.h" + +#define EAT_STREAM_PARAMETERS \ + true ? (void)0 : base::Log::Voidify() & (*base::Log::swallow_stream) + +#define LOG base::Log(__FILE__, __LINE__) + +#ifdef _DEBUG +#define DLOG base::Log(__FILE__, __LINE__) +#else +#define DLOG EAT_STREAM_PARAMETERS +#endif + +namespace base { + +class Log { + public: + class Voidify { + public: + Voidify() = default; + // This has to be an operator with a precedence lower than << but + // higher than ?: + void operator&(Log&) {} + }; + + Log(const char* file, int line); + ~Log(); + + template + Log& operator<<(const T& arg) { + stream_ << arg; + return *this; + } + + static Log* swallow_stream; + + private: + const char* file_; + const int line_; + std::ostringstream stream_; +}; + +} // namespace base + +#endif // LOG_H diff --git a/src/base/mem.h b/src/base/mem.h new file mode 100644 index 0000000..81b5c58 --- /dev/null +++ b/src/base/mem.h @@ -0,0 +1,52 @@ +#ifndef MEM_H +#define MEM_H + +#include +#include +#include + +#if defined(__ANDROID__) +#include +#endif + +#define ALIGN_MEM(alignment) __attribute__((aligned(alignment))) + +namespace internal { + +struct ScopedAlignedFree { + inline void operator()(void* x) const { + if (x) + free(x); + } +}; + +} // namespace internal + +namespace base { + +template +struct AlignedMem { + using ScoppedPtr = std::unique_ptr; +}; + +template +inline void* AlignedAlloc(size_t size) { + void* ptr = NULL; +#if defined(__ANDROID__) + ptr = memalign(kAlignment, size); +#else + if (posix_memalign(&ptr, kAlignment, size)) + ptr = NULL; +#endif + assert(ptr); + // assert(((unsigned)ptr & (kAlignment - 1)) == 0); + return ptr; +} + +inline void AlignedFree(void* mem) { + free(mem); +} + +} // namespace base + +#endif // MEM_H diff --git a/src/base/misc.h b/src/base/misc.h new file mode 100644 index 0000000..13fc0ef --- /dev/null +++ b/src/base/misc.h @@ -0,0 +1,34 @@ +#ifndef MISC_H +#define MISC_H + +#define CRASH *((int*)nullptr) = 0; + +namespace base { + +// ToDo: x86 has the bsr instruction. +inline int GetHighestBitPos(int value) { + return (0xFFFF0000 & value ? value &= 0xFFFF0000, 1 : 0) * 0x10 + + (0xFF00FF00 & value ? value &= 0xFF00FF00, 1 : 0) * 0x08 + + (0xF0F0F0F0 & value ? value &= 0xF0F0F0F0, 1 : 0) * 0x04 + + (0xCCCCCCCC & value ? value &= 0xCCCCCCCC, 1 : 0) * 0x02 + + (0xAAAAAAAA & value ? 1 : 0) * 0x01; +} + +// Get the highest set bit in an integer number +inline int GetHighestBit(int value) { + return 0x1 << GetHighestBitPos(value); +} + +// Check if the given integer is a power of two, ie if only one bit is set. +inline bool IsPow2(int value) { + return GetHighestBit(value) == value; +} + +inline int RoundUpToPow2(int val) { + int i = GetHighestBit(val); + return val == i ? val : i << 1; +} + +} // namespace base + +#endif // MISC_H diff --git a/src/base/random.cc b/src/base/random.cc new file mode 100644 index 0000000..c9c7066 --- /dev/null +++ b/src/base/random.cc @@ -0,0 +1,26 @@ +#include "random.h" + +#include + +#include "interpolation.h" + +namespace base { + +Random::Random() { + std::random_device rd; + generator_ = std::mt19937(rd()); + real_distribution_ = std::uniform_real_distribution(0, 1); +} + +Random::Random(unsigned seed) { + generator_ = std::mt19937(seed); + real_distribution_ = std::uniform_real_distribution(0, 1); +} + +Random::~Random() = default; + +int Random::Roll(int sides) { + return Lerp(1, sides, GetFloat()); +} + +} // namespace base diff --git a/src/base/random.h b/src/base/random.h new file mode 100644 index 0000000..aab1f10 --- /dev/null +++ b/src/base/random.h @@ -0,0 +1,27 @@ +#ifndef RANDOM_GENERATOR_H +#define RANDOM_GENERATOR_H + +#include + +namespace base { + +class Random { + public: + Random(); + Random(unsigned seed); + ~Random(); + + // Returns a random float between 0 and 1. + float GetFloat() { return real_distribution_(generator_); } + + // Roll dice with the given number of sides. + int Roll(int sides); + + private: + std::mt19937 generator_; + std::uniform_real_distribution real_distribution_; +}; + +} // namespace base + +#endif // RANDOM_GENERATOR_H diff --git a/src/base/task_runner.cc b/src/base/task_runner.cc new file mode 100644 index 0000000..30261de --- /dev/null +++ b/src/base/task_runner.cc @@ -0,0 +1,30 @@ +#include "task_runner.h" + +namespace base { + +void TaskRunner::Enqueue(base::Closure task) { + std::unique_lock scoped_lock(mutex_); + thread_tasks_.emplace_back(std::move(task)); +} + +void TaskRunner::Run() { + for (;;) { + base::Closure task; + { + std::unique_lock scoped_lock(mutex_); + if (!thread_tasks_.empty()) { + task.swap(thread_tasks_.front()); + thread_tasks_.pop_front(); + } + } + if (!task) + break; + task(); + } +} + +bool TaskRunner::IsBoundToCurrentThread() { + return thread_id_ == std::this_thread::get_id(); +} + +} // namespace base diff --git a/src/base/task_runner.h b/src/base/task_runner.h new file mode 100644 index 0000000..943f0ed --- /dev/null +++ b/src/base/task_runner.h @@ -0,0 +1,32 @@ +#ifndef TASK_RUNNER_H +#define TASK_RUNNER_H + +#include +#include +#include +#include "closure.h" + +namespace base { + +class TaskRunner { + public: + TaskRunner() = default; + ~TaskRunner() = default; + + void Enqueue(base::Closure cb); + void Run(); + + bool IsBoundToCurrentThread(); + + private: + std::thread::id thread_id_ = std::this_thread::get_id(); + std::mutex mutex_; + std::deque thread_tasks_; + + TaskRunner(TaskRunner const&) = delete; + TaskRunner& operator=(TaskRunner const&) = delete; +}; + +} // namespace base + +#endif // TASK_RUNNER_H diff --git a/src/base/timer.cc b/src/base/timer.cc new file mode 100644 index 0000000..e30b30f --- /dev/null +++ b/src/base/timer.cc @@ -0,0 +1,28 @@ +#include "timer.h" + +namespace base { + +Timer::Timer() { + Reset(); +} + +void Timer::Reset() { + gettimeofday(&last_time_, nullptr); + + seconds_passed_ = 0.0f; + seconds_accumulated_ = 0.0f; +} + +void Timer::Update() { + timeval currentTime; + gettimeofday(¤tTime, nullptr); + seconds_passed_ = + (float)(currentTime.tv_sec - last_time_.tv_sec) + + 0.000001f * (float)(currentTime.tv_usec - last_time_.tv_usec); + + last_time_ = currentTime; + + seconds_accumulated_ += seconds_passed_; +} + +} // namespace base diff --git a/src/base/timer.h b/src/base/timer.h new file mode 100644 index 0000000..24d222f --- /dev/null +++ b/src/base/timer.h @@ -0,0 +1,29 @@ +#ifndef TIMER_H +#define TIMER_H + +#include + +namespace base { + +class Timer { + public: + Timer(); + ~Timer() = default; + + void Reset(); + + void Update(); + + float GetSecondsPassed() const { return seconds_passed_; } + float GetSecondsAccumulated() const { return seconds_accumulated_; } + + private: + float seconds_passed_ = 0.0f; + float seconds_accumulated_ = 0.0f; + + timeval last_time_; +}; + +} // namespace base + +#endif // TIMER_H diff --git a/src/base/vecmath.cc b/src/base/vecmath.cc new file mode 100644 index 0000000..126106b --- /dev/null +++ b/src/base/vecmath.cc @@ -0,0 +1,15 @@ +#include "vecmath.h" + +namespace base { + +Matrix4x4 Ortho(float left, float right, float bottom, float top) { + Matrix4x4 m(1); + m.c[0].x = 2.0f / (right - left); + m.c[1].y = 2.0f / (top - bottom); + m.c[2].z = -1.0f; + m.c[3].x = -(right + left) / (right - left); + m.c[3].y = -(top + bottom) / (top - bottom); + return m; +} + +} // namespace base diff --git a/src/base/vecmath.h b/src/base/vecmath.h new file mode 100644 index 0000000..6649a3d --- /dev/null +++ b/src/base/vecmath.h @@ -0,0 +1,163 @@ +#ifndef VEC_MATH_H +#define VEC_MATH_H + +#include +#include + +namespace base { + +struct Vector2 { + float x, y; + + Vector2() {} + Vector2(float _x, float _y) : x(_x), y(_y) {} + + float Magnitude() { return sqrt(x * x + y * y); } + + Vector2 Normalize() { + float m = Magnitude(); + x /= m; + y /= m; + return *this; + } + + float DotProduct(const Vector2& v) { return x * v.x + y * v.y; } + + float CrossProduct(const Vector2& v) { return x * v.y - y * v.x; } + + Vector2 operator-() { return Vector2(x * -1.0f, y * -1.0f); } + + Vector2 operator+=(const Vector2& v) { + x += v.x; + y += v.y; + return *this; + } + + Vector2 operator-=(const Vector2& v) { + x -= v.x; + y -= v.y; + return *this; + } + + Vector2 operator*=(const Vector2& v) { + x *= v.x; + y *= v.y; + return *this; + } + + Vector2 operator*=(float s) { + x *= s; + y *= s; + return *this; + } + + Vector2 operator/=(const Vector2& v) { + x /= v.x; + y /= v.y; + return *this; + } + + Vector2 operator/=(float s) { + x /= s; + y /= s; + return *this; + } + + const float* GetData() const { return &x; } +}; + +inline Vector2 operator+(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x + v2.x, v1.y + v2.y); +} + +inline Vector2 operator-(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x - v2.x, v1.y - v2.y); +} + +inline Vector2 operator*(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x * v2.x, v1.y * v2.y); +} + +inline Vector2 operator/(const Vector2& v1, const Vector2& v2) { + return Vector2(v1.x / v2.x, v1.y / v2.y); +} + +inline Vector2 operator*(const Vector2& v, float s) { + return Vector2(v.x * s, v.y * s); +} + +inline Vector2 operator/(const Vector2& v, float s) { + return Vector2(v.x / s, v.y / s); +} + +inline bool operator==(const Vector2& v1, const Vector2& v2) { + return v1.x == v2.x && v1.y == v2.y; +} + +inline bool operator!=(const Vector2& v1, const Vector2& v2) { + return v1.x != v2.x || v1.y != v2.y; +} + +struct Vector3 { + float x, y, z; + + Vector3() {} + Vector3(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {} + + const float* GetData() const { return &x; } +}; + +inline Vector3 operator+(const Vector3& v1, const Vector3& v2) { + return Vector3(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z); +} + +struct Vector4 { + float x, y, z, w; + + Vector4() {} + Vector4(float _x, float _y, float _z, float _w) + : x(_x), y(_y), z(_z), w(_w) {} + + Vector4 operator+=(const Vector4& v) { + x += v.x; + y += v.y; + z += v.z; + w += v.w; + return *this; + } + + const float* GetData() const { return &x; } +}; + +inline Vector4 operator*(const Vector4& v1, const Vector4& v2) { + return Vector4(v1.x * v2.x, v2.y * v2.y, v1.z * v2.z, v1.w * v2.w); +} + +inline Vector4 operator*(const Vector4& v, float s) { + return Vector4(v.x * s, v.y * s, v.z * s, v.w * s); +} + +inline Vector4 operator+(const Vector4& v1, const Vector4& v2) { + return Vector4(v1.x + v2.x, v1.y + v2.y, v1.z + v2.z, v1.w + v2.w); +} + +inline Vector4 operator-(const Vector4& v1, const Vector4& v2) { + return Vector4(v1.x - v2.x, v1.y - v2.y, v1.z - v2.z, v1.w - v2.w); +} + +struct Matrix4x4 { + Vector4 c[4]; + + Matrix4x4() {} + Matrix4x4(float s) + : c{Vector4(s, 0, 0, 0), Vector4(0, s, 0, 0), Vector4(0, 0, s, 0), + Vector4(0, 0, 0, s)} {} + + const float* GetData() const { return &c[0].x; } +}; + +Matrix4x4 Ortho(float left, float right, float bottom, float top); + +} // namespace base + +#endif // VEC_MATH_H diff --git a/src/base/worker.cc b/src/base/worker.cc new file mode 100644 index 0000000..e0ae406 --- /dev/null +++ b/src/base/worker.cc @@ -0,0 +1,68 @@ +#include "worker.h" +#include "log.h" + +namespace base { + +Worker::Worker(unsigned max_concurrency) : max_concurrency_(max_concurrency) { + if (max_concurrency_ > std::thread::hardware_concurrency() || + max_concurrency_ == 0) { + max_concurrency_ = std::thread::hardware_concurrency(); + if (max_concurrency_ == 0) + max_concurrency_ = 1; + } +} + +Worker::~Worker() = default; + +void Worker::Enqueue(base::Closure task) { + if (!active_) { + unsigned concurrency = max_concurrency_; + while (concurrency--) + threads_.emplace_back(&Worker::WorkerMain, this); + active_ = true; + } + + bool notify; + { + std::unique_lock scoped_lock(mutex_); + notify = tasks_.empty(); + tasks_.emplace_back(std::move(task)); + } + if (notify) + cv_.notify_all(); +} + +void Worker::Join() { + if (!active_) + return; + + { + std::unique_lock scoped_lock(mutex_); + quit_when_idle_ = true; + } + cv_.notify_all(); + for (auto& thread : threads_) + thread.join(); + threads_.clear(); + active_ = false; +} + +void Worker::WorkerMain() { + for (;;) { + base::Closure task; + { + std::unique_lock scoped_lock(mutex_); + while (tasks_.empty()) { + if (quit_when_idle_) + return; + cv_.wait(scoped_lock); + } + task.swap(tasks_.front()); + tasks_.pop_front(); + } + + task(); + } +} + +} // namespace base diff --git a/src/base/worker.h b/src/base/worker.h new file mode 100644 index 0000000..7059941 --- /dev/null +++ b/src/base/worker.h @@ -0,0 +1,40 @@ +#ifndef WORKER_H +#define WORKER_H + +#include +#include +#include +#include +#include +#include "closure.h" + +namespace base { + +// Feed the worker tasks and they will be called on a thread from the pool. +class Worker { + public: + Worker(unsigned max_concurrency = 0); + ~Worker(); + + void Enqueue(base::Closure task); + void Join(); + + private: + bool active_ = false; + unsigned max_concurrency_ = 0; + + std::condition_variable cv_; + std::mutex mutex_; + std::vector threads_; + std::deque tasks_; + bool quit_when_idle_ = false; + + void WorkerMain(); + + Worker(Worker const&) = delete; + Worker& operator=(Worker const&) = delete; +}; + +} // namespace base + +#endif // WORKER_H diff --git a/src/demo/credits.cc b/src/demo/credits.cc new file mode 100644 index 0000000..095f584 --- /dev/null +++ b/src/demo/credits.cc @@ -0,0 +1,138 @@ +#include "credits.h" + +#include "../base/log.h" +#include "../base/vecmath.h" +#include "../base/worker.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/input_event.h" +#include "../engine/renderer/texture.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr char kCreditsLines[Credits::kNumLines][15] = { + "Credits", "Code:", "Attila Uygun", "Graphics:", "Erkan Erturk"}; + +constexpr float kLineSpaces[Credits::kNumLines - 1] = {1.5f, 0.5f, 1.5f, 0.5f}; + +const Vector4 kTextColor = {0.3f, 0.55f, 1.0f, 1}; +constexpr float kFadeSpeed = 0.2f; + +} // namespace + +Credits::Credits() = default; + +Credits::~Credits() = default; + +bool Credits::Initialize() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + max_text_width_ = -1; + for (int i = 0; i < kNumLines; ++i) { + int width, height; + font.CalculateBoundingBox(kCreditsLines[i], width, height); + if (width > max_text_width_) + max_text_width_ = width; + } + + for (int i = 0; i < kNumLines; ++i) + text_animator_.Attach(&text_[i]); + + return true; +} + +void Credits::Update(float delta_time) { + text_animator_.Update(delta_time); +} + +void Credits::OnInputEvent(std::unique_ptr event) { + if ((event->GetType() == InputEvent::kTap || + event->GetType() == InputEvent::kDragEnd || + event->GetType() == InputEvent::kNavigateBack) && + !text_animator_.IsPlaying(Animator::kBlending)) { + Hide(); + Engine& engine = Engine::Get(); + static_cast(engine.GetGame())->EnterMenuState(); + } +} + +void Credits::Draw() { + for (int i = 0; i < kNumLines; ++i) + text_[i].Draw(); +} + +void Credits::ContextLost() { + if (tex_) + tex_->Update(CreateImage()); +} + +void Credits::Show() { + tex_ = Engine::Get().CreateRenderResource(); + tex_->Update(CreateImage()); + + for (int i = 0; i < kNumLines; ++i) { + text_[i].Create(tex_, {1, kNumLines}); + text_[i].SetOffset({0, 0}); + text_[i].SetScale({1, 1}); + text_[i].AutoScale(); + text_[i].SetColor(kTextColor * Vector4(1, 1, 1, 0)); + text_[i].SetFrame(i); + + if (i > 0) { + text_[i].PlaceToBottomOf(text_[i - 1]); + text_[i].Translate(text_[i - 1].GetOffset() * Vector2(0, 1)); + text_[i].Translate({0, text_[i - 1].GetScale().y * -kLineSpaces[i - 1]}); + } + } + + float center_offset_y = + (text_[0].GetOffset().y - text_[kNumLines - 1].GetOffset().y) / 2; + for (int i = 0; i < kNumLines; ++i) + text_[i].Translate({0, center_offset_y}); + + text_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + text_animator_.SetEndCallback(Animator::kBlending, nullptr); + }); + text_animator_.SetBlending(kTextColor, kFadeSpeed); + text_animator_.Play(Animator::kBlending, false); + text_animator_.SetVisible(true); +} + +void Credits::Hide() { + text_animator_.SetEndCallback(Animator::kBlending, [&]() -> void { + for (int i = 0; i < kNumLines; ++i) + text_[i].Destory(); + tex_.reset(); + text_animator_.SetEndCallback(Animator::kBlending, nullptr); + text_animator_.SetVisible(false); + }); + text_animator_.SetBlending(kTextColor * Vector4(1, 1, 1, 0), kFadeSpeed); + text_animator_.Play(Animator::kBlending, false); +} + +std::unique_ptr Credits::CreateImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + int line_height = font.GetLineHeight() + 1; + auto image = std::make_unique(); + image->Create(max_text_width_, line_height * kNumLines); + image->Clear({1, 1, 1, 0}); + + Worker worker(kNumLines); + for (int i = 0; i < kNumLines; ++i) { + int w, h; + font.CalculateBoundingBox(kCreditsLines[i], w, h); + float x = (image->GetWidth() - w) / 2; + float y = line_height * i; + worker.Enqueue(std::bind(&Font::Print, &font, x, y, kCreditsLines[i], + image->GetBuffer(), image->GetWidth())); + } + worker.Join(); + + return image; +} diff --git a/src/demo/credits.h b/src/demo/credits.h new file mode 100644 index 0000000..d5a3447 --- /dev/null +++ b/src/demo/credits.h @@ -0,0 +1,47 @@ +#ifndef CREDITS_H +#define CREDITS_H + +#include +#include + +#include "../engine/animator.h" +#include "../engine/image_quad.h" + +namespace eng { +class Image; +class InputEvent; +class Texture; +} // namespace eng + +class Credits { + public: + static constexpr int kNumLines = 5; + + Credits(); + ~Credits(); + + bool Initialize(); + + void Update(float delta_time); + + void OnInputEvent(std::unique_ptr event); + + void Draw(); + + void ContextLost(); + + void Show(); + void Hide(); + + private: + std::shared_ptr tex_; + + eng::ImageQuad text_[kNumLines]; + eng::Animator text_animator_; + + int max_text_width_ = 0; + + std::unique_ptr CreateImage(); +}; + +#endif // CREDITS_H diff --git a/src/demo/damage_type.h b/src/demo/damage_type.h new file mode 100644 index 0000000..6757427 --- /dev/null +++ b/src/demo/damage_type.h @@ -0,0 +1,20 @@ +#ifndef DAMAGE_TYPE_H +#define DAMAGE_TYPE_H + +enum DamageType { + kDamageType_Invalid = -1, + kDamageType_Green, + kDamageType_Blue, + kDamageType_Any, + kDamageType_Max +}; + +enum EnemyType { + kEnemyType_Invalid = -1, + kEnemyType_Skull, + kEnemyType_Bug, + kEnemyType_Tank, + kEnemyType_Max +}; + +#endif // DAMAGE_TYPE_H diff --git a/src/demo/demo.cc b/src/demo/demo.cc new file mode 100644 index 0000000..7ea118d --- /dev/null +++ b/src/demo/demo.cc @@ -0,0 +1,251 @@ +#include "demo.h" + +#include +#include + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/random.h" +#include "../engine/engine.h" +#include "../engine/game_factory.h" +#include "../engine/input_event.h" + +DECLARE_GAME_BEGIN +DECLARE_GAME(Demo) +DECLARE_GAME_END + +using namespace base; +using namespace eng; + +bool Demo::Initialize() { + if (!font_.Load("PixelCaps!.ttf")) + return false; + + if (!sky_.Create()) { + LOG << "Could not create the sky."; + return false; + } + + if (!enemy_.Initialize()) { + LOG << "Failed to create the enemy."; + return false; + } + + if (!player_.Initialize()) { + LOG << "Failed to create the enemy."; + return false; + } + + if (!hud_.Initialize()) { + LOG << "Failed to create the hud."; + return false; + } + + if (!menu_.Initialize()) { + LOG << "Failed to create the menu."; + return false; + } + + if (!credits_.Initialize()) { + LOG << "Failed to create the credits."; + return false; + } + + EnterMenuState(); + + return true; +} + +void Demo::Update(float delta_time) { + Engine& engine = Engine::Get(); + + while (std::unique_ptr event = engine.GetNextInputEvent()) { + if (state_ == kMenu) + menu_.OnInputEvent(std::move(event)); + else if (state_ == kCredits) + credits_.OnInputEvent(std::move(event)); + else + player_.OnInputEvent(std::move(event)); + } + + if (delayed_work_timer_ > 0) { + delayed_work_timer_ -= delta_time; + if (delayed_work_timer_ <= 0) { + base::Closure cb = std::move(delayed_work_cb_); + delayed_work_cb_ = nullptr; + cb(); + } + } + + if (add_score_ > 0) { + score_ += add_score_; + add_score_ = 0; + hud_.PrintScore(score_, true); + } + + hud_.Update(delta_time); + menu_.Update(delta_time); + credits_.Update(delta_time); + + if (state_ == kMenu) + UpdateMenuState(delta_time); + else if (state_ == kGame) + UpdateGameState(delta_time); +} + +void Demo::Draw(float frame_frac) { + sky_.Draw(frame_frac); + player_.Draw(frame_frac); + enemy_.Draw(frame_frac); + hud_.Draw(); + menu_.Draw(); + credits_.Draw(); +} + +void Demo::ContextLost() { + enemy_.ContextLost(); + player_.ContextLost(); + hud_.ContextLost(); + menu_.ContextLost(); + credits_.ContextLost(); + sky_.ContextLost(); +} + +void Demo::LostFocus() { + if (state_ == kGame) + EnterMenuState(); +} + +void Demo::GainedFocus() {} + +void Demo::AddScore(int score) { + add_score_ += score; +} + +void Demo::EnterMenuState() { + if (state_ == kMenu) + return; + if (wave_ == 0) { + menu_.SetOptionEnabled(Menu::kContinue, false); + } else { + menu_.SetOptionEnabled(Menu::kContinue, true); + menu_.SetOptionEnabled(Menu::kNewGame, false); + } + menu_.Show(); + state_ = kMenu; +} + +void Demo::EnterCreditsState() { + if (state_ == kCredits) + return; + credits_.Show(); + state_ = kCredits; +} + +void Demo::EnterGameState() { + if (state_ == kGame) + return; + hud_.Show(); + state_ = kGame; +} + +void Demo::UpdateMenuState(float delta_time) { + switch (menu_.selected_option()) { + case Menu::kOption_Invalid: + break; + case Menu::kContinue: + menu_.Hide(); + Continue(); + break; + case Menu::kNewGame: + menu_.Hide(); + StartNewGame(); + break; + case Menu::kCredits: + menu_.Hide(); + EnterCreditsState(); + break; + case Menu::kExit: + Engine::Get().Exit(); + break; + default: + assert(false); + } +} + +void Demo::UpdateGameState(float delta_time) { + sky_.Update(delta_time); + player_.Update(delta_time); + enemy_.Update(delta_time); + + if (waiting_for_next_wave_) + return; + + if (enemy_.num_enemies_killed_in_current_wave() != last_num_enemies_killed_) { + last_num_enemies_killed_ = enemy_.num_enemies_killed_in_current_wave(); + int enemies_remaining = total_enemies_ - last_num_enemies_killed_; + + if (enemies_remaining <= 0) { + waiting_for_next_wave_ = true; + hud_.SetProgress(wave_ > 0 ? 0 : 1); + + enemy_.OnWaveFinished(); + + SetDelayedWork(1, [&]() -> void { + Random& rnd = Engine::Get().GetRandomGenerator(); + int dominant_channel = rnd.Roll(3) - 1; + if (dominant_channel == last_dominant_channel_) + dominant_channel = (dominant_channel + 1) % 3; + last_dominant_channel_ = dominant_channel; + + float weights[3] = {0, 0, 0}; + weights[dominant_channel] = 1; + Vector4 c = {Lerp(0.75f, 0.95f, rnd.GetFloat()) * weights[0], + Lerp(0.75f, 0.95f, rnd.GetFloat()) * weights[1], + Lerp(0.75f, 0.95f, rnd.GetFloat()) * weights[2], 1}; + c += {Lerp(0.1f, 0.5f, rnd.GetFloat()) * (1 - weights[0]), + Lerp(0.1f, 0.5f, rnd.GetFloat()) * (1 - weights[1]), + Lerp(0.1f, 0.5f, rnd.GetFloat()) * (1 - weights[2]), 1}; + sky_.SwitchColor(c); + + ++wave_; + hud_.PrintScore(score_, true); + hud_.PrintWave(wave_, true); + hud_.SetProgress(1); + + float factor = 3 * (log10(5 * (float)wave_) / log10(1.2f)) - 25; + total_enemies_ = (int)(6 * factor); + last_num_enemies_killed_ = 0; + DLOG << "wave: " << wave_ << " total_enemies_: " << total_enemies_; + + enemy_.OnWaveStarted(wave_); + + waiting_for_next_wave_ = false; + }); + } else { + hud_.SetProgress((float)enemies_remaining / (float)total_enemies_); + } + } +} + +void Demo::Continue() { + EnterGameState(); +} + +void Demo::StartNewGame() { + score_ = 0; + add_score_ = 0; + wave_ = 0; + last_num_enemies_killed_ = -1; + total_enemies_ = 0; + waiting_for_next_wave_ = false; + delayed_work_timer_ = 0; + delayed_work_cb_ = nullptr; + EnterGameState(); +} + +void Demo::SetDelayedWork(float seconds, base::Closure cb) { + assert(delayed_work_cb_ == nullptr); + delayed_work_cb_ = std::move(cb); + delayed_work_timer_ = seconds; +} diff --git a/src/demo/demo.h b/src/demo/demo.h new file mode 100644 index 0000000..6c06aab --- /dev/null +++ b/src/demo/demo.h @@ -0,0 +1,82 @@ +#ifndef DEMO_H +#define DEMO_H + +#include "../base/closure.h" +#include "../engine/font.h" +#include "../engine/game.h" +#include "credits.h" +#include "enemy.h" +#include "hud.h" +#include "menu.h" +#include "player.h" +#include "sky_quad.h" + +class Demo : public eng::Game { + public: + Demo() = default; + ~Demo() override = default; + + bool Initialize() override; + + void Update(float delta_time) override; + + void Draw(float frame_frac) override; + + void ContextLost() override; + + void LostFocus() override; + + void GainedFocus() override; + + void AddScore(int score); + + void EnterMenuState(); + void EnterCreditsState(); + void EnterGameState(); + + const eng::Font& GetFont() { return font_; } + + Player& GetPlayer() { return player_; } + Enemy& GetEnemy() { return enemy_; } + + int wave() { return wave_; } + + private: + enum State { kState_Invalid = -1, kMenu, kGame, kCredits, kState_Max }; + + State state_ = kState_Invalid; + + Player player_; + Enemy enemy_; + Hud hud_; + Menu menu_; + Credits credits_; + + SkyQuad sky_; + int last_dominant_channel_ = -1; + + eng::Font font_; + + int score_ = 0; + int add_score_ = 0; + + int wave_ = 0; + + int last_num_enemies_killed_ = -1; + int total_enemies_ = 0; + + int waiting_for_next_wave_ = false; + + float delayed_work_timer_ = 0; + base::Closure delayed_work_cb_; + + void UpdateMenuState(float delta_time); + void UpdateGameState(float delta_time); + + void Continue(); + void StartNewGame(); + + void SetDelayedWork(float seconds, base::Closure cb); +}; + +#endif // DEMO_H diff --git a/src/demo/enemy.cc b/src/demo/enemy.cc new file mode 100644 index 0000000..3584f92 --- /dev/null +++ b/src/demo/enemy.cc @@ -0,0 +1,476 @@ +#include "enemy.h" + +#include +#include +#include + +#include "../base/collusion_test.h" +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/renderer/texture.h" +#include "../engine/sound.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr int enemy_frame_start[][3] = {{0, 50, -1}, + {13, 33, -1}, + {-1, -1, 100}}; +constexpr int enemy_frame_count[][3] = {{7, 7, -1}, {6, 6, -1}, {-1, -1, 7}}; +constexpr int enemy_frame_speed = 12; + +constexpr int enemy_scores[] = {100, 150, 300}; + +constexpr float kSpawnPeriod[kEnemyType_Max][2] = {{2, 5}, + {15, 25}, + {110, 130}}; + +void SetupFadeOutAnim(Animator& animator, float delay) { + animator.SetEndCallback(Animator::kTimer, [&]() -> void { + animator.SetBlending({1, 1, 1, 0}, 0.5f, + std::bind(Acceleration, std::placeholders::_1, -1)); + animator.Play(Animator::kBlending, false); + }); + animator.SetEndCallback(Animator::kBlending, + [&]() -> void { animator.SetVisible(false); }); + animator.SetTimer(delay); +} + +} // namespace + +Enemy::Enemy() + : skull_tex_(Engine::Get().CreateRenderResource()), + bug_tex_(Engine::Get().CreateRenderResource()), + target_tex_(Engine::Get().CreateRenderResource()), + blast_tex_(Engine::Get().CreateRenderResource()), + score_tex_{Engine::Get().CreateRenderResource(), + Engine::Get().CreateRenderResource(), + Engine::Get().CreateRenderResource()} {} + +Enemy::~Enemy() = default; + +bool Enemy::Initialize() { + explosion_sound_ = std::make_shared(); + if (!explosion_sound_->Load("explosion.mp3")) + return false; + + return CreateRenderResources(); +} + +void Enemy::ContextLost() { + CreateRenderResources(); +} + +void Enemy::Update(float delta_time) { + if (!waiting_for_next_wave_) { + if (spawn_factor_interpolator_ < 1) { + spawn_factor_interpolator_ += delta_time * 0.1f; + if (spawn_factor_interpolator_ > 1) + spawn_factor_interpolator_ = 1; + } + + for (int i = 0; i < kEnemyType_Max; ++i) + seconds_since_last_spawn_[i] += delta_time; + + SpawnNextEnemy(); + } + + for (auto it = enemies_.begin(); it != enemies_.end(); ++it) { + if (it->marked_for_removal) { + it = enemies_.erase(it); + continue; + } + it->sprite_animator.Update(delta_time); + it->target_animator.Update(delta_time); + it->blast_animator.Update(delta_time); + it->health_animator.Update(delta_time); + it->score_animator.Update(delta_time); + it->movement_animator.Update(delta_time); + } +} + +void Enemy::Draw(float frame_frac) { + for (auto& e : enemies_) { + e.sprite.Draw(); + e.target.Draw(); + e.blast.Draw(); + e.health_base.Draw(); + e.health_bar.Draw(); + e.score.Draw(); + } +} + +bool Enemy::HasTarget(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + return GetTarget(damage_type) ? true : false; +} + +Vector2 Enemy::GetTargetPos(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + EnemyUnit* target = GetTarget(damage_type); + if (target) + return target->sprite.GetOffset() - + Vector2(0, target->sprite.GetScale().y / 2.5f); + return {0, 0}; +} + +void Enemy::SelectTarget(DamageType damage_type, + const Vector2& origin, + const Vector2& dir, + float snap_factor) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + if (waiting_for_next_wave_) + return; + + EnemyUnit* best_enemy = nullptr; + + float closest_dist = std::numeric_limits::max(); + for (auto& e : enemies_) { + if (e.hit_points <= 0 || e.marked_for_removal) + continue; + + if (e.targetted_by_weapon_ == damage_type) { + e.targetted_by_weapon_ = kDamageType_Invalid; + e.target.SetVisible(false); + e.target_animator.Stop(Animator::kAllAnimations); + } + + if (!base::Intersection(e.sprite.GetOffset(), + e.sprite.GetScale() * snap_factor, origin, dir)) + continue; + + Vector2 weapon_enemy_dir = e.sprite.GetOffset() - origin; + float enemy_weapon_dist = weapon_enemy_dir.Magnitude(); + if (closest_dist > enemy_weapon_dist) { + closest_dist = enemy_weapon_dist; + best_enemy = &e; + } + } + + if (best_enemy) { + best_enemy->targetted_by_weapon_ = damage_type; + best_enemy->target.SetVisible(true); + if (damage_type == kDamageType_Green) { + best_enemy->target.SetFrame(0); + best_enemy->target_animator.SetFrames(6, 28); + } else { + best_enemy->target.SetFrame(6); + best_enemy->target_animator.SetFrames(6, 28); + } + best_enemy->target_animator.Play(Animator::kFrames, false); + } +} + +void Enemy::DeselectTarget(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + EnemyUnit* target = GetTarget(damage_type); + if (target) { + target->targetted_by_weapon_ = kDamageType_Invalid; + target->target.SetVisible(false); + target->target_animator.Stop(Animator::kAllAnimations); + } +} + +void Enemy::HitTarget(DamageType damage_type) { + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Any); + + if (waiting_for_next_wave_) + return; + + EnemyUnit* target = GetTarget(damage_type); + + if (target) { + target->target.SetVisible(false); + target->target_animator.Stop(Animator::kAllAnimations); + } + + if (!target || (target->damage_type != kDamageType_Any && + target->damage_type != damage_type)) + return; + + TakeDamage(target, 1); +} + +void Enemy::OnWaveFinished() { + for (auto& e : enemies_) { + if (!e.marked_for_removal && e.hit_points > 0) + e.movement_animator.Pause(Animator::kMovement); + } + waiting_for_next_wave_ = true; +} + +void Enemy::OnWaveStarted(int wave) { + for (auto& e : enemies_) { + if (!e.marked_for_removal && e.hit_points > 0) { + if (wave == 1) + e.marked_for_removal = true; + else + TakeDamage(&e, 100); + } + } + num_enemies_killed_in_current_wave_ = 0; + seconds_since_last_spawn_ = {0, 0, 0}; + seconds_to_next_spawn_ = {0, 0, 0}; + spawn_factor_ = 1 / (log10(0.25f * (wave + 4) + 1.468f) * 6); + spawn_factor_interpolator_ = 0; + waiting_for_next_wave_ = false; +} + +void Enemy::TakeDamage(EnemyUnit* target, int damage) { + assert(!target->marked_for_removal); + assert(target->hit_points > 0); + + target->blast.SetVisible(true); + target->blast_animator.Play(Animator::kFrames, false); + + target->hit_points -= damage; + if (target->hit_points <= 0) { + if (!waiting_for_next_wave_) + ++num_enemies_killed_in_current_wave_; + + target->sprite.SetVisible(false); + target->health_base.SetVisible(false); + target->health_bar.SetVisible(false); + target->score.SetVisible(true); + + target->score_animator.Play(Animator::kTimer | Animator::kMovement, false); + target->movement_animator.Pause(Animator::kMovement); + + target->explosion_.Play(false); + + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + game->AddScore(GetScore(target->enemy_type)); + } else { + target->targetted_by_weapon_ = kDamageType_Invalid; + + Vector2 s = target->sprite.GetScale() * Vector2(0.6f, 0.01f); + s.x *= (float)target->hit_points / (float)target->total_health; + float t = (s.x - target->health_bar.GetScale().x) / 2; + target->health_bar.SetScale(s); + target->health_bar.Translate({t, 0}); + + target->health_base.SetVisible(true); + target->health_bar.SetVisible(true); + + target->health_animator.Stop(Animator::kTimer | Animator::kBlending); + target->health_animator.Play(Animator::kTimer, false); + } +} + +void Enemy::SpawnNextEnemy() { + Engine& engine = Engine::Get(); + Random& rnd = engine.GetRandomGenerator(); + + float factor = Lerp(1.0f, spawn_factor_, spawn_factor_interpolator_); + EnemyType enemy_type = kEnemyType_Invalid; + + for (int i = 0; i < kEnemyType_Max; ++i) { + if (seconds_since_last_spawn_[i] >= seconds_to_next_spawn_[i]) { + if (seconds_to_next_spawn_[i] > 0) + enemy_type = (EnemyType)i; + + seconds_since_last_spawn_[i] = 0; + seconds_to_next_spawn_[i] = + Lerp(kSpawnPeriod[i][0] * factor, kSpawnPeriod[i][1] * factor, + rnd.GetFloat()); + break; + } + } + + if (enemy_type == kEnemyType_Invalid) + return; + + DamageType damage_type = enemy_type == kEnemyType_Tank + ? kDamageType_Any + : (DamageType)(rnd.Roll(2) - 1); + + Vector2 s = engine.GetScreenSize(); + int col; + col = rnd.Roll(4) - 1; + if (col == last_spawn_col_) + col = (col + 1) % 4; + last_spawn_col_ = col; + float x = (s.x / 4) / 2 + (s.x / 4) * col - s.x / 2; + Vector2 pos = {x, s.y / 2}; + float speed = + enemy_type == kEnemyType_Tank ? 36.0f : (rnd.Roll(4) == 4 ? 6.0f : 10.0f); + + Spawn(enemy_type, damage_type, pos, speed); +} + +void Enemy::Spawn(EnemyType enemy_type, + DamageType damage_type, + const Vector2& pos, + float speed) { + assert(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max); + assert(damage_type > kDamageType_Invalid && damage_type < kDamageType_Max); + + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + + auto& e = enemies_.emplace_back(); + e.enemy_type = enemy_type; + e.damage_type = damage_type; + if (enemy_type == kEnemyType_Skull) { + e.total_health = e.hit_points = 1; + e.sprite.Create(skull_tex_, {10, 13}, 100, 100); + } else if (enemy_type == kEnemyType_Bug) { + e.total_health = e.hit_points = 2; + e.sprite.Create(bug_tex_, {10, 4}); + } else { // kEnemyType_Tank + e.total_health = e.hit_points = 6; + e.sprite.Create(skull_tex_, {10, 13}, 100, 100); + } + e.sprite.AutoScale(); + e.sprite.SetVisible(true); + Vector2 spawn_pos = pos + Vector2(0, e.sprite.GetScale().y / 2); + e.sprite.SetOffset(spawn_pos); + + e.sprite.SetFrame(enemy_frame_start[enemy_type][damage_type]); + e.sprite_animator.SetFrames(enemy_frame_count[enemy_type][damage_type], + enemy_frame_speed); + + e.sprite_animator.Attach(&e.sprite); + e.sprite_animator.Play(Animator::kFrames, true); + + e.target.Create(target_tex_, {6, 2}); + e.target.AutoScale(); + e.target.SetOffset(spawn_pos); + + e.blast.Create(blast_tex_, {6, 2}); + e.blast.AutoScale(); + e.blast.SetOffset(spawn_pos); + + e.health_base.Scale(e.sprite.GetScale() * Vector2(0.6f, 0.01f)); + e.health_base.SetOffset(spawn_pos); + e.health_base.PlaceToBottomOf(e.sprite); + e.health_base.SetColor({0.5f, 0.5f, 0.5f, 1}); + + e.health_bar.Scale(e.sprite.GetScale() * Vector2(0.6f, 0.01f)); + e.health_bar.SetOffset(spawn_pos); + e.health_bar.PlaceToBottomOf(e.sprite); + e.health_bar.SetColor({0.161f, 0.89f, 0.322f, 1}); + + e.score.Create(score_tex_[e.enemy_type]); + e.score.AutoScale(); + e.score.SetColor({1, 1, 1, 1}); + e.score.SetOffset(spawn_pos); + + e.target_animator.Attach(&e.target); + + e.blast_animator.SetEndCallback(Animator::kFrames, + [&]() -> void { e.blast.SetVisible(false); }); + if (damage_type == kDamageType_Green) { + e.blast.SetFrame(0); + e.blast_animator.SetFrames(6, 28); + } else { + e.blast.SetFrame(6); + e.blast_animator.SetFrames(6, 28); + } + e.blast_animator.Attach(&e.blast); + + SetupFadeOutAnim(e.health_animator, 1); + e.health_animator.Attach(&e.health_base); + e.health_animator.Attach(&e.health_bar); + + SetupFadeOutAnim(e.score_animator, 0.2f); + e.score_animator.SetMovement({0, engine.GetScreenSize().y / 2}, 2.0f); + e.score_animator.SetEndCallback( + Animator::kMovement, [&]() -> void { e.marked_for_removal = true; }); + e.score_animator.Attach(&e.score); + + float max_distance = + engine.GetScreenSize().y - game->GetPlayer().GetWeaponScale().y / 2; + + e.movement_animator.SetMovement( + {0, -max_distance}, speed, + std::bind(Acceleration, std::placeholders::_1, -0.15f)); + e.movement_animator.SetEndCallback(Animator::kMovement, [&]() -> void { + e.sprite.SetVisible(false); + e.target.SetVisible(false); + e.blast.SetVisible(false); + e.marked_for_removal = true; + }); + e.movement_animator.Attach(&e.sprite); + e.movement_animator.Attach(&e.target); + e.movement_animator.Attach(&e.blast); + e.movement_animator.Attach(&e.health_base); + e.movement_animator.Attach(&e.health_bar); + e.movement_animator.Attach(&e.score); + e.movement_animator.Play(Animator::kMovement, false); + + e.explosion_.SetSound(explosion_sound_); + e.explosion_.SetVariate(true); + e.explosion_.SetSimulateStereo(true); +} + +Enemy::EnemyUnit* Enemy::GetTarget(DamageType damage_type) { + for (auto& e : enemies_) { + if (e.targetted_by_weapon_ == damage_type && e.hit_points > 0 && + !e.marked_for_removal) + return &e; + } + return nullptr; +} + +int Enemy::GetScore(EnemyType enemy_type) { + assert(enemy_type > kEnemyType_Invalid && enemy_type < kEnemyType_Max); + return enemy_scores[enemy_type]; +} + +std::unique_ptr Enemy::GetScoreImage(int score) { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + std::string text = std::to_string(score); + int width, height; + font.CalculateBoundingBox(text.c_str(), width, height); + + auto image = std::make_unique(); + image->Create(width, height); + image->Clear({1, 1, 1, 0}); + + font.Print(0, 0, text.c_str(), image->GetBuffer(), image->GetWidth()); + + return image; +} + +bool Enemy::CreateRenderResources() { + auto skull_image = std::make_unique(); + if (!skull_image->Load("enemy_anims_01_frames_ok.png")) + return false; + auto bug_image = std::make_unique(); + if (!bug_image->Load("enemy_anims_02_frames_ok.png")) + return false; + auto target_image = std::make_unique(); + if (!target_image->Load("enemy_target_single_ok.png")) + return false; + auto blast_image = std::make_unique(); + if (!blast_image->Load("enemy_anims_blast_ok.png")) + return false; + + skull_image->Compress(); + bug_image->Compress(); + target_image->Compress(); + blast_image->Compress(); + + skull_tex_->Update(std::move(skull_image)); + bug_tex_->Update(std::move(bug_image)); + target_tex_->Update(std::move(target_image)); + blast_tex_->Update(std::move(blast_image)); + + for (int i = 0; i < kEnemyType_Max; ++i) + score_tex_[i]->Update(GetScoreImage(GetScore((EnemyType)i))); + + return true; +} diff --git a/src/demo/enemy.h b/src/demo/enemy.h new file mode 100644 index 0000000..0b578de --- /dev/null +++ b/src/demo/enemy.h @@ -0,0 +1,119 @@ +#ifndef ENEMY_H +#define ENEMY_H + +#include +#include +#include + +#include "../base/vecmath.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" +#include "../engine/solid_quad.h" +#include "../engine/sound_player.h" +#include "damage_type.h" + +namespace eng { +class Image; +class Sound; +class Texture; +} // namespace eng + +class Enemy { + public: + Enemy(); + ~Enemy(); + + bool Initialize(); + + void ContextLost(); + + void Update(float delta_time); + + void Draw(float frame_frac); + + bool HasTarget(DamageType damage_type); + base::Vector2 GetTargetPos(DamageType damage_type); + + void SelectTarget(DamageType damage_type, + const base::Vector2& origin, + const base::Vector2& dir, + float snap_factor); + void DeselectTarget(DamageType damage_type); + + void HitTarget(DamageType damage_type); + + void OnWaveFinished(); + void OnWaveStarted(int wave); + + int num_enemies_killed_in_current_wave() { + return num_enemies_killed_in_current_wave_; + } + + private: + struct EnemyUnit { + EnemyType enemy_type = kEnemyType_Invalid; + DamageType damage_type = kDamageType_Invalid; + + bool marked_for_removal = false; + DamageType targetted_by_weapon_ = kDamageType_Invalid; + int total_health = 0; + int hit_points = 0; + + eng::ImageQuad sprite; + eng::ImageQuad target; + eng::ImageQuad blast; + eng::ImageQuad score; + eng::SolidQuad health_base; + eng::SolidQuad health_bar; + + eng::Animator movement_animator; + eng::Animator sprite_animator; + eng::Animator target_animator; + eng::Animator blast_animator; + eng::Animator health_animator; + eng::Animator score_animator; + + eng::SoundPlayer explosion_; + }; + + std::shared_ptr skull_tex_; + std::shared_ptr bug_tex_; + std::shared_ptr target_tex_; + std::shared_ptr blast_tex_; + std::shared_ptr score_tex_[kEnemyType_Max]; + + std::shared_ptr explosion_sound_; + + std::list enemies_; + + int num_enemies_killed_in_current_wave_ = 0; + + std::array seconds_since_last_spawn_ = {0, 0, 0}; + std::array seconds_to_next_spawn_ = {0, 0, 0}; + + float spawn_factor_ = 0; + float spawn_factor_interpolator_ = 0; + + bool waiting_for_next_wave_ = false; + + int last_spawn_col_ = 0; + + void TakeDamage(EnemyUnit* target, int damage); + + void SpawnNextEnemy(); + + void Spawn(EnemyType enemy_type, + DamageType damage_type, + const base::Vector2& pos, + float speed); + + EnemyUnit* GetTarget(DamageType damage_type); + + int GetScore(EnemyType enemy_type); + + std::unique_ptr GetScoreImage(int score); + + bool CreateRenderResources(); +}; + +#endif // ENEMY_H diff --git a/src/demo/hud.cc b/src/demo/hud.cc new file mode 100644 index 0000000..bda90ab --- /dev/null +++ b/src/demo/hud.cc @@ -0,0 +1,164 @@ +#include "hud.h" + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/vecmath.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/renderer/texture.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr float kHorizontalMargin = 0.07f; +constexpr float kVerticalMargin = 0.025f; + +const Vector4 kPprogressBarColor[2] = {{0.256f, 0.434f, 0.72f, 1}, + {0.905f, 0.493f, 0.194f, 1}}; +const Vector4 kTextColor = {0.895f, 0.692f, 0.24f, 1}; + +} // namespace + +Hud::Hud() { + text_[0].Create(Engine::Get().CreateRenderResource()); + text_[1].Create(Engine::Get().CreateRenderResource()); +} + +Hud::~Hud() = default; + +bool Hud::Initialize() { + Engine& engine = Engine::Get(); + const Font& font = static_cast(engine.GetGame())->GetFont(); + + int tmp; + font.CalculateBoundingBox("big_enough_text", max_text_width_, tmp); + + for (int i = 0; i < 2; ++i) { + auto image = CreateImage(); + + text_[i].GetTexture()->Update(std::move(image)); + text_[i].AutoScale(); + text_[i].SetColor(kTextColor); + + Vector2 pos = (engine.GetScreenSize() / 2 - text_[i].GetScale() / 2); + pos -= engine.GetScreenSize() * Vector2(kHorizontalMargin, kVerticalMargin); + + Vector2 scale = engine.GetScreenSize() * Vector2(1, 0); + scale -= engine.GetScreenSize() * Vector2(kHorizontalMargin * 4, 0); + scale += text_[0].GetScale() * Vector2(0, 0.3f); + + progress_bar_[i].Scale(scale); + progress_bar_[i].Translate(pos * Vector2(0, 1)); + progress_bar_[i].SetColor(kPprogressBarColor[i] * Vector4(1, 1, 1, 0)); + + pos -= progress_bar_[i].GetScale() * Vector2(0, 4); + text_[i].Translate(pos * Vector2(i ? 1 : -1, 1)); + + progress_bar_animator_[i].Attach(&progress_bar_[i]); + + text_animator_cb_[i] = [&, i]() -> void { + text_animator_[i].SetEndCallback(Animator::kBlending, nullptr); + text_animator_[i].SetBlending( + kTextColor, 2, std::bind(Acceleration, std::placeholders::_1, -1)); + text_animator_[i].Play(Animator::kBlending, false); + }; + text_animator_[i].Attach(&text_[i]); + } + + return true; +} + +void Hud::Update(float delta_time) { + for (int i = 0; i < 2; ++i) { + text_animator_[i].Update(delta_time); + progress_bar_animator_[i].Update(delta_time); + } +} + +void Hud::Draw() { + for (int i = 0; i < 2; ++i) { + progress_bar_[i].Draw(); + text_[i].Draw(); + } +} + +void Hud::ContextLost() { + PrintScore(last_score_, false); + PrintWave(last_wave_, false); +} + +void Hud::Show() { + if (text_[0].IsVisible()) + return; + + for (int i = 0; i < 2; ++i) { + progress_bar_[i].SetVisible(true); + text_[i].SetVisible(true); + progress_bar_animator_[i].SetBlending(kPprogressBarColor[i], 0.3f); + progress_bar_animator_[i].Play(Animator::kBlending, false); + } +} + +void Hud::PrintScore(int score, bool flash) { + last_score_ = score; + Print(0, std::to_string(score)); + + if (flash) { + text_animator_[0].SetEndCallback(Animator::kBlending, text_animator_cb_[0]); + text_animator_[0].SetBlending( + {1, 1, 1, 1}, 0.1f, std::bind(Acceleration, std::placeholders::_1, 1)); + text_animator_[0].Play(Animator::kBlending, false); + } +} + +void Hud::PrintWave(int wave, bool flash) { + last_wave_ = wave; + std::string text = "wave "; + text += std::to_string(wave); + Print(1, text.c_str()); + + if (flash) { + text_animator_[1].SetEndCallback(Animator::kBlending, text_animator_cb_[1]); + text_animator_[1].SetBlending({1, 1, 1, 1}, 0.08f); + text_animator_[1].Play(Animator::kBlending, false); + } +} + +void Hud::SetProgress(float progress) { + progress = std::min(std::max(0.0f, progress), 1.0f); + last_progress_ = progress; + Vector2 s = progress_bar_[0].GetScale() * Vector2(progress, 1); + float t = (s.x - progress_bar_[1].GetScale().x) / 2; + progress_bar_[1].SetScale(s); + progress_bar_[1].Translate({t, 0}); +} + +void Hud::Print(int i, const std::string& text) { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + auto image = CreateImage(); + + float x = 0; + if (i == 1) { + int w, h; + font.CalculateBoundingBox(text.c_str(), w, h); + x = image->GetWidth() - w; + } + + font.Print(x, 0, text.c_str(), image->GetBuffer(), image->GetWidth()); + + text_[i].GetTexture()->Update(std::move(image)); +} + +std::unique_ptr Hud::CreateImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + auto image = std::make_unique(); + image->Create(max_text_width_, font.GetLineHeight()); + image->Clear({1, 1, 1, 0}); + return image; +} diff --git a/src/demo/hud.h b/src/demo/hud.h new file mode 100644 index 0000000..fea2edb --- /dev/null +++ b/src/demo/hud.h @@ -0,0 +1,54 @@ +#ifndef HUD_H +#define HUD_H + +#include +#include + +#include "../base/closure.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" +#include "../engine/solid_quad.h" + +namespace eng { +class Image; +} // namespace eng + +class Hud { + public: + Hud(); + ~Hud(); + + bool Initialize(); + + void Update(float delta_time); + + void Draw(); + + void ContextLost(); + + void Show(); + + void PrintScore(int score, bool flash); + void PrintWave(int wave, bool flash); + void SetProgress(float progress); + + private: + eng::SolidQuad progress_bar_[2]; + eng::ImageQuad text_[2]; + + eng::Animator progress_bar_animator_[2]; + eng::Animator text_animator_[2]; + base::Closure text_animator_cb_[2]; + + int max_text_width_ = 0; + + int last_score_ = 0; + int last_wave_ = 0; + float last_progress_ = 0; + + void Print(int i, const std::string& text); + + std::unique_ptr CreateImage(); +}; + +#endif // HUD_H diff --git a/src/demo/menu.cc b/src/demo/menu.cc new file mode 100644 index 0000000..c19fff6 --- /dev/null +++ b/src/demo/menu.cc @@ -0,0 +1,214 @@ +#include "menu.h" + +#include +#include +#include + +#include "../base/collusion_test.h" +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/worker.h" +#include "../engine/engine.h" +#include "../engine/font.h" +#include "../engine/image.h" +#include "../engine/input_event.h" +#include "../engine/renderer/texture.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr char kMenuOption[Menu::kOption_Max][10] = {"continue", "start", + "credits", "exit"}; + +constexpr float kMenuOptionSpace = 1.5f; + +const Vector4 kColorNormal = {1, 1, 1, 1}; +const Vector4 kColorHighlight = {5, 5, 5, 1}; +constexpr float kBlendingSpeed = 0.12f; + +const Vector4 kColorFadeOut = {1, 1, 1, 0}; +constexpr float kFadeSpeed = 0.2f; + +} // namespace + +Menu::Menu() : tex_(Engine::Get().CreateRenderResource()) {} + +Menu::~Menu() = default; + +bool Menu::Initialize() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + max_text_width_ = -1; + for (int i = 0; i < kOption_Max; ++i) { + int width, height; + font.CalculateBoundingBox(kMenuOption[i], width, height); + if (width > max_text_width_) + max_text_width_ = width; + } + + tex_->Update(CreateImage()); + + for (int i = 0; i < kOption_Max; ++i) { + items_[i].text.Create(tex_, {1, 4}); + items_[i].text.AutoScale(); + items_[i].text.Scale(1.5f); + items_[i].text.SetColor(kColorFadeOut); + items_[i].text.SetVisible(false); + items_[i].text.SetFrame(i); + + items_[i].select_item_cb_ = [&, i]() -> void { + items_[i].text_animator.SetEndCallback( + Animator::kBlending, [&, i]() -> void { + items_[i].text_animator.SetEndCallback(Animator::kBlending, + nullptr); + selected_option_ = (Option)i; + }); + items_[i].text_animator.SetBlending(kColorNormal, kBlendingSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + }; + items_[i].text_animator.Attach(&items_[i].text); + } + // Get the item positions calculated. + SetOptionEnabled(kContinue, true); + + return true; +} + +void Menu::Update(float delta_time) { + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + items_[i].text_animator.Update(delta_time); + } +} + +void Menu::OnInputEvent(std::unique_ptr event) { + if (event->GetType() == InputEvent::kTap || + event->GetType() == InputEvent::kDragStart) + tap_pos_[0] = tap_pos_[1] = event->GetVector(0); + else if (event->GetType() == InputEvent::kDrag) + tap_pos_[1] = event->GetVector(0); + + if ((event->GetType() != InputEvent::kTap && + event->GetType() != InputEvent::kDragEnd) || + IsAnimating()) + return; + + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + if (!Intersection(items_[i].text.GetOffset(), + items_[i].text.GetScale() * Vector2(1.2f, 2), + tap_pos_[0])) + continue; + if (!Intersection(items_[i].text.GetOffset(), + items_[i].text.GetScale() * Vector2(1.2f, 2), + tap_pos_[1])) + continue; + + items_[i].text_animator.SetEndCallback(Animator::kBlending, + items_[i].select_item_cb_); + items_[i].text_animator.SetBlending(kColorHighlight, kBlendingSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + } +} + +void Menu::Draw() { + for (int i = 0; i < kOption_Max; ++i) + items_[i].text.Draw(); +} + +void Menu::ContextLost() { + tex_->Update(CreateImage()); +} + +void Menu::SetOptionEnabled(Option o, bool enable) { + int first = -1, last = -1; + for (int i = 0; i < kOption_Max; ++i) { + if (i == o) + items_[i].hide = !enable; + if (!items_[i].hide) { + items_[i].text.SetOffset({0, 0}); + if (last >= 0) { + items_[i].text.PlaceToBottomOf(items_[last].text); + items_[i].text.Translate(items_[last].text.GetOffset() * Vector2(0, 1)); + items_[i].text.Translate( + {0, items_[last].text.GetScale().y * -kMenuOptionSpace}); + } + if (first < 0) + first = i; + last = i; + } + } + + float center_offset_y = + (items_[first].text.GetOffset().y - items_[last].text.GetOffset().y) / 2; + for (int i = 0; i < kOption_Max; ++i) { + if (!items_[i].hide) + items_[i].text.Translate({0, center_offset_y}); + } +} + +void Menu::Show() { + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + items_[i].text_animator.SetEndCallback( + Animator::kBlending, [&, i]() -> void { + items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr); + }); + items_[i].text_animator.SetBlending(kColorNormal, kFadeSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + items_[i].text.SetVisible(true); + } +} + +void Menu::Hide() { + selected_option_ = kOption_Invalid; + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].hide) + continue; + items_[i].text_animator.SetEndCallback( + Animator::kBlending, [&, i]() -> void { + items_[i].text_animator.SetEndCallback(Animator::kBlending, nullptr); + items_[i].text.SetVisible(false); + }); + items_[i].text_animator.SetBlending(kColorFadeOut, kFadeSpeed); + items_[i].text_animator.Play(Animator::kBlending, false); + } +} + +std::unique_ptr Menu::CreateImage() { + const Font& font = static_cast(Engine::Get().GetGame())->GetFont(); + + int line_height = font.GetLineHeight() + 1; + auto image = std::make_unique(); + image->Create(max_text_width_, line_height * kOption_Max); + + // Fill the area of each menu item with gradient. + image->GradientV({1.0f, 1.0f, 1.0f, 0}, {.0f, .0f, 1.0f, 0}, line_height); + + base::Worker worker(kOption_Max); + for (int i = 0; i < kOption_Max; ++i) { + int w, h; + font.CalculateBoundingBox(kMenuOption[i], w, h); + float x = (image->GetWidth() - w) / 2; + float y = line_height * i; + worker.Enqueue(std::bind(&Font::Print, &font, x, y, kMenuOption[i], + image->GetBuffer(), image->GetWidth())); + } + worker.Join(); + + return image; +} + +bool Menu::IsAnimating() { + for (int i = 0; i < kOption_Max; ++i) { + if (items_[i].text_animator.IsPlaying(Animator::kBlending)) + return true; + } + return false; +} diff --git a/src/demo/menu.h b/src/demo/menu.h new file mode 100644 index 0000000..d0b3027 --- /dev/null +++ b/src/demo/menu.h @@ -0,0 +1,72 @@ +#ifndef MENU_H +#define MENU_H + +#include +#include + +#include "../base/closure.h" +#include "../base/vecmath.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" + +namespace eng { +class Image; +class InputEvent; +class Texture; +} // namespace eng + +class Menu { + public: + enum Option { + kOption_Invalid = -1, + kContinue, + kNewGame, + kCredits, + kExit, + kOption_Max, + }; + + Menu(); + ~Menu(); + + bool Initialize(); + + void Update(float delta_time); + + void OnInputEvent(std::unique_ptr event); + + void Draw(); + + void ContextLost(); + + void SetOptionEnabled(Option o, bool enable); + + void Show(); + void Hide(); + + Option selected_option() const { return selected_option_; } + + private: + struct Item { + eng::ImageQuad text; + eng::Animator text_animator; + base::Closure select_item_cb_; + bool hide = false; + }; + + std::shared_ptr tex_; + + Item items_[kOption_Max]; + + int max_text_width_ = 0; + + Option selected_option_ = kOption_Invalid; + + base::Vector2 tap_pos_[2] = {{0, 0}, {0, 0}}; + + std::unique_ptr CreateImage(); + + bool IsAnimating(); +}; + +#endif // MENU_H diff --git a/src/demo/player.cc b/src/demo/player.cc new file mode 100644 index 0000000..5f0bbbd --- /dev/null +++ b/src/demo/player.cc @@ -0,0 +1,354 @@ +#include "player.h" + +#include + +#include "../base/log.h" +#include "../engine/engine.h" +#include "../engine/image.h" +#include "../engine/input_event.h" +#include "demo.h" + +using namespace base; +using namespace eng; + +namespace { + +constexpr int wepon_warmup_frame[] = {1, 9}; +constexpr int wepon_warmup_frame_count = 4; +constexpr int wepon_cooldown_frame[] = {5, 13}; +constexpr int wepon_cooldown_frame_count = 3; +constexpr int wepon_anim_speed = 48; + +} // namespace + +Player::Player() + : weapon_tex_(Engine::Get().CreateRenderResource()), + beam_tex_(Engine::Get().CreateRenderResource()) {} + +Player::~Player() = default; + +bool Player::Initialize() { + if (!CreateRenderResources()) + return false; + SetupWeapons(); + return true; +} + +void Player::ContextLost() { + CreateRenderResources(); +} + +void Player::Update(float delta_time) { + for (int i = 0; i < 2; ++i) { + warmup_animator_[i].Update(delta_time); + cooldown_animator_[i].Update(delta_time); + beam_animator_[i].Update(delta_time); + spark_animator_[i].Update(delta_time); + } + + if (active_weapon_ != kDamageType_Invalid) + UpdateTarget(); +} + +void Player::OnInputEvent(std::unique_ptr event) { + if (event->GetType() == InputEvent::kNavigateBack) + NavigateBack(); + else if (event->GetType() == InputEvent::kDragStart) + DragStart(event->GetVector(0)); + else if (event->GetType() == InputEvent::kDrag) + Drag(event->GetVector(0)); + else if (event->GetType() == InputEvent::kDragEnd) + DragEnd(); + else if (event->GetType() == InputEvent::kDragCancel) + DragCancel(); +} + +void Player::Draw(float frame_frac) { + for (int i = 0; i < 2; ++i) { + drag_sign_[i].Draw(); + beam_[i].Draw(); + beam_spark_[i].Draw(); + weapon_[i].Draw(); + } +} + +Vector2 Player::GetWeaponPos(DamageType type) const { + return Engine::Get().GetScreenSize() / + Vector2(type == kDamageType_Green ? 3.5f : -3.5f, -2) + + Vector2(0, weapon_[type].GetScale().y * 0.7f); +} + +Vector2 Player::GetWeaponScale() const { + return weapon_[0].GetScale(); +} + +DamageType Player::GetWeaponType(const Vector2& pos) { + DamageType closest_weapon = kDamageType_Invalid; + float closest_dist = std::numeric_limits::max(); + for (int i = 0; i < 2; ++i) { + float dist = (pos - weapon_[i].GetOffset()).Magnitude(); + if (dist < closest_dist) { + closest_dist = dist; + closest_weapon = (DamageType)i; + } + } + + assert(closest_weapon != kDamageType_Invalid); + if (closest_dist < weapon_[closest_weapon].GetScale().x * 0.9f) + return closest_weapon; + return kDamageType_Invalid; +} + +void Player::SetBeamLength(DamageType type, float len) { + beam_[type].SetOffset({0, 0}); + beam_[type].SetScale({len, beam_[type].GetScale().y}); + beam_[type].PlaceToRightOf(weapon_[type]); + beam_[type].Translate(weapon_[type].GetScale() * Vector2(-0.5f, 0)); + beam_[type].SetPivot(beam_[type].GetOffset()); + beam_[type].Translate(weapon_[type].GetOffset()); +} + +void Player::WarmupWeapon(DamageType type) { + cooldown_animator_[type].Stop(Animator::kFrames); + warmup_animator_[type].Play(Animator::kFrames, false); +} + +void Player::CooldownWeapon(DamageType type) { + warmup_animator_[type].Stop(Animator::kFrames); + cooldown_animator_[type].Play(Animator::kFrames, false); +} + +void Player::Fire(DamageType type, Vector2 dir) { + Engine& engine = Engine::Get(); + Enemy& enemy = static_cast(engine.GetGame())->GetEnemy(); + + if (enemy.HasTarget(type)) + dir = weapon_[type].GetOffset() - enemy.GetTargetPos(type); + else + dir *= engine.GetScreenSize().y * 1.3f; + + float len = dir.Magnitude(); + SetBeamLength(type, len); + + dir.Normalize(); + float cos_theta = dir.DotProduct(Vector2(1, 0)); + float theta = acos(cos_theta) + M_PI; + beam_[type].SetTheta(theta); + beam_spark_[type].SetTheta(theta); + + beam_[type].SetColor({1, 1, 1, 1}); + beam_[type].SetVisible(true); + beam_spark_[type].SetVisible(true); + + spark_animator_[type].Stop(Animator::kMovement); + float length = beam_[type].GetScale().x * 0.85f; + Vector2 movement = dir * -length; + // Convert from units per second to duration. + float speed = 1.0f / (18.0f / length); + spark_animator_[type].SetMovement(movement, speed); + spark_animator_[type].Play(Animator::kMovement, false); +} + +bool Player::IsFiring(DamageType type) { + return beam_animator_[type].IsPlaying(Animator::kBlending) || + spark_animator_[type].IsPlaying(Animator::kMovement); +} + +void Player::SetupWeapons() { + for (int i = 0; i < 2; ++i) { + // Setup draw sign. + drag_sign_[i].Create(weapon_tex_, {8, 2}); + drag_sign_[i].AutoScale(); + drag_sign_[i].SetFrame(i * 8); + + // Setup weapon. + weapon_[i].Create(weapon_tex_, {8, 2}); + weapon_[i].AutoScale(); + weapon_[i].SetVisible(true); + weapon_[i].SetFrame(wepon_warmup_frame[i]); + + // Setup beam. + beam_[i].Create(beam_tex_, {1, 2}); + beam_[i].AutoScale(); + beam_[i].SetFrame(i); + beam_[i].PlaceToRightOf(weapon_[i]); + beam_[i].Translate(weapon_[i].GetScale() * Vector2(-0.5f, 0)); + beam_[i].SetPivot(beam_[i].GetOffset()); + + // Setup beam spark. + beam_spark_[i].Create(weapon_tex_, {8, 2}); + beam_spark_[i].AutoScale(); + beam_spark_[i].SetFrame(i * 8 + 1); + beam_spark_[i].PlaceToRightOf(weapon_[i]); + beam_spark_[i].Translate(weapon_[i].GetScale() * Vector2(-0.5f, 0)); + beam_spark_[i].SetPivot(beam_spark_[i].GetOffset()); + + // Place parts on the screen. + Vector2 offset = GetWeaponPos((DamageType)i); + beam_[i].Translate(offset); + beam_spark_[i].Translate(offset); + weapon_[i].Translate(offset); + + // Setup animators. + weapon_[i].SetFrame(wepon_cooldown_frame[i]); + cooldown_animator_[i].SetFrames(wepon_cooldown_frame_count, + wepon_anim_speed); + cooldown_animator_[i].SetEndCallback(Animator::kFrames, [&, i]() -> void { + weapon_[i].SetFrame(wepon_warmup_frame[i]); + }); + cooldown_animator_[i].Attach(&weapon_[i]); + + weapon_[i].SetFrame(wepon_warmup_frame[i]); + warmup_animator_[i].SetFrames(wepon_warmup_frame_count, wepon_anim_speed); + warmup_animator_[i].SetRotation(M_PI * 2, 8.0f); + warmup_animator_[i].Attach(&weapon_[i]); + warmup_animator_[i].Play(Animator::kRotation, true); + + spark_animator_[i].SetEndCallback(Animator::kMovement, [&, i]() -> void { + beam_spark_[i].SetVisible(false); + beam_animator_[i].Play(Animator::kBlending, false); + static_cast(Engine::Get().GetGame()) + ->GetEnemy() + .HitTarget((DamageType)i); + }); + spark_animator_[i].Attach(&beam_spark_[i]); + + beam_animator_[i].SetEndCallback( + Animator::kBlending, [&, i]() -> void { beam_[i].SetVisible(false); }); + beam_animator_[i].SetBlending({1, 1, 1, 0}, 0.16f); + beam_animator_[i].Attach(&beam_[i]); + } +} + +void Player::UpdateTarget() { + if (IsFiring(active_weapon_)) + return; + + Engine& engine = Engine::Get(); + Demo* game = static_cast(engine.GetGame()); + + if (drag_valid_) { + Vector2 dir = (drag_end_ - drag_start_).Normalize(); + game->GetEnemy().SelectTarget(active_weapon_, drag_start_, dir, 1.2f); + if (!game->GetEnemy().HasTarget(active_weapon_)) + game->GetEnemy().SelectTarget(active_weapon_, drag_start_, dir, 2); + } else { + game->GetEnemy().DeselectTarget(active_weapon_); + } +} + +void Player::DragStart(const Vector2& pos) { + active_weapon_ = GetWeaponType(pos); + if (active_weapon_ == kDamageType_Invalid) + return; + + drag_start_ = drag_end_ = pos; + + drag_sign_[active_weapon_].SetOffset(drag_start_); + drag_sign_[active_weapon_].SetVisible(true); +} + +void Player::Drag(const Vector2& pos) { + if (active_weapon_ == kDamageType_Invalid) + return; + + drag_end_ = pos; + drag_sign_[active_weapon_].SetOffset(drag_end_); + + if (ValidateDrag()) { + if (!drag_valid_ && !IsFiring(active_weapon_)) + WarmupWeapon(active_weapon_); + drag_valid_ = true; + } else { + if (drag_valid_ && !IsFiring(active_weapon_)) + CooldownWeapon(active_weapon_); + drag_valid_ = false; + } +} + +void Player::DragEnd() { + if (active_weapon_ == kDamageType_Invalid) + return; + + UpdateTarget(); + + DamageType type = active_weapon_; + active_weapon_ = kDamageType_Invalid; + drag_sign_[type].SetVisible(false); + + Vector2 fire_dir = (drag_start_ - drag_end_).Normalize(); + + if (drag_valid_ && !IsFiring(type)) { + if (warmup_animator_[type].IsPlaying(Animator::kFrames)) { + warmup_animator_[type].SetEndCallback( + Animator::kFrames, [&, type, fire_dir]() -> void { + warmup_animator_[type].SetEndCallback(Animator::kFrames, nullptr); + CooldownWeapon(type); + Fire(type, fire_dir); + }); + } else { + CooldownWeapon(type); + Fire(type, fire_dir); + } + } + + drag_valid_ = false; + drag_start_ = drag_end_ = {0, 0}; +} + +void Player::DragCancel() { + if (active_weapon_ == kDamageType_Invalid) + return; + + DamageType type = active_weapon_; + active_weapon_ = kDamageType_Invalid; + drag_sign_[type].SetVisible(false); + + if (drag_valid_ && !IsFiring(type)) { + if (warmup_animator_[type].IsPlaying(Animator::kFrames)) { + warmup_animator_[type].SetEndCallback( + Animator::kFrames, [&, type]() -> void { + warmup_animator_[type].SetEndCallback(Animator::kFrames, nullptr); + CooldownWeapon(type); + }); + } else { + CooldownWeapon(type); + } + } + + drag_valid_ = false; + drag_start_ = drag_end_ = {0, 0}; +} + +bool Player::ValidateDrag() { + Vector2 dir = drag_end_ - drag_start_; + float len = dir.Magnitude(); + dir.Normalize(); + if (len < weapon_[active_weapon_].GetScale().y / 4) + return false; + if (dir.DotProduct(Vector2(0, 1)) < 0) + return false; + return true; +} + +void Player::NavigateBack() { + DragCancel(); + Engine& engine = Engine::Get(); + static_cast(engine.GetGame())->EnterMenuState(); +} + +bool Player::CreateRenderResources() { + auto weapon_image = std::make_unique(); + if (!weapon_image->Load("enemy_anims_flare_ok.png")) + return false; + auto beam_image = std::make_unique(); + if (!beam_image->Load("enemy_ray_ok.png")) + return false; + + weapon_image->Compress(); + beam_image->Compress(); + + weapon_tex_->Update(std::move(weapon_image)); + beam_tex_->Update(std::move(beam_image)); + return true; +} diff --git a/src/demo/player.h b/src/demo/player.h new file mode 100644 index 0000000..76d1822 --- /dev/null +++ b/src/demo/player.h @@ -0,0 +1,79 @@ +#ifndef PLAYER_H +#define PLAYER_H + +#include + +#include "../base/vecmath.h" +#include "../engine/animator.h" +#include "../engine/image_quad.h" +#include "../engine/renderer/texture.h" +#include "damage_type.h" + +namespace eng { +class InputEvent; +} // namespace eng + +class Player { + public: + Player(); + ~Player(); + + bool Initialize(); + + void ContextLost(); + + void Update(float delta_time); + + void OnInputEvent(std::unique_ptr event); + + void Draw(float frame_frac); + + base::Vector2 GetWeaponPos(DamageType type) const; + base::Vector2 GetWeaponScale() const; + + private: + std::shared_ptr weapon_tex_; + std::shared_ptr beam_tex_; + + eng::ImageQuad drag_sign_[2]; + eng::ImageQuad weapon_[2]; + eng::ImageQuad beam_[2]; + eng::ImageQuad beam_spark_[2]; + + eng::Animator warmup_animator_[2]; + eng::Animator cooldown_animator_[2]; + eng::Animator beam_animator_[2]; + eng::Animator spark_animator_[2]; + + DamageType active_weapon_ = kDamageType_Invalid; + + base::Vector2 drag_start_ = {0, 0}; + base::Vector2 drag_end_ = {0, 0}; + bool drag_valid_ = false; + + DamageType GetWeaponType(const base::Vector2& pos); + + void SetBeamLength(DamageType type, float len); + + void WarmupWeapon(DamageType type); + void CooldownWeapon(DamageType type); + + void Fire(DamageType type, base::Vector2 dir); + bool IsFiring(DamageType type); + + void SetupWeapons(); + + void UpdateTarget(); + + void DragStart(const base::Vector2& pos); + void Drag(const base::Vector2& pos); + void DragEnd(); + void DragCancel(); + bool ValidateDrag(); + + void NavigateBack(); + + bool CreateRenderResources(); +}; + +#endif // PLAYER_H diff --git a/src/demo/sky_quad.cc b/src/demo/sky_quad.cc new file mode 100644 index 0000000..b9bab2e --- /dev/null +++ b/src/demo/sky_quad.cc @@ -0,0 +1,64 @@ +#include "sky_quad.h" + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/random.h" +#include "../engine/engine.h" +#include "../engine/renderer/geometry.h" +#include "../engine/renderer/shader.h" +#include "../engine/shader_source.h" + +using namespace base; +using namespace eng; + +SkyQuad::SkyQuad() + : shader_(Engine::Get().CreateRenderResource()), + sky_offset_{ + 0, Lerp(0.0f, 10.0f, Engine::Get().GetRandomGenerator().GetFloat())} { +} + +SkyQuad::~SkyQuad() = default; + +bool SkyQuad::Create() { + Engine& engine = Engine::Get(); + + auto source = std::make_unique(); + if (!source->Load("sky.glsl")) + return false; + shader_->Create(std::move(source), engine.GetQuad()->vertex_description()); + + scale_ = engine.GetScreenSize(); + + color_animator_.Attach(this); + + return true; +} + +void SkyQuad::Update(float delta_time) { + sky_offset_ += {0, delta_time * 0.04f}; + color_animator_.Update(delta_time); +} + +void SkyQuad::Draw(float frame_frac) { + Vector2 sky_offset = Lerp(last_sky_offset_, sky_offset_, frame_frac); + + shader_->Activate(); + shader_->SetUniform("scale", scale_); + shader_->SetUniform("projection", Engine::Get().GetProjectionMarix()); + shader_->SetUniform("sky_offset", sky_offset); + shader_->SetUniform("nebula_color", + {nebula_color_.x, nebula_color_.y, nebula_color_.z}); + + Engine::Get().GetQuad()->Draw(); + last_sky_offset_ = sky_offset_; +} + +void SkyQuad::ContextLost() { + Create(); +} + +void SkyQuad::SwitchColor(const Vector4& color) { + color_animator_.SetBlending(color, 5, + std::bind(SmoothStep, std::placeholders::_1)); + color_animator_.Play(Animator::kBlending, false); +} diff --git a/src/demo/sky_quad.h b/src/demo/sky_quad.h new file mode 100644 index 0000000..271c7bf --- /dev/null +++ b/src/demo/sky_quad.h @@ -0,0 +1,52 @@ +#ifndef SKY_QUAD_H +#define SKY_QUAD_H + +#include "../base/vecmath.h" +#include "../engine/animatable.h" +#include "../engine/animator.h" + +#include +#include +#include +#include + +namespace eng { +class Shader; +} // namespace eng + +class SkyQuad : public eng::Animatable { + public: + SkyQuad(); + ~SkyQuad(); + + SkyQuad(const SkyQuad&) = delete; + SkyQuad& operator=(const SkyQuad&) = delete; + + bool Create(); + + void Update(float delta_time); + + // Animatable interface. + void SetFrame(size_t frame) override {} + size_t GetFrame() const override { return 0; } + size_t GetNumFrames() const override { return 0; } + void SetColor(const base::Vector4& color) override { nebula_color_ = color; } + base::Vector4 GetColor() const override { return nebula_color_; } + + void Draw(float frame_frac); + void ContextLost(); + + void SwitchColor(const base::Vector4& color); + + private: + std::shared_ptr shader_; + + base::Vector2 sky_offset_ = {0, 0}; + base::Vector2 last_sky_offset_ = {0, 0}; + base::Vector4 nebula_color_ = {0, 0, 0, 1}; + base::Vector2 scale_ = {1, 1}; + + eng::Animator color_animator_; +}; + +#endif // SKY_QUAD_H diff --git a/src/engine/animatable.cc b/src/engine/animatable.cc new file mode 100644 index 0000000..4f25921 --- /dev/null +++ b/src/engine/animatable.cc @@ -0,0 +1,33 @@ +#include "animatable.h" + +#include + +using namespace base; + +namespace eng { + +void Animatable::Translate(const Vector2& offset) { + offset_ += offset; +} + +void Animatable::Scale(const Vector2& scale) { + scale_ *= scale; +} + +void Animatable::Scale(float scale) { + scale_ *= scale; +} + +void Animatable::Rotate(float angle) { + theta_ += angle; + rotation_.x = sin(theta_); + rotation_.y = cos(theta_); +} + +void Animatable::SetTheta(float theta) { + theta_ = theta; + rotation_.x = sin(theta_); + rotation_.y = cos(theta_); +} + +} // namespace eng diff --git a/src/engine/animatable.h b/src/engine/animatable.h new file mode 100644 index 0000000..446e3d0 --- /dev/null +++ b/src/engine/animatable.h @@ -0,0 +1,66 @@ +#ifndef SHAPE_H +#define SHAPE_H + +#include "../base/vecmath.h" + +namespace eng { + +class Animatable { + public: + Animatable() = default; + virtual ~Animatable() = default; + + void Translate(const base::Vector2& offset); + void Scale(const base::Vector2& scale); + void Scale(float scale); + void Rotate(float angle); + + void SetOffset(const base::Vector2& offset) { offset_ = offset; } + void SetScale(const base::Vector2& scale) { scale_ = scale; } + void SetPivot(const base::Vector2& pivot) { pivot_ = pivot; } + void SetTheta(float theta); + + base::Vector2 GetOffset() const { return offset_; } + base::Vector2 GetScale() const { return scale_; } + base::Vector2 GetPivot() const { return pivot_; } + float GetTheta() const { return theta_; } + + // Pure virtuals for frame animation support. + virtual void SetFrame(size_t frame) = 0; + virtual size_t GetFrame() const = 0; + virtual size_t GetNumFrames() const = 0; + + virtual void SetColor(const base::Vector4& color) = 0; + virtual base::Vector4 GetColor() const = 0; + + void SetVisible(bool visible) { visible_ = visible; } + bool IsVisible() const { return visible_; } + + void PlaceToLeftOf(const Animatable& s) { + Translate({s.GetScale().x / -2.0f + GetScale().x / -2.0f, 0}); + } + + void PlaceToRightOf(const Animatable& s) { + Translate({s.GetScale().x / 2.0f + GetScale().x / 2.0f, 0}); + } + + void PlaceToTopOf(const Animatable& s) { + Translate({0, s.GetScale().y / 2.0f + GetScale().y / 2.0f}); + } + + void PlaceToBottomOf(const Animatable& s) { + Translate({0, s.GetScale().y / -2.0f + GetScale().y / -2.0f}); + } + + protected: + base::Vector2 offset_ = {0, 0}; + base::Vector2 scale_ = {1, 1}; + base::Vector2 pivot_ = {0, 0}; + base::Vector2 rotation_ = {0, 1}; + float theta_ = 0; + bool visible_ = false; +}; + +} // namespace eng + +#endif // SHAPE_H diff --git a/src/engine/animator.cc b/src/engine/animator.cc new file mode 100644 index 0000000..2a34669 --- /dev/null +++ b/src/engine/animator.cc @@ -0,0 +1,276 @@ +#include "animator.h" + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "animatable.h" + +using namespace base; + +namespace eng { + +void Animator::Attach(Animatable* animatable) { + elements_.push_back({animatable, + {0, 0}, + 0, + animatable->GetColor(), + (int)animatable->GetFrame()}); +} + +void Animator::Play(int animation, bool loop) { + play_flags_ |= animation; + loop_flags_ |= loop ? animation : 0; +} + +void Animator::Pause(int animation) { + play_flags_ &= ~animation; +} + +void Animator::Stop(int animation) { + if ((animation & kMovement) != 0) + movement_time_ = 0; + if ((animation & kRotation) != 0) + rotation_time_ = 0; + if ((animation & kBlending) != 0) + blending_time_ = 0; + if ((animation & kFrames) != 0) + frame_time_ = 0; + if ((animation & kTimer) != 0) + timer_time_ = 0; + + play_flags_ |= animation; + Update(0); + play_flags_ &= ~animation; + loop_flags_ &= ~animation; +} + +void Animator::SetEndCallback(int animation, base::Closure cb) { + if ((inside_cb_ & animation) != 0) { + has_pending_cb_ = true; + pending_cb_ = std::move(cb); + } + if ((animation & kMovement) != 0 && inside_cb_ != kMovement) + movement_cb_ = std::move(cb); + if ((animation & kRotation) != 0 && inside_cb_ != kRotation) + rotation_cb_ = std::move(cb); + if ((animation & kBlending) != 0 && inside_cb_ != kBlending) + blending_cb_ = std::move(cb); + if ((animation & kFrames) != 0 && inside_cb_ != kFrames) + frame_cb_ = std::move(cb); + if ((animation & kTimer) != 0 && inside_cb_ != kTimer) + timer_cb_ = std::move(cb); +} + +void Animator::SetMovement(Vector2 direction, + float duration, + Interpolator interpolator) { + movement_direction_ = direction; + movement_speed_ = 1.0f / duration; + movement_interpolator_ = std::move(interpolator); + + for (auto& a : elements_) + a.movement_last_offset = {0, 0}; +} + +void Animator::SetRotation(float trget, + float duration, + Interpolator interpolator) { + rotation_target_ = trget; + rotation_speed_ = 1.0f / duration; + rotation_interpolator_ = std::move(interpolator); + + for (auto& a : elements_) + a.rotation_last_theta = 0; +} + +void Animator::SetBlending(Vector4 target, + float duration, + Interpolator interpolator) { + blending_target_ = target; + blending_speed_ = 1.0f / duration; + for (auto& a : elements_) + a.blending_start = a.animatable->GetColor(); + blending_interpolator_ = std::move(interpolator); +} + +void Animator::SetFrames(int count, + int frames_per_second, + Interpolator interpolator) { + frame_count_ = count; + frame_speed_ = (float)frames_per_second / (float)count; + for (auto& a : elements_) + a.frame_start_ = a.animatable->GetFrame(); + frame_interpolator_ = std::move(interpolator); +} + +void Animator::SetTimer(float duration) { + timer_speed_ = 1.0f / duration; +} + +void Animator::SetVisible(bool visible) { + for (auto& a : elements_) + a.animatable->SetVisible(visible); +} + +void Animator::Update(float delta_time) { + if (play_flags_ & kMovement) + UpdateMovement(delta_time); + if (play_flags_ & kRotation) + UpdateRotation(delta_time); + if (play_flags_ & kBlending) + UpdateBlending(delta_time); + if (play_flags_ & kFrames) + UpdateFrame(delta_time); + if (play_flags_ & kTimer) + UpdateTimer(delta_time); + + for (auto& a : elements_) { + if (play_flags_ & kMovement) { + float interpolated_time = movement_interpolator_ + ? movement_interpolator_(movement_time_) + : movement_time_; + Vector2 offset = + base::Lerp({0, 0}, movement_direction_, interpolated_time); + a.animatable->Translate(offset - a.movement_last_offset); + a.movement_last_offset = offset; + } + + if (play_flags_ & kRotation) { + float interpolated_time = rotation_interpolator_ + ? rotation_interpolator_(rotation_time_) + : rotation_time_; + float theta = base::Lerp(0.0f, rotation_target_, interpolated_time); + a.animatable->Rotate(theta - a.rotation_last_theta); + a.rotation_last_theta = theta; + } + + if (play_flags_ & kBlending) { + float interpolated_time = blending_interpolator_ + ? blending_interpolator_(blending_time_) + : blending_time_; + Vector4 r = + base::Lerp(a.blending_start, blending_target_, interpolated_time); + a.animatable->SetColor(r); + } + + if (play_flags_ & kFrames) { + float interpolated_time = + frame_interpolator_ ? frame_interpolator_(frame_time_) : frame_time_; + int target = a.frame_start_ + frame_count_; + int r = base::Lerp(a.frame_start_, target, interpolated_time); + if (r < target) + a.animatable->SetFrame(r); + } + } +} + +void Animator::UpdateMovement(float delta_time) { + if ((loop_flags_ & kMovement) == 0 && movement_time_ == 1.0f) { + movement_time_ = 0; + play_flags_ &= ~kMovement; + if (movement_cb_) { + inside_cb_ = kMovement; + movement_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + movement_cb_ = std::move(pending_cb_); + } + } + return; + } + + movement_time_ += movement_speed_ * delta_time; + if (movement_time_ > 1) + movement_time_ = + (loop_flags_ & kMovement) == 0 ? 1 : fmod(movement_time_, 1.0f); +} + +void Animator::UpdateRotation(float delta_time) { + if ((loop_flags_ & kRotation) == 0 && rotation_time_ == 1.0f) { + rotation_time_ = 0; + play_flags_ &= ~kRotation; + if (rotation_cb_) { + inside_cb_ = kRotation; + rotation_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + rotation_cb_ = std::move(pending_cb_); + } + } + return; + } + + rotation_time_ += rotation_speed_ * delta_time; + if (rotation_time_ > 1) + rotation_time_ = + (loop_flags_ & kRotation) == 0 ? 1 : fmod(rotation_time_, 1.0f); +} + +void Animator::UpdateBlending(float delta_time) { + if ((loop_flags_ & kBlending) == 0 && blending_time_ == 1.0f) { + blending_time_ = 0; + play_flags_ &= ~kBlending; + if (blending_cb_) { + inside_cb_ = kBlending; + blending_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + blending_cb_ = std::move(pending_cb_); + } + } + return; + } + + blending_time_ += blending_speed_ * delta_time; + if (blending_time_ > 1) + blending_time_ = + (loop_flags_ & kBlending) == 0 ? 1 : fmod(blending_time_, 1.0f); +} + +void Animator::UpdateFrame(float delta_time) { + if ((loop_flags_ & kFrames) == 0 && frame_time_ == 1.0f) { + frame_time_ = 0; + play_flags_ &= ~kFrames; + if (frame_cb_) { + inside_cb_ = kFrames; + frame_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + frame_cb_ = std::move(pending_cb_); + } + } + return; + } else if ((loop_flags_ & kFrames) != 0 && frame_time_ == 1.0f) { + frame_time_ = 0; + } + + frame_time_ += frame_speed_ * delta_time; + if (frame_time_ > 1) + frame_time_ = 1; +} + +void Animator::UpdateTimer(float delta_time) { + if (timer_time_ == 1.0f) { + timer_time_ = 0; + play_flags_ &= ~kTimer; + if (timer_cb_) { + inside_cb_ = kTimer; + timer_cb_(); + inside_cb_ = kNone; + if (has_pending_cb_) { + has_pending_cb_ = false; + timer_cb_ = std::move(pending_cb_); + } + } + return; + } + + timer_time_ += timer_speed_ * delta_time; + if (timer_time_ > 1) + timer_time_ = 1; +} + +} // namespace eng diff --git a/src/engine/animator.h b/src/engine/animator.h new file mode 100644 index 0000000..4c2d952 --- /dev/null +++ b/src/engine/animator.h @@ -0,0 +1,136 @@ +#ifndef ANIMATOR_H +#define ANIMATOR_H + +#include + +#include "../base/closure.h" +#include "../base/vecmath.h" + +namespace eng { + +class Animatable; + +class Animator { + public: + // Animation type flags. + enum Flags { + kNone = 0, + kMovement = 1, + kRotation = 2, + kBlending = 4, + kFrames = 8, + kTimer = 16, + kAllAnimations = kMovement | kRotation | kBlending | kFrames + }; + + using Interpolator = std::function; + + Animator() = default; + ~Animator() = default; + + // Attached the given animatable to this animator and sets the start values. + void Attach(Animatable* animatable); + + void Play(int animation, bool loop); + void Pause(int animation); + void Stop(int animation); + + // Set callback for the given animations. It's called for each animation once + // it ends. Not that it's not called for looping animations. + void SetEndCallback(int animation, base::Closure cb); + + // Set movement animation parameters. Movement is relative to the attached + // animatable's current position. Distance is calculated from the magnitude of + // direction vector. Duration is in seconds. + void SetMovement(base::Vector2 direction, + float duration, + Interpolator interpolator = nullptr); + + // Set rotation animation parameters. Rotation is relative to the attached + // animatable's current rotation. Duration is in seconds. + void SetRotation(float target, + float duration, + Interpolator interpolator = nullptr); + + // Set color blending animation parameters. Color blending animation is + // absolute. The absolute start colors are obtained from the attached + // animatables. Duration is in seconds. + void SetBlending(base::Vector4 target, + float duration, + Interpolator interpolator = nullptr); + + // Set frame playback animation parameters. Frame animation is absolute. The + // absolute start frames are obtained from the attached animatables. Plays + // count number of frames. + void SetFrames(int count, + int frames_per_second, + Interpolator interpolator = nullptr); + + // Set Timer parameters. Timer doesn't play any animation. Usefull for + // triggering a callback after the given seconds passed. Loop parameter is + // ignored when played. + void SetTimer(float duration); + + // Set visibility of all attached animatables. + void SetVisible(bool visible); + + void Update(float delta_time); + + bool IsPlaying(int animation) const { return play_flags_ & animation; } + + private: + struct Element { + Animatable* animatable; + base::Vector2 movement_last_offset = {0, 0}; + float rotation_last_theta = 0; + base::Vector4 blending_start = {0, 0, 0, 0}; + int frame_start_ = 0; + }; + + unsigned int play_flags_ = 0; + unsigned int loop_flags_ = 0; + std::vector elements_; + + base::Vector2 movement_direction_ = {0, 0}; + float movement_speed_ = 0; + float movement_time_ = 0; + Interpolator movement_interpolator_; + base::Closure movement_cb_; + + float rotation_target_ = 0; + float rotation_speed_ = 0; + float rotation_time_ = 0; + Interpolator rotation_interpolator_; + base::Closure rotation_cb_; + + base::Vector4 blending_target_ = {0, 0, 0, 0}; + float blending_speed_ = 0; + float blending_time_ = 0; + Interpolator blending_interpolator_; + base::Closure blending_cb_; + + int frame_count_ = 0; + float frame_speed_ = 0; + float frame_time_ = 0; + Interpolator frame_interpolator_; + base::Closure frame_cb_; + + float timer_speed_ = 0; + float timer_time_ = 0; + base::Closure timer_cb_; + + // State used to set new callback during a callback. + bool has_pending_cb_ = false; + base::Closure pending_cb_; + Flags inside_cb_ = kNone; + + void UpdateMovement(float delta_time); + void UpdateRotation(float delta_time); + void UpdateBlending(float delta_time); + void UpdateFrame(float delta_time); + void UpdateTimer(float delta_time); +}; + +} // namespace eng + +#endif // ANIMATOR_H diff --git a/src/engine/audio/audio.h b/src/engine/audio/audio.h new file mode 100644 index 0000000..ee1989a --- /dev/null +++ b/src/engine/audio/audio.h @@ -0,0 +1,20 @@ +#ifndef AUDIO_H +#define AUDIO_H + +#if defined(__ANDROID__) +#include "audio_oboe.h" +#elif defined(__linux__) +#include "audio_alsa.h" +#endif + +namespace eng { + +#if defined(__ANDROID__) +using Audio = AudioOboe; +#elif defined(__linux__) +using Audio = AudioAlsa; +#endif + +} // namespace eng + +#endif // AUDIO_H diff --git a/src/engine/audio/audio_alsa.cc b/src/engine/audio/audio_alsa.cc new file mode 100644 index 0000000..cc39e96 --- /dev/null +++ b/src/engine/audio/audio_alsa.cc @@ -0,0 +1,187 @@ +#include "audio_alsa.h" + +#include + +#include "../../base/log.h" +#include "audio_resource.h" + +using namespace base; + +namespace eng { + +AudioAlsa::AudioAlsa() = default; + +AudioAlsa::~AudioAlsa() = default; + +bool AudioAlsa::Initialize() { + LOG << "Initializing audio system."; + + int err; + + // Contains information about the hardware. + snd_pcm_hw_params_t* hw_params; + + // "default" is usualy PulseAudio. Use "plughw:CARD=PCH" instead for direct + // hardware device with software format conversion. + if ((err = snd_pcm_open(&pcm_handle_, "plughw:CARD=PCH", + SND_PCM_STREAM_PLAYBACK, 0)) < 0) { + LOG << "Cannot open audio device. Error: " << snd_strerror(err); + return false; + } + + do { + // Allocate the snd_pcm_hw_params_t structure on the stack. + snd_pcm_hw_params_alloca(&hw_params); + + // Init hw_params with full configuration space. + if ((err = snd_pcm_hw_params_any(pcm_handle_, hw_params)) < 0) { + LOG << "Cannot initialize hardware parameter structure. Error: " + << snd_strerror(err); + break; + } + + if ((err = snd_pcm_hw_params_set_access( + pcm_handle_, hw_params, SND_PCM_ACCESS_RW_INTERLEAVED)) < 0) { + LOG << "Cannot set access type. Error: " << snd_strerror(err); + break; + } + + if ((err = snd_pcm_hw_params_set_format(pcm_handle_, hw_params, + SND_PCM_FORMAT_FLOAT)) < 0) { + LOG << "Cannot set sample format. Error: " << snd_strerror(err); + break; + } + + // Disable software resampler. + if ((err = snd_pcm_hw_params_set_rate_resample(pcm_handle_, hw_params, 0)) < + 0) { + LOG << "Cannot disbale software resampler. Error: " << snd_strerror(err); + break; + } + + unsigned sample_rate = 48000; + if ((err = snd_pcm_hw_params_set_rate_near(pcm_handle_, hw_params, + &sample_rate, 0)) < 0) { + LOG << "Cannot set sample rate. Error: " << snd_strerror(err); + break; + } + + if ((err = snd_pcm_hw_params_set_channels(pcm_handle_, hw_params, 2)) < 0) { + LOG << "Cannot set channel count. Error: " << snd_strerror(err); + break; + } + + // Set period time to 4 ms. The latency will be 12 ms for 3 perods. + unsigned period_time = 4000; + if ((err = snd_pcm_hw_params_set_period_time_near(pcm_handle_, hw_params, + &period_time, 0)) < 0) { + LOG << "Cannot set periods. Error: " << snd_strerror(err); + break; + } + + unsigned periods = 3; + if ((err = snd_pcm_hw_params_set_periods_near(pcm_handle_, hw_params, + &periods, 0)) < 0) { + LOG << "Cannot set periods. Error: " << snd_strerror(err); + break; + } + + // Apply HW parameter settings to PCM device and prepare device. + if ((err = snd_pcm_hw_params(pcm_handle_, hw_params)) < 0) { + LOG << "Cannot set parameters. Error: " << snd_strerror(err); + break; + } + + if ((err = snd_pcm_prepare(pcm_handle_)) < 0) { + LOG << "Cannot prepare audio interface for use. Error: " + << snd_strerror(err); + break; + } + + snd_pcm_access_t access; + unsigned num_channels; + snd_pcm_format_t format; + snd_pcm_uframes_t period_size; + snd_pcm_uframes_t buffer_size; + + snd_pcm_hw_params_get_access(hw_params, &access); + snd_pcm_hw_params_get_channels(hw_params, &num_channels); + snd_pcm_hw_params_get_format(hw_params, &format); + snd_pcm_hw_params_get_period_size(hw_params, &period_size, nullptr); + snd_pcm_hw_params_get_period_time(hw_params, &period_time, nullptr); + snd_pcm_hw_params_get_periods(hw_params, &periods, nullptr); + snd_pcm_hw_params_get_buffer_size(hw_params, &buffer_size); + + LOG << "Alsa Audio:"; + LOG << " access: " << snd_pcm_access_name(access); + LOG << " format: " << snd_pcm_format_name(format); + LOG << " channel count: " << num_channels; + LOG << " sample rate: " << sample_rate; + LOG << " period size: " << period_size; + LOG << " period time: " << period_time; + LOG << " periods: " << periods; + LOG << " buffer_size: " << buffer_size; + + num_channels_ = num_channels; + sample_rate_ = sample_rate; + period_size_ = period_size; + + StartWorker(); + + return true; + } while (false); + + snd_pcm_close(pcm_handle_); + return false; +} + +void AudioAlsa::Shutdown() { + LOG << "Shutting down audio system."; + TerminateWorker(); + snd_pcm_drop(pcm_handle_); + snd_pcm_close(pcm_handle_); +} + +size_t AudioAlsa::GetSampleRate() { + return sample_rate_; +} + +bool AudioAlsa::StartWorker() { + LOG << "Starting audio thread."; + + std::promise promise; + std::future future = promise.get_future(); + worker_thread_ = + std::thread(&AudioAlsa::WorkerMain, this, std::move(promise)); + return future.get(); +} + +void AudioAlsa::TerminateWorker() { + // Notify worker thread and wait for it to terminate. + if (terminate_worker_) + return; + terminate_worker_ = true; + LOG << "Terminating audio thread"; + worker_thread_.join(); +} + +void AudioAlsa::WorkerMain(std::promise promise) { + promise.set_value(true); + + size_t num_frames = period_size_ / (num_channels_ * sizeof(float)); + auto buffer = std::make_unique(num_frames * 2); + + for (;;) { + if (terminate_worker_) + return; + + RenderAudio(buffer.get(), num_frames); + + while (snd_pcm_writei(pcm_handle_, buffer.get(), num_frames) < 0) { + snd_pcm_prepare(pcm_handle_); + LOG << "Audio buffer underrun!"; + } + } +} + +} // namespace eng diff --git a/src/engine/audio/audio_alsa.h b/src/engine/audio/audio_alsa.h new file mode 100644 index 0000000..fd54f2f --- /dev/null +++ b/src/engine/audio/audio_alsa.h @@ -0,0 +1,46 @@ +#ifndef AUDIO_ALSA_H +#define AUDIO_ALSA_H + +#include +#include +#include + +#include "audio_base.h" + +typedef struct _snd_pcm snd_pcm_t; + +namespace eng { + +class AudioResource; + +class AudioAlsa : public AudioBase { + public: + AudioAlsa(); + ~AudioAlsa(); + + bool Initialize(); + + void Shutdown(); + + size_t GetSampleRate(); + + private: + // Handle for the PCM device. + snd_pcm_t* pcm_handle_; + + std::thread worker_thread_; + bool terminate_worker_ = false; + + size_t num_channels_ = 0; + size_t sample_rate_ = 0; + size_t period_size_ = 0; + + bool StartWorker(); + void TerminateWorker(); + + void WorkerMain(std::promise promise); +}; + +} // namespace eng + +#endif // AUDIO_ALSA_H diff --git a/src/engine/audio/audio_base.cc b/src/engine/audio/audio_base.cc new file mode 100644 index 0000000..e6ea592 --- /dev/null +++ b/src/engine/audio/audio_base.cc @@ -0,0 +1,138 @@ +#include "audio_base.h" + +#include + +#include "../../base/log.h" +#include "../sound.h" + +using namespace base; + +namespace eng { + +AudioBase::AudioBase() = default; + +AudioBase::~AudioBase() { + worker_.Join(); +} + +void AudioBase::Play(std::shared_ptr sample) { + std::unique_lock scoped_lock(mutex_); + samples_[0].push_back(sample); +} + +void AudioBase::Update() { + task_runner_.Run(); +} + +void AudioBase::RenderAudio(float* output_buffer, size_t num_frames) { + { + std::unique_lock scoped_lock(mutex_); + samples_[1].splice(samples_[1].end(), samples_[0]); + } + + memset(output_buffer, 0, sizeof(float) * num_frames * kChannelCount); + + for (auto it = samples_[1].begin(); it != samples_[1].end();) { + AudioSample* sample = it->get(); + + unsigned flags = sample->flags; + bool remove = false; + + if (flags & AudioSample::kStopped) { + remove = true; + } else { + auto sound = sample->sound.get(); + + const float* src[2] = {const_cast(sound)->GetBuffer(0), + const_cast(sound)->GetBuffer(1)}; + if (!src[1]) + src[1] = src[0]; // mono. + + size_t num_samples = sound->GetNumSamples(); + size_t num_channels = sound->num_channels(); + size_t src_index = sample->src_index; + size_t step = sample->step; + size_t accumulator = sample->accumulator; + float amplitude = sample->amplitude; + float amplitude_inc = sample->amplitude_inc; + float max_amplitude = sample->max_amplitude; + + size_t channel_offset = + (flags & AudioSample::kSimulateStereo) && num_channels == 1 + ? sound->hz() / 10 + : 0; + + for (size_t i = 0; i < num_frames * kChannelCount;) { + // Mix the 1st channel. + output_buffer[i++] += src[0][src_index] * amplitude; + + // Mix the 2nd channel. Offset the source index for stereo simulation. + size_t ind = channel_offset + src_index; + if (ind < num_samples) + output_buffer[i++] += src[1][ind] * amplitude; + else if (flags & AudioSample::kLoop) + output_buffer[i++] += src[1][ind % num_samples] * amplitude; + else + i++; + + // Apply amplitude modification. + amplitude += amplitude_inc; + if (amplitude <= 0) { + remove = true; + break; + } else if (amplitude > max_amplitude) { + amplitude = max_amplitude; + } + + // Basic resampling for variations. + accumulator += step; + src_index += accumulator / 10; + accumulator %= 10; + + // Advance source index. + if (src_index >= num_samples) { + if (!sound->is_streaming_sound()) { + if (flags & AudioSample::kLoop) { + src_index %= num_samples; + } else { + remove = true; + break; + } + } else if (!sound->IsStreamingInProgress()) { + if (sound->eof()) { + remove = true; + break; + } + + src_index = 0; + + // Swap buffers and start streaming in background. + sound->SwapBuffers(); + src[0] = const_cast(sound)->GetBuffer(0); + src[1] = const_cast(sound)->GetBuffer(1); + + worker_.Enqueue(std::bind(&Sound::Stream, sample->sound, + flags & AudioSample::kLoop)); + } else { + LOG << "Buffer underrun!"; + src_index = 0; + } + } + } + + sample->src_index = src_index; + sample->accumulator = accumulator; + sample->amplitude = amplitude; + } + + if (remove) { + task_runner_.Enqueue(sample->end_cb); + sample->active = false; + it = samples_[1].erase(it); + } else { + ++it; + } + } +} + +} // namespace eng diff --git a/src/engine/audio/audio_base.h b/src/engine/audio/audio_base.h new file mode 100644 index 0000000..217bd84 --- /dev/null +++ b/src/engine/audio/audio_base.h @@ -0,0 +1,41 @@ +#ifndef AUDIO_BASE_H +#define AUDIO_BASE_H + +#include +#include +#include + +#include "../../base/closure.h" +#include "../../base/task_runner.h" +#include "../../base/worker.h" +#include "audio_sample.h" + +namespace eng { + +class Sound; + +class AudioBase { + public: + void Play(std::shared_ptr impl_data); + + void Update(); + + protected: + static constexpr int kChannelCount = 2; + + std::list> samples_[2]; + std::mutex mutex_; + + base::Worker worker_{1}; + + base::TaskRunner task_runner_; + + AudioBase(); + ~AudioBase(); + + void RenderAudio(float* output_buffer, size_t num_frames); +}; + +} // namespace eng + +#endif // AUDIO_BASE_H diff --git a/src/engine/audio/audio_forward.h b/src/engine/audio/audio_forward.h new file mode 100644 index 0000000..d3ec438 --- /dev/null +++ b/src/engine/audio/audio_forward.h @@ -0,0 +1,16 @@ +#ifndef AUDIO_FORWARD_H +#define AUDIO_FORWARD_H + +namespace eng { + +#if defined(__ANDROID__) +class AudioOboe; +using Audio = AudioOboe; +#elif defined(__linux__) +class AudioAlsa; +using Audio = AudioAlsa; +#endif + +} // namespace eng + +#endif // AUDIO_FORWARD_H diff --git a/src/engine/audio/audio_oboe.cc b/src/engine/audio/audio_oboe.cc new file mode 100644 index 0000000..fc626d8 --- /dev/null +++ b/src/engine/audio/audio_oboe.cc @@ -0,0 +1,78 @@ +#include "audio_oboe.h" + +#include "../../base/log.h" +#include "../../third_party/oboe/include/oboe/Oboe.h" +#include "audio_resource.h" + +using namespace base; + +namespace eng { + +AudioOboe::AudioOboe() : callback_(std::make_unique(this)) {} + +AudioOboe::~AudioOboe() = default; + +bool AudioOboe::Initialize() { + LOG << "Initializing audio system."; + + return RestartStream(); +} + +void AudioOboe::Shutdown() { + LOG << "Shutting down audio system."; +} + +size_t AudioOboe::GetSampleRate() { + return stream_->getSampleRate(); +} + +AudioOboe::StreamCallback::StreamCallback(AudioOboe* audio) : audio_(audio) {} + +AudioOboe::StreamCallback::~StreamCallback() = default; + +oboe::DataCallbackResult AudioOboe::StreamCallback::onAudioReady( + oboe::AudioStream* oboe_stream, + void* audio_data, + int32_t num_frames) { + float* output_buffer = static_cast(audio_data); + audio_->RenderAudio(output_buffer, num_frames); + return oboe::DataCallbackResult::Continue; +} + +void AudioOboe::StreamCallback::onErrorAfterClose( + oboe::AudioStream* oboe_stream, + oboe::Result error) { + LOG << "Error after close. Error: " << oboe::convertToText(error); + + audio_->RestartStream(); +} + +bool AudioOboe::RestartStream() { + oboe::AudioStreamBuilder builder; + oboe::Result result = + builder.setSharingMode(oboe::SharingMode::Exclusive) + ->setPerformanceMode(oboe::PerformanceMode::LowLatency) + ->setFormat(oboe::AudioFormat::Float) + ->setChannelCount(kChannelCount) + ->setDirection(oboe::Direction::Output) + ->setUsage(oboe::Usage::Game) + ->setCallback(callback_.get()) + ->openManagedStream(stream_); + + LOG << "Oboe Audio Stream:"; + LOG << " performance mode: " << (int)stream_->getPerformanceMode(); + LOG << " format: " << (int)stream_->getFormat(); + LOG << " channel count: " << stream_->getChannelCount(); + LOG << " sample rate: " << stream_->getSampleRate(); + + if (result != oboe::Result::OK) { + LOG << "Failed to create the playback stream. Error: " + << oboe::convertToText(result); + return false; + } + + stream_->start(); + return true; +} + +} // namespace eng diff --git a/src/engine/audio/audio_oboe.h b/src/engine/audio/audio_oboe.h new file mode 100644 index 0000000..ae91a35 --- /dev/null +++ b/src/engine/audio/audio_oboe.h @@ -0,0 +1,50 @@ +#ifndef AUDIO_OBOE_H +#define AUDIO_OBOE_H + +#include + +#include "../../third_party/oboe/include/oboe/AudioStream.h" +#include "../../third_party/oboe/include/oboe/AudioStreamCallback.h" +#include "audio_base.h" + +namespace eng { + +class AudioResource; + +class AudioOboe : public AudioBase { + public: + AudioOboe(); + ~AudioOboe(); + + bool Initialize(); + + void Shutdown(); + + size_t GetSampleRate(); + + private: + class StreamCallback : public oboe::AudioStreamCallback { + public: + StreamCallback(AudioOboe* audio); + ~StreamCallback() override; + + oboe::DataCallbackResult onAudioReady(oboe::AudioStream* oboe_stream, + void* audio_data, + int32_t num_frames) override; + + void onErrorAfterClose(oboe::AudioStream* oboe_stream, + oboe::Result error) override; + + private: + AudioOboe* audio_; + }; + + oboe::ManagedStream stream_; + std::unique_ptr callback_; + + bool RestartStream(); +}; + +} // namespace eng + +#endif // AUDIO_OBOE_H diff --git a/src/engine/audio/audio_resource.cc b/src/engine/audio/audio_resource.cc new file mode 100644 index 0000000..abdcda6 --- /dev/null +++ b/src/engine/audio/audio_resource.cc @@ -0,0 +1,72 @@ +#include "audio_resource.h" + +#include "../../base/log.h" +#include "../sound.h" +#include "audio.h" +#include "audio_sample.h" + +namespace eng { + +AudioResource::AudioResource(Audio* audio) + : sample_(std::make_shared()), audio_(audio) {} + +AudioResource::~AudioResource() { + sample_->flags |= AudioSample::kStopped; +} + +void AudioResource::Play(std::shared_ptr sound, + float amplitude, + bool reset_pos) { + if (sample_->active) + return; + + if (reset_pos) { + sample_->src_index = 0; + sample_->accumulator = 0; + } + sample_->flags &= ~AudioSample::kStopped; + sample_->sound = sound; + sample_->amplitude = amplitude; + sample_->active = true; + + audio_->Play(sample_); +} + +void AudioResource::Stop() { + if (!sample_->active) + return; + + sample_->flags |= AudioSample::kStopped; +} + +void AudioResource::SetLoop(bool loop) { + if (loop) + sample_->flags |= AudioSample::kLoop; + else + sample_->flags &= ~AudioSample::kLoop; +} + +void AudioResource::SetSimulateStereo(bool simulate) { + if (simulate) + sample_->flags |= AudioSample::kSimulateStereo; + else + sample_->flags &= ~AudioSample::kSimulateStereo; +} + +void AudioResource::SetResampleStep(size_t step) { + sample_->step = step + 10; +} + +void AudioResource::SetMaxAmplitude(float max_amplitude) { + sample_->max_amplitude = max_amplitude; +} + +void AudioResource::SetAmplitudeInc(float amplitude_inc) { + sample_->amplitude_inc = amplitude_inc; +} + +void AudioResource::SetEndCallback(base::Closure cb) { + sample_->end_cb = cb; +} + +} // namespace eng diff --git a/src/engine/audio/audio_resource.h b/src/engine/audio/audio_resource.h new file mode 100644 index 0000000..b791227 --- /dev/null +++ b/src/engine/audio/audio_resource.h @@ -0,0 +1,41 @@ +#ifndef AUDIO_RESOURCE_H +#define AUDIO_RESOURCE_H + +#include + +#include "../../base/closure.h" +#include "audio_forward.h" + +namespace eng { + +struct AudioSample; +class Sound; + +class AudioResource { + public: + AudioResource(Audio* audio); + ~AudioResource(); + + void Play(std::shared_ptr sound, float amplitude, bool reset_pos); + + void Stop(); + + void SetLoop(bool loop); + void SetSimulateStereo(bool simulate); + void SetResampleStep(size_t step); + void SetMaxAmplitude(float max_amplitude); + void SetAmplitudeInc(float amplitude_inc); + void SetEndCallback(base::Closure cb); + + private: + std::shared_ptr sample_; + + Audio* audio_ = nullptr; + + AudioResource(const AudioResource&) = delete; + AudioResource& operator=(const AudioResource&) = delete; +}; + +} // namespace eng + +#endif // AUDIO_RESOURCE_H diff --git a/src/engine/audio/audio_sample.h b/src/engine/audio/audio_sample.h new file mode 100644 index 0000000..5d11d7a --- /dev/null +++ b/src/engine/audio/audio_sample.h @@ -0,0 +1,32 @@ +#ifndef AUDIO_SAMPLE_H +#define AUDIO_SAMPLE_H + +#include + +#include "../../base/closure.h" + +namespace eng { + +class Sound; + +struct AudioSample { + enum SampleFlags { kLoop = 1, kStopped = 2, kSimulateStereo = 4 }; + + // Read-only accessed by the audio thread. + std::shared_ptr sound; + unsigned flags = 0; + size_t step = 10; + float amplitude_inc = 0; + float max_amplitude = 1.0f; + base::Closure end_cb; + + // Write accessed by the audio thread. + size_t src_index = 0; + size_t accumulator = 0; + float amplitude = 1.0f; + bool active = false; +}; + +} // namespace eng + +#endif // AUDIO_SAMPLE_H diff --git a/src/engine/engine.cc b/src/engine/engine.cc new file mode 100644 index 0000000..f41846d --- /dev/null +++ b/src/engine/engine.cc @@ -0,0 +1,314 @@ +#include "engine.h" + +#include "../base/log.h" +#include "../base/worker.h" +#include "../third_party/texture_compressor/texture_compressor.h" +#include "audio/audio.h" +#include "audio/audio_resource.h" +#include "font.h" +#include "game.h" +#include "game_factory.h" +#include "image.h" +#include "input_event.h" +#include "mesh.h" +#include "platform/platform.h" +#include "renderer/geometry.h" +#include "renderer/render_command.h" +#include "renderer/renderer.h" +#include "renderer/shader.h" +#include "renderer/texture.h" +#include "shader_source.h" + +using namespace base; + +namespace eng { + +Engine* Engine::singleton = nullptr; + +Engine::Engine(Platform* platform, Renderer* renderer, Audio* audio) + : platform_(platform), renderer_(renderer), audio_(audio) { + assert(!singleton); + singleton = this; + + renderer_->SetContextLostCB(std::bind(&Engine::ContextLost, this)); + + quad_ = CreateRenderResource(); + pass_through_shader_ = CreateRenderResource(); + solid_shader_ = CreateRenderResource(); +} + +Engine::~Engine() { + singleton = nullptr; +} + +Engine& Engine::Get() { + return *singleton; +} + +bool Engine::Initialize() { + // The orthogonal viewport is (-1.0 .. 1.0) for the short edge of the screen. + // For the long endge, it's calculated from aspect ratio. + if (GetScreenWidth() > GetScreenHeight()) { + float aspect_ratio = (float)GetScreenWidth() / (float)GetScreenHeight(); + LOG << "aspect ratio: " << aspect_ratio; + screen_size_ = {aspect_ratio * 2.0f, 2.0f}; + projection_ = base::Ortho(-aspect_ratio, aspect_ratio, -1.0f, 1.0f); + } else { + float aspect_ratio = (float)GetScreenHeight() / (float)GetScreenWidth(); + LOG << "aspect_ratio: " << aspect_ratio; + screen_size_ = {2.0f, aspect_ratio * 2.0f}; + projection_ = base::Ortho(-1.0, 1.0, -aspect_ratio, aspect_ratio); + } + + if (renderer_->SupportsDXT5()) { + tex_comp_alpha_ = TextureCompressor::Create(TextureCompressor::kFormatDXT5); + } else if (renderer_->SupportsATC()) { + tex_comp_alpha_ = + TextureCompressor::Create(TextureCompressor::kFormatATCIA); + } + + if (renderer_->SupportsDXT1()) { + tex_comp_opaque_ = + TextureCompressor::Create(TextureCompressor::kFormatDXT1); + } else if (renderer_->SupportsATC()) { + tex_comp_opaque_ = TextureCompressor::Create(TextureCompressor::kFormatATC); + } else if (renderer_->SupportsETC1()) { + tex_comp_opaque_ = + TextureCompressor::Create(TextureCompressor::kFormatETC1); + } + + system_font_ = std::make_unique(); + system_font_->Load("engine/RobotoMono-Regular.ttf"); + + if (!CreateRenderResources()) + return false; + + game_ = GameFactoryBase::CreateGame(""); + if (!game_) { + printf("No game found to run.\n"); + return false; + } + + if (!game_->Initialize()) { + LOG << "Failed to initialize the game."; + return false; + } + + return true; +} + +void Engine::Shutdown() { + LOG << "Shutting down engine."; +} + +void Engine::Update(float delta_time) { + seconds_accumulated_ += delta_time; + + audio_->Update(); + renderer_->Update(); + + game_->Update(delta_time); + + fps_seconds_ += delta_time; + if (fps_seconds_ >= 1) { + fps_ = renderer_->GetAndResetFPS(); + fps_seconds_ = 0; + } + + if (stats_.IsVisible()) + PrintStats(); +} + +void Engine::Draw(float frame_frac) { + auto cmd = std::make_unique(); + cmd->rgba = {0, 0, 0, 1}; + renderer_->EnqueueCommand(std::move(cmd)); + renderer_->EnqueueCommand(std::make_unique()); + + game_->Draw(frame_frac); + + if (stats_.IsVisible()) + stats_.Draw(); + + renderer_->EnqueueCommand(std::make_unique()); +} + +void Engine::LostFocus() { + if (game_) + game_->LostFocus(); +} + +void Engine::GainedFocus() { + if (game_) + game_->GainedFocus(); +} + +void Engine::Exit() { + platform_->Exit(); +} + +Vector2 Engine::ToScale(const Vector2& vec) { + return GetScreenSize() * vec / + Vector2((float)GetScreenWidth(), (float)GetScreenHeight()); +} + +Vector2 Engine::ToPosition(const Vector2& vec) { + return ToScale(vec) - GetScreenSize() / 2.0f; +} + +std::shared_ptr Engine::CreateAudioResource() { + return std::make_shared(audio_); +} + +void Engine::AddInputEvent(std::unique_ptr event) { + switch (event->GetType()) { + case InputEvent::kTap: + if (((GetScreenSize() / 2) * 0.9f - event->GetVector(0)).Magnitude() <= + 0.25f) { + SetSatsVisible(!stats_.IsVisible()); + // Consume event. + return; + } + break; + case InputEvent::kKeyPress: + if (event->GetKeyPress() == 's') { + SetSatsVisible(!stats_.IsVisible()); + // Consume event. + return; + } + break; + case InputEvent::kDrag: + if (stats_.IsVisible()) { + if ((stats_.GetOffset() - event->GetVector(0)).Magnitude() <= + stats_.GetScale().y) + stats_.SetOffset(event->GetVector(0)); + // TODO: Enqueue DragCancel so we can consume this event. + } + break; + default: + break; + } + + input_queue_.push_back(std::move(event)); +} + +std::unique_ptr Engine::GetNextInputEvent() { + std::unique_ptr event; + if (!input_queue_.empty()) { + event.swap(input_queue_.front()); + input_queue_.pop_front(); + } + return event; +} + +TextureCompressor* Engine::GetTextureCompressor(bool opacity) { + return opacity ? tex_comp_alpha_.get() : tex_comp_opaque_.get(); +} + +int Engine::GetScreenWidth() const { + return renderer_->screen_width(); +} + +int Engine::GetScreenHeight() const { + return renderer_->screen_height(); +} + +int Engine::GetDeviceDpi() const { + return platform_->GetDeviceDpi(); +} + +const std::string& Engine::GetRootPath() const { + return platform_->GetRootPath(); +} + +size_t Engine::GetAudioSampleRate() { + return audio_->GetSampleRate(); +} + +bool Engine::IsMobile() const { + return platform_->mobile_device(); +} + +std::shared_ptr Engine::CreateRenderResourceInternal( + RenderResourceFactoryBase& factory) { + return renderer_->CreateResource(factory); +} + +void Engine::ContextLost() { + CreateRenderResources(); + + game_->ContextLost(); +} + +bool Engine::CreateRenderResources() { + // Create the quad geometry we can reuse for all sprites. + auto quad_mesh = std::make_unique(); + if (!quad_mesh->Load("engine/quad.mesh")) { + LOG << "Could not create quad mesh."; + return false; + } + quad_->Create(std::move(quad_mesh)); + + // Create the shader we can reuse for texture rendering. + auto source = std::make_unique(); + if (!source->Load("engine/pass_through.glsl")) { + LOG << "Could not create pass through shader."; + return false; + } + pass_through_shader_->Create(std::move(source), quad_->vertex_description()); + + // Create the shader we can reuse for solid rendering. + source = std::make_unique(); + if (!source->Load("engine/solid.glsl")) { + LOG << "Could not create solid shader."; + return false; + } + solid_shader_->Create(std::move(source), quad_->vertex_description()); + + return true; +} + +void Engine::SetSatsVisible(bool visible) { + stats_.SetVisible(visible); + if (visible) + stats_.Create(CreateRenderResource()); + else + stats_.Destory(); +} + +void Engine::PrintStats() { + constexpr int width = 200; + std::vector lines; + std::string line; + line = "fps: "; + line += std::to_string(fps_); + lines.push_back(line); + line = "cmd: "; + line += std::to_string(renderer_->global_queue_size() + + renderer_->render_queue_size()); + lines.push_back(line); + + constexpr int margin = 5; + int line_height = system_font_->GetLineHeight(); + int image_width = width + margin * 2; + int image_height = (line_height + margin) * lines.size() + margin; + + auto image = std::make_unique(); + image->Create(image_width, image_height); + image->Clear({1, 1, 1, 0.08f}); + + Worker worker(2); + int y = margin; + for (auto& text : lines) { + worker.Enqueue(std::bind(&Font::Print, system_font_.get(), margin, y, + text.c_str(), image->GetBuffer(), + image->GetWidth())); + y += line_height + margin; + } + worker.Join(); + + stats_.GetTexture()->Update(std::move(image)); + stats_.AutoScale(); +} + +} // namespace eng diff --git a/src/engine/engine.h b/src/engine/engine.h new file mode 100644 index 0000000..1c40181 --- /dev/null +++ b/src/engine/engine.h @@ -0,0 +1,148 @@ +#ifndef ENGINE_H +#define ENGINE_H + +#include +#include +#include + +#include "../base/random.h" +#include "../base/vecmath.h" +#include "audio/audio_forward.h" +#include "image_quad.h" +#include "renderer/render_resource.h" + +class TextureCompressor; + +namespace eng { + +class AudioResource; +class Font; +class Game; +class InputEvent; +class Renderer; +struct RenderCommand; +class Platform; +class Geometry; +class Shader; + +class Engine { + public: + Engine(Platform* platform, Renderer* renderer, Audio* audio); + ~Engine(); + + static Engine& Get(); + + bool Initialize(); + + void Shutdown(); + + void Update(float delta_time); + void Draw(float frame_frac); + + void LostFocus(); + void GainedFocus(); + + void Exit(); + + // Convert size from pixels to viewport scale. + base::Vector2 ToScale(const base::Vector2& vec); + + // Convert position form pixels to viewport coordinates. + base::Vector2 ToPosition(const base::Vector2& vec); + + template + std::shared_ptr CreateRenderResource() { + RenderResourceFactory factory; + return std::dynamic_pointer_cast(CreateRenderResourceInternal(factory)); + } + + std::shared_ptr CreateAudioResource(); + + void AddInputEvent(std::unique_ptr event); + std::unique_ptr GetNextInputEvent(); + + // Access to the render resources. + std::shared_ptr GetQuad() { return quad_; } + std::shared_ptr GetPassThroughShader() { + return pass_through_shader_; + } + std::shared_ptr GetSolidShader() { return solid_shader_; } + + const Font* GetSystemFont() { return system_font_.get(); } + + base::Random& GetRandomGenerator() { return random_; } + + TextureCompressor* GetTextureCompressor(bool opacity); + + Game* GetGame() { return game_.get(); } + + // Return screen width/height in pixels. + int GetScreenWidth() const; + int GetScreenHeight() const; + + // Return screen size in viewport scale. + base::Vector2 GetScreenSize() const { return screen_size_; } + + const base::Matrix4x4& GetProjectionMarix() const { return projection_; } + + int GetDeviceDpi() const; + + const std::string& GetRootPath() const; + + size_t GetAudioSampleRate(); + + bool IsMobile() const; + + float seconds_accumulated() const { return seconds_accumulated_; } + + private: + static Engine* singleton; + + Platform* platform_ = nullptr; + + Renderer* renderer_ = nullptr; + + Audio* audio_; + + std::unique_ptr game_; + + std::shared_ptr quad_; + std::shared_ptr pass_through_shader_; + std::shared_ptr solid_shader_; + + base::Vector2 screen_size_ = {0, 0}; + base::Matrix4x4 projection_; + + std::unique_ptr system_font_; + + std::unique_ptr tex_comp_opaque_; + std::unique_ptr tex_comp_alpha_; + + ImageQuad stats_; + + float fps_seconds_ = 0; + int fps_ = 0; + + float seconds_accumulated_ = 0.0f; + + std::deque> input_queue_; + + base::Random random_; + + std::shared_ptr CreateRenderResourceInternal( + RenderResourceFactoryBase& factory); + + void ContextLost(); + + bool CreateRenderResources(); + + void SetSatsVisible(bool visible); + void PrintStats(); + + Engine(const Engine&) = delete; + Engine& operator=(const Engine&) = delete; +}; + +} // namespace eng + +#endif // ENGINE_H diff --git a/src/engine/font.cc b/src/engine/font.cc new file mode 100644 index 0000000..057a8a6 --- /dev/null +++ b/src/engine/font.cc @@ -0,0 +1,193 @@ +#include "font.h" + +#include "../base/log.h" +#include "engine.h" +#include "platform/asset_file.h" + +#define STB_TRUETYPE_IMPLEMENTATION +#include "../third_party/stb/stb_truetype.h" + +namespace eng { + +bool Font::Load(const std::string& file_name) { + // Read the font file. + size_t buffer_size = 0; + auto buffer = AssetFile::ReadWholeFile( + file_name.c_str(), Engine::Get().GetRootPath().c_str(), &buffer_size); + if (!buffer) { + LOG << "Failed to read font file."; + return false; + } + + do { + // Allocate a cache bitmap for the glyphs. + // This is one 8 bit channel intensity data. + // It's tighly packed. + glyph_cache_ = std::make_unique(kGlyphSize * kGlyphSize); + if (!glyph_cache_) { + LOG << "Failed to allocate glyph cache."; + break; + } + + // Rasterize glyphs and pack them into the cache. + const float kFontHeight = 32.0f; + if (stbtt_BakeFontBitmap((unsigned char*)buffer.get(), 0, kFontHeight, + glyph_cache_.get(), kGlyphSize, kGlyphSize, + kFirstChar, kNumChars, glyph_info_) <= 0) { + LOG << "Failed to bake the glyph cache: "; + glyph_cache_.reset(); + break; + } + + int x0, y0, x1, y1; + CalculateBoundingBox("`IlfKgjy_{)", x0, y0, x1, y1); + line_height_ = y1 - y0; + yoff_ = -y0; + + return true; + } while (false); + + glyph_cache_.reset(); + + return false; +} + +static void StretchBlit_I8_to_RGBA32(int dst_x0, + int dst_y0, + int dst_x1, + int dst_y1, + int src_x0, + int src_y0, + int src_x1, + int src_y1, + uint8_t* dst_rgba, + int dst_pitch, + const uint8_t* src_i, + int src_pitch) { + // LOG << "-- StretchBlit: --"; + // LOG << "dst: rect(" << dst_x0 << ", " << dst_y0 << ")..(" + // << dst_x1 << ".." << dst_y1 << "), pitch(" << dst_pitch << ")"; + // LOG << "src: rect(" << src_x0 << ", " << src_y0 << ")..(" + // << src_x1 << ".." << src_y1 << "), pitch(" << src_pitch << ")"; + + int dst_width = dst_x1 - dst_x0, dst_height = dst_y1 - dst_y0, + src_width = src_x1 - src_x0, src_height = src_y1 - src_y0; + + // int dst_dx = dst_width > 0 ? 1 : -1, + // dst_dy = dst_height > 0 ? 1 : -1; + + // LOG << "dst_width = " << dst_width << ", dst_height = " << dst_height; + // LOG << "src_width = " << src_width << ", src_height = " << src_height; + + uint8_t* dst = dst_rgba + (dst_x0 + dst_y0 * dst_pitch) * 4; + const uint8_t* src = src_i + (src_x0 + src_y0 * src_pitch) * 1; + + // First check if we have to stretch at all. + if ((dst_width == src_width) && (dst_height == src_height)) { + // No, straight blit then. + for (int y = 0; y < dst_height; ++y) { + for (int x = 0; x < dst_width; ++x) { + // Alpha test, no blending for now. + if (src[x]) { +#if 0 + dst[x * 4 + 0] = src[x]; + dst[x * 4 + 1] = src[x]; + dst[x * 4 + 2] = src[x]; + dst[x * 4 + 3] = 255; +#else + dst[x * 4 + 3] = src[x]; +#endif + } + } + + dst += dst_pitch * 4; + src += src_pitch * 1; + } + } else { + // ToDo + } +} + +void Font::CalculateBoundingBox(const std::string& text, + int& x0, + int& y0, + int& x1, + int& y1) const { + x0 = 0; + y0 = 0; + x1 = 0; + y1 = 0; + + if (!glyph_cache_) + return; + + float x = 0, y = 0; + + const char* ptr = text.c_str(); + while (*ptr) { + if (*ptr >= kFirstChar /*&& *ptr < (kFirstChar + kNumChars)*/) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(glyph_info_, kGlyphSize, kGlyphSize, *ptr - kFirstChar, + &x, &y, &q, 1); + + int ix0 = (int)q.x0, iy0 = (int)q.y0, ix1 = (int)q.x1, iy1 = (int)q.y1; + + if (ix0 < x0) + x0 = ix0; + if (iy0 < y0) + y0 = iy0; + if (ix1 > x1) + x1 = ix1; + if (iy1 > y1) + y1 = iy1; + + ++ptr; + } + } +} + +void Font::CalculateBoundingBox(const std::string& text, + int& width, + int& height) const { + int x0, y0, x1, y1; + CalculateBoundingBox(text, x0, y0, x1, y1); + width = x1 - x0; + height = y1 - y0; + // LOG << "width = " << width << ", height = " << height; +} + +void Font::Print(int x, + int y, + const std::string& text, + uint8_t* buffer, + int width) const { + // LOG("Font::Print() = %s\n", text); + + if (!glyph_cache_) + return; + + float fx = (float)x, fy = (float)y + (float)yoff_; + + const char* ptr = text.c_str(); + while (*ptr) { + if (*ptr >= kFirstChar /*&& *ptr < (kFirstChar + kNumChars)*/) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(glyph_info_, kGlyphSize, kGlyphSize, *ptr - kFirstChar, + &fx, &fy, &q, 1); + + // LOG("-- glyph --\nxy = (%f %f) .. (%f %f)\nuv = (%f %f) .. (%f %f)\n", + // q.x0, q.y0, q.x1, q.y1, q.s0, q.t0, q.s1, q.t1); + + int ix0 = (int)q.x0, iy0 = (int)q.y0, ix1 = (int)q.x1, iy1 = (int)q.y1, + iu0 = (int)(q.s0 * kGlyphSize), iv0 = (int)(q.t0 * kGlyphSize), + iu1 = (int)(q.s1 * kGlyphSize), iv1 = (int)(q.t1 * kGlyphSize); + + StretchBlit_I8_to_RGBA32(ix0, iy0, ix1, iy1, iu0, iv0, iu1, iv1, buffer, + width, glyph_cache_.get(), kGlyphSize); + + ++ptr; + } + } +} + +} // namespace eng diff --git a/src/engine/font.h b/src/engine/font.h new file mode 100644 index 0000000..7f5f604 --- /dev/null +++ b/src/engine/font.h @@ -0,0 +1,54 @@ +#ifndef FONT_H +#define FONT_H + +#include +#include +#include + +#include "../third_party/stb/stb_truetype.h" + +namespace eng { + +class Font { + public: + Font() = default; + ~Font() = default; + + bool Load(const std::string& file_name); + + void CalculateBoundingBox(const std::string& text, + int& width, + int& height) const; + void CalculateBoundingBox(const std::string& text, + int& x0, + int& y0, + int& x1, + int& y1) const; + + void Print(int x, + int y, + const std::string& text, + uint8_t* buffer, + int width) const; + + int GetLineHeight() const { return line_height_; } + + bool IsValid() const { return !!glyph_cache_; } + + private: + enum Constants { + kGlyphSize = 512, + kFirstChar = 32, // ' ' (space) + kNumChars = 96 // Covers almost all ASCII chars. + }; + + std::unique_ptr glyph_cache_; // Image data. + stbtt_bakedchar glyph_info_[kNumChars]; // Coordinates and advance. + + int line_height_ = 0; + int yoff_ = 0; +}; + +} // namespace eng + +#endif // FONT_H diff --git a/src/engine/game.h b/src/engine/game.h new file mode 100644 index 0000000..5ae6525 --- /dev/null +++ b/src/engine/game.h @@ -0,0 +1,30 @@ +#ifndef GAME_H +#define GAME_H + +namespace eng { + +class Game { + public: + Game() = default; + virtual ~Game() = default; + + virtual bool Initialize() = 0; + + virtual void Update(float delta_time) = 0; + + virtual void Draw(float frame_frac) = 0; + + virtual void ContextLost() = 0; + + virtual void LostFocus() = 0; + + virtual void GainedFocus() = 0; + + private: + Game(const Game&) = delete; + Game& operator=(const Game&) = delete; +}; + +} // namespace eng + +#endif // GAME_H diff --git a/src/engine/game_factory.h b/src/engine/game_factory.h new file mode 100644 index 0000000..e982621 --- /dev/null +++ b/src/engine/game_factory.h @@ -0,0 +1,55 @@ +#ifndef GAME_FACTORY_H +#define GAME_FACTORY_H + +#include +#include +#include + +#define DECLARE_GAME_BEGIN \ + std::vector> \ + eng::GameFactoryBase::game_classes = { +#define DECLARE_GAME(CLASS) {#CLASS, new eng::GameFactory()}, +#define DECLARE_GAME_END }; + +namespace eng { + +class Game; + +class GameFactoryBase { + public: + virtual ~GameFactoryBase() = default; + + static std::unique_ptr CreateGame(const std::string& name) { + if (name.empty()) + return game_classes.size() > 0 + ? game_classes.begin()->second->CreateGame() + : nullptr; + for (auto& element : game_classes) { + if (element.first == name) + return element.second->CreateGame(); + } + return nullptr; + } + + private: + virtual std::unique_ptr CreateGame() { return nullptr; } + + static std::vector> game_classes; +}; + +template +class GameFactory : public GameFactoryBase { + public: + ~GameFactory() override = default; + + private: + using GameType = Type; + + std::unique_ptr CreateGame() override { + return std::make_unique(); + } +}; + +} // namespace eng + +#endif // GAME_FACTORY_H diff --git a/src/engine/image.cc b/src/engine/image.cc new file mode 100644 index 0000000..ee4aa7b --- /dev/null +++ b/src/engine/image.cc @@ -0,0 +1,359 @@ +#include "image.h" + +#include +#include + +#include "../base/interpolation.h" +#include "../base/log.h" +#include "../base/misc.h" +#include "../third_party/texture_compressor/texture_compressor.h" +#include "engine.h" +#include "platform/asset_file.h" + +// This 3rd party library is written in C and uses malloc, which means that we +// have to do the same. +#define STBI_NO_STDIO +#include "../third_party/stb/stb_image.h" + +using namespace base; + +namespace { + +// Blend between two colors with equal weights. +uint32_t Mix2(uint32_t p0, uint32_t p1) { + uint32_t r = (((p0 >> 0) & 0xff) + ((p1 >> 0) & 0xff)) / 2; + uint32_t g = (((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff)) / 2; + uint32_t b = (((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff)) / 2; + uint32_t a = (((p0 >> 24) & 0xff) + ((p1 >> 24) & 0xff)) / 2; + + return (r << 0) | (g << 8) | (b << 16) | (a << 24); +} + +// Blend between four colors with equal weights. +uint32_t Mix4(uint32_t p0, uint32_t p1, uint32_t p2, uint32_t p3) { + uint32_t r = (((p0 >> 0) & 0xff) + ((p1 >> 0) & 0xff) + ((p2 >> 0) & 0xff) + + ((p3 >> 0) & 0xff)) / + 4; + uint32_t g = (((p0 >> 8) & 0xff) + ((p1 >> 8) & 0xff) + ((p2 >> 8) & 0xff) + + ((p3 >> 8) & 0xff)) / + 4; + uint32_t b = (((p0 >> 16) & 0xff) + ((p1 >> 16) & 0xff) + + ((p2 >> 16) & 0xff) + ((p3 >> 16) & 0xff)) / + 4; + uint32_t a = (((p0 >> 24) & 0xff) + ((p1 >> 24) & 0xff) + + ((p2 >> 24) & 0xff) + ((p3 >> 24) & 0xff)) / + 4; + + return (r << 0) | (g << 8) | (b << 16) | (a << 24); +} + +// Anisotropic blending of colors. +void MipNonUniform(void* dst, const void* src, size_t length) { + const uint32_t* s = reinterpret_cast(src); + uint32_t* d = reinterpret_cast(dst); + for (size_t y = 0; y < length; ++y) { + *d++ = Mix2(s[0], s[1]); + s += 2; + } +} + +} // namespace + +namespace eng { + +Image::Image() = default; + +Image::Image(const Image& other) { + Copy(other); +} + +Image::~Image() = default; + +Image& Image::operator=(const Image& other) { + Copy(other); + return *this; +} + +bool Image::Create(int w, int h) { + width_ = w; + height_ = h; + + buffer_.reset((uint8_t*)AlignedAlloc<16>(w * h * 4 * sizeof(uint8_t))); + + return true; +} + +void Image::Copy(const Image& other) { + if (other.buffer_) { + int size = other.GetSize(); + buffer_.reset((uint8_t*)AlignedAlloc<16>(size)); + memcpy(buffer_.get(), other.buffer_.get(), size); + } + width_ = other.width_; + height_ = other.height_; + format_ = other.format_; +} + +bool Image::CreateMip(const Image& other) { + if (other.width_ <= 1 || other.height_ <= 1 || other.GetFormat() != kRGBA32) + return false; + + // Reduce the dimensions. + width_ = std::max(other.width_ >> 1, 1); + height_ = std::max(other.height_ >> 1, 1); + format_ = kRGBA32; + buffer_.reset((uint8_t*)AlignedAlloc<16>(GetSize())); + + // If the width isn't perfectly divisable with two, then we end up skewing + // the image because the source offset isn't updated properly. + bool unaligned_width = other.width_ & 1; + + // Special case the non-uniform/anisotropic cases, eg 4:1 or 1:4 textures. + // This is only an issue once we reach the highest mip levels where one + // dimension is one pixel. + if (other.width_ == 1) { + // Interestingly the horizontal and vertical case becomes the same code, + // it's only about which value to use as the run length that differs. + MipNonUniform(buffer_.get(), other.buffer_.get(), height_); + } else if (other.height_ == 1) { + MipNonUniform(buffer_.get(), other.buffer_.get(), width_); + } else { + const uint32_t* s = reinterpret_cast(other.buffer_.get()); + uint32_t* d = reinterpret_cast(buffer_.get()); + for (size_t y = 0; y < height_; ++y) { + for (size_t x = 0; x < width_; ++x) { + *d++ = Mix4(s[0], s[1], s[other.width_], s[other.width_ + 1]); + s += 2; + } + if (unaligned_width) + ++s; + s += other.width_; + } + } + + return true; +} + +bool Image::Load(const std::string& file_name) { + size_t buffer_size = 0; + auto file_buffer = AssetFile::ReadWholeFile( + file_name.c_str(), Engine::Get().GetRootPath().c_str(), &buffer_size); + if (!file_buffer) { + LOG << "Failed to read file: " << file_name; + return false; + } + + int w, h, c; + buffer_.reset((uint8_t*)stbi_load_from_memory( + (const stbi_uc*)file_buffer.get(), buffer_size, &w, &h, &c, 0)); + if (!buffer_) { + LOG << "Failed to load image file: " << file_name; + return false; + } + + uint8_t* converted_buffer = NULL; + switch (c) { + case 1: + // LOG("Converting image from 1 to 4 channels.\n"); + // Assume it's an intensity, duplicate it to RGB and fill A with opaque. + converted_buffer = + (uint8_t*)AlignedAlloc<16>(w * h * 4 * sizeof(uint8_t)); + for (int i = 0; i < w * h; ++i) { + converted_buffer[i * 4 + 0] = buffer_[i]; + converted_buffer[i * 4 + 1] = buffer_[i]; + converted_buffer[i * 4 + 2] = buffer_[i]; + converted_buffer[i * 4 + 3] = 255; + } + break; + + case 3: + // LOG("Converting image from 3 to 4 channels.\n"); + // Add an opaque channel. + converted_buffer = + (uint8_t*)AlignedAlloc<16>(w * h * 4 * sizeof(uint8_t)); + for (int i = 0; i < w * h; ++i) { + converted_buffer[i * 4 + 0] = buffer_[i * 3 + 0]; + converted_buffer[i * 4 + 1] = buffer_[i * 3 + 1]; + converted_buffer[i * 4 + 2] = buffer_[i * 3 + 2]; + converted_buffer[i * 4 + 3] = 255; + } + break; + + case 4: + break; // This is the wanted format. + + case 2: + default: + LOG << "Image had unsuitable number of color components: " << c << " " + << file_name; + buffer_.reset(); + return false; + } + + if (converted_buffer) + buffer_.reset(converted_buffer); + + width_ = w; + height_ = h; + +#if 0 // Fill the alpha channel with transparent gradient alpha for testing + uint8_t* modifyBuf = buffer; + for (int j = 0; j < height; ++j, modifyBuf += width * 4) + { + for (int i = 0; i < width; ++i) + { + float dist = sqrt(float(i*i + j*j)); + float alpha = (((dist > 0.0f ? dist : 0.0f) / sqrt((float)(width * width + height * height))) * 255.0f); + modifyBuf[i * 4 + 3] = (unsigned char)alpha; + } + } +#endif + + return !!buffer_; +} + +size_t Image::GetSize() const { + switch (format_) { + case kRGBA32: + return width_ * height_ * 4; + case kDXT1: + case kATC: + return ((width_ + 3) / 4) * ((height_ + 3) / 4) * 8; + case kDXT5: + case kATCIA: + return ((width_ + 3) / 4) * ((height_ + 3) / 4) * 16; + case kETC1: + return (width_ * height_ * 4) / 8; + default: + return 0; + } +} + +void Image::ConvertToPow2() { + int new_width = RoundUpToPow2(width_); + int new_height = RoundUpToPow2(height_); + if ((new_width != width_) || (new_height != height_)) { + LOG << "Converting image from (" << width_ << ", " << height_ << ") to (" + << new_width << ", " << new_height << ")"; + + int bigger_size = new_width * new_height * 4 * sizeof(uint8_t); + uint8_t* bigger_buffer = (uint8_t*)AlignedAlloc<16>(bigger_size); + + // Fill it with black. + memset(bigger_buffer, 0, bigger_size); + + // Copy over the old bitmap. +#if 0 + // Centered in the new bitmap. + int offset_x = (new_width - width_) / 2; + int offset_y = (new_height - height_) / 2; + for (int y = 0; y < height_; ++y) + memcpy(bigger_buffer + (offset_x + (y + offset_y) * new_width) * 4, + buffer_.get() + y * width_ * 4, width_ * 4); +#else + for (int y = 0; y < height_; ++y) + memcpy(bigger_buffer + (y * new_width) * 4, + buffer_.get() + y * width_ * 4, width_ * 4); +#endif + + // Swap the buffers and dimensions. + buffer_.reset(bigger_buffer); + width_ = new_width; + height_ = new_height; + } +} + +bool Image::Compress() { + if (IsCompressed()) + return true; + + TextureCompressor* tc = Engine::Get().GetTextureCompressor(true); + if (!tc) + return false; + + switch (tc->format()) { + case TextureCompressor::kFormatATC: + format_ = kATC; + break; + case TextureCompressor::kFormatATCIA: + format_ = kATCIA; + break; + case TextureCompressor::kFormatDXT1: + format_ = kDXT1; + break; + case TextureCompressor::kFormatDXT5: + format_ = kDXT5; + break; + case TextureCompressor::kFormatETC1: + format_ = kETC1; + break; + default: + return false; + } + + LOG << "Compressing image. Format: " << format_; + + unsigned compressedSize = GetSize(); + uint8_t* compressedBuffer = + (uint8_t*)AlignedAlloc<16>(compressedSize * sizeof(uint8_t)); + + const uint8_t* src = buffer_.get(); + uint8_t* dst = compressedBuffer; + + tc->Compress(src, dst, width_, height_, TextureCompressor::kQualityHigh); + + buffer_.reset(compressedBuffer); + return true; +} + +uint8_t* Image::GetBuffer() { + return buffer_.get(); +} + +void Image::Clear(Vector4 rgba) { + // Quantize the color to target resolution. + uint8_t r = (uint8_t)(rgba.x * 255.0f), g = (uint8_t)(rgba.y * 255.0f), + b = (uint8_t)(rgba.z * 255.0f), a = (uint8_t)(rgba.w * 255.0f); + + // Fill out the first line manually. + for (int w = 0; w < width_; ++w) { + buffer_.get()[w * 4 + 0] = r; + buffer_.get()[w * 4 + 1] = g; + buffer_.get()[w * 4 + 2] = b; + buffer_.get()[w * 4 + 3] = a; + } + + // Copy the first line to the rest of them. + for (int h = 1; h < height_; ++h) + memcpy(buffer_.get() + h * width_ * 4, buffer_.get(), width_ * 4); +} + +void Image::GradientH() { + // Fill out the first line manually. + for (int x = 0; x < width_; ++x) { + uint8_t intensity = x > 255 ? 255 : x; + buffer_.get()[x * 4 + 0] = intensity; + buffer_.get()[x * 4 + 1] = intensity; + buffer_.get()[x * 4 + 2] = intensity; + buffer_.get()[x * 4 + 3] = 255; + } + + // Copy the first line to the rest of them. + for (int h = 1; h < height_; ++h) + memcpy(buffer_.get() + h * width_ * 4, buffer_.get(), width_ * 4); +} + +void Image::GradientV(const Vector4& c1, const Vector4& c2, int height) { + // Fill each section with gradient. + for (int h = 0; h < height_; ++h) { + Vector4 c = Lerp(c1, c2, fmod(h, height) / (float)height); + for (int x = 0; x < width_; ++x) { + buffer_.get()[h * width_ * 4 + x * 4 + 0] = c.x * 255; + buffer_.get()[h * width_ * 4 + x * 4 + 1] = c.y * 255; + buffer_.get()[h * width_ * 4 + x * 4 + 2] = c.z * 255; + buffer_.get()[h * width_ * 4 + x * 4 + 3] = 0; + } + } +} + +} // namespace eng diff --git a/src/engine/image.h b/src/engine/image.h new file mode 100644 index 0000000..5835d7b --- /dev/null +++ b/src/engine/image.h @@ -0,0 +1,59 @@ +#ifndef IMAGE_H +#define IMAGE_H + +#include +#include + +#include "../base/mem.h" +#include "../base/vecmath.h" + +namespace eng { + +class Image { + public: + enum Format { kRGBA32, kDXT1, kDXT5, kETC1, kATC, kATCIA }; + + Image(); + Image(const Image& other); + ~Image(); + + Image& operator=(const Image& other); + + bool Create(int width, int height); + void Copy(const Image& other); + bool CreateMip(const Image& other); + bool Load(const std::string& file_name); + + bool Compress(); + + void ConvertToPow2(); + + int GetWidth() const { return width_; } + int GetHeight() const { return height_; } + + Format GetFormat() const { return format_; } + bool IsCompressed() const { return format_ > kRGBA32; } + + size_t GetSize() const; + + const uint8_t* GetBuffer() const { return buffer_.get(); } + uint8_t* GetBuffer(); + + bool IsValid() const { return !!buffer_; } + + void Clear(base::Vector4 rgba); + void GradientH(); + void GradientV(const base::Vector4& c1, const base::Vector4& c2, int height); + + private: + base::AlignedMem::ScoppedPtr buffer_; + int width_ = 0; + int height_ = 0; + Format format_ = kRGBA32; + + std::string name_; +}; + +} // namespace eng + +#endif // IMAGE_H diff --git a/src/engine/image_quad.cc b/src/engine/image_quad.cc new file mode 100644 index 0000000..038cdf0 --- /dev/null +++ b/src/engine/image_quad.cc @@ -0,0 +1,87 @@ +#include "image_quad.h" + +#include + +#include "engine.h" +#include "renderer/geometry.h" +#include "renderer/shader.h" +#include "renderer/texture.h" + +using namespace base; + +namespace eng { + +void ImageQuad::Create(std::shared_ptr texture, + std::array num_frames, + int frame_width, + int frame_height) { + texture_ = texture; + num_frames_ = std::move(num_frames); + frame_width_ = frame_width; + frame_height_ = frame_height; +} + +void ImageQuad::Destory() { + texture_.reset(); +} + +void ImageQuad::AutoScale() { + Vector2 dimensions = {GetFrameWidth(), GetFrameHeight()}; + SetScale(Engine::Get().ToScale(dimensions)); + Scale((float)Engine::Get().GetDeviceDpi() / 200.0f); +} + +void ImageQuad::SetFrame(size_t frame) { + assert(frame < GetNumFrames()); + current_frame_ = frame; +} + +size_t ImageQuad::GetNumFrames() const { + return num_frames_[0] * num_frames_[1]; +} + +void ImageQuad::Draw() { + if (!IsVisible() || !texture_ || !texture_->IsValid()) + return; + + texture_->Activate(); + + Vector2 tex_scale = {GetFrameWidth() / texture_->GetWidth(), + GetFrameHeight() / texture_->GetHeight()}; + + std::shared_ptr quad = Engine::Get().GetQuad(); + std::shared_ptr shader = Engine::Get().GetPassThroughShader(); + + shader->Activate(); + shader->SetUniform("offset", offset_); + shader->SetUniform("scale", scale_); + shader->SetUniform("pivot", pivot_); + shader->SetUniform("rotation", rotation_); + shader->SetUniform("tex_offset", GetUVOffset(current_frame_)); + shader->SetUniform("tex_scale", tex_scale); + shader->SetUniform("projection", Engine::Get().GetProjectionMarix()); + shader->SetUniform("color", color_); + shader->SetUniform("texture", 0); + + quad->Draw(); +} + +float ImageQuad::GetFrameWidth() const { + return frame_width_ > 0 ? (float)frame_width_ + : texture_->GetWidth() / (float)num_frames_[0]; +} + +float ImageQuad::GetFrameHeight() const { + return frame_height_ > 0 ? (float)frame_height_ + : texture_->GetHeight() / (float)num_frames_[1]; +} + +// Return the uv offset for the given frame. +Vector2 ImageQuad::GetUVOffset(int frame) const { + assert(frame < num_frames_[0] * num_frames_[1]); + if (num_frames_[0] == 1 && num_frames_[1] == 1) + return {0, 0}; + return {(float)(frame % num_frames_[0]), (float)(frame / num_frames_[0])}; +} + +} // namespace eng diff --git a/src/engine/image_quad.h b/src/engine/image_quad.h new file mode 100644 index 0000000..e09a2a8 --- /dev/null +++ b/src/engine/image_quad.h @@ -0,0 +1,57 @@ +#ifndef IMAGE_QUAD_H +#define IMAGE_QUAD_H + +#include "../base/vecmath.h" +#include "animatable.h" + +#include +#include + +namespace eng { + +class Texture; + +class ImageQuad : public Animatable { + public: + ImageQuad() = default; + ~ImageQuad() override = default; + + void Create(std::shared_ptr texture, + std::array num_frames = {1, 1}, + int frame_width = 0, + int frame_height = 0); + + void Destory(); + + void AutoScale(); + + // Animatable interface. + void SetFrame(size_t frame) override; + size_t GetFrame() const override { return current_frame_; } + size_t GetNumFrames() const override; + void SetColor(const base::Vector4& color) override { color_ = color; } + base::Vector4 GetColor() const override { return color_; } + + void Draw(); + + std::shared_ptr GetTexture() { return texture_; } + + private: + std::shared_ptr texture_; + + size_t current_frame_ = 0; + std::array num_frames_ = {1, 1}; // horizontal, vertical + int frame_width_ = 0; + int frame_height_ = 0; + + base::Vector4 color_ = {1, 1, 1, 1}; + + float GetFrameWidth() const; + float GetFrameHeight() const; + + base::Vector2 GetUVOffset(int frame) const; +}; + +} // namespace eng + +#endif // IMAGE_QUAD_H diff --git a/src/engine/input_event.h b/src/engine/input_event.h new file mode 100644 index 0000000..c022eb8 --- /dev/null +++ b/src/engine/input_event.h @@ -0,0 +1,49 @@ +#ifndef INPUT_EVENT_H +#define INPUT_EVENT_H + +#include +#include "../base/vecmath.h" + +namespace eng { + +class InputEvent { + public: + enum Type { + kInvalid, + kTap, + kDoubleTap, + kDragStart, + kDrag, + kDragEnd, + kDragCancel, + kPinchStart, + kPinch, + kNavigateBack, + kKeyPress, + kType_Max // Not a type. + }; + + InputEvent(Type type) : type_(type) {} + InputEvent(Type type, const base::Vector2& vec1) + : type_(type), vec_{vec1, {0, 0}} {} + InputEvent(Type type, const base::Vector2& vec1, const base::Vector2& vec2) + : type_(type), vec_{vec1, vec2} {} + InputEvent(Type type, char key) : type_(type), key_(key) {} + ~InputEvent() = default; + + Type GetType() { return type_; } + base::Vector2 GetVector(size_t i) { + assert(i < 2); + return vec_[i]; + } + char GetKeyPress() { return key_; } + + private: + Type type_ = kInvalid; + base::Vector2 vec_[2] = {{0, 0}, {0, 0}}; + char key_ = 0; +}; + +} // namespace eng + +#endif // INPUT_EVENT_H diff --git a/src/engine/mesh.cc b/src/engine/mesh.cc new file mode 100644 index 0000000..6ed9dea --- /dev/null +++ b/src/engine/mesh.cc @@ -0,0 +1,169 @@ +#include "mesh.h" + +#include +#include + +#include "../base/log.h" +#include "../third_party/jsoncpp/json.h" +#include "engine.h" +#include "platform/asset_file.h" + +namespace eng { + +// Used to parse the vertex layout, +// e.g. "p3f;c4b" for "position 3 floats, color 4 bytes". +const char Mesh::kLayoutDelimiter[] = ";/ \t"; + +bool Mesh::Create(Primitive primitive, + const std::string& vertex_description, + size_t num_vertices, + const void* vertices, + DataType index_description, + size_t num_indices, + const void* indices) { + primitive_ = primitive; + num_vertices_ = num_vertices; + index_description_ = index_description; + num_indices_ = num_indices; + + if (!ParseVertexDescription(vertex_description, vertex_description_)) { + LOG << "Failed to parse vertex description."; + return false; + } + + int vertex_buffer_size = GetVertexSize() * num_vertices_; + if (vertex_buffer_size > 0) { + vertices_ = std::make_unique(vertex_buffer_size); + memcpy(vertices_.get(), vertices, vertex_buffer_size); + } + + if (!indices) + return true; + + int index_buffer_size = GetIndexSize() * num_indices_; + if (index_buffer_size > 0) { + indices_ = std::make_unique(index_buffer_size); + memcpy(indices_.get(), indices, index_buffer_size); + } + + return true; +} + +bool Mesh::Load(const std::string& file_name) { + size_t buffer_size = 0; + auto json_mesh = AssetFile::ReadWholeFile(file_name.c_str(), + Engine::Get().GetRootPath().c_str(), + &buffer_size, true); + if (!json_mesh) { + LOG << "Failed to read file: " << file_name; + return false; + } + + std::string err; + Json::Value root; + Json::CharReaderBuilder builder; + const std::unique_ptr reader(builder.newCharReader()); + if (!reader->parse(json_mesh.get(), json_mesh.get() + buffer_size, &root, + &err)) { + LOG << "Failed to load mesh. Json parser error: " << err; + return false; + } + + const std::string& primitive_str = root["primitive"].asString(); + if (primitive_str == "Triangles") { + primitive_ = kPrimitive_Triangles; + } else if (primitive_str == "TriangleStrip") { + primitive_ = kPrimitive_TriangleStrip; + } else { + LOG << "Failed to load mesh. Invalid primitive: " << primitive_str; + return false; + } + + num_vertices_ = root["num_vertices"].asUInt(); + + if (!ParseVertexDescription(root["vertex_description"].asString(), + vertex_description_)) { + LOG << "Failed to parse vertex description."; + return false; + } + + size_t array_size = 0; + for (auto& attr : vertex_description_) { + array_size += std::get<2>(attr); + } + array_size *= num_vertices_; + + const Json::Value vertices = root["vertices"]; + if (vertices.size() != array_size) { + LOG << "Failed to load mesh. Vertex array size: " << vertices.size() + << ", expected " << array_size; + return false; + } + + int vertex_buffer_size = GetVertexSize() * num_vertices_; + if (vertex_buffer_size <= 0) { + LOG << "Failed to load mesh. Invalid vertex size."; + return false; + } + + vertices_ = std::make_unique(vertex_buffer_size); + + char* dst = vertices_.get(); + int i = 0; + while (i < vertices.size()) { + for (auto& attr : vertex_description_) { + auto [attrib_type, data_type, num_elements, type_size] = attr; + while (num_elements--) { + switch (data_type) { + case kDataType_Byte: + *((unsigned char*)dst) = (unsigned char)vertices[i].asUInt(); + break; + case kDataType_Float: + *((float*)dst) = (float)vertices[i].asFloat(); + break; + case kDataType_Int: + *((int*)dst) = vertices[i].asInt(); + break; + case kDataType_Short: + *((short*)dst) = (short)vertices[i].asInt(); + break; + case kDataType_UInt: + *((unsigned int*)dst) = vertices[i].asUInt(); + break; + case kDataType_UShort: + *((unsigned short*)dst) = (unsigned short)vertices[i].asUInt(); + break; + default: + assert(false); + return false; + } + dst += type_size; + ++i; + } + } + } + return true; +} + +size_t Mesh::GetVertexSize() const { + unsigned int size = 0; + for (auto& attr : vertex_description_) { + size += std::get<2>(attr) * std::get<3>(attr); + } + return size; +} + +size_t Mesh::GetIndexSize() const { + switch (index_description_) { + case kDataType_Byte: + return sizeof(char); + case kDataType_UShort: + return sizeof(unsigned short); + case kDataType_UInt: + return sizeof(unsigned int); + default: + return 0; + } +} + +} // namespace eng diff --git a/src/engine/mesh.h b/src/engine/mesh.h new file mode 100644 index 0000000..e246e2d --- /dev/null +++ b/src/engine/mesh.h @@ -0,0 +1,55 @@ +#ifndef MESH_H +#define MESH_H + +#include +#include +#include "renderer/renderer_types.h" + +namespace eng { + +class Mesh { + public: + static const char kLayoutDelimiter[]; + + Mesh() = default; + ~Mesh() = default; + + bool Create(Primitive primitive, + const std::string& vertex_description, + size_t num_vertices, + const void* vertices, + DataType index_description = kDataType_Invalid, + size_t num_indices = 0, + const void* indices = nullptr); + + bool Load(const std::string& file_name); + + const void* GetVertices() const { return (void*)vertices_.get(); } + const void* GetIndices() const { return (void*)indices_.get(); } + + size_t GetVertexSize() const; + size_t GetIndexSize() const; + + Primitive primitive() const { return primitive_; } + const VertexDescripton& vertex_description() const { + return vertex_description_; + } + size_t num_vertices() const { return num_vertices_; } + DataType index_description() const { return index_description_; } + size_t num_indices() const { return num_indices_; } + + bool IsValid() const { return !!vertices_.get(); } + + protected: + Primitive primitive_ = kPrimitive_TriangleStrip; + VertexDescripton vertex_description_; + size_t num_vertices_ = 0; + DataType index_description_ = kDataType_Invalid; + size_t num_indices_ = 0; + std::unique_ptr vertices_; + std::unique_ptr indices_; +}; + +} // namespace eng + +#endif // MESH_H diff --git a/src/engine/platform/asset_file.cc b/src/engine/platform/asset_file.cc new file mode 100644 index 0000000..b88f5d3 --- /dev/null +++ b/src/engine/platform/asset_file.cc @@ -0,0 +1,41 @@ +#include "asset_file.h" +#include "../../base/log.h" + +namespace eng { + +std::unique_ptr AssetFile::ReadWholeFile(const std::string& file_name, + const std::string& root_path, + size_t* length, + bool null_terminate) { + AssetFile file; + if (!file.Open(file_name, root_path)) + return nullptr; + + size_t size = file.GetSize(); + if (size == 0) + return nullptr; + + // Allocate a new buffer and add space for a null terminator. + std::unique_ptr buffer = + std::make_unique(size + (null_terminate ? 1 : 0)); + + // Read all of it. + size_t bytes_read = file.Read(buffer.get(), size); + if (!bytes_read) { + LOG << "Failed to read a buffer of size: " << size << " from file " + << file_name; + return nullptr; + } + + // Return the buffer size if the caller is interested. + if (length) + *length = bytes_read; + + // Null terminate the buffer. + if (null_terminate) + buffer[size] = 0; + + return buffer; +} + +} // namespace eng diff --git a/src/engine/platform/asset_file.h b/src/engine/platform/asset_file.h new file mode 100644 index 0000000..a65a4f2 --- /dev/null +++ b/src/engine/platform/asset_file.h @@ -0,0 +1,43 @@ +#ifndef ASSET_FILE_H +#define ASSET_FILE_H + +#if defined(__ANDROID__) +#include +#include "../../third_party/minizip/unzip.h" +#elif defined(__linux__) +#include "../../base/file.h" +#endif +#include +#include + +namespace eng { + +class AssetFile { + public: + AssetFile(); + ~AssetFile(); + + bool Open(const std::string& file_name, const std::string& root_path); + void Close(); + + size_t GetSize(); + + size_t Read(char* data, size_t size); + + static std::unique_ptr ReadWholeFile(const std::string& file_name, + const std::string& root_path, + size_t* length = 0, + bool null_terminate = false); + + private: +#if defined(__ANDROID__) + unzFile archive_ = 0; + size_t uncompressed_size_ = 0; +#elif defined(__linux) + base::ScopedFILE file_; +#endif +}; + +} // namespace eng + +#endif // ASSET_FILE_H diff --git a/src/engine/platform/asset_file_android.cc b/src/engine/platform/asset_file_android.cc new file mode 100644 index 0000000..515d73e --- /dev/null +++ b/src/engine/platform/asset_file_android.cc @@ -0,0 +1,74 @@ +#include +#include +#include "../../base/log.h" +#include "asset_file.h" + +namespace eng { + +AssetFile::AssetFile() = default; + +AssetFile::~AssetFile() { + Close(); +} + +bool AssetFile::Open(const std::string& file_name, + const std::string& root_path) { + do { + // Try to open the zip archive. + archive_ = unzOpen(root_path.c_str()); + if (!archive_) { + LOG << "Failed to open zip file: " << root_path; + break; + } + + // Try to find the file. + std::string full_name = "assets/" + file_name; + if (UNZ_OK != unzLocateFile(archive_, full_name.c_str(), 1)) { + LOG << "Failed to locate file in zip archive: " << file_name; + break; + } + + // Need to get the uncompressed size of the file. + unz_file_info info; + if (UNZ_OK != + unzGetCurrentFileInfo(archive_, &info, NULL, 0, NULL, 0, NULL, 0)) { + LOG << "Failed to get file info: " << file_name; + break; + } + uncompressed_size_ = info.uncompressed_size; + + // Open the current file. + if (UNZ_OK != unzOpenCurrentFile(archive_)) { + LOG << "Failed to open file: " << file_name; + break; + } + + return true; + } while (false); + + Close(); + return false; +} + +void AssetFile::Close() { + if (archive_) { + // This could potentially be called without having opened a file, but that + // should be a harmless nop. + unzCloseCurrentFile(archive_); + + unzClose(archive_); + archive_ = 0; + } +} + +size_t AssetFile::GetSize() { + return uncompressed_size_; +} + +size_t AssetFile::Read(char* data, size_t size) { + // Uncompress data into the buffer. + int result = unzReadCurrentFile(archive_, data, size); + return result < 0 ? 0 : result; +} + +} // namespace eng diff --git a/src/engine/platform/asset_file_linux.cc b/src/engine/platform/asset_file_linux.cc new file mode 100644 index 0000000..505bafe --- /dev/null +++ b/src/engine/platform/asset_file_linux.cc @@ -0,0 +1,41 @@ +#include +#include "asset_file.h" + +namespace eng { + +AssetFile::AssetFile() = default; + +AssetFile::~AssetFile() = default; + +bool AssetFile::Open(const std::string& file_name, + const std::string& root_path) { + std::string full_path = root_path + "assets/" + file_name; + file_.reset(fopen(full_path.c_str(), "rb")); + return !!file_; +} + +void AssetFile::Close() { + file_.reset(); +} + +size_t AssetFile::GetSize() { + size_t size = 0; + + if (file_) { + if (!fseek(file_.get(), 0, SEEK_END)) { + size = ftell(file_.get()); + rewind(file_.get()); + } + } + + return size; +} + +size_t AssetFile::Read(char* data, size_t size) { + if (file_) + return fread(data, 1, size, file_.get()); + + return 0; +} + +} // namespace eng diff --git a/src/engine/platform/platform.cc b/src/engine/platform/platform.cc new file mode 100644 index 0000000..f179661 --- /dev/null +++ b/src/engine/platform/platform.cc @@ -0,0 +1,77 @@ +#include "platform.h" + +#include + +#include "../../base/log.h" +#include "../audio/audio.h" +#include "../engine.h" +#include "../renderer/renderer.h" + +// Save battery on mobile devices. +#define USE_SLEEP + +namespace eng { + +Platform::InternalError Platform::internal_error; + +void Platform::Shutdown() { + LOG << "Shutting down platform."; + audio_->Shutdown(); + renderer_->Shutdown(); +} + +void Platform::RunMainLoop() { + engine_ = std::make_unique(this, renderer_.get(), audio_.get()); + if (!engine_->Initialize()) { + LOG << "Failed to initialize the engine."; + throw internal_error; + } + + // Use fixed time steps. + constexpr float time_step = 1.0f / 60.0f; + +#ifdef USE_SLEEP + constexpr float epsilon = 0.0001f; +#endif // USE_SLEEP + + timer_.Reset(); + float accumulator = 0.0; + float frame_frac = 0.0f; + + for (;;) { + engine_->Draw(frame_frac); + + // Accumulate time. +#ifdef USE_SLEEP + while (accumulator < time_step) { + timer_.Update(); + accumulator += timer_.GetSecondsPassed(); + if (time_step - accumulator > epsilon) { + float sleep_time = time_step - accumulator - epsilon; + std::this_thread::sleep_for( + std::chrono::microseconds((int)(sleep_time * 1000000.0f))); + } + }; +#else + timer_.Update(); + accumulator += timer_.GetSecondsPassed(); +#endif // USE_SLEEP + + // Subdivide the frame time. + while (accumulator >= time_step) { + Update(); + if (should_exit_) { + engine_->Shutdown(); + engine_.reset(); + return; + } + engine_->Update(time_step); + accumulator -= time_step; + }; + + // Calculate frame fraction from remainder of the frame time. + frame_frac = accumulator / time_step; + } +} + +} // namespace eng diff --git a/src/engine/platform/platform.h b/src/engine/platform/platform.h new file mode 100644 index 0000000..32e23d3 --- /dev/null +++ b/src/engine/platform/platform.h @@ -0,0 +1,88 @@ +#ifndef PLATFORM_H +#define PLATFORM_H + +#include +#include +#include + +#include "../../base/timer.h" +#include "../audio/audio_forward.h" + +#if defined(__ANDROID__) +struct android_app; +struct AInputEvent; + +namespace ndk_helper { +class TapDetector; +class PinchDetector; +class DragDetector; +} // namespace ndk_helper +#endif + +namespace eng { + +class Renderer; +class Engine; + +class Platform { + public: + Platform(); + ~Platform(); + +#if defined(__ANDROID__) + void Initialize(android_app* app); +#elif defined(__linux__) + void Initialize(); +#endif + + void Shutdown(); + + void Update(); + + void RunMainLoop(); + + void Exit(); + + int GetDeviceDpi() const { return device_dpi_; } + + const std::string& GetRootPath() const { return root_path_; } + + bool mobile_device() const { return mobile_device_; } + + static class InternalError : public std::exception { + } internal_error; + + private: + base::Timer timer_; + + bool mobile_device_ = false; + int device_dpi_ = 200; + std::string root_path_; + + bool has_focus_ = false; + bool should_exit_ = false; + + std::unique_ptrJSON value. + * + * This class is a discriminated union wrapper that can represents a: + * - signed integer [range: Value::minInt - Value::maxInt] + * - unsigned integer (range: 0 - Value::maxUInt) + * - double + * - UTF-8 string + * - boolean + * - 'null' + * - an ordered list of Value + * - collection of name/value pairs (javascript object) + * + * The type of the held value is represented by a #ValueType and + * can be obtained using type(). + * + * Values of an #objectValue or #arrayValue can be accessed using operator[]() + * methods. + * Non-const methods will automatically create the a #nullValue element + * if it does not exist. + * The sequence of an #arrayValue will be automatically resized and initialized + * with #nullValue. resize() can be used to enlarge or truncate an #arrayValue. + * + * The get() methods can be used to obtain default value in the case the + * required element does not exist. + * + * It is possible to iterate over the list of a #objectValue values using + * the getMemberNames() method. + * + * \note #Value string-length fit in size_t, but keys must be < 2^30. + * (The reason is an implementation detail.) A #CharReader will raise an + * exception if a bound is exceeded to avoid security holes in your app, + * but the Value API does *not* check bounds. That is the responsibility + * of the caller. + */ +class JSON_API Value { + friend class ValueIteratorBase; +public: + typedef std::vector Members; + typedef ValueIterator iterator; + typedef ValueConstIterator const_iterator; + typedef Json::UInt UInt; + typedef Json::Int Int; +#if defined(JSON_HAS_INT64) + typedef Json::UInt64 UInt64; + typedef Json::Int64 Int64; +#endif // defined(JSON_HAS_INT64) + typedef Json::LargestInt LargestInt; + typedef Json::LargestUInt LargestUInt; + typedef Json::ArrayIndex ArrayIndex; + + static const Value& null; ///< We regret this reference to a global instance; prefer the simpler Value(). + static const Value& nullRef; ///< just a kludge for binary-compatibility; same as null + /// Minimum signed integer value that can be stored in a Json::Value. + static const LargestInt minLargestInt; + /// Maximum signed integer value that can be stored in a Json::Value. + static const LargestInt maxLargestInt; + /// Maximum unsigned integer value that can be stored in a Json::Value. + static const LargestUInt maxLargestUInt; + + /// Minimum signed int value that can be stored in a Json::Value. + static const Int minInt; + /// Maximum signed int value that can be stored in a Json::Value. + static const Int maxInt; + /// Maximum unsigned int value that can be stored in a Json::Value. + static const UInt maxUInt; + +#if defined(JSON_HAS_INT64) + /// Minimum signed 64 bits int value that can be stored in a Json::Value. + static const Int64 minInt64; + /// Maximum signed 64 bits int value that can be stored in a Json::Value. + static const Int64 maxInt64; + /// Maximum unsigned 64 bits int value that can be stored in a Json::Value. + static const UInt64 maxUInt64; +#endif // defined(JSON_HAS_INT64) + +private: +#ifndef JSONCPP_DOC_EXCLUDE_IMPLEMENTATION + class CZString { + public: + enum DuplicationPolicy { + noDuplication = 0, + duplicate, + duplicateOnCopy + }; + CZString(ArrayIndex index); + CZString(char const* str, unsigned length, DuplicationPolicy allocate); + CZString(CZString const& other); +#if JSON_HAS_RVALUE_REFERENCES + CZString(CZString&& other); +#endif + ~CZString(); + CZString& operator=(CZString other); + bool operator<(CZString const& other) const; + bool operator==(CZString const& other) const; + ArrayIndex index() const; + //const char* c_str() const; ///< \deprecated + char const* data() const; + unsigned length() const; + bool isStaticString() const; + + private: + void swap(CZString& other); + + struct StringStorage { + unsigned policy_: 2; + unsigned length_: 30; // 1GB max + }; + + char const* cstr_; // actually, a prefixed string, unless policy is noDup + union { + ArrayIndex index_; + StringStorage storage_; + }; + }; + +public: +#ifndef JSON_USE_CPPTL_SMALLMAP + typedef std::map ObjectValues; +#else + typedef CppTL::SmallMap ObjectValues; +#endif // ifndef JSON_USE_CPPTL_SMALLMAP +#endif // ifndef JSONCPP_DOC_EXCLUDE_IMPLEMENTATION + +public: + /** \brief Create a default Value of the given type. + + This is a very useful constructor. + To create an empty array, pass arrayValue. + To create an empty object, pass objectValue. + Another Value can then be set to this one by assignment. +This is useful since clear() and resize() will not alter types. + + Examples: +\code +Json::Value null_value; // null +Json::Value arr_value(Json::arrayValue); // [] +Json::Value obj_value(Json::objectValue); // {} +\endcode + */ + Value(ValueType type = nullValue); + Value(Int value); + Value(UInt value); +#if defined(JSON_HAS_INT64) + Value(Int64 value); + Value(UInt64 value); +#endif // if defined(JSON_HAS_INT64) + Value(double value); + Value(const char* value); ///< Copy til first 0. (NULL causes to seg-fault.) + Value(const char* begin, const char* end); ///< Copy all, incl zeroes. + /** \brief Constructs a value from a static string. + + * Like other value string constructor but do not duplicate the string for + * internal storage. The given string must remain alive after the call to this + * constructor. + * \note This works only for null-terminated strings. (We cannot change the + * size of this class, so we have nowhere to store the length, + * which might be computed later for various operations.) + * + * Example of usage: + * \code + * static StaticString foo("some text"); + * Json::Value aValue(foo); + * \endcode + */ + Value(const StaticString& value); + Value(const std::string& value); ///< Copy data() til size(). Embedded zeroes too. +#ifdef JSON_USE_CPPTL + Value(const CppTL::ConstString& value); +#endif + Value(bool value); + /// Deep copy. + Value(const Value& other); +#if JSON_HAS_RVALUE_REFERENCES + /// Move constructor + Value(Value&& other); +#endif + ~Value(); + + /// Deep copy, then swap(other). + /// \note Over-write existing comments. To preserve comments, use #swapPayload(). + Value& operator=(Value other); + /// Swap everything. + void swap(Value& other); + /// Swap values but leave comments and source offsets in place. + void swapPayload(Value& other); + + ValueType type() const; + + /// Compare payload only, not comments etc. + bool operator<(const Value& other) const; + bool operator<=(const Value& other) const; + bool operator>=(const Value& other) const; + bool operator>(const Value& other) const; + bool operator==(const Value& other) const; + bool operator!=(const Value& other) const; + int compare(const Value& other) const; + + const char* asCString() const; ///< Embedded zeroes could cause you trouble! + std::string asString() const; ///< Embedded zeroes are possible. + /** Get raw char* of string-value. + * \return false if !string. (Seg-fault if str or end are NULL.) + */ + bool getString( + char const** begin, char const** end) const; +#ifdef JSON_USE_CPPTL + CppTL::ConstString asConstString() const; +#endif + Int asInt() const; + UInt asUInt() const; +#if defined(JSON_HAS_INT64) + Int64 asInt64() const; + UInt64 asUInt64() const; +#endif // if defined(JSON_HAS_INT64) + LargestInt asLargestInt() const; + LargestUInt asLargestUInt() const; + float asFloat() const; + double asDouble() const; + bool asBool() const; + + bool isNull() const; + bool isBool() const; + bool isInt() const; + bool isInt64() const; + bool isUInt() const; + bool isUInt64() const; + bool isIntegral() const; + bool isDouble() const; + bool isNumeric() const; + bool isString() const; + bool isArray() const; + bool isObject() const; + + bool isConvertibleTo(ValueType other) const; + + /// Number of values in array or object + ArrayIndex size() const; + + /// \brief Return true if empty array, empty object, or null; + /// otherwise, false. + bool empty() const; + + /// Return isNull() + bool operator!() const; + + /// Remove all object members and array elements. + /// \pre type() is arrayValue, objectValue, or nullValue + /// \post type() is unchanged + void clear(); + + /// Resize the array to size elements. + /// New elements are initialized to null. + /// May only be called on nullValue or arrayValue. + /// \pre type() is arrayValue or nullValue + /// \post type() is arrayValue + void resize(ArrayIndex size); + + /// Access an array element (zero based index ). + /// If the array contains less than index element, then null value are + /// inserted + /// in the array so that its size is index+1. + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + Value& operator[](ArrayIndex index); + + /// Access an array element (zero based index ). + /// If the array contains less than index element, then null value are + /// inserted + /// in the array so that its size is index+1. + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + Value& operator[](int index); + + /// Access an array element (zero based index ) + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + const Value& operator[](ArrayIndex index) const; + + /// Access an array element (zero based index ) + /// (You may need to say 'value[0u]' to get your compiler to distinguish + /// this from the operator[] which takes a string.) + const Value& operator[](int index) const; + + /// If the array contains at least index+1 elements, returns the element + /// value, + /// otherwise returns defaultValue. + Value get(ArrayIndex index, const Value& defaultValue) const; + /// Return true if index < size(). + bool isValidIndex(ArrayIndex index) const; + /// \brief Append value to array at the end. + /// + /// Equivalent to jsonvalue[jsonvalue.size()] = value; + Value& append(const Value& value); + + /// Access an object value by name, create a null member if it does not exist. + /// \note Because of our implementation, keys are limited to 2^30 -1 chars. + /// Exceeding that will cause an exception. + Value& operator[](const char* key); + /// Access an object value by name, returns null if there is no member with + /// that name. + const Value& operator[](const char* key) const; + /// Access an object value by name, create a null member if it does not exist. + /// \param key may contain embedded nulls. + Value& operator[](const std::string& key); + /// Access an object value by name, returns null if there is no member with + /// that name. + /// \param key may contain embedded nulls. + const Value& operator[](const std::string& key) const; + /** \brief Access an object value by name, create a null member if it does not + exist. + + * If the object has no entry for that name, then the member name used to store + * the new entry is not duplicated. + * Example of use: + * \code + * Json::Value object; + * static const StaticString code("code"); + * object[code] = 1234; + * \endcode + */ + Value& operator[](const StaticString& key); +#ifdef JSON_USE_CPPTL + /// Access an object value by name, create a null member if it does not exist. + Value& operator[](const CppTL::ConstString& key); + /// Access an object value by name, returns null if there is no member with + /// that name. + const Value& operator[](const CppTL::ConstString& key) const; +#endif + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + Value get(const char* key, const Value& defaultValue) const; + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + /// \note key may contain embedded nulls. + Value get(const char* begin, const char* end, const Value& defaultValue) const; + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + /// \param key may contain embedded nulls. + Value get(const std::string& key, const Value& defaultValue) const; +#ifdef JSON_USE_CPPTL + /// Return the member named key if it exist, defaultValue otherwise. + /// \note deep copy + Value get(const CppTL::ConstString& key, const Value& defaultValue) const; +#endif + /// Most general and efficient version of isMember()const, get()const, + /// and operator[]const + /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 + Value const* find(char const* begin, char const* end) const; + /// Most general and efficient version of object-mutators. + /// \note As stated elsewhere, behavior is undefined if (end-begin) >= 2^30 + /// \return non-zero, but JSON_ASSERT if this is neither object nor nullValue. + Value const* demand(char const* begin, char const* end); + /// \brief Remove and return the named member. + /// + /// Do nothing if it did not exist. + /// \return the removed Value, or null. + /// \pre type() is objectValue or nullValue + /// \post type() is unchanged + /// \deprecated + Value removeMember(const char* key); + /// Same as removeMember(const char*) + /// \param key may contain embedded nulls. + /// \deprecated + Value removeMember(const std::string& key); + /// Same as removeMember(const char* begin, const char* end, Value* removed), + /// but 'key' is null-terminated. + bool removeMember(const char* key, Value* removed); + /** \brief Remove the named map member. + + Update 'removed' iff removed. + \param key may contain embedded nulls. + \return true iff removed (no exceptions) + */ + bool removeMember(std::string const& key, Value* removed); + /// Same as removeMember(std::string const& key, Value* removed) + bool removeMember(const char* begin, const char* end, Value* removed); + /** \brief Remove the indexed array element. + + O(n) expensive operations. + Update 'removed' iff removed. + \return true iff removed (no exceptions) + */ + bool removeIndex(ArrayIndex i, Value* removed); + + /// Return true if the object has a member named key. + /// \note 'key' must be null-terminated. + bool isMember(const char* key) const; + /// Return true if the object has a member named key. + /// \param key may contain embedded nulls. + bool isMember(const std::string& key) const; + /// Same as isMember(std::string const& key)const + bool isMember(const char* begin, const char* end) const; +#ifdef JSON_USE_CPPTL + /// Return true if the object has a member named key. + bool isMember(const CppTL::ConstString& key) const; +#endif + + /// \brief Return a list of the member names. + /// + /// If null, return an empty list. + /// \pre type() is objectValue or nullValue + /// \post if type() was nullValue, it remains nullValue + Members getMemberNames() const; + + //# ifdef JSON_USE_CPPTL + // EnumMemberNames enumMemberNames() const; + // EnumValues enumValues() const; + //# endif + + /// \deprecated Always pass len. + JSONCPP_DEPRECATED("Use setComment(std::string const&) instead.") + void setComment(const char* comment, CommentPlacement placement); + /// Comments must be //... or /* ... */ + void setComment(const char* comment, size_t len, CommentPlacement placement); + /// Comments must be //... or /* ... */ + void setComment(const std::string& comment, CommentPlacement placement); + bool hasComment(CommentPlacement placement) const; + /// Include delimiters and embedded newlines. + std::string getComment(CommentPlacement placement) const; + + std::string toStyledString() const; + + const_iterator begin() const; + const_iterator end() const; + + iterator begin(); + iterator end(); + + // Accessors for the [start, limit) range of bytes within the JSON text from + // which this value was parsed, if any. + void setOffsetStart(size_t start); + void setOffsetLimit(size_t limit); + size_t getOffsetStart() const; + size_t getOffsetLimit() const; + +private: + void initBasic(ValueType type, bool allocated = false); + + Value& resolveReference(const char* key); + Value& resolveReference(const char* key, const char* end); + + struct CommentInfo { + CommentInfo(); + ~CommentInfo(); + + void setComment(const char* text, size_t len); + + char* comment_; + }; + + // struct MemberNamesTransform + //{ + // typedef const char *result_type; + // const char *operator()( const CZString &name ) const + // { + // return name.c_str(); + // } + //}; + + union ValueHolder { + LargestInt int_; + LargestUInt uint_; + double real_; + bool bool_; + char* string_; // actually ptr to unsigned, followed by str, unless !allocated_ + ObjectValues* map_; + } value_; + ValueType type_ : 8; + unsigned int allocated_ : 1; // Notes: if declared as bool, bitfield is useless. + // If not allocated_, string_ must be null-terminated. + CommentInfo* comments_; + + // [start, limit) byte offsets in the source JSON text from which this Value + // was extracted. + size_t start_; + size_t limit_; +}; + +/** \brief Experimental and untested: represents an element of the "path" to + * access a node. + */ +class JSON_API PathArgument { +public: + friend class Path; + + PathArgument(); + PathArgument(ArrayIndex index); + PathArgument(const char* key); + PathArgument(const std::string& key); + +private: + enum Kind { + kindNone = 0, + kindIndex, + kindKey + }; + std::string key_; + ArrayIndex index_; + Kind kind_; +}; + +/** \brief Experimental and untested: represents a "path" to access a node. + * + * Syntax: + * - "." => root node + * - ".[n]" => elements at index 'n' of root node (an array value) + * - ".name" => member named 'name' of root node (an object value) + * - ".name1.name2.name3" + * - ".[0][1][2].name1[3]" + * - ".%" => member name is provided as parameter + * - ".[%]" => index is provied as parameter + */ +class JSON_API Path { +public: + Path(const std::string& path, + const PathArgument& a1 = PathArgument(), + const PathArgument& a2 = PathArgument(), + const PathArgument& a3 = PathArgument(), + const PathArgument& a4 = PathArgument(), + const PathArgument& a5 = PathArgument()); + + const Value& resolve(const Value& root) const; + Value resolve(const Value& root, const Value& defaultValue) const; + /// Creates the "path" to access the specified node and returns a reference on + /// the node. + Value& make(Value& root) const; + +private: + typedef std::vector InArgs; + typedef std::vector Args; + + void makePath(const std::string& path, const InArgs& in); + void addPathInArg(const std::string& path, + const InArgs& in, + InArgs::const_iterator& itInArg, + PathArgument::Kind kind); + void invalidPath(const std::string& path, int location); + + Args args_; +}; + +/** \brief base class for Value iterators. + * + */ +class JSON_API ValueIteratorBase { +public: + typedef std::bidirectional_iterator_tag iterator_category; + typedef unsigned int size_t; + typedef int difference_type; + typedef ValueIteratorBase SelfType; + + bool operator==(const SelfType& other) const { return isEqual(other); } + + bool operator!=(const SelfType& other) const { return !isEqual(other); } + + difference_type operator-(const SelfType& other) const { + return other.computeDistance(*this); + } + + /// Return either the index or the member name of the referenced value as a + /// Value. + Value key() const; + + /// Return the index of the referenced Value, or -1 if it is not an arrayValue. + UInt index() const; + + /// Return the member name of the referenced Value, or "" if it is not an + /// objectValue. + /// \note Avoid `c_str()` on result, as embedded zeroes are possible. + std::string name() const; + + /// Return the member name of the referenced Value. "" if it is not an + /// objectValue. + /// \deprecated This cannot be used for UTF-8 strings, since there can be embedded nulls. + JSONCPP_DEPRECATED("Use `key = name();` instead.") + char const* memberName() const; + /// Return the member name of the referenced Value, or NULL if it is not an + /// objectValue. + /// \note Better version than memberName(). Allows embedded nulls. + char const* memberName(char const** end) const; + +protected: + Value& deref() const; + + void increment(); + + void decrement(); + + difference_type computeDistance(const SelfType& other) const; + + bool isEqual(const SelfType& other) const; + + void copy(const SelfType& other); + +private: + Value::ObjectValues::iterator current_; + // Indicates that iterator is for a null value. + bool isNull_; + +public: + // For some reason, BORLAND needs these at the end, rather + // than earlier. No idea why. + ValueIteratorBase(); + explicit ValueIteratorBase(const Value::ObjectValues::iterator& current); +}; + +/** \brief const iterator for object and array value. + * + */ +class JSON_API ValueConstIterator : public ValueIteratorBase { + friend class Value; + +public: + typedef const Value value_type; + //typedef unsigned int size_t; + //typedef int difference_type; + typedef const Value& reference; + typedef const Value* pointer; + typedef ValueConstIterator SelfType; + + ValueConstIterator(); + ValueConstIterator(ValueIterator const& other); + +private: +/*! \internal Use by Value to create an iterator. + */ + explicit ValueConstIterator(const Value::ObjectValues::iterator& current); +public: + SelfType& operator=(const ValueIteratorBase& other); + + SelfType operator++(int) { + SelfType temp(*this); + ++*this; + return temp; + } + + SelfType operator--(int) { + SelfType temp(*this); + --*this; + return temp; + } + + SelfType& operator--() { + decrement(); + return *this; + } + + SelfType& operator++() { + increment(); + return *this; + } + + reference operator*() const { return deref(); } + + pointer operator->() const { return &deref(); } +}; + +/** \brief Iterator for object and array value. + */ +class JSON_API ValueIterator : public ValueIteratorBase { + friend class Value; + +public: + typedef Value value_type; + typedef unsigned int size_t; + typedef int difference_type; + typedef Value& reference; + typedef Value* pointer; + typedef ValueIterator SelfType; + + ValueIterator(); + explicit ValueIterator(const ValueConstIterator& other); + ValueIterator(const ValueIterator& other); + +private: +/*! \internal Use by Value to create an iterator. + */ + explicit ValueIterator(const Value::ObjectValues::iterator& current); +public: + SelfType& operator=(const SelfType& other); + + SelfType operator++(int) { + SelfType temp(*this); + ++*this; + return temp; + } + + SelfType operator--(int) { + SelfType temp(*this); + --*this; + return temp; + } + + SelfType& operator--() { + decrement(); + return *this; + } + + SelfType& operator++() { + increment(); + return *this; + } + + reference operator*() const { return deref(); } + + pointer operator->() const { return &deref(); } +}; + +} // namespace Json + + +namespace std { +/// Specialize std::swap() for Json::Value. +template<> +inline void swap(Json::Value& a, Json::Value& b) { a.swap(b); } +} + + +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(pop) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +#endif // CPPTL_JSON_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/value.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/reader.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef CPPTL_JSON_READER_H_INCLUDED +#define CPPTL_JSON_READER_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "features.h" +#include "value.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include + +// Disable warning C4251: : needs to have dll-interface to +// be used by... +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(push) +#pragma warning(disable : 4251) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +namespace Json { + +/** \brief Unserialize a JSON document into a + *Value. + * + * \deprecated Use CharReader and CharReaderBuilder. + */ +class JSON_API Reader { +public: + typedef char Char; + typedef const Char* Location; + + /** \brief An error tagged with where in the JSON text it was encountered. + * + * The offsets give the [start, limit) range of bytes within the text. Note + * that this is bytes, not codepoints. + * + */ + struct StructuredError { + size_t offset_start; + size_t offset_limit; + std::string message; + }; + + /** \brief Constructs a Reader allowing all features + * for parsing. + */ + Reader(); + + /** \brief Constructs a Reader allowing the specified feature set + * for parsing. + */ + Reader(const Features& features); + + /** \brief Read a Value from a JSON + * document. + * \param document UTF-8 encoded string containing the document to read. + * \param root [out] Contains the root value of the document if it was + * successfully parsed. + * \param collectComments \c true to collect comment and allow writing them + * back during + * serialization, \c false to discard comments. + * This parameter is ignored if + * Features::allowComments_ + * is \c false. + * \return \c true if the document was successfully parsed, \c false if an + * error occurred. + */ + bool + parse(const std::string& document, Value& root, bool collectComments = true); + + /** \brief Read a Value from a JSON + document. + * \param beginDoc Pointer on the beginning of the UTF-8 encoded string of the + document to read. + * \param endDoc Pointer on the end of the UTF-8 encoded string of the + document to read. + * Must be >= beginDoc. + * \param root [out] Contains the root value of the document if it was + * successfully parsed. + * \param collectComments \c true to collect comment and allow writing them + back during + * serialization, \c false to discard comments. + * This parameter is ignored if + Features::allowComments_ + * is \c false. + * \return \c true if the document was successfully parsed, \c false if an + error occurred. + */ + bool parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments = true); + + /// \brief Parse from input stream. + /// \see Json::operator>>(std::istream&, Json::Value&). + bool parse(std::istream& is, Value& root, bool collectComments = true); + + /** \brief Returns a user friendly string that list errors in the parsed + * document. + * \return Formatted error message with the list of errors with their location + * in + * the parsed document. An empty string is returned if no error + * occurred + * during parsing. + * \deprecated Use getFormattedErrorMessages() instead (typo fix). + */ + JSONCPP_DEPRECATED("Use getFormattedErrorMessages() instead.") + std::string getFormatedErrorMessages() const; + + /** \brief Returns a user friendly string that list errors in the parsed + * document. + * \return Formatted error message with the list of errors with their location + * in + * the parsed document. An empty string is returned if no error + * occurred + * during parsing. + */ + std::string getFormattedErrorMessages() const; + + /** \brief Returns a vector of structured erros encounted while parsing. + * \return A (possibly empty) vector of StructuredError objects. Currently + * only one error can be returned, but the caller should tolerate + * multiple + * errors. This can occur if the parser recovers from a non-fatal + * parse error and then encounters additional errors. + */ + std::vector getStructuredErrors() const; + + /** \brief Add a semantic error message. + * \param value JSON Value location associated with the error + * \param message The error message. + * \return \c true if the error was successfully added, \c false if the + * Value offset exceeds the document size. + */ + bool pushError(const Value& value, const std::string& message); + + /** \brief Add a semantic error message with extra context. + * \param value JSON Value location associated with the error + * \param message The error message. + * \param extra Additional JSON Value location to contextualize the error + * \return \c true if the error was successfully added, \c false if either + * Value offset exceeds the document size. + */ + bool pushError(const Value& value, const std::string& message, const Value& extra); + + /** \brief Return whether there are any errors. + * \return \c true if there are no errors to report \c false if + * errors have occurred. + */ + bool good() const; + +private: + enum TokenType { + tokenEndOfStream = 0, + tokenObjectBegin, + tokenObjectEnd, + tokenArrayBegin, + tokenArrayEnd, + tokenString, + tokenNumber, + tokenTrue, + tokenFalse, + tokenNull, + tokenArraySeparator, + tokenMemberSeparator, + tokenComment, + tokenError + }; + + class Token { + public: + TokenType type_; + Location start_; + Location end_; + }; + + class ErrorInfo { + public: + Token token_; + std::string message_; + Location extra_; + }; + + typedef std::deque Errors; + + bool readToken(Token& token); + void skipSpaces(); + bool match(Location pattern, int patternLength); + bool readComment(); + bool readCStyleComment(); + bool readCppStyleComment(); + bool readString(); + void readNumber(); + bool readValue(); + bool readObject(Token& token); + bool readArray(Token& token); + bool decodeNumber(Token& token); + bool decodeNumber(Token& token, Value& decoded); + bool decodeString(Token& token); + bool decodeString(Token& token, std::string& decoded); + bool decodeDouble(Token& token); + bool decodeDouble(Token& token, Value& decoded); + bool decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool addError(const std::string& message, Token& token, Location extra = 0); + bool recoverFromError(TokenType skipUntilToken); + bool addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken); + void skipUntilSpace(); + Value& currentValue(); + Char getNextChar(); + void + getLocationLineAndColumn(Location location, int& line, int& column) const; + std::string getLocationLineAndColumn(Location location) const; + void addComment(Location begin, Location end, CommentPlacement placement); + void skipCommentTokens(Token& token); + + typedef std::stack Nodes; + Nodes nodes_; + Errors errors_; + std::string document_; + Location begin_; + Location end_; + Location current_; + Location lastValueEnd_; + Value* lastValue_; + std::string commentsBefore_; + Features features_; + bool collectComments_; +}; // Reader + +/** Interface for reading JSON from a char array. + */ +class JSON_API CharReader { +public: + virtual ~CharReader() {} + /** \brief Read a Value from a JSON + document. + * The document must be a UTF-8 encoded string containing the document to read. + * + * \param beginDoc Pointer on the beginning of the UTF-8 encoded string of the + document to read. + * \param endDoc Pointer on the end of the UTF-8 encoded string of the + document to read. + * Must be >= beginDoc. + * \param root [out] Contains the root value of the document if it was + * successfully parsed. + * \param errs [out] Formatted error messages (if not NULL) + * a user friendly string that lists errors in the parsed + * document. + * \return \c true if the document was successfully parsed, \c false if an + error occurred. + */ + virtual bool parse( + char const* beginDoc, char const* endDoc, + Value* root, std::string* errs) = 0; + + class JSON_API Factory { + public: + virtual ~Factory() {} + /** \brief Allocate a CharReader via operator new(). + * \throw std::exception if something goes wrong (e.g. invalid settings) + */ + virtual CharReader* newCharReader() const = 0; + }; // Factory +}; // CharReader + +/** \brief Build a CharReader implementation. + +Usage: +\code + using namespace Json; + CharReaderBuilder builder; + builder["collectComments"] = false; + Value value; + std::string errs; + bool ok = parseFromStream(builder, std::cin, &value, &errs); +\endcode +*/ +class JSON_API CharReaderBuilder : public CharReader::Factory { +public: + // Note: We use a Json::Value so that we can add data-members to this class + // without a major version bump. + /** Configuration of this builder. + These are case-sensitive. + Available settings (case-sensitive): + - `"collectComments": false or true` + - true to collect comment and allow writing them + back during serialization, false to discard comments. + This parameter is ignored if allowComments is false. + - `"allowComments": false or true` + - true if comments are allowed. + - `"strictRoot": false or true` + - true if root must be either an array or an object value + - `"allowDroppedNullPlaceholders": false or true` + - true if dropped null placeholders are allowed. (See StreamWriterBuilder.) + - `"allowNumericKeys": false or true` + - true if numeric object keys are allowed. + - `"allowSingleQuotes": false or true` + - true if '' are allowed for strings (both keys and values) + - `"stackLimit": integer` + - Exceeding stackLimit (recursive depth of `readValue()`) will + cause an exception. + - This is a security issue (seg-faults caused by deeply nested JSON), + so the default is low. + - `"failIfExtra": false or true` + - If true, `parse()` returns false when extra non-whitespace trails + the JSON value in the input string. + - `"rejectDupKeys": false or true` + - If true, `parse()` returns false when a key is duplicated within an object. + - `"allowSpecialFloats": false or true` + - If true, special float values (NaNs and infinities) are allowed + and their values are lossfree restorable. + + You can examine 'settings_` yourself + to see the defaults. You can also write and read them just like any + JSON Value. + \sa setDefaults() + */ + Json::Value settings_; + + CharReaderBuilder(); + ~CharReaderBuilder() override; + + CharReader* newCharReader() const override; + + /** \return true if 'settings' are legal and consistent; + * otherwise, indicate bad settings via 'invalid'. + */ + bool validate(Json::Value* invalid) const; + + /** A simple way to update a specific setting. + */ + Value& operator[](std::string key); + + /** Called by ctor, but you can use this to reset settings_. + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_reader.cpp CharReaderBuilderDefaults + */ + static void setDefaults(Json::Value* settings); + /** Same as old Features::strictMode(). + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_reader.cpp CharReaderBuilderStrictMode + */ + static void strictMode(Json::Value* settings); +}; + +/** Consume entire stream and use its begin/end. + * Someday we might have a real StreamReader, but for now this + * is convenient. + */ +bool JSON_API parseFromStream( + CharReader::Factory const&, + std::istream&, + Value* root, std::string* errs); + +/** \brief Read from 'sin' into 'root'. + + Always keep comments from the input JSON. + + This can be used to read a file into a particular sub-object. + For example: + \code + Json::Value root; + cin >> root["dir"]["file"]; + cout << root; + \endcode + Result: + \verbatim + { + "dir": { + "file": { + // The input stream JSON would be nested here. + } + } + } + \endverbatim + \throw std::exception on parse error. + \see Json::operator<<() +*/ +JSON_API std::istream& operator>>(std::istream&, Value&); + +} // namespace Json + +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(pop) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +#endif // CPPTL_JSON_READER_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/reader.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/writer.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef JSON_WRITER_H_INCLUDED +#define JSON_WRITER_H_INCLUDED + +#if !defined(JSON_IS_AMALGAMATION) +#include "value.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include + +// Disable warning C4251: : needs to have dll-interface to +// be used by... +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(push) +#pragma warning(disable : 4251) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +namespace Json { + +class Value; + +/** + +Usage: +\code + using namespace Json; + void writeToStdout(StreamWriter::Factory const& factory, Value const& value) { + std::unique_ptr const writer( + factory.newStreamWriter()); + writer->write(value, &std::cout); + std::cout << std::endl; // add lf and flush + } +\endcode +*/ +class JSON_API StreamWriter { +protected: + std::ostream* sout_; // not owned; will not delete +public: + StreamWriter(); + virtual ~StreamWriter(); + /** Write Value into document as configured in sub-class. + Do not take ownership of sout, but maintain a reference during function. + \pre sout != NULL + \return zero on success (For now, we always return zero, so check the stream instead.) + \throw std::exception possibly, depending on configuration + */ + virtual int write(Value const& root, std::ostream* sout) = 0; + + /** \brief A simple abstract factory. + */ + class JSON_API Factory { + public: + virtual ~Factory(); + /** \brief Allocate a CharReader via operator new(). + * \throw std::exception if something goes wrong (e.g. invalid settings) + */ + virtual StreamWriter* newStreamWriter() const = 0; + }; // Factory +}; // StreamWriter + +/** \brief Write into stringstream, then return string, for convenience. + * A StreamWriter will be created from the factory, used, and then deleted. + */ +std::string JSON_API writeString(StreamWriter::Factory const& factory, Value const& root); + + +/** \brief Build a StreamWriter implementation. + +Usage: +\code + using namespace Json; + Value value = ...; + StreamWriterBuilder builder; + builder["commentStyle"] = "None"; + builder["indentation"] = " "; // or whatever you like + std::unique_ptr writer( + builder.newStreamWriter()); + writer->write(value, &std::cout); + std::cout << std::endl; // add lf and flush +\endcode +*/ +class JSON_API StreamWriterBuilder : public StreamWriter::Factory { +public: + // Note: We use a Json::Value so that we can add data-members to this class + // without a major version bump. + /** Configuration of this builder. + Available settings (case-sensitive): + - "commentStyle": "None" or "All" + - "indentation": "" + - "enableYAMLCompatibility": false or true + - slightly change the whitespace around colons + - "dropNullPlaceholders": false or true + - Drop the "null" string from the writer's output for nullValues. + Strictly speaking, this is not valid JSON. But when the output is being + fed to a browser's Javascript, it makes for smaller output and the + browser can handle the output just fine. + - "useSpecialFloats": false or true + - If true, outputs non-finite floating point values in the following way: + NaN values as "NaN", positive infinity as "Infinity", and negative infinity + as "-Infinity". + + You can examine 'settings_` yourself + to see the defaults. You can also write and read them just like any + JSON Value. + \sa setDefaults() + */ + Json::Value settings_; + + StreamWriterBuilder(); + ~StreamWriterBuilder() override; + + /** + * \throw std::exception if something goes wrong (e.g. invalid settings) + */ + StreamWriter* newStreamWriter() const override; + + /** \return true if 'settings' are legal and consistent; + * otherwise, indicate bad settings via 'invalid'. + */ + bool validate(Json::Value* invalid) const; + /** A simple way to update a specific setting. + */ + Value& operator[](std::string key); + + /** Called by ctor, but you can use this to reset settings_. + * \pre 'settings' != NULL (but Json::null is fine) + * \remark Defaults: + * \snippet src/lib_json/json_writer.cpp StreamWriterBuilderDefaults + */ + static void setDefaults(Json::Value* settings); +}; + +/** \brief Abstract class for writers. + * \deprecated Use StreamWriter. (And really, this is an implementation detail.) + */ +class JSON_API Writer { +public: + virtual ~Writer(); + + virtual std::string write(const Value& root) = 0; +}; + +/** \brief Outputs a Value in JSON format + *without formatting (not human friendly). + * + * The JSON document is written in a single line. It is not intended for 'human' + *consumption, + * but may be usefull to support feature such as RPC where bandwith is limited. + * \sa Reader, Value + * \deprecated Use StreamWriterBuilder. + */ +class JSON_API FastWriter : public Writer { + +public: + FastWriter(); + ~FastWriter() override {} + + void enableYAMLCompatibility(); + + /** \brief Drop the "null" string from the writer's output for nullValues. + * Strictly speaking, this is not valid JSON. But when the output is being + * fed to a browser's Javascript, it makes for smaller output and the + * browser can handle the output just fine. + */ + void dropNullPlaceholders(); + + void omitEndingLineFeed(); + +public: // overridden from Writer + std::string write(const Value& root) override; + +private: + void writeValue(const Value& value); + + std::string document_; + bool yamlCompatiblityEnabled_; + bool dropNullPlaceholders_; + bool omitEndingLineFeed_; +}; + +/** \brief Writes a Value in JSON format in a + *human friendly way. + * + * The rules for line break and indent are as follow: + * - Object value: + * - if empty then print {} without indent and line break + * - if not empty the print '{', line break & indent, print one value per + *line + * and then unindent and line break and print '}'. + * - Array value: + * - if empty then print [] without indent and line break + * - if the array contains no object value, empty array or some other value + *types, + * and all the values fit on one lines, then print the array on a single + *line. + * - otherwise, it the values do not fit on one line, or the array contains + * object or non empty array, then print one value per line. + * + * If the Value have comments then they are outputed according to their + *#CommentPlacement. + * + * \sa Reader, Value, Value::setComment() + * \deprecated Use StreamWriterBuilder. + */ +class JSON_API StyledWriter : public Writer { +public: + StyledWriter(); + ~StyledWriter() override {} + +public: // overridden from Writer + /** \brief Serialize a Value in JSON format. + * \param root Value to serialize. + * \return String containing the JSON document that represents the root value. + */ + std::string write(const Value& root) override; + +private: + void writeValue(const Value& value); + void writeArrayValue(const Value& value); + bool isMultineArray(const Value& value); + void pushValue(const std::string& value); + void writeIndent(); + void writeWithIndent(const std::string& value); + void indent(); + void unindent(); + void writeCommentBeforeValue(const Value& root); + void writeCommentAfterValueOnSameLine(const Value& root); + bool hasCommentForValue(const Value& value); + static std::string normalizeEOL(const std::string& text); + + typedef std::vector ChildValues; + + ChildValues childValues_; + std::string document_; + std::string indentString_; + int rightMargin_; + int indentSize_; + bool addChildValues_; +}; + +/** \brief Writes a Value in JSON format in a + human friendly way, + to a stream rather than to a string. + * + * The rules for line break and indent are as follow: + * - Object value: + * - if empty then print {} without indent and line break + * - if not empty the print '{', line break & indent, print one value per + line + * and then unindent and line break and print '}'. + * - Array value: + * - if empty then print [] without indent and line break + * - if the array contains no object value, empty array or some other value + types, + * and all the values fit on one lines, then print the array on a single + line. + * - otherwise, it the values do not fit on one line, or the array contains + * object or non empty array, then print one value per line. + * + * If the Value have comments then they are outputed according to their + #CommentPlacement. + * + * \param indentation Each level will be indented by this amount extra. + * \sa Reader, Value, Value::setComment() + * \deprecated Use StreamWriterBuilder. + */ +class JSON_API StyledStreamWriter { +public: + StyledStreamWriter(std::string indentation = "\t"); + ~StyledStreamWriter() {} + +public: + /** \brief Serialize a Value in JSON format. + * \param out Stream to write to. (Can be ostringstream, e.g.) + * \param root Value to serialize. + * \note There is no point in deriving from Writer, since write() should not + * return a value. + */ + void write(std::ostream& out, const Value& root); + +private: + void writeValue(const Value& value); + void writeArrayValue(const Value& value); + bool isMultineArray(const Value& value); + void pushValue(const std::string& value); + void writeIndent(); + void writeWithIndent(const std::string& value); + void indent(); + void unindent(); + void writeCommentBeforeValue(const Value& root); + void writeCommentAfterValueOnSameLine(const Value& root); + bool hasCommentForValue(const Value& value); + static std::string normalizeEOL(const std::string& text); + + typedef std::vector ChildValues; + + ChildValues childValues_; + std::ostream* document_; + std::string indentString_; + int rightMargin_; + std::string indentation_; + bool addChildValues_ : 1; + bool indented_ : 1; +}; + +#if defined(JSON_HAS_INT64) +std::string JSON_API valueToString(Int value); +std::string JSON_API valueToString(UInt value); +#endif // if defined(JSON_HAS_INT64) +std::string JSON_API valueToString(LargestInt value); +std::string JSON_API valueToString(LargestUInt value); +std::string JSON_API valueToString(double value); +std::string JSON_API valueToString(bool value); +std::string JSON_API valueToQuotedString(const char* value); + +/// \brief Output using the StyledStreamWriter. +/// \see Json::operator>>() +JSON_API std::ostream& operator<<(std::ostream&, const Value& root); + +} // namespace Json + +#if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) +#pragma warning(pop) +#endif // if defined(JSONCPP_DISABLE_DLL_INTERFACE_WARNING) + +#endif // JSON_WRITER_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/writer.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: include/json/assertions.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef CPPTL_JSON_ASSERTIONS_H_INCLUDED +#define CPPTL_JSON_ASSERTIONS_H_INCLUDED + +#include +#include + +#if !defined(JSON_IS_AMALGAMATION) +#include "config.h" +#endif // if !defined(JSON_IS_AMALGAMATION) + +/** It should not be possible for a maliciously designed file to + * cause an abort() or seg-fault, so these macros are used only + * for pre-condition violations and internal logic errors. + */ +#if JSON_USE_EXCEPTION + +// @todo <= add detail about condition in exception +# define JSON_ASSERT(condition) \ + {if (!(condition)) {Json::throwLogicError( "assert json failed" );}} + +# define JSON_FAIL_MESSAGE(message) \ + { \ + std::ostringstream oss; oss << message; \ + Json::throwLogicError(oss.str()); \ + abort(); \ + } + +#else // JSON_USE_EXCEPTION + +# define JSON_ASSERT(condition) assert(condition) + +// The call to assert() will show the failure message in debug builds. In +// release builds we abort, for a core-dump or debugger. +# define JSON_FAIL_MESSAGE(message) \ + { \ + std::ostringstream oss; oss << message; \ + assert(false && oss.str().c_str()); \ + abort(); \ + } + + +#endif + +#define JSON_ASSERT_MESSAGE(condition, message) \ + if (!(condition)) { \ + JSON_FAIL_MESSAGE(message); \ + } + +#endif // CPPTL_JSON_ASSERTIONS_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: include/json/assertions.h +// ////////////////////////////////////////////////////////////////////// + + + + + +#endif //ifndef JSON_AMALGATED_H_INCLUDED diff --git a/src/third_party/jsoncpp/jsoncpp.cc b/src/third_party/jsoncpp/jsoncpp.cc new file mode 100644 index 0000000..39131de --- /dev/null +++ b/src/third_party/jsoncpp/jsoncpp.cc @@ -0,0 +1,5192 @@ +/// Json-cpp amalgated source (http://jsoncpp.sourceforge.net/). +/// It is intended to be used with #include "json/json.h" + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + +/* +The JsonCpp library's source code, including accompanying documentation, +tests and demonstration applications, are licensed under the following +conditions... + +The author (Baptiste Lepilleur) explicitly disclaims copyright in all +jurisdictions which recognize such a disclaimer. In such jurisdictions, +this software is released into the Public Domain. + +In jurisdictions which do not recognize Public Domain property (e.g. Germany as of +2010), this software is Copyright (c) 2007-2010 by Baptiste Lepilleur, and is +released under the terms of the MIT License (see below). + +In jurisdictions which recognize Public Domain property, the user of this +software may choose to accept it either as 1) Public Domain, 2) under the +conditions of the MIT License (see below), or 3) under the terms of dual +Public Domain/MIT License conditions described here, as they choose. + +The MIT License is about as close to Public Domain as a license can get, and is +described in clear, concise terms at: + + http://en.wikipedia.org/wiki/MIT_License + +The full text of the MIT License follows: + +======================================================================== +Copyright (c) 2007-2010 Baptiste Lepilleur + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, copy, +modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS +BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN +ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +======================================================================== +(END LICENSE TEXT) + +The MIT license is compatible with both the GPL and commercial +software, affording one all of the rights of Public Domain with the +minor nuisance of being required to keep the above copyright notice +and license text in the source code. Note also that by accepting the +Public Domain "license" you can re-license your copy using whatever +license you like. + +*/ + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: LICENSE +// ////////////////////////////////////////////////////////////////////// + + + + + + +#include "json.h" + +#ifndef JSON_IS_AMALGAMATION +#error "Compile with -I PATH_TO_JSON_DIRECTORY" +#endif + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_tool.h +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#ifndef LIB_JSONCPP_JSON_TOOL_H_INCLUDED +#define LIB_JSONCPP_JSON_TOOL_H_INCLUDED + +/* This header provides common string manipulation support, such as UTF-8, + * portable conversion from/to string... + * + * It is an internal header that must not be exposed. + */ + +namespace Json { + +/// Converts a unicode code-point to UTF-8. +static inline std::string codePointToUTF8(unsigned int cp) { + std::string result; + + // based on description from http://en.wikipedia.org/wiki/UTF-8 + + if (cp <= 0x7f) { + result.resize(1); + result[0] = static_cast(cp); + } else if (cp <= 0x7FF) { + result.resize(2); + result[1] = static_cast(0x80 | (0x3f & cp)); + result[0] = static_cast(0xC0 | (0x1f & (cp >> 6))); + } else if (cp <= 0xFFFF) { + result.resize(3); + result[2] = static_cast(0x80 | (0x3f & cp)); + result[1] = static_cast(0x80 | (0x3f & (cp >> 6))); + result[0] = static_cast(0xE0 | (0xf & (cp >> 12))); + } else if (cp <= 0x10FFFF) { + result.resize(4); + result[3] = static_cast(0x80 | (0x3f & cp)); + result[2] = static_cast(0x80 | (0x3f & (cp >> 6))); + result[1] = static_cast(0x80 | (0x3f & (cp >> 12))); + result[0] = static_cast(0xF0 | (0x7 & (cp >> 18))); + } + + return result; +} + +/// Returns true if ch is a control character (in range [1,31]). +static inline bool isControlCharacter(char ch) { return ch > 0 && ch <= 0x1F; } + +enum { + /// Constant that specify the size of the buffer that must be passed to + /// uintToString. + uintToStringBufferSize = 3 * sizeof(LargestUInt) + 1 +}; + +// Defines a char buffer for use with uintToString(). +typedef char UIntToStringBuffer[uintToStringBufferSize]; + +/** Converts an unsigned integer to string. + * @param value Unsigned interger to convert to string + * @param current Input/Output string buffer. + * Must have at least uintToStringBufferSize chars free. + */ +static inline void uintToString(LargestUInt value, char*& current) { + *--current = 0; + do { + *--current = static_cast(value % 10U + static_cast('0')); + value /= 10; + } while (value != 0); +} + +/** Change ',' to '.' everywhere in buffer. + * + * We had a sophisticated way, but it did not work in WinCE. + * @see https://github.com/open-source-parsers/jsoncpp/pull/9 + */ +static inline void fixNumericLocale(char* begin, char* end) { + while (begin < end) { + if (*begin == ',') { + *begin = '.'; + } + ++begin; + } +} + +} // namespace Json { + +#endif // LIB_JSONCPP_JSON_TOOL_H_INCLUDED + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_tool.h +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_reader.cpp +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2011 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include "json_tool.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) +#if !defined(WINCE) && defined(__STDC_SECURE_LIB__) && _MSC_VER >= 1500 // VC++ 9.0 and above +#define snprintf sprintf_s +#elif _MSC_VER >= 1900 // VC++ 14.0 and above +#define snprintf std::snprintf +#else +#define snprintf _snprintf +#endif +#elif defined(__ANDROID__) || defined(__QNXNTO__) +#define snprintf snprintf +#elif __cplusplus >= 201103L +#define snprintf std::snprintf +#endif + +#if defined(__QNXNTO__) +#define sscanf std::sscanf +#endif + +#if defined(_MSC_VER) && _MSC_VER >= 1400 // VC++ 8.0 +// Disable warning about strdup being deprecated. +#pragma warning(disable : 4996) +#endif + +static int const stackLimit_g = 1000; +static int stackDepth_g = 0; // see readValue() + +namespace Json { + +#if __cplusplus >= 201103L || (defined(_CPPLIB_VER) && _CPPLIB_VER >= 520) +typedef std::unique_ptr CharReaderPtr; +#else +typedef std::auto_ptr CharReaderPtr; +#endif + +// Implementation of class Features +// //////////////////////////////// + +Features::Features() + : allowComments_(true), strictRoot_(false), + allowDroppedNullPlaceholders_(false), allowNumericKeys_(false) {} + +Features Features::all() { return Features(); } + +Features Features::strictMode() { + Features features; + features.allowComments_ = false; + features.strictRoot_ = true; + features.allowDroppedNullPlaceholders_ = false; + features.allowNumericKeys_ = false; + return features; +} + +// Implementation of class Reader +// //////////////////////////////// + +static bool containsNewLine(Reader::Location begin, Reader::Location end) { + for (; begin < end; ++begin) + if (*begin == '\n' || *begin == '\r') + return true; + return false; +} + +// Class Reader +// ////////////////////////////////////////////////////////////////// + +Reader::Reader() + : errors_(), document_(), begin_(), end_(), current_(), lastValueEnd_(), + lastValue_(), commentsBefore_(), features_(Features::all()), + collectComments_() {} + +Reader::Reader(const Features& features) + : errors_(), document_(), begin_(), end_(), current_(), lastValueEnd_(), + lastValue_(), commentsBefore_(), features_(features), collectComments_() { +} + +bool +Reader::parse(const std::string& document, Value& root, bool collectComments) { + document_ = document; + const char* begin = document_.c_str(); + const char* end = begin + document_.length(); + return parse(begin, end, root, collectComments); +} + +bool Reader::parse(std::istream& sin, Value& root, bool collectComments) { + // std::istream_iterator begin(sin); + // std::istream_iterator end; + // Those would allow streamed input from a file, if parse() were a + // template function. + + // Since std::string is reference-counted, this at least does not + // create an extra copy. + std::string doc; + std::getline(sin, doc, (char)EOF); + return parse(doc, root, collectComments); +} + +bool Reader::parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments) { + if (!features_.allowComments_) { + collectComments = false; + } + + begin_ = beginDoc; + end_ = endDoc; + collectComments_ = collectComments; + current_ = begin_; + lastValueEnd_ = 0; + lastValue_ = 0; + commentsBefore_ = ""; + errors_.clear(); + while (!nodes_.empty()) + nodes_.pop(); + nodes_.push(&root); + + stackDepth_g = 0; // Yes, this is bad coding, but options are limited. + bool successful = readValue(); + Token token; + skipCommentTokens(token); + if (collectComments_ && !commentsBefore_.empty()) + root.setComment(commentsBefore_, commentAfter); + if (features_.strictRoot_) { + if (!root.isArray() && !root.isObject()) { + // Set error location to start of doc, ideally should be first token found + // in doc + token.type_ = tokenError; + token.start_ = beginDoc; + token.end_ = endDoc; + addError( + "A valid JSON document must be either an array or an object value.", + token); + return false; + } + } + return successful; +} + +bool Reader::readValue() { + // This is a non-reentrant way to support a stackLimit. Terrible! + // But this deprecated class has a security problem: Bad input can + // cause a seg-fault. This seems like a fair, binary-compatible way + // to prevent the problem. + if (stackDepth_g >= stackLimit_g) throwRuntimeError("Exceeded stackLimit in readValue()."); + ++stackDepth_g; + + Token token; + skipCommentTokens(token); + bool successful = true; + + if (collectComments_ && !commentsBefore_.empty()) { + currentValue().setComment(commentsBefore_, commentBefore); + commentsBefore_ = ""; + } + + switch (token.type_) { + case tokenObjectBegin: + successful = readObject(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenArrayBegin: + successful = readArray(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenNumber: + successful = decodeNumber(token); + break; + case tokenString: + successful = decodeString(token); + break; + case tokenTrue: + { + Value v(true); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenFalse: + { + Value v(false); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNull: + { + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenArraySeparator: + case tokenObjectEnd: + case tokenArrayEnd: + if (features_.allowDroppedNullPlaceholders_) { + // "Un-read" the current token and mark the current value as a null + // token. + current_--; + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(current_ - begin_ - 1); + currentValue().setOffsetLimit(current_ - begin_); + break; + } // Else, fall through... + default: + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return addError("Syntax error: value, object or array expected.", token); + } + + if (collectComments_) { + lastValueEnd_ = current_; + lastValue_ = ¤tValue(); + } + + --stackDepth_g; + return successful; +} + +void Reader::skipCommentTokens(Token& token) { + if (features_.allowComments_) { + do { + readToken(token); + } while (token.type_ == tokenComment); + } else { + readToken(token); + } +} + +bool Reader::readToken(Token& token) { + skipSpaces(); + token.start_ = current_; + Char c = getNextChar(); + bool ok = true; + switch (c) { + case '{': + token.type_ = tokenObjectBegin; + break; + case '}': + token.type_ = tokenObjectEnd; + break; + case '[': + token.type_ = tokenArrayBegin; + break; + case ']': + token.type_ = tokenArrayEnd; + break; + case '"': + token.type_ = tokenString; + ok = readString(); + break; + case '/': + token.type_ = tokenComment; + ok = readComment(); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + token.type_ = tokenNumber; + readNumber(); + break; + case 't': + token.type_ = tokenTrue; + ok = match("rue", 3); + break; + case 'f': + token.type_ = tokenFalse; + ok = match("alse", 4); + break; + case 'n': + token.type_ = tokenNull; + ok = match("ull", 3); + break; + case ',': + token.type_ = tokenArraySeparator; + break; + case ':': + token.type_ = tokenMemberSeparator; + break; + case 0: + token.type_ = tokenEndOfStream; + break; + default: + ok = false; + break; + } + if (!ok) + token.type_ = tokenError; + token.end_ = current_; + return true; +} + +void Reader::skipSpaces() { + while (current_ != end_) { + Char c = *current_; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + ++current_; + else + break; + } +} + +bool Reader::match(Location pattern, int patternLength) { + if (end_ - current_ < patternLength) + return false; + int index = patternLength; + while (index--) + if (current_[index] != pattern[index]) + return false; + current_ += patternLength; + return true; +} + +bool Reader::readComment() { + Location commentBegin = current_ - 1; + Char c = getNextChar(); + bool successful = false; + if (c == '*') + successful = readCStyleComment(); + else if (c == '/') + successful = readCppStyleComment(); + if (!successful) + return false; + + if (collectComments_) { + CommentPlacement placement = commentBefore; + if (lastValueEnd_ && !containsNewLine(lastValueEnd_, commentBegin)) { + if (c != '*' || !containsNewLine(commentBegin, current_)) + placement = commentAfterOnSameLine; + } + + addComment(commentBegin, current_, placement); + } + return true; +} + +static std::string normalizeEOL(Reader::Location begin, Reader::Location end) { + std::string normalized; + normalized.reserve(end - begin); + Reader::Location current = begin; + while (current != end) { + char c = *current++; + if (c == '\r') { + if (current != end && *current == '\n') + // convert dos EOL + ++current; + // convert Mac EOL + normalized += '\n'; + } else { + normalized += c; + } + } + return normalized; +} + +void +Reader::addComment(Location begin, Location end, CommentPlacement placement) { + assert(collectComments_); + const std::string& normalized = normalizeEOL(begin, end); + if (placement == commentAfterOnSameLine) { + assert(lastValue_ != 0); + lastValue_->setComment(normalized, placement); + } else { + commentsBefore_ += normalized; + } +} + +bool Reader::readCStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '*' && *current_ == '/') + break; + } + return getNextChar() == '/'; +} + +bool Reader::readCppStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '\n') + break; + if (c == '\r') { + // Consume DOS EOL. It will be normalized in addComment. + if (current_ != end_ && *current_ == '\n') + getNextChar(); + // Break on Moc OS 9 EOL. + break; + } + } + return true; +} + +void Reader::readNumber() { + const char *p = current_; + char c = '0'; // stopgap for already consumed character + // integral part + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + // fractional part + if (c == '.') { + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } + // exponential part + if (c == 'e' || c == 'E') { + c = (current_ = p) < end_ ? *p++ : 0; + if (c == '+' || c == '-') + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } +} + +bool Reader::readString() { + Char c = 0; + while (current_ != end_) { + c = getNextChar(); + if (c == '\\') + getNextChar(); + else if (c == '"') + break; + } + return c == '"'; +} + +bool Reader::readObject(Token& tokenStart) { + Token tokenName; + std::string name; + Value init(objectValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + while (readToken(tokenName)) { + bool initialTokenOk = true; + while (tokenName.type_ == tokenComment && initialTokenOk) + initialTokenOk = readToken(tokenName); + if (!initialTokenOk) + break; + if (tokenName.type_ == tokenObjectEnd && name.empty()) // empty object + return true; + name = ""; + if (tokenName.type_ == tokenString) { + if (!decodeString(tokenName, name)) + return recoverFromError(tokenObjectEnd); + } else if (tokenName.type_ == tokenNumber && features_.allowNumericKeys_) { + Value numberName; + if (!decodeNumber(tokenName, numberName)) + return recoverFromError(tokenObjectEnd); + name = numberName.asString(); + } else { + break; + } + + Token colon; + if (!readToken(colon) || colon.type_ != tokenMemberSeparator) { + return addErrorAndRecover( + "Missing ':' after object member name", colon, tokenObjectEnd); + } + Value& value = currentValue()[name]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenObjectEnd); + + Token comma; + if (!readToken(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && + comma.type_ != tokenComment)) { + return addErrorAndRecover( + "Missing ',' or '}' in object declaration", comma, tokenObjectEnd); + } + bool finalizeTokenOk = true; + while (comma.type_ == tokenComment && finalizeTokenOk) + finalizeTokenOk = readToken(comma); + if (comma.type_ == tokenObjectEnd) + return true; + } + return addErrorAndRecover( + "Missing '}' or object member name", tokenName, tokenObjectEnd); +} + +bool Reader::readArray(Token& tokenStart) { + Value init(arrayValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + skipSpaces(); + if (*current_ == ']') // empty array + { + Token endArray; + readToken(endArray); + return true; + } + int index = 0; + for (;;) { + Value& value = currentValue()[index++]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenArrayEnd); + + Token token; + // Accept Comment after last item in the array. + ok = readToken(token); + while (token.type_ == tokenComment && ok) { + ok = readToken(token); + } + bool badTokenType = + (token.type_ != tokenArraySeparator && token.type_ != tokenArrayEnd); + if (!ok || badTokenType) { + return addErrorAndRecover( + "Missing ',' or ']' in array declaration", token, tokenArrayEnd); + } + if (token.type_ == tokenArrayEnd) + break; + } + return true; +} + +bool Reader::decodeNumber(Token& token) { + Value decoded; + if (!decodeNumber(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool Reader::decodeNumber(Token& token, Value& decoded) { + // Attempts to parse the number as an integer. If the number is + // larger than the maximum supported value of an integer then + // we decode the number as a double. + Location current = token.start_; + bool isNegative = *current == '-'; + if (isNegative) + ++current; + // TODO: Help the compiler do the div and mod at compile time or get rid of them. + Value::LargestUInt maxIntegerValue = + isNegative ? Value::LargestUInt(Value::maxLargestInt) + 1 + : Value::maxLargestUInt; + Value::LargestUInt threshold = maxIntegerValue / 10; + Value::LargestUInt value = 0; + while (current < token.end_) { + Char c = *current++; + if (c < '0' || c > '9') + return decodeDouble(token, decoded); + Value::UInt digit(c - '0'); + if (value >= threshold) { + // We've hit or exceeded the max value divided by 10 (rounded down). If + // a) we've only just touched the limit, b) this is the last digit, and + // c) it's small enough to fit in that rounding delta, we're okay. + // Otherwise treat this number as a double to avoid overflow. + if (value > threshold || current != token.end_ || + digit > maxIntegerValue % 10) { + return decodeDouble(token, decoded); + } + } + value = value * 10 + digit; + } + if (isNegative && value == maxIntegerValue) + decoded = Value::minLargestInt; + else if (isNegative) + decoded = -Value::LargestInt(value); + else if (value <= Value::LargestUInt(Value::maxInt)) + decoded = Value::LargestInt(value); + else + decoded = value; + return true; +} + +bool Reader::decodeDouble(Token& token) { + Value decoded; + if (!decodeDouble(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool Reader::decodeDouble(Token& token, Value& decoded) { + double value = 0; + std::string buffer(token.start_, token.end_); + std::istringstream is(buffer); + if (!(is >> value)) + return addError("'" + std::string(token.start_, token.end_) + + "' is not a number.", + token); + decoded = value; + return true; +} + +bool Reader::decodeString(Token& token) { + std::string decoded_string; + if (!decodeString(token, decoded_string)) + return false; + Value decoded(decoded_string); + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool Reader::decodeString(Token& token, std::string& decoded) { + decoded.reserve(token.end_ - token.start_ - 2); + Location current = token.start_ + 1; // skip '"' + Location end = token.end_ - 1; // do not include '"' + while (current != end) { + Char c = *current++; + if (c == '"') + break; + else if (c == '\\') { + if (current == end) + return addError("Empty escape sequence in string", token, current); + Char escape = *current++; + switch (escape) { + case '"': + decoded += '"'; + break; + case '/': + decoded += '/'; + break; + case '\\': + decoded += '\\'; + break; + case 'b': + decoded += '\b'; + break; + case 'f': + decoded += '\f'; + break; + case 'n': + decoded += '\n'; + break; + case 'r': + decoded += '\r'; + break; + case 't': + decoded += '\t'; + break; + case 'u': { + unsigned int unicode; + if (!decodeUnicodeCodePoint(token, current, end, unicode)) + return false; + decoded += codePointToUTF8(unicode); + } break; + default: + return addError("Bad escape sequence in string", token, current); + } + } else { + decoded += c; + } + } + return true; +} + +bool Reader::decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + + if (!decodeUnicodeEscapeSequence(token, current, end, unicode)) + return false; + if (unicode >= 0xD800 && unicode <= 0xDBFF) { + // surrogate pairs + if (end - current < 6) + return addError( + "additional six characters expected to parse unicode surrogate pair.", + token, + current); + unsigned int surrogatePair; + if (*(current++) == '\\' && *(current++) == 'u') { + if (decodeUnicodeEscapeSequence(token, current, end, surrogatePair)) { + unicode = 0x10000 + ((unicode & 0x3FF) << 10) + (surrogatePair & 0x3FF); + } else + return false; + } else + return addError("expecting another \\u token to begin the second half of " + "a unicode surrogate pair", + token, + current); + } + return true; +} + +bool Reader::decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + if (end - current < 4) + return addError( + "Bad unicode escape sequence in string: four digits expected.", + token, + current); + unicode = 0; + for (int index = 0; index < 4; ++index) { + Char c = *current++; + unicode *= 16; + if (c >= '0' && c <= '9') + unicode += c - '0'; + else if (c >= 'a' && c <= 'f') + unicode += c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + unicode += c - 'A' + 10; + else + return addError( + "Bad unicode escape sequence in string: hexadecimal digit expected.", + token, + current); + } + return true; +} + +bool +Reader::addError(const std::string& message, Token& token, Location extra) { + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = extra; + errors_.push_back(info); + return false; +} + +bool Reader::recoverFromError(TokenType skipUntilToken) { + int errorCount = int(errors_.size()); + Token skip; + for (;;) { + if (!readToken(skip)) + errors_.resize(errorCount); // discard errors caused by recovery + if (skip.type_ == skipUntilToken || skip.type_ == tokenEndOfStream) + break; + } + errors_.resize(errorCount); + return false; +} + +bool Reader::addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken) { + addError(message, token); + return recoverFromError(skipUntilToken); +} + +Value& Reader::currentValue() { return *(nodes_.top()); } + +Reader::Char Reader::getNextChar() { + if (current_ == end_) + return 0; + return *current_++; +} + +void Reader::getLocationLineAndColumn(Location location, + int& line, + int& column) const { + Location current = begin_; + Location lastLineStart = current; + line = 0; + while (current < location && current != end_) { + Char c = *current++; + if (c == '\r') { + if (*current == '\n') + ++current; + lastLineStart = current; + ++line; + } else if (c == '\n') { + lastLineStart = current; + ++line; + } + } + // column & line start at 1 + column = int(location - lastLineStart) + 1; + ++line; +} + +std::string Reader::getLocationLineAndColumn(Location location) const { + int line, column; + getLocationLineAndColumn(location, line, column); + char buffer[18 + 16 + 16 + 1]; + snprintf(buffer, sizeof(buffer), "Line %d, Column %d", line, column); + return buffer; +} + +// Deprecated. Preserved for backward compatibility +std::string Reader::getFormatedErrorMessages() const { + return getFormattedErrorMessages(); +} + +std::string Reader::getFormattedErrorMessages() const { + std::string formattedMessage; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + formattedMessage += + "* " + getLocationLineAndColumn(error.token_.start_) + "\n"; + formattedMessage += " " + error.message_ + "\n"; + if (error.extra_) + formattedMessage += + "See " + getLocationLineAndColumn(error.extra_) + " for detail.\n"; + } + return formattedMessage; +} + +std::vector Reader::getStructuredErrors() const { + std::vector allErrors; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + Reader::StructuredError structured; + structured.offset_start = error.token_.start_ - begin_; + structured.offset_limit = error.token_.end_ - begin_; + structured.message = error.message_; + allErrors.push_back(structured); + } + return allErrors; +} + +bool Reader::pushError(const Value& value, const std::string& message) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = end_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = 0; + errors_.push_back(info); + return true; +} + +bool Reader::pushError(const Value& value, const std::string& message, const Value& extra) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length + || extra.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = begin_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = begin_ + extra.getOffsetStart(); + errors_.push_back(info); + return true; +} + +bool Reader::good() const { + return !errors_.size(); +} + +// exact copy of Features +class OurFeatures { +public: + static OurFeatures all(); + bool allowComments_; + bool strictRoot_; + bool allowDroppedNullPlaceholders_; + bool allowNumericKeys_; + bool allowSingleQuotes_; + bool failIfExtra_; + bool rejectDupKeys_; + bool allowSpecialFloats_; + int stackLimit_; +}; // OurFeatures + +// exact copy of Implementation of class Features +// //////////////////////////////// + +OurFeatures OurFeatures::all() { return OurFeatures(); } + +// Implementation of class Reader +// //////////////////////////////// + +// exact copy of Reader, renamed to OurReader +class OurReader { +public: + typedef char Char; + typedef const Char* Location; + struct StructuredError { + size_t offset_start; + size_t offset_limit; + std::string message; + }; + + OurReader(OurFeatures const& features); + bool parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments = true); + std::string getFormattedErrorMessages() const; + std::vector getStructuredErrors() const; + bool pushError(const Value& value, const std::string& message); + bool pushError(const Value& value, const std::string& message, const Value& extra); + bool good() const; + +private: + OurReader(OurReader const&); // no impl + void operator=(OurReader const&); // no impl + + enum TokenType { + tokenEndOfStream = 0, + tokenObjectBegin, + tokenObjectEnd, + tokenArrayBegin, + tokenArrayEnd, + tokenString, + tokenNumber, + tokenTrue, + tokenFalse, + tokenNull, + tokenNaN, + tokenPosInf, + tokenNegInf, + tokenArraySeparator, + tokenMemberSeparator, + tokenComment, + tokenError + }; + + class Token { + public: + TokenType type_; + Location start_; + Location end_; + }; + + class ErrorInfo { + public: + Token token_; + std::string message_; + Location extra_; + }; + + typedef std::deque Errors; + + bool readToken(Token& token); + void skipSpaces(); + bool match(Location pattern, int patternLength); + bool readComment(); + bool readCStyleComment(); + bool readCppStyleComment(); + bool readString(); + bool readStringSingleQuote(); + bool readNumber(bool checkInf); + bool readValue(); + bool readObject(Token& token); + bool readArray(Token& token); + bool decodeNumber(Token& token); + bool decodeNumber(Token& token, Value& decoded); + bool decodeString(Token& token); + bool decodeString(Token& token, std::string& decoded); + bool decodeDouble(Token& token); + bool decodeDouble(Token& token, Value& decoded); + bool decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode); + bool addError(const std::string& message, Token& token, Location extra = 0); + bool recoverFromError(TokenType skipUntilToken); + bool addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken); + void skipUntilSpace(); + Value& currentValue(); + Char getNextChar(); + void + getLocationLineAndColumn(Location location, int& line, int& column) const; + std::string getLocationLineAndColumn(Location location) const; + void addComment(Location begin, Location end, CommentPlacement placement); + void skipCommentTokens(Token& token); + + typedef std::stack Nodes; + Nodes nodes_; + Errors errors_; + std::string document_; + Location begin_; + Location end_; + Location current_; + Location lastValueEnd_; + Value* lastValue_; + std::string commentsBefore_; + int stackDepth_; + + OurFeatures const features_; + bool collectComments_; +}; // OurReader + +// complete copy of Read impl, for OurReader + +OurReader::OurReader(OurFeatures const& features) + : errors_(), document_(), begin_(), end_(), current_(), lastValueEnd_(), + lastValue_(), commentsBefore_(), + stackDepth_(0), + features_(features), collectComments_() { +} + +bool OurReader::parse(const char* beginDoc, + const char* endDoc, + Value& root, + bool collectComments) { + if (!features_.allowComments_) { + collectComments = false; + } + + begin_ = beginDoc; + end_ = endDoc; + collectComments_ = collectComments; + current_ = begin_; + lastValueEnd_ = 0; + lastValue_ = 0; + commentsBefore_ = ""; + errors_.clear(); + while (!nodes_.empty()) + nodes_.pop(); + nodes_.push(&root); + + stackDepth_ = 0; + bool successful = readValue(); + Token token; + skipCommentTokens(token); + if (features_.failIfExtra_) { + if (token.type_ != tokenError && token.type_ != tokenEndOfStream) { + addError("Extra non-whitespace after JSON value.", token); + return false; + } + } + if (collectComments_ && !commentsBefore_.empty()) + root.setComment(commentsBefore_, commentAfter); + if (features_.strictRoot_) { + if (!root.isArray() && !root.isObject()) { + // Set error location to start of doc, ideally should be first token found + // in doc + token.type_ = tokenError; + token.start_ = beginDoc; + token.end_ = endDoc; + addError( + "A valid JSON document must be either an array or an object value.", + token); + return false; + } + } + return successful; +} + +bool OurReader::readValue() { + if (stackDepth_ >= features_.stackLimit_) throwRuntimeError("Exceeded stackLimit in readValue()."); + ++stackDepth_; + Token token; + skipCommentTokens(token); + bool successful = true; + + if (collectComments_ && !commentsBefore_.empty()) { + currentValue().setComment(commentsBefore_, commentBefore); + commentsBefore_ = ""; + } + + switch (token.type_) { + case tokenObjectBegin: + successful = readObject(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenArrayBegin: + successful = readArray(token); + currentValue().setOffsetLimit(current_ - begin_); + break; + case tokenNumber: + successful = decodeNumber(token); + break; + case tokenString: + successful = decodeString(token); + break; + case tokenTrue: + { + Value v(true); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenFalse: + { + Value v(false); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNull: + { + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNaN: + { + Value v(std::numeric_limits::quiet_NaN()); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenPosInf: + { + Value v(std::numeric_limits::infinity()); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenNegInf: + { + Value v(-std::numeric_limits::infinity()); + currentValue().swapPayload(v); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + } + break; + case tokenArraySeparator: + case tokenObjectEnd: + case tokenArrayEnd: + if (features_.allowDroppedNullPlaceholders_) { + // "Un-read" the current token and mark the current value as a null + // token. + current_--; + Value v; + currentValue().swapPayload(v); + currentValue().setOffsetStart(current_ - begin_ - 1); + currentValue().setOffsetLimit(current_ - begin_); + break; + } // else, fall through ... + default: + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return addError("Syntax error: value, object or array expected.", token); + } + + if (collectComments_) { + lastValueEnd_ = current_; + lastValue_ = ¤tValue(); + } + + --stackDepth_; + return successful; +} + +void OurReader::skipCommentTokens(Token& token) { + if (features_.allowComments_) { + do { + readToken(token); + } while (token.type_ == tokenComment); + } else { + readToken(token); + } +} + +bool OurReader::readToken(Token& token) { + skipSpaces(); + token.start_ = current_; + Char c = getNextChar(); + bool ok = true; + switch (c) { + case '{': + token.type_ = tokenObjectBegin; + break; + case '}': + token.type_ = tokenObjectEnd; + break; + case '[': + token.type_ = tokenArrayBegin; + break; + case ']': + token.type_ = tokenArrayEnd; + break; + case '"': + token.type_ = tokenString; + ok = readString(); + break; + case '\'': + if (features_.allowSingleQuotes_) { + token.type_ = tokenString; + ok = readStringSingleQuote(); + break; + } // else continue + case '/': + token.type_ = tokenComment; + ok = readComment(); + break; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + token.type_ = tokenNumber; + readNumber(false); + break; + case '-': + if (readNumber(true)) { + token.type_ = tokenNumber; + } else { + token.type_ = tokenNegInf; + ok = features_.allowSpecialFloats_ && match("nfinity", 7); + } + break; + case 't': + token.type_ = tokenTrue; + ok = match("rue", 3); + break; + case 'f': + token.type_ = tokenFalse; + ok = match("alse", 4); + break; + case 'n': + token.type_ = tokenNull; + ok = match("ull", 3); + break; + case 'N': + if (features_.allowSpecialFloats_) { + token.type_ = tokenNaN; + ok = match("aN", 2); + } else { + ok = false; + } + break; + case 'I': + if (features_.allowSpecialFloats_) { + token.type_ = tokenPosInf; + ok = match("nfinity", 7); + } else { + ok = false; + } + break; + case ',': + token.type_ = tokenArraySeparator; + break; + case ':': + token.type_ = tokenMemberSeparator; + break; + case 0: + token.type_ = tokenEndOfStream; + break; + default: + ok = false; + break; + } + if (!ok) + token.type_ = tokenError; + token.end_ = current_; + return true; +} + +void OurReader::skipSpaces() { + while (current_ != end_) { + Char c = *current_; + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') + ++current_; + else + break; + } +} + +bool OurReader::match(Location pattern, int patternLength) { + if (end_ - current_ < patternLength) + return false; + int index = patternLength; + while (index--) + if (current_[index] != pattern[index]) + return false; + current_ += patternLength; + return true; +} + +bool OurReader::readComment() { + Location commentBegin = current_ - 1; + Char c = getNextChar(); + bool successful = false; + if (c == '*') + successful = readCStyleComment(); + else if (c == '/') + successful = readCppStyleComment(); + if (!successful) + return false; + + if (collectComments_) { + CommentPlacement placement = commentBefore; + if (lastValueEnd_ && !containsNewLine(lastValueEnd_, commentBegin)) { + if (c != '*' || !containsNewLine(commentBegin, current_)) + placement = commentAfterOnSameLine; + } + + addComment(commentBegin, current_, placement); + } + return true; +} + +void +OurReader::addComment(Location begin, Location end, CommentPlacement placement) { + assert(collectComments_); + const std::string& normalized = normalizeEOL(begin, end); + if (placement == commentAfterOnSameLine) { + assert(lastValue_ != 0); + lastValue_->setComment(normalized, placement); + } else { + commentsBefore_ += normalized; + } +} + +bool OurReader::readCStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '*' && *current_ == '/') + break; + } + return getNextChar() == '/'; +} + +bool OurReader::readCppStyleComment() { + while (current_ != end_) { + Char c = getNextChar(); + if (c == '\n') + break; + if (c == '\r') { + // Consume DOS EOL. It will be normalized in addComment. + if (current_ != end_ && *current_ == '\n') + getNextChar(); + // Break on Moc OS 9 EOL. + break; + } + } + return true; +} + +bool OurReader::readNumber(bool checkInf) { + const char *p = current_; + if (checkInf && p != end_ && *p == 'I') { + current_ = ++p; + return false; + } + char c = '0'; // stopgap for already consumed character + // integral part + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + // fractional part + if (c == '.') { + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } + // exponential part + if (c == 'e' || c == 'E') { + c = (current_ = p) < end_ ? *p++ : 0; + if (c == '+' || c == '-') + c = (current_ = p) < end_ ? *p++ : 0; + while (c >= '0' && c <= '9') + c = (current_ = p) < end_ ? *p++ : 0; + } + return true; +} +bool OurReader::readString() { + Char c = 0; + while (current_ != end_) { + c = getNextChar(); + if (c == '\\') + getNextChar(); + else if (c == '"') + break; + } + return c == '"'; +} + + +bool OurReader::readStringSingleQuote() { + Char c = 0; + while (current_ != end_) { + c = getNextChar(); + if (c == '\\') + getNextChar(); + else if (c == '\'') + break; + } + return c == '\''; +} + +bool OurReader::readObject(Token& tokenStart) { + Token tokenName; + std::string name; + Value init(objectValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + while (readToken(tokenName)) { + bool initialTokenOk = true; + while (tokenName.type_ == tokenComment && initialTokenOk) + initialTokenOk = readToken(tokenName); + if (!initialTokenOk) + break; + if (tokenName.type_ == tokenObjectEnd && name.empty()) // empty object + return true; + name = ""; + if (tokenName.type_ == tokenString) { + if (!decodeString(tokenName, name)) + return recoverFromError(tokenObjectEnd); + } else if (tokenName.type_ == tokenNumber && features_.allowNumericKeys_) { + Value numberName; + if (!decodeNumber(tokenName, numberName)) + return recoverFromError(tokenObjectEnd); + name = numberName.asString(); + } else { + break; + } + + Token colon; + if (!readToken(colon) || colon.type_ != tokenMemberSeparator) { + return addErrorAndRecover( + "Missing ':' after object member name", colon, tokenObjectEnd); + } + if (name.length() >= (1U<<30)) throwRuntimeError("keylength >= 2^30"); + if (features_.rejectDupKeys_ && currentValue().isMember(name)) { + std::string msg = "Duplicate key: '" + name + "'"; + return addErrorAndRecover( + msg, tokenName, tokenObjectEnd); + } + Value& value = currentValue()[name]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenObjectEnd); + + Token comma; + if (!readToken(comma) || + (comma.type_ != tokenObjectEnd && comma.type_ != tokenArraySeparator && + comma.type_ != tokenComment)) { + return addErrorAndRecover( + "Missing ',' or '}' in object declaration", comma, tokenObjectEnd); + } + bool finalizeTokenOk = true; + while (comma.type_ == tokenComment && finalizeTokenOk) + finalizeTokenOk = readToken(comma); + if (comma.type_ == tokenObjectEnd) + return true; + } + return addErrorAndRecover( + "Missing '}' or object member name", tokenName, tokenObjectEnd); +} + +bool OurReader::readArray(Token& tokenStart) { + Value init(arrayValue); + currentValue().swapPayload(init); + currentValue().setOffsetStart(tokenStart.start_ - begin_); + skipSpaces(); + if (*current_ == ']') // empty array + { + Token endArray; + readToken(endArray); + return true; + } + int index = 0; + for (;;) { + Value& value = currentValue()[index++]; + nodes_.push(&value); + bool ok = readValue(); + nodes_.pop(); + if (!ok) // error already set + return recoverFromError(tokenArrayEnd); + + Token token; + // Accept Comment after last item in the array. + ok = readToken(token); + while (token.type_ == tokenComment && ok) { + ok = readToken(token); + } + bool badTokenType = + (token.type_ != tokenArraySeparator && token.type_ != tokenArrayEnd); + if (!ok || badTokenType) { + return addErrorAndRecover( + "Missing ',' or ']' in array declaration", token, tokenArrayEnd); + } + if (token.type_ == tokenArrayEnd) + break; + } + return true; +} + +bool OurReader::decodeNumber(Token& token) { + Value decoded; + if (!decodeNumber(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool OurReader::decodeNumber(Token& token, Value& decoded) { + // Attempts to parse the number as an integer. If the number is + // larger than the maximum supported value of an integer then + // we decode the number as a double. + Location current = token.start_; + bool isNegative = *current == '-'; + if (isNegative) + ++current; + // TODO: Help the compiler do the div and mod at compile time or get rid of them. + Value::LargestUInt maxIntegerValue = + isNegative ? Value::LargestUInt(-Value::minLargestInt) + : Value::maxLargestUInt; + Value::LargestUInt threshold = maxIntegerValue / 10; + Value::LargestUInt value = 0; + while (current < token.end_) { + Char c = *current++; + if (c < '0' || c > '9') + return decodeDouble(token, decoded); + Value::UInt digit(c - '0'); + if (value >= threshold) { + // We've hit or exceeded the max value divided by 10 (rounded down). If + // a) we've only just touched the limit, b) this is the last digit, and + // c) it's small enough to fit in that rounding delta, we're okay. + // Otherwise treat this number as a double to avoid overflow. + if (value > threshold || current != token.end_ || + digit > maxIntegerValue % 10) { + return decodeDouble(token, decoded); + } + } + value = value * 10 + digit; + } + if (isNegative) + decoded = -Value::LargestInt(value); + else if (value <= Value::LargestUInt(Value::maxInt)) + decoded = Value::LargestInt(value); + else + decoded = value; + return true; +} + +bool OurReader::decodeDouble(Token& token) { + Value decoded; + if (!decodeDouble(token, decoded)) + return false; + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool OurReader::decodeDouble(Token& token, Value& decoded) { + double value = 0; + const int bufferSize = 32; + int count; + int length = int(token.end_ - token.start_); + + // Sanity check to avoid buffer overflow exploits. + if (length < 0) { + return addError("Unable to parse token length", token); + } + + // Avoid using a string constant for the format control string given to + // sscanf, as this can cause hard to debug crashes on OS X. See here for more + // info: + // + // http://developer.apple.com/library/mac/#DOCUMENTATION/DeveloperTools/gcc-4.0.1/gcc/Incompatibilities.html + char format[] = "%lf"; + + if (length <= bufferSize) { + Char buffer[bufferSize + 1]; + memcpy(buffer, token.start_, length); + buffer[length] = 0; + count = sscanf(buffer, format, &value); + } else { + std::string buffer(token.start_, token.end_); + count = sscanf(buffer.c_str(), format, &value); + } + + if (count != 1) + return addError("'" + std::string(token.start_, token.end_) + + "' is not a number.", + token); + decoded = value; + return true; +} + +bool OurReader::decodeString(Token& token) { + std::string decoded_string; + if (!decodeString(token, decoded_string)) + return false; + Value decoded(decoded_string); + currentValue().swapPayload(decoded); + currentValue().setOffsetStart(token.start_ - begin_); + currentValue().setOffsetLimit(token.end_ - begin_); + return true; +} + +bool OurReader::decodeString(Token& token, std::string& decoded) { + decoded.reserve(token.end_ - token.start_ - 2); + Location current = token.start_ + 1; // skip '"' + Location end = token.end_ - 1; // do not include '"' + while (current != end) { + Char c = *current++; + if (c == '"') + break; + else if (c == '\\') { + if (current == end) + return addError("Empty escape sequence in string", token, current); + Char escape = *current++; + switch (escape) { + case '"': + decoded += '"'; + break; + case '/': + decoded += '/'; + break; + case '\\': + decoded += '\\'; + break; + case 'b': + decoded += '\b'; + break; + case 'f': + decoded += '\f'; + break; + case 'n': + decoded += '\n'; + break; + case 'r': + decoded += '\r'; + break; + case 't': + decoded += '\t'; + break; + case 'u': { + unsigned int unicode; + if (!decodeUnicodeCodePoint(token, current, end, unicode)) + return false; + decoded += codePointToUTF8(unicode); + } break; + default: + return addError("Bad escape sequence in string", token, current); + } + } else { + decoded += c; + } + } + return true; +} + +bool OurReader::decodeUnicodeCodePoint(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + + if (!decodeUnicodeEscapeSequence(token, current, end, unicode)) + return false; + if (unicode >= 0xD800 && unicode <= 0xDBFF) { + // surrogate pairs + if (end - current < 6) + return addError( + "additional six characters expected to parse unicode surrogate pair.", + token, + current); + unsigned int surrogatePair; + if (*(current++) == '\\' && *(current++) == 'u') { + if (decodeUnicodeEscapeSequence(token, current, end, surrogatePair)) { + unicode = 0x10000 + ((unicode & 0x3FF) << 10) + (surrogatePair & 0x3FF); + } else + return false; + } else + return addError("expecting another \\u token to begin the second half of " + "a unicode surrogate pair", + token, + current); + } + return true; +} + +bool OurReader::decodeUnicodeEscapeSequence(Token& token, + Location& current, + Location end, + unsigned int& unicode) { + if (end - current < 4) + return addError( + "Bad unicode escape sequence in string: four digits expected.", + token, + current); + unicode = 0; + for (int index = 0; index < 4; ++index) { + Char c = *current++; + unicode *= 16; + if (c >= '0' && c <= '9') + unicode += c - '0'; + else if (c >= 'a' && c <= 'f') + unicode += c - 'a' + 10; + else if (c >= 'A' && c <= 'F') + unicode += c - 'A' + 10; + else + return addError( + "Bad unicode escape sequence in string: hexadecimal digit expected.", + token, + current); + } + return true; +} + +bool +OurReader::addError(const std::string& message, Token& token, Location extra) { + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = extra; + errors_.push_back(info); + return false; +} + +bool OurReader::recoverFromError(TokenType skipUntilToken) { + int errorCount = int(errors_.size()); + Token skip; + for (;;) { + if (!readToken(skip)) + errors_.resize(errorCount); // discard errors caused by recovery + if (skip.type_ == skipUntilToken || skip.type_ == tokenEndOfStream) + break; + } + errors_.resize(errorCount); + return false; +} + +bool OurReader::addErrorAndRecover(const std::string& message, + Token& token, + TokenType skipUntilToken) { + addError(message, token); + return recoverFromError(skipUntilToken); +} + +Value& OurReader::currentValue() { return *(nodes_.top()); } + +OurReader::Char OurReader::getNextChar() { + if (current_ == end_) + return 0; + return *current_++; +} + +void OurReader::getLocationLineAndColumn(Location location, + int& line, + int& column) const { + Location current = begin_; + Location lastLineStart = current; + line = 0; + while (current < location && current != end_) { + Char c = *current++; + if (c == '\r') { + if (*current == '\n') + ++current; + lastLineStart = current; + ++line; + } else if (c == '\n') { + lastLineStart = current; + ++line; + } + } + // column & line start at 1 + column = int(location - lastLineStart) + 1; + ++line; +} + +std::string OurReader::getLocationLineAndColumn(Location location) const { + int line, column; + getLocationLineAndColumn(location, line, column); + char buffer[18 + 16 + 16 + 1]; + snprintf(buffer, sizeof(buffer), "Line %d, Column %d", line, column); + return buffer; +} + +std::string OurReader::getFormattedErrorMessages() const { + std::string formattedMessage; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + formattedMessage += + "* " + getLocationLineAndColumn(error.token_.start_) + "\n"; + formattedMessage += " " + error.message_ + "\n"; + if (error.extra_) + formattedMessage += + "See " + getLocationLineAndColumn(error.extra_) + " for detail.\n"; + } + return formattedMessage; +} + +std::vector OurReader::getStructuredErrors() const { + std::vector allErrors; + for (Errors::const_iterator itError = errors_.begin(); + itError != errors_.end(); + ++itError) { + const ErrorInfo& error = *itError; + OurReader::StructuredError structured; + structured.offset_start = error.token_.start_ - begin_; + structured.offset_limit = error.token_.end_ - begin_; + structured.message = error.message_; + allErrors.push_back(structured); + } + return allErrors; +} + +bool OurReader::pushError(const Value& value, const std::string& message) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = end_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = 0; + errors_.push_back(info); + return true; +} + +bool OurReader::pushError(const Value& value, const std::string& message, const Value& extra) { + size_t length = end_ - begin_; + if(value.getOffsetStart() > length + || value.getOffsetLimit() > length + || extra.getOffsetLimit() > length) + return false; + Token token; + token.type_ = tokenError; + token.start_ = begin_ + value.getOffsetStart(); + token.end_ = begin_ + value.getOffsetLimit(); + ErrorInfo info; + info.token_ = token; + info.message_ = message; + info.extra_ = begin_ + extra.getOffsetStart(); + errors_.push_back(info); + return true; +} + +bool OurReader::good() const { + return !errors_.size(); +} + + +class OurCharReader : public CharReader { + bool const collectComments_; + OurReader reader_; +public: + OurCharReader( + bool collectComments, + OurFeatures const& features) + : collectComments_(collectComments) + , reader_(features) + {} + bool parse( + char const* beginDoc, char const* endDoc, + Value* root, std::string* errs) override { + bool ok = reader_.parse(beginDoc, endDoc, *root, collectComments_); + if (errs) { + *errs = reader_.getFormattedErrorMessages(); + } + return ok; + } +}; + +CharReaderBuilder::CharReaderBuilder() +{ + setDefaults(&settings_); +} +CharReaderBuilder::~CharReaderBuilder() +{} +CharReader* CharReaderBuilder::newCharReader() const +{ + bool collectComments = settings_["collectComments"].asBool(); + OurFeatures features = OurFeatures::all(); + features.allowComments_ = settings_["allowComments"].asBool(); + features.strictRoot_ = settings_["strictRoot"].asBool(); + features.allowDroppedNullPlaceholders_ = settings_["allowDroppedNullPlaceholders"].asBool(); + features.allowNumericKeys_ = settings_["allowNumericKeys"].asBool(); + features.allowSingleQuotes_ = settings_["allowSingleQuotes"].asBool(); + features.stackLimit_ = settings_["stackLimit"].asInt(); + features.failIfExtra_ = settings_["failIfExtra"].asBool(); + features.rejectDupKeys_ = settings_["rejectDupKeys"].asBool(); + features.allowSpecialFloats_ = settings_["allowSpecialFloats"].asBool(); + return new OurCharReader(collectComments, features); +} +static void getValidReaderKeys(std::set* valid_keys) +{ + valid_keys->clear(); + valid_keys->insert("collectComments"); + valid_keys->insert("allowComments"); + valid_keys->insert("strictRoot"); + valid_keys->insert("allowDroppedNullPlaceholders"); + valid_keys->insert("allowNumericKeys"); + valid_keys->insert("allowSingleQuotes"); + valid_keys->insert("stackLimit"); + valid_keys->insert("failIfExtra"); + valid_keys->insert("rejectDupKeys"); + valid_keys->insert("allowSpecialFloats"); +} +bool CharReaderBuilder::validate(Json::Value* invalid) const +{ + Json::Value my_invalid; + if (!invalid) invalid = &my_invalid; // so we do not need to test for NULL + Json::Value& inv = *invalid; + std::set valid_keys; + getValidReaderKeys(&valid_keys); + Value::Members keys = settings_.getMemberNames(); + size_t n = keys.size(); + for (size_t i = 0; i < n; ++i) { + std::string const& key = keys[i]; + if (valid_keys.find(key) == valid_keys.end()) { + inv[key] = settings_[key]; + } + } + return 0u == inv.size(); +} +Value& CharReaderBuilder::operator[](std::string key) +{ + return settings_[key]; +} +// static +void CharReaderBuilder::strictMode(Json::Value* settings) +{ +//! [CharReaderBuilderStrictMode] + (*settings)["allowComments"] = false; + (*settings)["strictRoot"] = true; + (*settings)["allowDroppedNullPlaceholders"] = false; + (*settings)["allowNumericKeys"] = false; + (*settings)["allowSingleQuotes"] = false; + (*settings)["stackLimit"] = 1000; + (*settings)["failIfExtra"] = true; + (*settings)["rejectDupKeys"] = true; + (*settings)["allowSpecialFloats"] = false; +//! [CharReaderBuilderStrictMode] +} +// static +void CharReaderBuilder::setDefaults(Json::Value* settings) +{ +//! [CharReaderBuilderDefaults] + (*settings)["collectComments"] = true; + (*settings)["allowComments"] = true; + (*settings)["strictRoot"] = false; + (*settings)["allowDroppedNullPlaceholders"] = false; + (*settings)["allowNumericKeys"] = false; + (*settings)["allowSingleQuotes"] = false; + (*settings)["stackLimit"] = 1000; + (*settings)["failIfExtra"] = false; + (*settings)["rejectDupKeys"] = false; + (*settings)["allowSpecialFloats"] = false; +//! [CharReaderBuilderDefaults] +} + +////////////////////////////////// +// global functions + +bool parseFromStream( + CharReader::Factory const& fact, std::istream& sin, + Value* root, std::string* errs) +{ + std::ostringstream ssin; + ssin << sin.rdbuf(); + std::string doc = ssin.str(); + char const* begin = doc.data(); + char const* end = begin + doc.size(); + // Note that we do not actually need a null-terminator. + CharReaderPtr const reader(fact.newCharReader()); + return reader->parse(begin, end, root, errs); +} + +std::istream& operator>>(std::istream& sin, Value& root) { + CharReaderBuilder b; + std::string errs; + bool ok = parseFromStream(b, sin, &root, &errs); + if (!ok) { + fprintf(stderr, + "Error from reader: %s", + errs.c_str()); + + throwRuntimeError(errs); + } + return sin; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_reader.cpp +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_valueiterator.inl +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2007-2010 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +// included by json_value.cpp + +namespace Json { + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class ValueIteratorBase +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +ValueIteratorBase::ValueIteratorBase() + : current_(), isNull_(true) { +} + +ValueIteratorBase::ValueIteratorBase( + const Value::ObjectValues::iterator& current) + : current_(current), isNull_(false) {} + +Value& ValueIteratorBase::deref() const { + return current_->second; +} + +void ValueIteratorBase::increment() { + ++current_; +} + +void ValueIteratorBase::decrement() { + --current_; +} + +ValueIteratorBase::difference_type +ValueIteratorBase::computeDistance(const SelfType& other) const { +#ifdef JSON_USE_CPPTL_SMALLMAP + return other.current_ - current_; +#else + // Iterator for null value are initialized using the default + // constructor, which initialize current_ to the default + // std::map::iterator. As begin() and end() are two instance + // of the default std::map::iterator, they can not be compared. + // To allow this, we handle this comparison specifically. + if (isNull_ && other.isNull_) { + return 0; + } + + // Usage of std::distance is not portable (does not compile with Sun Studio 12 + // RogueWave STL, + // which is the one used by default). + // Using a portable hand-made version for non random iterator instead: + // return difference_type( std::distance( current_, other.current_ ) ); + difference_type myDistance = 0; + for (Value::ObjectValues::iterator it = current_; it != other.current_; + ++it) { + ++myDistance; + } + return myDistance; +#endif +} + +bool ValueIteratorBase::isEqual(const SelfType& other) const { + if (isNull_) { + return other.isNull_; + } + return current_ == other.current_; +} + +void ValueIteratorBase::copy(const SelfType& other) { + current_ = other.current_; + isNull_ = other.isNull_; +} + +Value ValueIteratorBase::key() const { + const Value::CZString czstring = (*current_).first; + if (czstring.data()) { + if (czstring.isStaticString()) + return Value(StaticString(czstring.data())); + return Value(czstring.data(), czstring.data() + czstring.length()); + } + return Value(czstring.index()); +} + +UInt ValueIteratorBase::index() const { + const Value::CZString czstring = (*current_).first; + if (!czstring.data()) + return czstring.index(); + return Value::UInt(-1); +} + +std::string ValueIteratorBase::name() const { + char const* keey; + char const* end; + keey = memberName(&end); + if (!keey) return std::string(); + return std::string(keey, end); +} + +char const* ValueIteratorBase::memberName() const { + const char* cname = (*current_).first.data(); + return cname ? cname : ""; +} + +char const* ValueIteratorBase::memberName(char const** end) const { + const char* cname = (*current_).first.data(); + if (!cname) { + *end = NULL; + return NULL; + } + *end = cname + (*current_).first.length(); + return cname; +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class ValueConstIterator +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +ValueConstIterator::ValueConstIterator() {} + +ValueConstIterator::ValueConstIterator( + const Value::ObjectValues::iterator& current) + : ValueIteratorBase(current) {} + +ValueConstIterator::ValueConstIterator(ValueIterator const& other) + : ValueIteratorBase(other) {} + +ValueConstIterator& ValueConstIterator:: +operator=(const ValueIteratorBase& other) { + copy(other); + return *this; +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class ValueIterator +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +ValueIterator::ValueIterator() {} + +ValueIterator::ValueIterator(const Value::ObjectValues::iterator& current) + : ValueIteratorBase(current) {} + +ValueIterator::ValueIterator(const ValueConstIterator& other) + : ValueIteratorBase(other) { + throwRuntimeError("ConstIterator to Iterator should never be allowed."); +} + +ValueIterator::ValueIterator(const ValueIterator& other) + : ValueIteratorBase(other) {} + +ValueIterator& ValueIterator::operator=(const SelfType& other) { + copy(other); + return *this; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_valueiterator.inl +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_value.cpp +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2011 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include +#ifdef JSON_USE_CPPTL +#include +#endif +#include // size_t +#include // min() + +#define JSON_ASSERT_UNREACHABLE assert(false) + +namespace Json { + +// This is a walkaround to avoid the static initialization of Value::null. +// kNull must be word-aligned to avoid crashing on ARM. We use an alignment of +// 8 (instead of 4) as a bit of future-proofing. +#if defined(__ARMEL__) +#define ALIGNAS(byte_alignment) __attribute__((aligned(byte_alignment))) +#else +#define ALIGNAS(byte_alignment) +#endif +static const unsigned char ALIGNAS(8) kNull[sizeof(Value)] = { 0 }; +const unsigned char& kNullRef = kNull[0]; +const Value& Value::null = reinterpret_cast(kNullRef); +const Value& Value::nullRef = null; + +const Int Value::minInt = Int(~(UInt(-1) / 2)); +const Int Value::maxInt = Int(UInt(-1) / 2); +const UInt Value::maxUInt = UInt(-1); +#if defined(JSON_HAS_INT64) +const Int64 Value::minInt64 = Int64(~(UInt64(-1) / 2)); +const Int64 Value::maxInt64 = Int64(UInt64(-1) / 2); +const UInt64 Value::maxUInt64 = UInt64(-1); +// The constant is hard-coded because some compiler have trouble +// converting Value::maxUInt64 to a double correctly (AIX/xlC). +// Assumes that UInt64 is a 64 bits integer. +static const double maxUInt64AsDouble = 18446744073709551615.0; +#endif // defined(JSON_HAS_INT64) +const LargestInt Value::minLargestInt = LargestInt(~(LargestUInt(-1) / 2)); +const LargestInt Value::maxLargestInt = LargestInt(LargestUInt(-1) / 2); +const LargestUInt Value::maxLargestUInt = LargestUInt(-1); + +#if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) +template +static inline bool InRange(double d, T min, U max) { + return d >= min && d <= max; +} +#else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) +static inline double integerToDouble(Json::UInt64 value) { + return static_cast(Int64(value / 2)) * 2.0 + Int64(value & 1); +} + +template static inline double integerToDouble(T value) { + return static_cast(value); +} + +template +static inline bool InRange(double d, T min, U max) { + return d >= integerToDouble(min) && d <= integerToDouble(max); +} +#endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + +/** Duplicates the specified string value. + * @param value Pointer to the string to duplicate. Must be zero-terminated if + * length is "unknown". + * @param length Length of the value. if equals to unknown, then it will be + * computed using strlen(value). + * @return Pointer on the duplicate instance of string. + */ +static inline char* duplicateStringValue(const char* value, + size_t length) { + // Avoid an integer overflow in the call to malloc below by limiting length + // to a sane value. + if (length >= (size_t)Value::maxInt) + length = Value::maxInt - 1; + + char* newString = static_cast(malloc(length + 1)); + if (newString == NULL) { + throwRuntimeError( + "in Json::Value::duplicateStringValue(): " + "Failed to allocate string value buffer"); + } + memcpy(newString, value, length); + newString[length] = 0; + return newString; +} + +/* Record the length as a prefix. + */ +static inline char* duplicateAndPrefixStringValue( + const char* value, + unsigned int length) +{ + // Avoid an integer overflow in the call to malloc below by limiting length + // to a sane value. + JSON_ASSERT_MESSAGE(length <= (unsigned)Value::maxInt - sizeof(unsigned) - 1U, + "in Json::Value::duplicateAndPrefixStringValue(): " + "length too big for prefixing"); + unsigned actualLength = length + static_cast(sizeof(unsigned)) + 1U; + char* newString = static_cast(malloc(actualLength)); + if (newString == 0) { + throwRuntimeError( + "in Json::Value::duplicateAndPrefixStringValue(): " + "Failed to allocate string value buffer"); + } + *reinterpret_cast(newString) = length; + memcpy(newString + sizeof(unsigned), value, length); + newString[actualLength - 1U] = 0; // to avoid buffer over-run accidents by users later + return newString; +} +inline static void decodePrefixedString( + bool isPrefixed, char const* prefixed, + unsigned* length, char const** value) +{ + if (!isPrefixed) { + *length = static_cast(strlen(prefixed)); + *value = prefixed; + } else { + *length = *reinterpret_cast(prefixed); + *value = prefixed + sizeof(unsigned); + } +} +/** Free the string duplicated by duplicateStringValue()/duplicateAndPrefixStringValue(). + */ +static inline void releaseStringValue(char* value) { free(value); } + +} // namespace Json + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ValueInternals... +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +#if !defined(JSON_IS_AMALGAMATION) + +#include "json_valueiterator.inl" +#endif // if !defined(JSON_IS_AMALGAMATION) + +namespace Json { + +Exception::Exception(std::string const& msg) + : msg_(msg) +{} +Exception::~Exception() throw() +{} +char const* Exception::what() const throw() +{ + return msg_.c_str(); +} +RuntimeError::RuntimeError(std::string const& msg) + : Exception(msg) +{} +LogicError::LogicError(std::string const& msg) + : Exception(msg) +{} +void throwRuntimeError(std::string const& msg) +{ + throw RuntimeError(msg); +} +void throwLogicError(std::string const& msg) +{ + throw LogicError(msg); +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class Value::CommentInfo +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +Value::CommentInfo::CommentInfo() : comment_(0) {} + +Value::CommentInfo::~CommentInfo() { + if (comment_) + releaseStringValue(comment_); +} + +void Value::CommentInfo::setComment(const char* text, size_t len) { + if (comment_) { + releaseStringValue(comment_); + comment_ = 0; + } + JSON_ASSERT(text != 0); + JSON_ASSERT_MESSAGE( + text[0] == '\0' || text[0] == '/', + "in Json::Value::setComment(): Comments must start with /"); + // It seems that /**/ style comments are acceptable as well. + comment_ = duplicateStringValue(text, len); +} + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class Value::CZString +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +// Notes: policy_ indicates if the string was allocated when +// a string is stored. + +Value::CZString::CZString(ArrayIndex aindex) : cstr_(0), index_(aindex) {} + +Value::CZString::CZString(char const* str, unsigned ulength, DuplicationPolicy allocate) + : cstr_(str) { + // allocate != duplicate + storage_.policy_ = allocate & 0x3; + storage_.length_ = ulength & 0x3FFFFFFF; +} + +Value::CZString::CZString(const CZString& other) + : cstr_(other.storage_.policy_ != noDuplication && other.cstr_ != 0 + ? duplicateStringValue(other.cstr_, other.storage_.length_) + : other.cstr_) { + storage_.policy_ = (other.cstr_ + ? (static_cast(other.storage_.policy_) == noDuplication + ? noDuplication : duplicate) + : static_cast(other.storage_.policy_)); + storage_.length_ = other.storage_.length_; +} + +#if JSON_HAS_RVALUE_REFERENCES +Value::CZString::CZString(CZString&& other) + : cstr_(other.cstr_), index_(other.index_) { + other.cstr_ = nullptr; +} +#endif + +Value::CZString::~CZString() { + if (cstr_ && storage_.policy_ == duplicate) + releaseStringValue(const_cast(cstr_)); +} + +void Value::CZString::swap(CZString& other) { + std::swap(cstr_, other.cstr_); + std::swap(index_, other.index_); +} + +Value::CZString& Value::CZString::operator=(CZString other) { + swap(other); + return *this; +} + +bool Value::CZString::operator<(const CZString& other) const { + if (!cstr_) return index_ < other.index_; + //return strcmp(cstr_, other.cstr_) < 0; + // Assume both are strings. + unsigned this_len = this->storage_.length_; + unsigned other_len = other.storage_.length_; + unsigned min_len = std::min(this_len, other_len); + int comp = memcmp(this->cstr_, other.cstr_, min_len); + if (comp < 0) return true; + if (comp > 0) return false; + return (this_len < other_len); +} + +bool Value::CZString::operator==(const CZString& other) const { + if (!cstr_) return index_ == other.index_; + //return strcmp(cstr_, other.cstr_) == 0; + // Assume both are strings. + unsigned this_len = this->storage_.length_; + unsigned other_len = other.storage_.length_; + if (this_len != other_len) return false; + int comp = memcmp(this->cstr_, other.cstr_, this_len); + return comp == 0; +} + +ArrayIndex Value::CZString::index() const { return index_; } + +//const char* Value::CZString::c_str() const { return cstr_; } +const char* Value::CZString::data() const { return cstr_; } +unsigned Value::CZString::length() const { return storage_.length_; } +bool Value::CZString::isStaticString() const { return storage_.policy_ == noDuplication; } + +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// class Value::Value +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// +// ////////////////////////////////////////////////////////////////// + +/*! \internal Default constructor initialization must be equivalent to: + * memset( this, 0, sizeof(Value) ) + * This optimization is used in ValueInternalMap fast allocator. + */ +Value::Value(ValueType vtype) { + initBasic(vtype); + switch (vtype) { + case nullValue: + break; + case intValue: + case uintValue: + value_.int_ = 0; + break; + case realValue: + value_.real_ = 0.0; + break; + case stringValue: + value_.string_ = 0; + break; + case arrayValue: + case objectValue: + value_.map_ = new ObjectValues(); + break; + case booleanValue: + value_.bool_ = false; + break; + default: + JSON_ASSERT_UNREACHABLE; + } +} + +Value::Value(Int value) { + initBasic(intValue); + value_.int_ = value; +} + +Value::Value(UInt value) { + initBasic(uintValue); + value_.uint_ = value; +} +#if defined(JSON_HAS_INT64) +Value::Value(Int64 value) { + initBasic(intValue); + value_.int_ = value; +} +Value::Value(UInt64 value) { + initBasic(uintValue); + value_.uint_ = value; +} +#endif // defined(JSON_HAS_INT64) + +Value::Value(double value) { + initBasic(realValue); + value_.real_ = value; +} + +Value::Value(const char* value) { + initBasic(stringValue, true); + value_.string_ = duplicateAndPrefixStringValue(value, static_cast(strlen(value))); +} + +Value::Value(const char* beginValue, const char* endValue) { + initBasic(stringValue, true); + value_.string_ = + duplicateAndPrefixStringValue(beginValue, static_cast(endValue - beginValue)); +} + +Value::Value(const std::string& value) { + initBasic(stringValue, true); + value_.string_ = + duplicateAndPrefixStringValue(value.data(), static_cast(value.length())); +} + +Value::Value(const StaticString& value) { + initBasic(stringValue); + value_.string_ = const_cast(value.c_str()); +} + +#ifdef JSON_USE_CPPTL +Value::Value(const CppTL::ConstString& value) { + initBasic(stringValue, true); + value_.string_ = duplicateAndPrefixStringValue(value, static_cast(value.length())); +} +#endif + +Value::Value(bool value) { + initBasic(booleanValue); + value_.bool_ = value; +} + +Value::Value(Value const& other) + : type_(other.type_), allocated_(false) + , + comments_(0), start_(other.start_), limit_(other.limit_) +{ + switch (type_) { + case nullValue: + case intValue: + case uintValue: + case realValue: + case booleanValue: + value_ = other.value_; + break; + case stringValue: + if (other.value_.string_ && other.allocated_) { + unsigned len; + char const* str; + decodePrefixedString(other.allocated_, other.value_.string_, + &len, &str); + value_.string_ = duplicateAndPrefixStringValue(str, len); + allocated_ = true; + } else { + value_.string_ = other.value_.string_; + allocated_ = false; + } + break; + case arrayValue: + case objectValue: + value_.map_ = new ObjectValues(*other.value_.map_); + break; + default: + JSON_ASSERT_UNREACHABLE; + } + if (other.comments_) { + comments_ = new CommentInfo[numberOfCommentPlacement]; + for (int comment = 0; comment < numberOfCommentPlacement; ++comment) { + const CommentInfo& otherComment = other.comments_[comment]; + if (otherComment.comment_) + comments_[comment].setComment( + otherComment.comment_, strlen(otherComment.comment_)); + } + } +} + +#if JSON_HAS_RVALUE_REFERENCES +// Move constructor +Value::Value(Value&& other) { + initBasic(nullValue); + swap(other); +} +#endif + +Value::~Value() { + switch (type_) { + case nullValue: + case intValue: + case uintValue: + case realValue: + case booleanValue: + break; + case stringValue: + if (allocated_) + releaseStringValue(value_.string_); + break; + case arrayValue: + case objectValue: + delete value_.map_; + break; + default: + JSON_ASSERT_UNREACHABLE; + } + + if (comments_) + delete[] comments_; +} + +Value& Value::operator=(Value other) { + swap(other); + return *this; +} + +void Value::swapPayload(Value& other) { + ValueType temp = type_; + type_ = other.type_; + other.type_ = temp; + std::swap(value_, other.value_); + int temp2 = allocated_; + allocated_ = other.allocated_; + other.allocated_ = temp2 & 0x1; +} + +void Value::swap(Value& other) { + swapPayload(other); + std::swap(comments_, other.comments_); + std::swap(start_, other.start_); + std::swap(limit_, other.limit_); +} + +ValueType Value::type() const { return type_; } + +int Value::compare(const Value& other) const { + if (*this < other) + return -1; + if (*this > other) + return 1; + return 0; +} + +bool Value::operator<(const Value& other) const { + int typeDelta = type_ - other.type_; + if (typeDelta) + return typeDelta < 0 ? true : false; + switch (type_) { + case nullValue: + return false; + case intValue: + return value_.int_ < other.value_.int_; + case uintValue: + return value_.uint_ < other.value_.uint_; + case realValue: + return value_.real_ < other.value_.real_; + case booleanValue: + return value_.bool_ < other.value_.bool_; + case stringValue: + { + if ((value_.string_ == 0) || (other.value_.string_ == 0)) { + if (other.value_.string_) return true; + else return false; + } + unsigned this_len; + unsigned other_len; + char const* this_str; + char const* other_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + decodePrefixedString(other.allocated_, other.value_.string_, &other_len, &other_str); + unsigned min_len = std::min(this_len, other_len); + int comp = memcmp(this_str, other_str, min_len); + if (comp < 0) return true; + if (comp > 0) return false; + return (this_len < other_len); + } + case arrayValue: + case objectValue: { + int delta = int(value_.map_->size() - other.value_.map_->size()); + if (delta) + return delta < 0; + return (*value_.map_) < (*other.value_.map_); + } + default: + JSON_ASSERT_UNREACHABLE; + } + return false; // unreachable +} + +bool Value::operator<=(const Value& other) const { return !(other < *this); } + +bool Value::operator>=(const Value& other) const { return !(*this < other); } + +bool Value::operator>(const Value& other) const { return other < *this; } + +bool Value::operator==(const Value& other) const { + // if ( type_ != other.type_ ) + // GCC 2.95.3 says: + // attempt to take address of bit-field structure member `Json::Value::type_' + // Beats me, but a temp solves the problem. + int temp = other.type_; + if (type_ != temp) + return false; + switch (type_) { + case nullValue: + return true; + case intValue: + return value_.int_ == other.value_.int_; + case uintValue: + return value_.uint_ == other.value_.uint_; + case realValue: + return value_.real_ == other.value_.real_; + case booleanValue: + return value_.bool_ == other.value_.bool_; + case stringValue: + { + if ((value_.string_ == 0) || (other.value_.string_ == 0)) { + return (value_.string_ == other.value_.string_); + } + unsigned this_len; + unsigned other_len; + char const* this_str; + char const* other_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + decodePrefixedString(other.allocated_, other.value_.string_, &other_len, &other_str); + if (this_len != other_len) return false; + int comp = memcmp(this_str, other_str, this_len); + return comp == 0; + } + case arrayValue: + case objectValue: + return value_.map_->size() == other.value_.map_->size() && + (*value_.map_) == (*other.value_.map_); + default: + JSON_ASSERT_UNREACHABLE; + } + return false; // unreachable +} + +bool Value::operator!=(const Value& other) const { return !(*this == other); } + +const char* Value::asCString() const { + JSON_ASSERT_MESSAGE(type_ == stringValue, + "in Json::Value::asCString(): requires stringValue"); + if (value_.string_ == 0) return 0; + unsigned this_len; + char const* this_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + return this_str; +} + +bool Value::getString(char const** str, char const** cend) const { + if (type_ != stringValue) return false; + if (value_.string_ == 0) return false; + unsigned length; + decodePrefixedString(this->allocated_, this->value_.string_, &length, str); + *cend = *str + length; + return true; +} + +std::string Value::asString() const { + switch (type_) { + case nullValue: + return ""; + case stringValue: + { + if (value_.string_ == 0) return ""; + unsigned this_len; + char const* this_str; + decodePrefixedString(this->allocated_, this->value_.string_, &this_len, &this_str); + return std::string(this_str, this_len); + } + case booleanValue: + return value_.bool_ ? "true" : "false"; + case intValue: + return valueToString(value_.int_); + case uintValue: + return valueToString(value_.uint_); + case realValue: + return valueToString(value_.real_); + default: + JSON_FAIL_MESSAGE("Type is not convertible to string"); + } +} + +#ifdef JSON_USE_CPPTL +CppTL::ConstString Value::asConstString() const { + unsigned len; + char const* str; + decodePrefixedString(allocated_, value_.string_, + &len, &str); + return CppTL::ConstString(str, len); +} +#endif + +Value::Int Value::asInt() const { + switch (type_) { + case intValue: + JSON_ASSERT_MESSAGE(isInt(), "LargestInt out of Int range"); + return Int(value_.int_); + case uintValue: + JSON_ASSERT_MESSAGE(isInt(), "LargestUInt out of Int range"); + return Int(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, minInt, maxInt), + "double out of Int range"); + return Int(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to Int."); +} + +Value::UInt Value::asUInt() const { + switch (type_) { + case intValue: + JSON_ASSERT_MESSAGE(isUInt(), "LargestInt out of UInt range"); + return UInt(value_.int_); + case uintValue: + JSON_ASSERT_MESSAGE(isUInt(), "LargestUInt out of UInt range"); + return UInt(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, 0, maxUInt), + "double out of UInt range"); + return UInt(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to UInt."); +} + +#if defined(JSON_HAS_INT64) + +Value::Int64 Value::asInt64() const { + switch (type_) { + case intValue: + return Int64(value_.int_); + case uintValue: + JSON_ASSERT_MESSAGE(isInt64(), "LargestUInt out of Int64 range"); + return Int64(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, minInt64, maxInt64), + "double out of Int64 range"); + return Int64(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to Int64."); +} + +Value::UInt64 Value::asUInt64() const { + switch (type_) { + case intValue: + JSON_ASSERT_MESSAGE(isUInt64(), "LargestInt out of UInt64 range"); + return UInt64(value_.int_); + case uintValue: + return UInt64(value_.uint_); + case realValue: + JSON_ASSERT_MESSAGE(InRange(value_.real_, 0, maxUInt64), + "double out of UInt64 range"); + return UInt64(value_.real_); + case nullValue: + return 0; + case booleanValue: + return value_.bool_ ? 1 : 0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to UInt64."); +} +#endif // if defined(JSON_HAS_INT64) + +LargestInt Value::asLargestInt() const { +#if defined(JSON_NO_INT64) + return asInt(); +#else + return asInt64(); +#endif +} + +LargestUInt Value::asLargestUInt() const { +#if defined(JSON_NO_INT64) + return asUInt(); +#else + return asUInt64(); +#endif +} + +double Value::asDouble() const { + switch (type_) { + case intValue: + return static_cast(value_.int_); + case uintValue: +#if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return static_cast(value_.uint_); +#else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return integerToDouble(value_.uint_); +#endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + case realValue: + return value_.real_; + case nullValue: + return 0.0; + case booleanValue: + return value_.bool_ ? 1.0 : 0.0; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to double."); +} + +float Value::asFloat() const { + switch (type_) { + case intValue: + return static_cast(value_.int_); + case uintValue: +#if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return static_cast(value_.uint_); +#else // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + return integerToDouble(value_.uint_); +#endif // if !defined(JSON_USE_INT64_DOUBLE_CONVERSION) + case realValue: + return static_cast(value_.real_); + case nullValue: + return 0.0; + case booleanValue: + return value_.bool_ ? 1.0f : 0.0f; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to float."); +} + +bool Value::asBool() const { + switch (type_) { + case booleanValue: + return value_.bool_; + case nullValue: + return false; + case intValue: + return value_.int_ ? true : false; + case uintValue: + return value_.uint_ ? true : false; + case realValue: + // This is kind of strange. Not recommended. + return (value_.real_ != 0.0) ? true : false; + default: + break; + } + JSON_FAIL_MESSAGE("Value is not convertible to bool."); +} + +bool Value::isConvertibleTo(ValueType other) const { + switch (other) { + case nullValue: + return (isNumeric() && asDouble() == 0.0) || + (type_ == booleanValue && value_.bool_ == false) || + (type_ == stringValue && asString() == "") || + (type_ == arrayValue && value_.map_->size() == 0) || + (type_ == objectValue && value_.map_->size() == 0) || + type_ == nullValue; + case intValue: + return isInt() || + (type_ == realValue && InRange(value_.real_, minInt, maxInt)) || + type_ == booleanValue || type_ == nullValue; + case uintValue: + return isUInt() || + (type_ == realValue && InRange(value_.real_, 0, maxUInt)) || + type_ == booleanValue || type_ == nullValue; + case realValue: + return isNumeric() || type_ == booleanValue || type_ == nullValue; + case booleanValue: + return isNumeric() || type_ == booleanValue || type_ == nullValue; + case stringValue: + return isNumeric() || type_ == booleanValue || type_ == stringValue || + type_ == nullValue; + case arrayValue: + return type_ == arrayValue || type_ == nullValue; + case objectValue: + return type_ == objectValue || type_ == nullValue; + } + JSON_ASSERT_UNREACHABLE; + return false; +} + +/// Number of values in array or object +ArrayIndex Value::size() const { + switch (type_) { + case nullValue: + case intValue: + case uintValue: + case realValue: + case booleanValue: + case stringValue: + return 0; + case arrayValue: // size of the array is highest index + 1 + if (!value_.map_->empty()) { + ObjectValues::const_iterator itLast = value_.map_->end(); + --itLast; + return (*itLast).first.index() + 1; + } + return 0; + case objectValue: + return ArrayIndex(value_.map_->size()); + } + JSON_ASSERT_UNREACHABLE; + return 0; // unreachable; +} + +bool Value::empty() const { + if (isNull() || isArray() || isObject()) + return size() == 0u; + else + return false; +} + +bool Value::operator!() const { return isNull(); } + +void Value::clear() { + JSON_ASSERT_MESSAGE(type_ == nullValue || type_ == arrayValue || + type_ == objectValue, + "in Json::Value::clear(): requires complex value"); + start_ = 0; + limit_ = 0; + switch (type_) { + case arrayValue: + case objectValue: + value_.map_->clear(); + break; + default: + break; + } +} + +void Value::resize(ArrayIndex newSize) { + JSON_ASSERT_MESSAGE(type_ == nullValue || type_ == arrayValue, + "in Json::Value::resize(): requires arrayValue"); + if (type_ == nullValue) + *this = Value(arrayValue); + ArrayIndex oldSize = size(); + if (newSize == 0) + clear(); + else if (newSize > oldSize) + (*this)[newSize - 1]; + else { + for (ArrayIndex index = newSize; index < oldSize; ++index) { + value_.map_->erase(index); + } + assert(size() == newSize); + } +} + +Value& Value::operator[](ArrayIndex index) { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == arrayValue, + "in Json::Value::operator[](ArrayIndex): requires arrayValue"); + if (type_ == nullValue) + *this = Value(arrayValue); + CZString key(index); + ObjectValues::iterator it = value_.map_->lower_bound(key); + if (it != value_.map_->end() && (*it).first == key) + return (*it).second; + + ObjectValues::value_type defaultValue(key, nullRef); + it = value_.map_->insert(it, defaultValue); + return (*it).second; +} + +Value& Value::operator[](int index) { + JSON_ASSERT_MESSAGE( + index >= 0, + "in Json::Value::operator[](int index): index cannot be negative"); + return (*this)[ArrayIndex(index)]; +} + +const Value& Value::operator[](ArrayIndex index) const { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == arrayValue, + "in Json::Value::operator[](ArrayIndex)const: requires arrayValue"); + if (type_ == nullValue) + return nullRef; + CZString key(index); + ObjectValues::const_iterator it = value_.map_->find(key); + if (it == value_.map_->end()) + return nullRef; + return (*it).second; +} + +const Value& Value::operator[](int index) const { + JSON_ASSERT_MESSAGE( + index >= 0, + "in Json::Value::operator[](int index) const: index cannot be negative"); + return (*this)[ArrayIndex(index)]; +} + +void Value::initBasic(ValueType vtype, bool allocated) { + type_ = vtype; + allocated_ = allocated; + comments_ = 0; + start_ = 0; + limit_ = 0; +} + +// Access an object value by name, create a null member if it does not exist. +// @pre Type of '*this' is object or null. +// @param key is null-terminated. +Value& Value::resolveReference(const char* key) { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::resolveReference(): requires objectValue"); + if (type_ == nullValue) + *this = Value(objectValue); + CZString actualKey( + key, static_cast(strlen(key)), CZString::noDuplication); // NOTE! + ObjectValues::iterator it = value_.map_->lower_bound(actualKey); + if (it != value_.map_->end() && (*it).first == actualKey) + return (*it).second; + + ObjectValues::value_type defaultValue(actualKey, nullRef); + it = value_.map_->insert(it, defaultValue); + Value& value = (*it).second; + return value; +} + +// @param key is not null-terminated. +Value& Value::resolveReference(char const* key, char const* cend) +{ + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::resolveReference(key, end): requires objectValue"); + if (type_ == nullValue) + *this = Value(objectValue); + CZString actualKey( + key, static_cast(cend-key), CZString::duplicateOnCopy); + ObjectValues::iterator it = value_.map_->lower_bound(actualKey); + if (it != value_.map_->end() && (*it).first == actualKey) + return (*it).second; + + ObjectValues::value_type defaultValue(actualKey, nullRef); + it = value_.map_->insert(it, defaultValue); + Value& value = (*it).second; + return value; +} + +Value Value::get(ArrayIndex index, const Value& defaultValue) const { + const Value* value = &((*this)[index]); + return value == &nullRef ? defaultValue : *value; +} + +bool Value::isValidIndex(ArrayIndex index) const { return index < size(); } + +Value const* Value::find(char const* key, char const* cend) const +{ + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::find(key, end, found): requires objectValue or nullValue"); + if (type_ == nullValue) return NULL; + CZString actualKey(key, static_cast(cend-key), CZString::noDuplication); + ObjectValues::const_iterator it = value_.map_->find(actualKey); + if (it == value_.map_->end()) return NULL; + return &(*it).second; +} +const Value& Value::operator[](const char* key) const +{ + Value const* found = find(key, key + strlen(key)); + if (!found) return nullRef; + return *found; +} +Value const& Value::operator[](std::string const& key) const +{ + Value const* found = find(key.data(), key.data() + key.length()); + if (!found) return nullRef; + return *found; +} + +Value& Value::operator[](const char* key) { + return resolveReference(key, key + strlen(key)); +} + +Value& Value::operator[](const std::string& key) { + return resolveReference(key.data(), key.data() + key.length()); +} + +Value& Value::operator[](const StaticString& key) { + return resolveReference(key.c_str()); +} + +#ifdef JSON_USE_CPPTL +Value& Value::operator[](const CppTL::ConstString& key) { + return resolveReference(key.c_str(), key.end_c_str()); +} +Value const& Value::operator[](CppTL::ConstString const& key) const +{ + Value const* found = find(key.c_str(), key.end_c_str()); + if (!found) return nullRef; + return *found; +} +#endif + +Value& Value::append(const Value& value) { return (*this)[size()] = value; } + +Value Value::get(char const* key, char const* cend, Value const& defaultValue) const +{ + Value const* found = find(key, cend); + return !found ? defaultValue : *found; +} +Value Value::get(char const* key, Value const& defaultValue) const +{ + return get(key, key + strlen(key), defaultValue); +} +Value Value::get(std::string const& key, Value const& defaultValue) const +{ + return get(key.data(), key.data() + key.length(), defaultValue); +} + + +bool Value::removeMember(const char* key, const char* cend, Value* removed) +{ + if (type_ != objectValue) { + return false; + } + CZString actualKey(key, static_cast(cend-key), CZString::noDuplication); + ObjectValues::iterator it = value_.map_->find(actualKey); + if (it == value_.map_->end()) + return false; + *removed = it->second; + value_.map_->erase(it); + return true; +} +bool Value::removeMember(const char* key, Value* removed) +{ + return removeMember(key, key + strlen(key), removed); +} +bool Value::removeMember(std::string const& key, Value* removed) +{ + return removeMember(key.data(), key.data() + key.length(), removed); +} +Value Value::removeMember(const char* key) +{ + JSON_ASSERT_MESSAGE(type_ == nullValue || type_ == objectValue, + "in Json::Value::removeMember(): requires objectValue"); + if (type_ == nullValue) + return nullRef; + + Value removed; // null + removeMember(key, key + strlen(key), &removed); + return removed; // still null if removeMember() did nothing +} +Value Value::removeMember(const std::string& key) +{ + return removeMember(key.c_str()); +} + +bool Value::removeIndex(ArrayIndex index, Value* removed) { + if (type_ != arrayValue) { + return false; + } + CZString key(index); + ObjectValues::iterator it = value_.map_->find(key); + if (it == value_.map_->end()) { + return false; + } + *removed = it->second; + ArrayIndex oldSize = size(); + // shift left all items left, into the place of the "removed" + for (ArrayIndex i = index; i < (oldSize - 1); ++i){ + CZString keey(i); + (*value_.map_)[keey] = (*this)[i + 1]; + } + // erase the last one ("leftover") + CZString keyLast(oldSize - 1); + ObjectValues::iterator itLast = value_.map_->find(keyLast); + value_.map_->erase(itLast); + return true; +} + +#ifdef JSON_USE_CPPTL +Value Value::get(const CppTL::ConstString& key, + const Value& defaultValue) const { + return get(key.c_str(), key.end_c_str(), defaultValue); +} +#endif + +bool Value::isMember(char const* key, char const* cend) const +{ + Value const* value = find(key, cend); + return NULL != value; +} +bool Value::isMember(char const* key) const +{ + return isMember(key, key + strlen(key)); +} +bool Value::isMember(std::string const& key) const +{ + return isMember(key.data(), key.data() + key.length()); +} + +#ifdef JSON_USE_CPPTL +bool Value::isMember(const CppTL::ConstString& key) const { + return isMember(key.c_str(), key.end_c_str()); +} +#endif + +Value::Members Value::getMemberNames() const { + JSON_ASSERT_MESSAGE( + type_ == nullValue || type_ == objectValue, + "in Json::Value::getMemberNames(), value must be objectValue"); + if (type_ == nullValue) + return Value::Members(); + Members members; + members.reserve(value_.map_->size()); + ObjectValues::const_iterator it = value_.map_->begin(); + ObjectValues::const_iterator itEnd = value_.map_->end(); + for (; it != itEnd; ++it) { + members.push_back(std::string((*it).first.data(), + (*it).first.length())); + } + return members; +} +// +//# ifdef JSON_USE_CPPTL +// EnumMemberNames +// Value::enumMemberNames() const +//{ +// if ( type_ == objectValue ) +// { +// return CppTL::Enum::any( CppTL::Enum::transform( +// CppTL::Enum::keys( *(value_.map_), CppTL::Type() ), +// MemberNamesTransform() ) ); +// } +// return EnumMemberNames(); +//} +// +// +// EnumValues +// Value::enumValues() const +//{ +// if ( type_ == objectValue || type_ == arrayValue ) +// return CppTL::Enum::anyValues( *(value_.map_), +// CppTL::Type() ); +// return EnumValues(); +//} +// +//# endif + +static bool IsIntegral(double d) { + double integral_part; + return modf(d, &integral_part) == 0.0; +} + +bool Value::isNull() const { return type_ == nullValue; } + +bool Value::isBool() const { return type_ == booleanValue; } + +bool Value::isInt() const { + switch (type_) { + case intValue: + return value_.int_ >= minInt && value_.int_ <= maxInt; + case uintValue: + return value_.uint_ <= UInt(maxInt); + case realValue: + return value_.real_ >= minInt && value_.real_ <= maxInt && + IsIntegral(value_.real_); + default: + break; + } + return false; +} + +bool Value::isUInt() const { + switch (type_) { + case intValue: + return value_.int_ >= 0 && LargestUInt(value_.int_) <= LargestUInt(maxUInt); + case uintValue: + return value_.uint_ <= maxUInt; + case realValue: + return value_.real_ >= 0 && value_.real_ <= maxUInt && + IsIntegral(value_.real_); + default: + break; + } + return false; +} + +bool Value::isInt64() const { +#if defined(JSON_HAS_INT64) + switch (type_) { + case intValue: + return true; + case uintValue: + return value_.uint_ <= UInt64(maxInt64); + case realValue: + // Note that maxInt64 (= 2^63 - 1) is not exactly representable as a + // double, so double(maxInt64) will be rounded up to 2^63. Therefore we + // require the value to be strictly less than the limit. + return value_.real_ >= double(minInt64) && + value_.real_ < double(maxInt64) && IsIntegral(value_.real_); + default: + break; + } +#endif // JSON_HAS_INT64 + return false; +} + +bool Value::isUInt64() const { +#if defined(JSON_HAS_INT64) + switch (type_) { + case intValue: + return value_.int_ >= 0; + case uintValue: + return true; + case realValue: + // Note that maxUInt64 (= 2^64 - 1) is not exactly representable as a + // double, so double(maxUInt64) will be rounded up to 2^64. Therefore we + // require the value to be strictly less than the limit. + return value_.real_ >= 0 && value_.real_ < maxUInt64AsDouble && + IsIntegral(value_.real_); + default: + break; + } +#endif // JSON_HAS_INT64 + return false; +} + +bool Value::isIntegral() const { +#if defined(JSON_HAS_INT64) + return isInt64() || isUInt64(); +#else + return isInt() || isUInt(); +#endif +} + +bool Value::isDouble() const { return type_ == realValue || isIntegral(); } + +bool Value::isNumeric() const { return isIntegral() || isDouble(); } + +bool Value::isString() const { return type_ == stringValue; } + +bool Value::isArray() const { return type_ == arrayValue; } + +bool Value::isObject() const { return type_ == objectValue; } + +void Value::setComment(const char* comment, size_t len, CommentPlacement placement) { + if (!comments_) + comments_ = new CommentInfo[numberOfCommentPlacement]; + if ((len > 0) && (comment[len-1] == '\n')) { + // Always discard trailing newline, to aid indentation. + len -= 1; + } + comments_[placement].setComment(comment, len); +} + +void Value::setComment(const char* comment, CommentPlacement placement) { + setComment(comment, strlen(comment), placement); +} + +void Value::setComment(const std::string& comment, CommentPlacement placement) { + setComment(comment.c_str(), comment.length(), placement); +} + +bool Value::hasComment(CommentPlacement placement) const { + return comments_ != 0 && comments_[placement].comment_ != 0; +} + +std::string Value::getComment(CommentPlacement placement) const { + if (hasComment(placement)) + return comments_[placement].comment_; + return ""; +} + +void Value::setOffsetStart(size_t start) { start_ = start; } + +void Value::setOffsetLimit(size_t limit) { limit_ = limit; } + +size_t Value::getOffsetStart() const { return start_; } + +size_t Value::getOffsetLimit() const { return limit_; } + +std::string Value::toStyledString() const { + StyledWriter writer; + return writer.write(*this); +} + +Value::const_iterator Value::begin() const { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return const_iterator(value_.map_->begin()); + break; + default: + break; + } + return const_iterator(); +} + +Value::const_iterator Value::end() const { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return const_iterator(value_.map_->end()); + break; + default: + break; + } + return const_iterator(); +} + +Value::iterator Value::begin() { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return iterator(value_.map_->begin()); + break; + default: + break; + } + return iterator(); +} + +Value::iterator Value::end() { + switch (type_) { + case arrayValue: + case objectValue: + if (value_.map_) + return iterator(value_.map_->end()); + break; + default: + break; + } + return iterator(); +} + +// class PathArgument +// ////////////////////////////////////////////////////////////////// + +PathArgument::PathArgument() : key_(), index_(), kind_(kindNone) {} + +PathArgument::PathArgument(ArrayIndex index) + : key_(), index_(index), kind_(kindIndex) {} + +PathArgument::PathArgument(const char* key) + : key_(key), index_(), kind_(kindKey) {} + +PathArgument::PathArgument(const std::string& key) + : key_(key.c_str()), index_(), kind_(kindKey) {} + +// class Path +// ////////////////////////////////////////////////////////////////// + +Path::Path(const std::string& path, + const PathArgument& a1, + const PathArgument& a2, + const PathArgument& a3, + const PathArgument& a4, + const PathArgument& a5) { + InArgs in; + in.push_back(&a1); + in.push_back(&a2); + in.push_back(&a3); + in.push_back(&a4); + in.push_back(&a5); + makePath(path, in); +} + +void Path::makePath(const std::string& path, const InArgs& in) { + const char* current = path.c_str(); + const char* end = current + path.length(); + InArgs::const_iterator itInArg = in.begin(); + while (current != end) { + if (*current == '[') { + ++current; + if (*current == '%') + addPathInArg(path, in, itInArg, PathArgument::kindIndex); + else { + ArrayIndex index = 0; + for (; current != end && *current >= '0' && *current <= '9'; ++current) + index = index * 10 + ArrayIndex(*current - '0'); + args_.push_back(index); + } + if (current == end || *current++ != ']') + invalidPath(path, int(current - path.c_str())); + } else if (*current == '%') { + addPathInArg(path, in, itInArg, PathArgument::kindKey); + ++current; + } else if (*current == '.') { + ++current; + } else { + const char* beginName = current; + while (current != end && !strchr("[.", *current)) + ++current; + args_.push_back(std::string(beginName, current)); + } + } +} + +void Path::addPathInArg(const std::string& /*path*/, + const InArgs& in, + InArgs::const_iterator& itInArg, + PathArgument::Kind kind) { + if (itInArg == in.end()) { + // Error: missing argument %d + } else if ((*itInArg)->kind_ != kind) { + // Error: bad argument type + } else { + args_.push_back(**itInArg); + } +} + +void Path::invalidPath(const std::string& /*path*/, int /*location*/) { + // Error: invalid path. +} + +const Value& Path::resolve(const Value& root) const { + const Value* node = &root; + for (Args::const_iterator it = args_.begin(); it != args_.end(); ++it) { + const PathArgument& arg = *it; + if (arg.kind_ == PathArgument::kindIndex) { + if (!node->isArray() || !node->isValidIndex(arg.index_)) { + // Error: unable to resolve path (array value expected at position... + } + node = &((*node)[arg.index_]); + } else if (arg.kind_ == PathArgument::kindKey) { + if (!node->isObject()) { + // Error: unable to resolve path (object value expected at position...) + } + node = &((*node)[arg.key_]); + if (node == &Value::nullRef) { + // Error: unable to resolve path (object has no member named '' at + // position...) + } + } + } + return *node; +} + +Value Path::resolve(const Value& root, const Value& defaultValue) const { + const Value* node = &root; + for (Args::const_iterator it = args_.begin(); it != args_.end(); ++it) { + const PathArgument& arg = *it; + if (arg.kind_ == PathArgument::kindIndex) { + if (!node->isArray() || !node->isValidIndex(arg.index_)) + return defaultValue; + node = &((*node)[arg.index_]); + } else if (arg.kind_ == PathArgument::kindKey) { + if (!node->isObject()) + return defaultValue; + node = &((*node)[arg.key_]); + if (node == &Value::nullRef) + return defaultValue; + } + } + return *node; +} + +Value& Path::make(Value& root) const { + Value* node = &root; + for (Args::const_iterator it = args_.begin(); it != args_.end(); ++it) { + const PathArgument& arg = *it; + if (arg.kind_ == PathArgument::kindIndex) { + if (!node->isArray()) { + // Error: node is not an array at position ... + } + node = &((*node)[arg.index_]); + } else if (arg.kind_ == PathArgument::kindKey) { + if (!node->isObject()) { + // Error: node is not an object at position... + } + node = &((*node)[arg.key_]); + } + } + return *node; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_value.cpp +// ////////////////////////////////////////////////////////////////////// + + + + + + +// ////////////////////////////////////////////////////////////////////// +// Beginning of content of file: src/lib_json/json_writer.cpp +// ////////////////////////////////////////////////////////////////////// + +// Copyright 2011 Baptiste Lepilleur +// Distributed under MIT license, or public domain if desired and +// recognized in your jurisdiction. +// See file LICENSE for detail or copy at http://jsoncpp.sourceforge.net/LICENSE + +#if !defined(JSON_IS_AMALGAMATION) +#include +#include "json_tool.h" +#endif // if !defined(JSON_IS_AMALGAMATION) +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_MSC_VER) && _MSC_VER >= 1200 && _MSC_VER < 1800 // Between VC++ 6.0 and VC++ 11.0 +#include +#define isfinite _finite +#elif defined(__sun) && defined(__SVR4) //Solaris +#if !defined(isfinite) +#include +#define isfinite finite +#endif +#elif defined(_AIX) +#if !defined(isfinite) +#include +#define isfinite finite +#endif +#elif defined(__hpux) +#if !defined(isfinite) +#if defined(__ia64) && !defined(finite) +#define isfinite(x) ((sizeof(x) == sizeof(float) ? \ + _Isfinitef(x) : _IsFinite(x))) +#else +#include +#define isfinite finite +#endif +#endif +#else +#include +#if !(defined(__QNXNTO__)) // QNX already defines isfinite +#define isfinite std::isfinite +#endif +#endif + +#if defined(_MSC_VER) +#if !defined(WINCE) && defined(__STDC_SECURE_LIB__) && _MSC_VER >= 1500 // VC++ 9.0 and above +#define snprintf sprintf_s +#elif _MSC_VER >= 1900 // VC++ 14.0 and above +#define snprintf std::snprintf +#else +#define snprintf _snprintf +#endif +#elif defined(__ANDROID__) || defined(__QNXNTO__) +#define snprintf snprintf +#elif __cplusplus >= 201103L +#define snprintf std::snprintf +#endif + +#if defined(__BORLANDC__) +#include +#define isfinite _finite +#define snprintf _snprintf +#endif + +#if defined(_MSC_VER) && _MSC_VER >= 1400 // VC++ 8.0 +// Disable warning about strdup being deprecated. +#pragma warning(disable : 4996) +#endif + +namespace Json { + +#if __cplusplus >= 201103L || (defined(_CPPLIB_VER) && _CPPLIB_VER >= 520) +typedef std::unique_ptr StreamWriterPtr; +#else +typedef std::auto_ptr StreamWriterPtr; +#endif + +static bool containsControlCharacter(const char* str) { + while (*str) { + if (isControlCharacter(*(str++))) + return true; + } + return false; +} + +static bool containsControlCharacter0(const char* str, unsigned len) { + char const* end = str + len; + while (end != str) { + if (isControlCharacter(*str) || 0==*str) + return true; + ++str; + } + return false; +} + +std::string valueToString(LargestInt value) { + UIntToStringBuffer buffer; + char* current = buffer + sizeof(buffer); + if (value == Value::minLargestInt) { + uintToString(LargestUInt(Value::maxLargestInt) + 1, current); + *--current = '-'; + } else if (value < 0) { + uintToString(LargestUInt(-value), current); + *--current = '-'; + } else { + uintToString(LargestUInt(value), current); + } + assert(current >= buffer); + return current; +} + +std::string valueToString(LargestUInt value) { + UIntToStringBuffer buffer; + char* current = buffer + sizeof(buffer); + uintToString(value, current); + assert(current >= buffer); + return current; +} + +#if defined(JSON_HAS_INT64) + +std::string valueToString(Int value) { + return valueToString(LargestInt(value)); +} + +std::string valueToString(UInt value) { + return valueToString(LargestUInt(value)); +} + +#endif // # if defined(JSON_HAS_INT64) + +std::string valueToString(double value, bool useSpecialFloats, unsigned int precision) { + // Allocate a buffer that is more than large enough to store the 16 digits of + // precision requested below. + char buffer[32]; + int len = -1; + + char formatString[6]; + sprintf(formatString, "%%.%dg", precision); + + // Print into the buffer. We need not request the alternative representation + // that always has a decimal point because JSON doesn't distingish the + // concepts of reals and integers. + if (isfinite(value)) { + len = snprintf(buffer, sizeof(buffer), formatString, value); + } else { + // IEEE standard states that NaN values will not compare to themselves + if (value != value) { + len = snprintf(buffer, sizeof(buffer), useSpecialFloats ? "NaN" : "null"); + } else if (value < 0) { + len = snprintf(buffer, sizeof(buffer), useSpecialFloats ? "-Infinity" : "-1e+9999"); + } else { + len = snprintf(buffer, sizeof(buffer), useSpecialFloats ? "Infinity" : "1e+9999"); + } + // For those, we do not need to call fixNumLoc, but it is fast. + } + assert(len >= 0); + fixNumericLocale(buffer, buffer + len); + return buffer; +} + +std::string valueToString(double value) { return valueToString(value, false, 17); } + +std::string valueToString(bool value) { return value ? "true" : "false"; } + +std::string valueToQuotedString(const char* value) { + if (value == NULL) + return ""; + // Not sure how to handle unicode... + if (strpbrk(value, "\"\\\b\f\n\r\t") == NULL && + !containsControlCharacter(value)) + return std::string("\"") + value + "\""; + // We have to walk value and escape any special characters. + // Appending to std::string is not efficient, but this should be rare. + // (Note: forward slashes are *not* rare, but I am not escaping them.) + std::string::size_type maxsize = + strlen(value) * 2 + 3; // allescaped+quotes+NULL + std::string result; + result.reserve(maxsize); // to avoid lots of mallocs + result += "\""; + for (const char* c = value; *c != 0; ++c) { + switch (*c) { + case '\"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + // case '/': + // Even though \/ is considered a legal escape in JSON, a bare + // slash is also legal, so I see no reason to escape it. + // (I hope I am not misunderstanding something. + // blep notes: actually escaping \/ may be useful in javascript to avoid (*c); + result += oss.str(); + } else { + result += *c; + } + break; + } + } + result += "\""; + return result; +} + +// https://github.com/upcaste/upcaste/blob/master/src/upcore/src/cstring/strnpbrk.cpp +static char const* strnpbrk(char const* s, char const* accept, size_t n) { + assert((s || !n) && accept); + + char const* const end = s + n; + for (char const* cur = s; cur < end; ++cur) { + int const c = *cur; + for (char const* a = accept; *a; ++a) { + if (*a == c) { + return cur; + } + } + } + return NULL; +} +static std::string valueToQuotedStringN(const char* value, unsigned length) { + if (value == NULL) + return ""; + // Not sure how to handle unicode... + if (strnpbrk(value, "\"\\\b\f\n\r\t", length) == NULL && + !containsControlCharacter0(value, length)) + return std::string("\"") + value + "\""; + // We have to walk value and escape any special characters. + // Appending to std::string is not efficient, but this should be rare. + // (Note: forward slashes are *not* rare, but I am not escaping them.) + std::string::size_type maxsize = + length * 2 + 3; // allescaped+quotes+NULL + std::string result; + result.reserve(maxsize); // to avoid lots of mallocs + result += "\""; + char const* end = value + length; + for (const char* c = value; c != end; ++c) { + switch (*c) { + case '\"': + result += "\\\""; + break; + case '\\': + result += "\\\\"; + break; + case '\b': + result += "\\b"; + break; + case '\f': + result += "\\f"; + break; + case '\n': + result += "\\n"; + break; + case '\r': + result += "\\r"; + break; + case '\t': + result += "\\t"; + break; + // case '/': + // Even though \/ is considered a legal escape in JSON, a bare + // slash is also legal, so I see no reason to escape it. + // (I hope I am not misunderstanding something.) + // blep notes: actually escaping \/ may be useful in javascript to avoid (*c); + result += oss.str(); + } else { + result += *c; + } + break; + } + } + result += "\""; + return result; +} + +// Class Writer +// ////////////////////////////////////////////////////////////////// +Writer::~Writer() {} + +// Class FastWriter +// ////////////////////////////////////////////////////////////////// + +FastWriter::FastWriter() + : yamlCompatiblityEnabled_(false), dropNullPlaceholders_(false), + omitEndingLineFeed_(false) {} + +void FastWriter::enableYAMLCompatibility() { yamlCompatiblityEnabled_ = true; } + +void FastWriter::dropNullPlaceholders() { dropNullPlaceholders_ = true; } + +void FastWriter::omitEndingLineFeed() { omitEndingLineFeed_ = true; } + +std::string FastWriter::write(const Value& root) { + document_ = ""; + writeValue(root); + if (!omitEndingLineFeed_) + document_ += "\n"; + return document_; +} + +void FastWriter::writeValue(const Value& value) { + switch (value.type()) { + case nullValue: + if (!dropNullPlaceholders_) + document_ += "null"; + break; + case intValue: + document_ += valueToString(value.asLargestInt()); + break; + case uintValue: + document_ += valueToString(value.asLargestUInt()); + break; + case realValue: + document_ += valueToString(value.asDouble()); + break; + case stringValue: + { + // Is NULL possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) document_ += valueToQuotedStringN(str, static_cast(end-str)); + break; + } + case booleanValue: + document_ += valueToString(value.asBool()); + break; + case arrayValue: { + document_ += '['; + int size = value.size(); + for (int index = 0; index < size; ++index) { + if (index > 0) + document_ += ','; + writeValue(value[index]); + } + document_ += ']'; + } break; + case objectValue: { + Value::Members members(value.getMemberNames()); + document_ += '{'; + for (Value::Members::iterator it = members.begin(); it != members.end(); + ++it) { + const std::string& name = *it; + if (it != members.begin()) + document_ += ','; + document_ += valueToQuotedStringN(name.data(), static_cast(name.length())); + document_ += yamlCompatiblityEnabled_ ? ": " : ":"; + writeValue(value[name]); + } + document_ += '}'; + } break; + } +} + +// Class StyledWriter +// ////////////////////////////////////////////////////////////////// + +StyledWriter::StyledWriter() + : rightMargin_(74), indentSize_(3), addChildValues_() {} + +std::string StyledWriter::write(const Value& root) { + document_ = ""; + addChildValues_ = false; + indentString_ = ""; + writeCommentBeforeValue(root); + writeValue(root); + writeCommentAfterValueOnSameLine(root); + document_ += "\n"; + return document_; +} + +void StyledWriter::writeValue(const Value& value) { + switch (value.type()) { + case nullValue: + pushValue("null"); + break; + case intValue: + pushValue(valueToString(value.asLargestInt())); + break; + case uintValue: + pushValue(valueToString(value.asLargestUInt())); + break; + case realValue: + pushValue(valueToString(value.asDouble())); + break; + case stringValue: + { + // Is NULL possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) pushValue(valueToQuotedStringN(str, static_cast(end-str))); + else pushValue(""); + break; + } + case booleanValue: + pushValue(valueToString(value.asBool())); + break; + case arrayValue: + writeArrayValue(value); + break; + case objectValue: { + Value::Members members(value.getMemberNames()); + if (members.empty()) + pushValue("{}"); + else { + writeWithIndent("{"); + indent(); + Value::Members::iterator it = members.begin(); + for (;;) { + const std::string& name = *it; + const Value& childValue = value[name]; + writeCommentBeforeValue(childValue); + writeWithIndent(valueToQuotedString(name.c_str())); + document_ += " : "; + writeValue(childValue); + if (++it == members.end()) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + document_ += ','; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("}"); + } + } break; + } +} + +void StyledWriter::writeArrayValue(const Value& value) { + unsigned size = value.size(); + if (size == 0) + pushValue("[]"); + else { + bool isArrayMultiLine = isMultineArray(value); + if (isArrayMultiLine) { + writeWithIndent("["); + indent(); + bool hasChildValue = !childValues_.empty(); + unsigned index = 0; + for (;;) { + const Value& childValue = value[index]; + writeCommentBeforeValue(childValue); + if (hasChildValue) + writeWithIndent(childValues_[index]); + else { + writeIndent(); + writeValue(childValue); + } + if (++index == size) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + document_ += ','; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("]"); + } else // output on a single line + { + assert(childValues_.size() == size); + document_ += "[ "; + for (unsigned index = 0; index < size; ++index) { + if (index > 0) + document_ += ", "; + document_ += childValues_[index]; + } + document_ += " ]"; + } + } +} + +bool StyledWriter::isMultineArray(const Value& value) { + int size = value.size(); + bool isMultiLine = size * 3 >= rightMargin_; + childValues_.clear(); + for (int index = 0; index < size && !isMultiLine; ++index) { + const Value& childValue = value[index]; + isMultiLine = ((childValue.isArray() || childValue.isObject()) && + childValue.size() > 0); + } + if (!isMultiLine) // check if line length > max line length + { + childValues_.reserve(size); + addChildValues_ = true; + int lineLength = 4 + (size - 1) * 2; // '[ ' + ', '*n + ' ]' + for (int index = 0; index < size; ++index) { + if (hasCommentForValue(value[index])) { + isMultiLine = true; + } + writeValue(value[index]); + lineLength += int(childValues_[index].length()); + } + addChildValues_ = false; + isMultiLine = isMultiLine || lineLength >= rightMargin_; + } + return isMultiLine; +} + +void StyledWriter::pushValue(const std::string& value) { + if (addChildValues_) + childValues_.push_back(value); + else + document_ += value; +} + +void StyledWriter::writeIndent() { + if (!document_.empty()) { + char last = document_[document_.length() - 1]; + if (last == ' ') // already indented + return; + if (last != '\n') // Comments may add new-line + document_ += '\n'; + } + document_ += indentString_; +} + +void StyledWriter::writeWithIndent(const std::string& value) { + writeIndent(); + document_ += value; +} + +void StyledWriter::indent() { indentString_ += std::string(indentSize_, ' '); } + +void StyledWriter::unindent() { + assert(int(indentString_.size()) >= indentSize_); + indentString_.resize(indentString_.size() - indentSize_); +} + +void StyledWriter::writeCommentBeforeValue(const Value& root) { + if (!root.hasComment(commentBefore)) + return; + + document_ += "\n"; + writeIndent(); + const std::string& comment = root.getComment(commentBefore); + std::string::const_iterator iter = comment.begin(); + while (iter != comment.end()) { + document_ += *iter; + if (*iter == '\n' && + (iter != comment.end() && *(iter + 1) == '/')) + writeIndent(); + ++iter; + } + + // Comments are stripped of trailing newlines, so add one here + document_ += "\n"; +} + +void StyledWriter::writeCommentAfterValueOnSameLine(const Value& root) { + if (root.hasComment(commentAfterOnSameLine)) + document_ += " " + root.getComment(commentAfterOnSameLine); + + if (root.hasComment(commentAfter)) { + document_ += "\n"; + document_ += root.getComment(commentAfter); + document_ += "\n"; + } +} + +bool StyledWriter::hasCommentForValue(const Value& value) { + return value.hasComment(commentBefore) || + value.hasComment(commentAfterOnSameLine) || + value.hasComment(commentAfter); +} + +// Class StyledStreamWriter +// ////////////////////////////////////////////////////////////////// + +StyledStreamWriter::StyledStreamWriter(std::string indentation) + : document_(NULL), rightMargin_(74), indentation_(indentation), + addChildValues_() {} + +void StyledStreamWriter::write(std::ostream& out, const Value& root) { + document_ = &out; + addChildValues_ = false; + indentString_ = ""; + indented_ = true; + writeCommentBeforeValue(root); + if (!indented_) writeIndent(); + indented_ = true; + writeValue(root); + writeCommentAfterValueOnSameLine(root); + *document_ << "\n"; + document_ = NULL; // Forget the stream, for safety. +} + +void StyledStreamWriter::writeValue(const Value& value) { + switch (value.type()) { + case nullValue: + pushValue("null"); + break; + case intValue: + pushValue(valueToString(value.asLargestInt())); + break; + case uintValue: + pushValue(valueToString(value.asLargestUInt())); + break; + case realValue: + pushValue(valueToString(value.asDouble())); + break; + case stringValue: + { + // Is NULL possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) pushValue(valueToQuotedStringN(str, static_cast(end-str))); + else pushValue(""); + break; + } + case booleanValue: + pushValue(valueToString(value.asBool())); + break; + case arrayValue: + writeArrayValue(value); + break; + case objectValue: { + Value::Members members(value.getMemberNames()); + if (members.empty()) + pushValue("{}"); + else { + writeWithIndent("{"); + indent(); + Value::Members::iterator it = members.begin(); + for (;;) { + const std::string& name = *it; + const Value& childValue = value[name]; + writeCommentBeforeValue(childValue); + writeWithIndent(valueToQuotedString(name.c_str())); + *document_ << " : "; + writeValue(childValue); + if (++it == members.end()) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *document_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("}"); + } + } break; + } +} + +void StyledStreamWriter::writeArrayValue(const Value& value) { + unsigned size = value.size(); + if (size == 0) + pushValue("[]"); + else { + bool isArrayMultiLine = isMultineArray(value); + if (isArrayMultiLine) { + writeWithIndent("["); + indent(); + bool hasChildValue = !childValues_.empty(); + unsigned index = 0; + for (;;) { + const Value& childValue = value[index]; + writeCommentBeforeValue(childValue); + if (hasChildValue) + writeWithIndent(childValues_[index]); + else { + if (!indented_) writeIndent(); + indented_ = true; + writeValue(childValue); + indented_ = false; + } + if (++index == size) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *document_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("]"); + } else // output on a single line + { + assert(childValues_.size() == size); + *document_ << "[ "; + for (unsigned index = 0; index < size; ++index) { + if (index > 0) + *document_ << ", "; + *document_ << childValues_[index]; + } + *document_ << " ]"; + } + } +} + +bool StyledStreamWriter::isMultineArray(const Value& value) { + int size = value.size(); + bool isMultiLine = size * 3 >= rightMargin_; + childValues_.clear(); + for (int index = 0; index < size && !isMultiLine; ++index) { + const Value& childValue = value[index]; + isMultiLine = ((childValue.isArray() || childValue.isObject()) && + childValue.size() > 0); + } + if (!isMultiLine) // check if line length > max line length + { + childValues_.reserve(size); + addChildValues_ = true; + int lineLength = 4 + (size - 1) * 2; // '[ ' + ', '*n + ' ]' + for (int index = 0; index < size; ++index) { + if (hasCommentForValue(value[index])) { + isMultiLine = true; + } + writeValue(value[index]); + lineLength += int(childValues_[index].length()); + } + addChildValues_ = false; + isMultiLine = isMultiLine || lineLength >= rightMargin_; + } + return isMultiLine; +} + +void StyledStreamWriter::pushValue(const std::string& value) { + if (addChildValues_) + childValues_.push_back(value); + else + *document_ << value; +} + +void StyledStreamWriter::writeIndent() { + // blep intended this to look at the so-far-written string + // to determine whether we are already indented, but + // with a stream we cannot do that. So we rely on some saved state. + // The caller checks indented_. + *document_ << '\n' << indentString_; +} + +void StyledStreamWriter::writeWithIndent(const std::string& value) { + if (!indented_) writeIndent(); + *document_ << value; + indented_ = false; +} + +void StyledStreamWriter::indent() { indentString_ += indentation_; } + +void StyledStreamWriter::unindent() { + assert(indentString_.size() >= indentation_.size()); + indentString_.resize(indentString_.size() - indentation_.size()); +} + +void StyledStreamWriter::writeCommentBeforeValue(const Value& root) { + if (!root.hasComment(commentBefore)) + return; + + if (!indented_) writeIndent(); + const std::string& comment = root.getComment(commentBefore); + std::string::const_iterator iter = comment.begin(); + while (iter != comment.end()) { + *document_ << *iter; + if (*iter == '\n' && + (iter != comment.end() && *(iter + 1) == '/')) + // writeIndent(); // would include newline + *document_ << indentString_; + ++iter; + } + indented_ = false; +} + +void StyledStreamWriter::writeCommentAfterValueOnSameLine(const Value& root) { + if (root.hasComment(commentAfterOnSameLine)) + *document_ << ' ' << root.getComment(commentAfterOnSameLine); + + if (root.hasComment(commentAfter)) { + writeIndent(); + *document_ << root.getComment(commentAfter); + } + indented_ = false; +} + +bool StyledStreamWriter::hasCommentForValue(const Value& value) { + return value.hasComment(commentBefore) || + value.hasComment(commentAfterOnSameLine) || + value.hasComment(commentAfter); +} + +////////////////////////// +// BuiltStyledStreamWriter + +/// Scoped enums are not available until C++11. +struct CommentStyle { + /// Decide whether to write comments. + enum Enum { + None, ///< Drop all comments. + Most, ///< Recover odd behavior of previous versions (not implemented yet). + All ///< Keep all comments. + }; +}; + +struct BuiltStyledStreamWriter : public StreamWriter +{ + BuiltStyledStreamWriter( + std::string const& indentation, + CommentStyle::Enum cs, + std::string const& colonSymbol, + std::string const& nullSymbol, + std::string const& endingLineFeedSymbol, + bool useSpecialFloats, + unsigned int precision); + int write(Value const& root, std::ostream* sout) override; +private: + void writeValue(Value const& value); + void writeArrayValue(Value const& value); + bool isMultineArray(Value const& value); + void pushValue(std::string const& value); + void writeIndent(); + void writeWithIndent(std::string const& value); + void indent(); + void unindent(); + void writeCommentBeforeValue(Value const& root); + void writeCommentAfterValueOnSameLine(Value const& root); + static bool hasCommentForValue(const Value& value); + + typedef std::vector ChildValues; + + ChildValues childValues_; + std::string indentString_; + int rightMargin_; + std::string indentation_; + CommentStyle::Enum cs_; + std::string colonSymbol_; + std::string nullSymbol_; + std::string endingLineFeedSymbol_; + bool addChildValues_ : 1; + bool indented_ : 1; + bool useSpecialFloats_ : 1; + unsigned int precision_; +}; +BuiltStyledStreamWriter::BuiltStyledStreamWriter( + std::string const& indentation, + CommentStyle::Enum cs, + std::string const& colonSymbol, + std::string const& nullSymbol, + std::string const& endingLineFeedSymbol, + bool useSpecialFloats, + unsigned int precision) + : rightMargin_(74) + , indentation_(indentation) + , cs_(cs) + , colonSymbol_(colonSymbol) + , nullSymbol_(nullSymbol) + , endingLineFeedSymbol_(endingLineFeedSymbol) + , addChildValues_(false) + , indented_(false) + , useSpecialFloats_(useSpecialFloats) + , precision_(precision) +{ +} +int BuiltStyledStreamWriter::write(Value const& root, std::ostream* sout) +{ + sout_ = sout; + addChildValues_ = false; + indented_ = true; + indentString_ = ""; + writeCommentBeforeValue(root); + if (!indented_) writeIndent(); + indented_ = true; + writeValue(root); + writeCommentAfterValueOnSameLine(root); + *sout_ << endingLineFeedSymbol_; + sout_ = NULL; + return 0; +} +void BuiltStyledStreamWriter::writeValue(Value const& value) { + switch (value.type()) { + case nullValue: + pushValue(nullSymbol_); + break; + case intValue: + pushValue(valueToString(value.asLargestInt())); + break; + case uintValue: + pushValue(valueToString(value.asLargestUInt())); + break; + case realValue: + pushValue(valueToString(value.asDouble(), useSpecialFloats_, precision_)); + break; + case stringValue: + { + // Is NULL is possible for value.string_? + char const* str; + char const* end; + bool ok = value.getString(&str, &end); + if (ok) pushValue(valueToQuotedStringN(str, static_cast(end-str))); + else pushValue(""); + break; + } + case booleanValue: + pushValue(valueToString(value.asBool())); + break; + case arrayValue: + writeArrayValue(value); + break; + case objectValue: { + Value::Members members(value.getMemberNames()); + if (members.empty()) + pushValue("{}"); + else { + writeWithIndent("{"); + indent(); + Value::Members::iterator it = members.begin(); + for (;;) { + std::string const& name = *it; + Value const& childValue = value[name]; + writeCommentBeforeValue(childValue); + writeWithIndent(valueToQuotedStringN(name.data(), static_cast(name.length()))); + *sout_ << colonSymbol_; + writeValue(childValue); + if (++it == members.end()) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *sout_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("}"); + } + } break; + } +} + +void BuiltStyledStreamWriter::writeArrayValue(Value const& value) { + unsigned size = value.size(); + if (size == 0) + pushValue("[]"); + else { + bool isMultiLine = (cs_ == CommentStyle::All) || isMultineArray(value); + if (isMultiLine) { + writeWithIndent("["); + indent(); + bool hasChildValue = !childValues_.empty(); + unsigned index = 0; + for (;;) { + Value const& childValue = value[index]; + writeCommentBeforeValue(childValue); + if (hasChildValue) + writeWithIndent(childValues_[index]); + else { + if (!indented_) writeIndent(); + indented_ = true; + writeValue(childValue); + indented_ = false; + } + if (++index == size) { + writeCommentAfterValueOnSameLine(childValue); + break; + } + *sout_ << ","; + writeCommentAfterValueOnSameLine(childValue); + } + unindent(); + writeWithIndent("]"); + } else // output on a single line + { + assert(childValues_.size() == size); + *sout_ << "["; + if (!indentation_.empty()) *sout_ << " "; + for (unsigned index = 0; index < size; ++index) { + if (index > 0) + *sout_ << ", "; + *sout_ << childValues_[index]; + } + if (!indentation_.empty()) *sout_ << " "; + *sout_ << "]"; + } + } +} + +bool BuiltStyledStreamWriter::isMultineArray(Value const& value) { + int size = value.size(); + bool isMultiLine = size * 3 >= rightMargin_; + childValues_.clear(); + for (int index = 0; index < size && !isMultiLine; ++index) { + Value const& childValue = value[index]; + isMultiLine = ((childValue.isArray() || childValue.isObject()) && + childValue.size() > 0); + } + if (!isMultiLine) // check if line length > max line length + { + childValues_.reserve(size); + addChildValues_ = true; + int lineLength = 4 + (size - 1) * 2; // '[ ' + ', '*n + ' ]' + for (int index = 0; index < size; ++index) { + if (hasCommentForValue(value[index])) { + isMultiLine = true; + } + writeValue(value[index]); + lineLength += int(childValues_[index].length()); + } + addChildValues_ = false; + isMultiLine = isMultiLine || lineLength >= rightMargin_; + } + return isMultiLine; +} + +void BuiltStyledStreamWriter::pushValue(std::string const& value) { + if (addChildValues_) + childValues_.push_back(value); + else + *sout_ << value; +} + +void BuiltStyledStreamWriter::writeIndent() { + // blep intended this to look at the so-far-written string + // to determine whether we are already indented, but + // with a stream we cannot do that. So we rely on some saved state. + // The caller checks indented_. + + if (!indentation_.empty()) { + // In this case, drop newlines too. + *sout_ << '\n' << indentString_; + } +} + +void BuiltStyledStreamWriter::writeWithIndent(std::string const& value) { + if (!indented_) writeIndent(); + *sout_ << value; + indented_ = false; +} + +void BuiltStyledStreamWriter::indent() { indentString_ += indentation_; } + +void BuiltStyledStreamWriter::unindent() { + assert(indentString_.size() >= indentation_.size()); + indentString_.resize(indentString_.size() - indentation_.size()); +} + +void BuiltStyledStreamWriter::writeCommentBeforeValue(Value const& root) { + if (cs_ == CommentStyle::None) return; + if (!root.hasComment(commentBefore)) + return; + + if (!indented_) writeIndent(); + const std::string& comment = root.getComment(commentBefore); + std::string::const_iterator iter = comment.begin(); + while (iter != comment.end()) { + *sout_ << *iter; + if (*iter == '\n' && + (iter != comment.end() && *(iter + 1) == '/')) + // writeIndent(); // would write extra newline + *sout_ << indentString_; + ++iter; + } + indented_ = false; +} + +void BuiltStyledStreamWriter::writeCommentAfterValueOnSameLine(Value const& root) { + if (cs_ == CommentStyle::None) return; + if (root.hasComment(commentAfterOnSameLine)) + *sout_ << " " + root.getComment(commentAfterOnSameLine); + + if (root.hasComment(commentAfter)) { + writeIndent(); + *sout_ << root.getComment(commentAfter); + } +} + +// static +bool BuiltStyledStreamWriter::hasCommentForValue(const Value& value) { + return value.hasComment(commentBefore) || + value.hasComment(commentAfterOnSameLine) || + value.hasComment(commentAfter); +} + +/////////////// +// StreamWriter + +StreamWriter::StreamWriter() + : sout_(NULL) +{ +} +StreamWriter::~StreamWriter() +{ +} +StreamWriter::Factory::~Factory() +{} +StreamWriterBuilder::StreamWriterBuilder() +{ + setDefaults(&settings_); +} +StreamWriterBuilder::~StreamWriterBuilder() +{} +StreamWriter* StreamWriterBuilder::newStreamWriter() const +{ + std::string indentation = settings_["indentation"].asString(); + std::string cs_str = settings_["commentStyle"].asString(); + bool eyc = settings_["enableYAMLCompatibility"].asBool(); + bool dnp = settings_["dropNullPlaceholders"].asBool(); + bool usf = settings_["useSpecialFloats"].asBool(); + unsigned int pre = settings_["precision"].asUInt(); + CommentStyle::Enum cs = CommentStyle::All; + if (cs_str == "All") { + cs = CommentStyle::All; + } else if (cs_str == "None") { + cs = CommentStyle::None; + } else { + throwRuntimeError("commentStyle must be 'All' or 'None'"); + } + std::string colonSymbol = " : "; + if (eyc) { + colonSymbol = ": "; + } else if (indentation.empty()) { + colonSymbol = ":"; + } + std::string nullSymbol = "null"; + if (dnp) { + nullSymbol = ""; + } + if (pre > 17) pre = 17; + std::string endingLineFeedSymbol = ""; + return new BuiltStyledStreamWriter( + indentation, cs, + colonSymbol, nullSymbol, endingLineFeedSymbol, usf, pre); +} +static void getValidWriterKeys(std::set* valid_keys) +{ + valid_keys->clear(); + valid_keys->insert("indentation"); + valid_keys->insert("commentStyle"); + valid_keys->insert("enableYAMLCompatibility"); + valid_keys->insert("dropNullPlaceholders"); + valid_keys->insert("useSpecialFloats"); + valid_keys->insert("precision"); +} +bool StreamWriterBuilder::validate(Json::Value* invalid) const +{ + Json::Value my_invalid; + if (!invalid) invalid = &my_invalid; // so we do not need to test for NULL + Json::Value& inv = *invalid; + std::set valid_keys; + getValidWriterKeys(&valid_keys); + Value::Members keys = settings_.getMemberNames(); + size_t n = keys.size(); + for (size_t i = 0; i < n; ++i) { + std::string const& key = keys[i]; + if (valid_keys.find(key) == valid_keys.end()) { + inv[key] = settings_[key]; + } + } + return 0u == inv.size(); +} +Value& StreamWriterBuilder::operator[](std::string key) +{ + return settings_[key]; +} +// static +void StreamWriterBuilder::setDefaults(Json::Value* settings) +{ + //! [StreamWriterBuilderDefaults] + (*settings)["commentStyle"] = "All"; + (*settings)["indentation"] = "\t"; + (*settings)["enableYAMLCompatibility"] = false; + (*settings)["dropNullPlaceholders"] = false; + (*settings)["useSpecialFloats"] = false; + (*settings)["precision"] = 17; + //! [StreamWriterBuilderDefaults] +} + +std::string writeString(StreamWriter::Factory const& builder, Value const& root) { + std::ostringstream sout; + StreamWriterPtr const writer(builder.newStreamWriter()); + writer->write(root, &sout); + return sout.str(); +} + +std::ostream& operator<<(std::ostream& sout, Value const& root) { + StreamWriterBuilder builder; + StreamWriterPtr const writer(builder.newStreamWriter()); + writer->write(root, &sout); + return sout; +} + +} // namespace Json + +// ////////////////////////////////////////////////////////////////////// +// End of content of file: src/lib_json/json_writer.cpp +// ////////////////////////////////////////////////////////////////////// + + + + + diff --git a/src/third_party/minimp3/minimp3.h b/src/third_party/minimp3/minimp3.h new file mode 100644 index 0000000..c7fb9cf --- /dev/null +++ b/src/third_party/minimp3/minimp3.h @@ -0,0 +1,1853 @@ +#ifndef MINIMP3_H +#define MINIMP3_H +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ +#include + +#define MINIMP3_MAX_SAMPLES_PER_FRAME (1152*2) + +typedef struct +{ + int frame_bytes, frame_offset, channels, hz, layer, bitrate_kbps; +} mp3dec_frame_info_t; + +typedef struct +{ + float mdct_overlap[2][9*32], qmf_state[15*2*32]; + int reserv, free_format_bytes; + unsigned char header[4], reserv_buf[511]; +} mp3dec_t; + +#ifdef __cplusplus +extern "C" { +#endif /* __cplusplus */ + +void mp3dec_init(mp3dec_t *dec); +#ifndef MINIMP3_FLOAT_OUTPUT +typedef int16_t mp3d_sample_t; +#else /* MINIMP3_FLOAT_OUTPUT */ +typedef float mp3d_sample_t; +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples); +#endif /* MINIMP3_FLOAT_OUTPUT */ +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info); + +#ifdef __cplusplus +} +#endif /* __cplusplus */ + +#endif /* MINIMP3_H */ +#if defined(MINIMP3_IMPLEMENTATION) && !defined(_MINIMP3_IMPLEMENTATION_GUARD) +#define _MINIMP3_IMPLEMENTATION_GUARD + +#include +#include + +#define MAX_FREE_FORMAT_FRAME_SIZE 2304 /* more than ISO spec's */ +#ifndef MAX_FRAME_SYNC_MATCHES +#define MAX_FRAME_SYNC_MATCHES 10 +#endif /* MAX_FRAME_SYNC_MATCHES */ + +#define MAX_L3_FRAME_PAYLOAD_BYTES MAX_FREE_FORMAT_FRAME_SIZE /* MUST be >= 320000/8/32000*1152 = 1440 */ + +#define MAX_BITRESERVOIR_BYTES 511 +#define SHORT_BLOCK_TYPE 2 +#define STOP_BLOCK_TYPE 3 +#define MODE_MONO 3 +#define MODE_JOINT_STEREO 1 +#define HDR_SIZE 4 +#define HDR_IS_MONO(h) (((h[3]) & 0xC0) == 0xC0) +#define HDR_IS_MS_STEREO(h) (((h[3]) & 0xE0) == 0x60) +#define HDR_IS_FREE_FORMAT(h) (((h[2]) & 0xF0) == 0) +#define HDR_IS_CRC(h) (!((h[1]) & 1)) +#define HDR_TEST_PADDING(h) ((h[2]) & 0x2) +#define HDR_TEST_MPEG1(h) ((h[1]) & 0x8) +#define HDR_TEST_NOT_MPEG25(h) ((h[1]) & 0x10) +#define HDR_TEST_I_STEREO(h) ((h[3]) & 0x10) +#define HDR_TEST_MS_STEREO(h) ((h[3]) & 0x20) +#define HDR_GET_STEREO_MODE(h) (((h[3]) >> 6) & 3) +#define HDR_GET_STEREO_MODE_EXT(h) (((h[3]) >> 4) & 3) +#define HDR_GET_LAYER(h) (((h[1]) >> 1) & 3) +#define HDR_GET_BITRATE(h) ((h[2]) >> 4) +#define HDR_GET_SAMPLE_RATE(h) (((h[2]) >> 2) & 3) +#define HDR_GET_MY_SAMPLE_RATE(h) (HDR_GET_SAMPLE_RATE(h) + (((h[1] >> 3) & 1) + ((h[1] >> 4) & 1))*3) +#define HDR_IS_FRAME_576(h) ((h[1] & 14) == 2) +#define HDR_IS_LAYER_1(h) ((h[1] & 6) == 6) + +#define BITS_DEQUANTIZER_OUT -1 +#define MAX_SCF (255 + BITS_DEQUANTIZER_OUT*4 - 210) +#define MAX_SCFI ((MAX_SCF + 3) & ~3) + +#define MINIMP3_MIN(a, b) ((a) > (b) ? (b) : (a)) +#define MINIMP3_MAX(a, b) ((a) < (b) ? (b) : (a)) + +#if !defined(MINIMP3_NO_SIMD) + +#if !defined(MINIMP3_ONLY_SIMD) && (defined(_M_X64) || defined(_M_ARM64) || defined(__x86_64__) || defined(__aarch64__)) +/* x64 always have SSE2, arm64 always have neon, no need for generic code */ +#define MINIMP3_ONLY_SIMD +#endif /* SIMD checks... */ + +#if (defined(_MSC_VER) && (defined(_M_IX86) || defined(_M_X64))) || ((defined(__i386__) || defined(__x86_64__)) && defined(__SSE2__)) +#if defined(_MSC_VER) +#include +#endif /* defined(_MSC_VER) */ +#include +#define HAVE_SSE 1 +#define HAVE_SIMD 1 +#define VSTORE _mm_storeu_ps +#define VLD _mm_loadu_ps +#define VSET _mm_set1_ps +#define VADD _mm_add_ps +#define VSUB _mm_sub_ps +#define VMUL _mm_mul_ps +#define VMAC(a, x, y) _mm_add_ps(a, _mm_mul_ps(x, y)) +#define VMSB(a, x, y) _mm_sub_ps(a, _mm_mul_ps(x, y)) +#define VMUL_S(x, s) _mm_mul_ps(x, _mm_set1_ps(s)) +#define VREV(x) _mm_shuffle_ps(x, x, _MM_SHUFFLE(0, 1, 2, 3)) +typedef __m128 f4; +#if defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) +#define minimp3_cpuid __cpuid +#else /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static __inline__ __attribute__((always_inline)) void minimp3_cpuid(int CPUInfo[], const int InfoType) +{ +#if defined(__PIC__) + __asm__ __volatile__( +#if defined(__x86_64__) + "push %%rbx\n" + "cpuid\n" + "xchgl %%ebx, %1\n" + "pop %%rbx\n" +#else /* defined(__x86_64__) */ + "xchgl %%ebx, %1\n" + "cpuid\n" + "xchgl %%ebx, %1\n" +#endif /* defined(__x86_64__) */ + : "=a" (CPUInfo[0]), "=r" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#else /* defined(__PIC__) */ + __asm__ __volatile__( + "cpuid" + : "=a" (CPUInfo[0]), "=b" (CPUInfo[1]), "=c" (CPUInfo[2]), "=d" (CPUInfo[3]) + : "a" (InfoType)); +#endif /* defined(__PIC__)*/ +} +#endif /* defined(_MSC_VER) || defined(MINIMP3_ONLY_SIMD) */ +static int have_simd() +{ +#ifdef MINIMP3_ONLY_SIMD + return 1; +#else /* MINIMP3_ONLY_SIMD */ + static int g_have_simd; + int CPUInfo[4]; +#ifdef MINIMP3_TEST + static int g_counter; + if (g_counter++ > 100) + return 0; +#endif /* MINIMP3_TEST */ + if (g_have_simd) + goto end; + minimp3_cpuid(CPUInfo, 0); + g_have_simd = 1; + if (CPUInfo[0] > 0) + { + minimp3_cpuid(CPUInfo, 1); + g_have_simd = (CPUInfo[3] & (1 << 26)) + 1; /* SSE2 */ + } +end: + return g_have_simd - 1; +#endif /* MINIMP3_ONLY_SIMD */ +} +#elif defined(__ARM_NEON) || defined(__aarch64__) +#include +#define HAVE_SSE 0 +#define HAVE_SIMD 1 +#define VSTORE vst1q_f32 +#define VLD vld1q_f32 +#define VSET vmovq_n_f32 +#define VADD vaddq_f32 +#define VSUB vsubq_f32 +#define VMUL vmulq_f32 +#define VMAC(a, x, y) vmlaq_f32(a, x, y) +#define VMSB(a, x, y) vmlsq_f32(a, x, y) +#define VMUL_S(x, s) vmulq_f32(x, vmovq_n_f32(s)) +#define VREV(x) vcombine_f32(vget_high_f32(vrev64q_f32(x)), vget_low_f32(vrev64q_f32(x))) +typedef float32x4_t f4; +static int have_simd() +{ /* TODO: detect neon for !MINIMP3_ONLY_SIMD */ + return 1; +} +#else /* SIMD checks... */ +#define HAVE_SSE 0 +#define HAVE_SIMD 0 +#ifdef MINIMP3_ONLY_SIMD +#error MINIMP3_ONLY_SIMD used, but SSE/NEON not enabled +#endif /* MINIMP3_ONLY_SIMD */ +#endif /* SIMD checks... */ +#else /* !defined(MINIMP3_NO_SIMD) */ +#define HAVE_SIMD 0 +#endif /* !defined(MINIMP3_NO_SIMD) */ + +#if defined(__ARM_ARCH) && (__ARM_ARCH >= 6) && !defined(__aarch64__) +#define HAVE_ARMV6 1 +static __inline__ __attribute__((always_inline)) int32_t minimp3_clip_int16_arm(int32_t a) +{ + int32_t x = 0; + __asm__ ("ssat %0, #16, %1" : "=r"(x) : "r"(a)); + return x; +} +#endif + +typedef struct +{ + const uint8_t *buf; + int pos, limit; +} bs_t; + +typedef struct +{ + float scf[3*64]; + uint8_t total_bands, stereo_bands, bitalloc[64], scfcod[64]; +} L12_scale_info; + +typedef struct +{ + uint8_t tab_offset, code_tab_width, band_count; +} L12_subband_alloc_t; + +typedef struct +{ + const uint8_t *sfbtab; + uint16_t part_23_length, big_values, scalefac_compress; + uint8_t global_gain, block_type, mixed_block_flag, n_long_sfb, n_short_sfb; + uint8_t table_select[3], region_count[3], subblock_gain[3]; + uint8_t preflag, scalefac_scale, count1_table, scfsi; +} L3_gr_info_t; + +typedef struct +{ + bs_t bs; + uint8_t maindata[MAX_BITRESERVOIR_BYTES + MAX_L3_FRAME_PAYLOAD_BYTES]; + L3_gr_info_t gr_info[4]; + float grbuf[2][576], scf[40], syn[18 + 15][2*32]; + uint8_t ist_pos[2][39]; +} mp3dec_scratch_t; + +static void bs_init(bs_t *bs, const uint8_t *data, int bytes) +{ + bs->buf = data; + bs->pos = 0; + bs->limit = bytes*8; +} + +static uint32_t get_bits(bs_t *bs, int n) +{ + uint32_t next, cache = 0, s = bs->pos & 7; + int shl = n + s; + const uint8_t *p = bs->buf + (bs->pos >> 3); + if ((bs->pos += n) > bs->limit) + return 0; + next = *p++ & (255 >> s); + while ((shl -= 8) > 0) + { + cache |= next << shl; + next = *p++; + } + return cache | (next >> -shl); +} + +static int hdr_valid(const uint8_t *h) +{ + return h[0] == 0xff && + ((h[1] & 0xF0) == 0xf0 || (h[1] & 0xFE) == 0xe2) && + (HDR_GET_LAYER(h) != 0) && + (HDR_GET_BITRATE(h) != 15) && + (HDR_GET_SAMPLE_RATE(h) != 3); +} + +static int hdr_compare(const uint8_t *h1, const uint8_t *h2) +{ + return hdr_valid(h2) && + ((h1[1] ^ h2[1]) & 0xFE) == 0 && + ((h1[2] ^ h2[2]) & 0x0C) == 0 && + !(HDR_IS_FREE_FORMAT(h1) ^ HDR_IS_FREE_FORMAT(h2)); +} + +static unsigned hdr_bitrate_kbps(const uint8_t *h) +{ + static const uint8_t halfrate[2][3][15] = { + { { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,4,8,12,16,20,24,28,32,40,48,56,64,72,80 }, { 0,16,24,28,32,40,48,56,64,72,80,88,96,112,128 } }, + { { 0,16,20,24,28,32,40,48,56,64,80,96,112,128,160 }, { 0,16,24,28,32,40,48,56,64,80,96,112,128,160,192 }, { 0,16,32,48,64,80,96,112,128,144,160,176,192,208,224 } }, + }; + return 2*halfrate[!!HDR_TEST_MPEG1(h)][HDR_GET_LAYER(h) - 1][HDR_GET_BITRATE(h)]; +} + +static unsigned hdr_sample_rate_hz(const uint8_t *h) +{ + static const unsigned g_hz[3] = { 44100, 48000, 32000 }; + return g_hz[HDR_GET_SAMPLE_RATE(h)] >> (int)!HDR_TEST_MPEG1(h) >> (int)!HDR_TEST_NOT_MPEG25(h); +} + +static unsigned hdr_frame_samples(const uint8_t *h) +{ + return HDR_IS_LAYER_1(h) ? 384 : (1152 >> (int)HDR_IS_FRAME_576(h)); +} + +static int hdr_frame_bytes(const uint8_t *h, int free_format_size) +{ + int frame_bytes = hdr_frame_samples(h)*hdr_bitrate_kbps(h)*125/hdr_sample_rate_hz(h); + if (HDR_IS_LAYER_1(h)) + { + frame_bytes &= ~3; /* slot align */ + } + return frame_bytes ? frame_bytes : free_format_size; +} + +static int hdr_padding(const uint8_t *h) +{ + return HDR_TEST_PADDING(h) ? (HDR_IS_LAYER_1(h) ? 4 : 1) : 0; +} + +#ifndef MINIMP3_ONLY_MP3 +static const L12_subband_alloc_t *L12_subband_alloc_table(const uint8_t *hdr, L12_scale_info *sci) +{ + const L12_subband_alloc_t *alloc; + int mode = HDR_GET_STEREO_MODE(hdr); + int nbands, stereo_bands = (mode == MODE_MONO) ? 0 : (mode == MODE_JOINT_STEREO) ? (HDR_GET_STEREO_MODE_EXT(hdr) << 2) + 4 : 32; + + if (HDR_IS_LAYER_1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L1[] = { { 76, 4, 32 } }; + alloc = g_alloc_L1; + nbands = 32; + } else if (!HDR_TEST_MPEG1(hdr)) + { + static const L12_subband_alloc_t g_alloc_L2M2[] = { { 60, 4, 4 }, { 44, 3, 7 }, { 44, 2, 19 } }; + alloc = g_alloc_L2M2; + nbands = 30; + } else + { + static const L12_subband_alloc_t g_alloc_L2M1[] = { { 0, 4, 3 }, { 16, 4, 8 }, { 32, 3, 12 }, { 40, 2, 7 } }; + int sample_rate_idx = HDR_GET_SAMPLE_RATE(hdr); + unsigned kbps = hdr_bitrate_kbps(hdr) >> (int)(mode != MODE_MONO); + if (!kbps) /* free-format */ + { + kbps = 192; + } + + alloc = g_alloc_L2M1; + nbands = 27; + if (kbps < 56) + { + static const L12_subband_alloc_t g_alloc_L2M1_lowrate[] = { { 44, 4, 2 }, { 44, 3, 10 } }; + alloc = g_alloc_L2M1_lowrate; + nbands = sample_rate_idx == 2 ? 12 : 8; + } else if (kbps >= 96 && sample_rate_idx != 1) + { + nbands = 30; + } + } + + sci->total_bands = (uint8_t)nbands; + sci->stereo_bands = (uint8_t)MINIMP3_MIN(stereo_bands, nbands); + + return alloc; +} + +static void L12_read_scalefactors(bs_t *bs, uint8_t *pba, uint8_t *scfcod, int bands, float *scf) +{ + static const float g_deq_L12[18*3] = { +#define DQ(x) 9.53674316e-07f/x, 7.56931807e-07f/x, 6.00777173e-07f/x + DQ(3),DQ(7),DQ(15),DQ(31),DQ(63),DQ(127),DQ(255),DQ(511),DQ(1023),DQ(2047),DQ(4095),DQ(8191),DQ(16383),DQ(32767),DQ(65535),DQ(3),DQ(5),DQ(9) + }; + int i, m; + for (i = 0; i < bands; i++) + { + float s = 0; + int ba = *pba++; + int mask = ba ? 4 + ((19 >> scfcod[i]) & 3) : 0; + for (m = 4; m; m >>= 1) + { + if (mask & m) + { + int b = get_bits(bs, 6); + s = g_deq_L12[ba*3 - 6 + b % 3]*(1 << 21 >> b/3); + } + *scf++ = s; + } + } +} + +static void L12_read_scale_info(const uint8_t *hdr, bs_t *bs, L12_scale_info *sci) +{ + static const uint8_t g_bitalloc_code_tab[] = { + 0,17, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,16, + 0,17,18, 3,19,4,5,16, + 0,17,18,16, + 0,17,18,19, 4,5,6, 7,8, 9,10,11,12,13,14,15, + 0,17,18, 3,19,4,5, 6,7, 8, 9,10,11,12,13,14, + 0, 2, 3, 4, 5,6,7, 8,9,10,11,12,13,14,15,16 + }; + const L12_subband_alloc_t *subband_alloc = L12_subband_alloc_table(hdr, sci); + + int i, k = 0, ba_bits = 0; + const uint8_t *ba_code_tab = g_bitalloc_code_tab; + + for (i = 0; i < sci->total_bands; i++) + { + uint8_t ba; + if (i == k) + { + k += subband_alloc->band_count; + ba_bits = subband_alloc->code_tab_width; + ba_code_tab = g_bitalloc_code_tab + subband_alloc->tab_offset; + subband_alloc++; + } + ba = ba_code_tab[get_bits(bs, ba_bits)]; + sci->bitalloc[2*i] = ba; + if (i < sci->stereo_bands) + { + ba = ba_code_tab[get_bits(bs, ba_bits)]; + } + sci->bitalloc[2*i + 1] = sci->stereo_bands ? ba : 0; + } + + for (i = 0; i < 2*sci->total_bands; i++) + { + sci->scfcod[i] = sci->bitalloc[i] ? HDR_IS_LAYER_1(hdr) ? 2 : get_bits(bs, 2) : 6; + } + + L12_read_scalefactors(bs, sci->bitalloc, sci->scfcod, sci->total_bands*2, sci->scf); + + for (i = sci->stereo_bands; i < sci->total_bands; i++) + { + sci->bitalloc[2*i + 1] = 0; + } +} + +static int L12_dequantize_granule(float *grbuf, bs_t *bs, L12_scale_info *sci, int group_size) +{ + int i, j, k, choff = 576; + for (j = 0; j < 4; j++) + { + float *dst = grbuf + group_size*j; + for (i = 0; i < 2*sci->total_bands; i++) + { + int ba = sci->bitalloc[i]; + if (ba != 0) + { + if (ba < 17) + { + int half = (1 << (ba - 1)) - 1; + for (k = 0; k < group_size; k++) + { + dst[k] = (float)((int)get_bits(bs, ba) - half); + } + } else + { + unsigned mod = (2 << (ba - 17)) + 1; /* 3, 5, 9 */ + unsigned code = get_bits(bs, mod + 2 - (mod >> 3)); /* 5, 7, 10 */ + for (k = 0; k < group_size; k++, code /= mod) + { + dst[k] = (float)((int)(code % mod - mod/2)); + } + } + } + dst += choff; + choff = 18 - choff; + } + } + return group_size*4; +} + +static void L12_apply_scf_384(L12_scale_info *sci, const float *scf, float *dst) +{ + int i, k; + memcpy(dst + 576 + sci->stereo_bands*18, dst + sci->stereo_bands*18, (sci->total_bands - sci->stereo_bands)*18*sizeof(float)); + for (i = 0; i < sci->total_bands; i++, dst += 18, scf += 6) + { + for (k = 0; k < 12; k++) + { + dst[k + 0] *= scf[0]; + dst[k + 576] *= scf[3]; + } + } +} +#endif /* MINIMP3_ONLY_MP3 */ + +static int L3_read_side_info(bs_t *bs, L3_gr_info_t *gr, const uint8_t *hdr) +{ + static const uint8_t g_scf_long[8][23] = { + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 12,12,12,12,12,12,16,20,24,28,32,40,48,56,64,76,90,2,2,2,2,2,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,18,22,26,32,38,46,54,62,70,76,36,0 }, + { 6,6,6,6,6,6,8,10,12,14,16,20,24,28,32,38,46,52,60,68,58,54,0 }, + { 4,4,4,4,4,4,6,6,8,8,10,12,16,20,24,28,34,42,50,54,76,158,0 }, + { 4,4,4,4,4,4,6,6,6,8,10,12,16,18,22,28,34,40,46,54,54,192,0 }, + { 4,4,4,4,4,4,6,6,8,10,12,16,20,24,30,38,46,56,68,84,102,26,0 } + }; + static const uint8_t g_scf_short[8][40] = { + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 8,8,8,8,8,8,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,4,4,4,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + static const uint8_t g_scf_mixed[8][40] = { + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 12,12,12,4,4,4,8,8,8,12,12,12,16,16,16,20,20,20,24,24,24,28,28,28,36,36,36,2,2,2,2,2,2,2,2,2,26,26,26,0 }, + { 6,6,6,6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,14,14,14,18,18,18,26,26,26,32,32,32,42,42,42,18,18,18,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,32,32,32,44,44,44,12,12,12,0 }, + { 6,6,6,6,6,6,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,24,24,24,30,30,30,40,40,40,18,18,18,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,10,10,10,12,12,12,14,14,14,18,18,18,22,22,22,30,30,30,56,56,56,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,6,6,6,10,10,10,12,12,12,14,14,14,16,16,16,20,20,20,26,26,26,66,66,66,0 }, + { 4,4,4,4,4,4,6,6,4,4,4,6,6,6,8,8,8,12,12,12,16,16,16,20,20,20,26,26,26,34,34,34,42,42,42,12,12,12,0 } + }; + + unsigned tables, scfsi = 0; + int main_data_begin, part_23_sum = 0; + int sr_idx = HDR_GET_MY_SAMPLE_RATE(hdr); sr_idx -= (sr_idx != 0); + int gr_count = HDR_IS_MONO(hdr) ? 1 : 2; + + if (HDR_TEST_MPEG1(hdr)) + { + gr_count *= 2; + main_data_begin = get_bits(bs, 9); + scfsi = get_bits(bs, 7 + gr_count); + } else + { + main_data_begin = get_bits(bs, 8 + gr_count) >> gr_count; + } + + do + { + if (HDR_IS_MONO(hdr)) + { + scfsi <<= 4; + } + gr->part_23_length = (uint16_t)get_bits(bs, 12); + part_23_sum += gr->part_23_length; + gr->big_values = (uint16_t)get_bits(bs, 9); + if (gr->big_values > 288) + { + return -1; + } + gr->global_gain = (uint8_t)get_bits(bs, 8); + gr->scalefac_compress = (uint16_t)get_bits(bs, HDR_TEST_MPEG1(hdr) ? 4 : 9); + gr->sfbtab = g_scf_long[sr_idx]; + gr->n_long_sfb = 22; + gr->n_short_sfb = 0; + if (get_bits(bs, 1)) + { + gr->block_type = (uint8_t)get_bits(bs, 2); + if (!gr->block_type) + { + return -1; + } + gr->mixed_block_flag = (uint8_t)get_bits(bs, 1); + gr->region_count[0] = 7; + gr->region_count[1] = 255; + if (gr->block_type == SHORT_BLOCK_TYPE) + { + scfsi &= 0x0F0F; + if (!gr->mixed_block_flag) + { + gr->region_count[0] = 8; + gr->sfbtab = g_scf_short[sr_idx]; + gr->n_long_sfb = 0; + gr->n_short_sfb = 39; + } else + { + gr->sfbtab = g_scf_mixed[sr_idx]; + gr->n_long_sfb = HDR_TEST_MPEG1(hdr) ? 8 : 6; + gr->n_short_sfb = 30; + } + } + tables = get_bits(bs, 10); + tables <<= 5; + gr->subblock_gain[0] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[1] = (uint8_t)get_bits(bs, 3); + gr->subblock_gain[2] = (uint8_t)get_bits(bs, 3); + } else + { + gr->block_type = 0; + gr->mixed_block_flag = 0; + tables = get_bits(bs, 15); + gr->region_count[0] = (uint8_t)get_bits(bs, 4); + gr->region_count[1] = (uint8_t)get_bits(bs, 3); + gr->region_count[2] = 255; + } + gr->table_select[0] = (uint8_t)(tables >> 10); + gr->table_select[1] = (uint8_t)((tables >> 5) & 31); + gr->table_select[2] = (uint8_t)((tables) & 31); + gr->preflag = HDR_TEST_MPEG1(hdr) ? get_bits(bs, 1) : (gr->scalefac_compress >= 500); + gr->scalefac_scale = (uint8_t)get_bits(bs, 1); + gr->count1_table = (uint8_t)get_bits(bs, 1); + gr->scfsi = (uint8_t)((scfsi >> 12) & 15); + scfsi <<= 4; + gr++; + } while(--gr_count); + + if (part_23_sum + bs->pos > bs->limit + main_data_begin*8) + { + return -1; + } + + return main_data_begin; +} + +static void L3_read_scalefactors(uint8_t *scf, uint8_t *ist_pos, const uint8_t *scf_size, const uint8_t *scf_count, bs_t *bitbuf, int scfsi) +{ + int i, k; + for (i = 0; i < 4 && scf_count[i]; i++, scfsi *= 2) + { + int cnt = scf_count[i]; + if (scfsi & 8) + { + memcpy(scf, ist_pos, cnt); + } else + { + int bits = scf_size[i]; + if (!bits) + { + memset(scf, 0, cnt); + memset(ist_pos, 0, cnt); + } else + { + int max_scf = (scfsi < 0) ? (1 << bits) - 1 : -1; + for (k = 0; k < cnt; k++) + { + int s = get_bits(bitbuf, bits); + ist_pos[k] = (s == max_scf ? -1 : s); + scf[k] = s; + } + } + } + ist_pos += cnt; + scf += cnt; + } + scf[0] = scf[1] = scf[2] = 0; +} + +static float L3_ldexp_q2(float y, int exp_q2) +{ + static const float g_expfrac[4] = { 9.31322575e-10f,7.83145814e-10f,6.58544508e-10f,5.53767716e-10f }; + int e; + do + { + e = MINIMP3_MIN(30*4, exp_q2); + y *= g_expfrac[e & 3]*(1 << 30 >> (e >> 2)); + } while ((exp_q2 -= e) > 0); + return y; +} + +static void L3_decode_scalefactors(const uint8_t *hdr, uint8_t *ist_pos, bs_t *bs, const L3_gr_info_t *gr, float *scf, int ch) +{ + static const uint8_t g_scf_partitions[3][28] = { + { 6,5,5, 5,6,5,5,5,6,5, 7,3,11,10,0,0, 7, 7, 7,0, 6, 6,6,3, 8, 8,5,0 }, + { 8,9,6,12,6,9,9,9,6,9,12,6,15,18,0,0, 6,15,12,0, 6,12,9,6, 6,18,9,0 }, + { 9,9,6,12,9,9,9,9,9,9,12,6,18,18,0,0,12,12,12,0,12, 9,9,6,15,12,9,0 } + }; + const uint8_t *scf_partition = g_scf_partitions[!!gr->n_short_sfb + !gr->n_long_sfb]; + uint8_t scf_size[4], iscf[40]; + int i, scf_shift = gr->scalefac_scale + 1, gain_exp, scfsi = gr->scfsi; + float gain; + + if (HDR_TEST_MPEG1(hdr)) + { + static const uint8_t g_scfc_decode[16] = { 0,1,2,3, 12,5,6,7, 9,10,11,13, 14,15,18,19 }; + int part = g_scfc_decode[gr->scalefac_compress]; + scf_size[1] = scf_size[0] = (uint8_t)(part >> 2); + scf_size[3] = scf_size[2] = (uint8_t)(part & 3); + } else + { + static const uint8_t g_mod[6*4] = { 5,5,4,4,5,5,4,1,4,3,1,1,5,6,6,1,4,4,4,1,4,3,1,1 }; + int k, modprod, sfc, ist = HDR_TEST_I_STEREO(hdr) && ch; + sfc = gr->scalefac_compress >> ist; + for (k = ist*3*4; sfc >= 0; sfc -= modprod, k += 4) + { + for (modprod = 1, i = 3; i >= 0; i--) + { + scf_size[i] = (uint8_t)(sfc / modprod % g_mod[k + i]); + modprod *= g_mod[k + i]; + } + } + scf_partition += k; + scfsi = -16; + } + L3_read_scalefactors(iscf, ist_pos, scf_size, scf_partition, bs, scfsi); + + if (gr->n_short_sfb) + { + int sh = 3 - scf_shift; + for (i = 0; i < gr->n_short_sfb; i += 3) + { + iscf[gr->n_long_sfb + i + 0] += gr->subblock_gain[0] << sh; + iscf[gr->n_long_sfb + i + 1] += gr->subblock_gain[1] << sh; + iscf[gr->n_long_sfb + i + 2] += gr->subblock_gain[2] << sh; + } + } else if (gr->preflag) + { + static const uint8_t g_preamp[10] = { 1,1,1,1,2,2,3,3,3,2 }; + for (i = 0; i < 10; i++) + { + iscf[11 + i] += g_preamp[i]; + } + } + + gain_exp = gr->global_gain + BITS_DEQUANTIZER_OUT*4 - 210 - (HDR_IS_MS_STEREO(hdr) ? 2 : 0); + gain = L3_ldexp_q2(1 << (MAX_SCFI/4), MAX_SCFI - gain_exp); + for (i = 0; i < (int)(gr->n_long_sfb + gr->n_short_sfb); i++) + { + scf[i] = L3_ldexp_q2(gain, iscf[i] << scf_shift); + } +} + +static const float g_pow43[129 + 16] = { + 0,-1,-2.519842f,-4.326749f,-6.349604f,-8.549880f,-10.902724f,-13.390518f,-16.000000f,-18.720754f,-21.544347f,-24.463781f,-27.473142f,-30.567351f,-33.741992f,-36.993181f, + 0,1,2.519842f,4.326749f,6.349604f,8.549880f,10.902724f,13.390518f,16.000000f,18.720754f,21.544347f,24.463781f,27.473142f,30.567351f,33.741992f,36.993181f,40.317474f,43.711787f,47.173345f,50.699631f,54.288352f,57.937408f,61.644865f,65.408941f,69.227979f,73.100443f,77.024898f,81.000000f,85.024491f,89.097188f,93.216975f,97.382800f,101.593667f,105.848633f,110.146801f,114.487321f,118.869381f,123.292209f,127.755065f,132.257246f,136.798076f,141.376907f,145.993119f,150.646117f,155.335327f,160.060199f,164.820202f,169.614826f,174.443577f,179.305980f,184.201575f,189.129918f,194.090580f,199.083145f,204.107210f,209.162385f,214.248292f,219.364564f,224.510845f,229.686789f,234.892058f,240.126328f,245.389280f,250.680604f,256.000000f,261.347174f,266.721841f,272.123723f,277.552547f,283.008049f,288.489971f,293.998060f,299.532071f,305.091761f,310.676898f,316.287249f,321.922592f,327.582707f,333.267377f,338.976394f,344.709550f,350.466646f,356.247482f,362.051866f,367.879608f,373.730522f,379.604427f,385.501143f,391.420496f,397.362314f,403.326427f,409.312672f,415.320884f,421.350905f,427.402579f,433.475750f,439.570269f,445.685987f,451.822757f,457.980436f,464.158883f,470.357960f,476.577530f,482.817459f,489.077615f,495.357868f,501.658090f,507.978156f,514.317941f,520.677324f,527.056184f,533.454404f,539.871867f,546.308458f,552.764065f,559.238575f,565.731879f,572.243870f,578.774440f,585.323483f,591.890898f,598.476581f,605.080431f,611.702349f,618.342238f,625.000000f,631.675540f,638.368763f,645.079578f +}; + +static float L3_pow_43(int x) +{ + float frac; + int sign, mult = 256; + + if (x < 129) + { + return g_pow43[16 + x]; + } + + if (x < 1024) + { + mult = 16; + x <<= 3; + } + + sign = 2*x & 64; + frac = (float)((x & 63) - sign) / ((x & ~63) + sign); + return g_pow43[16 + ((x + sign) >> 6)]*(1.f + frac*((4.f/3) + frac*(2.f/9)))*mult; +} + +static void L3_huffman(float *dst, bs_t *bs, const L3_gr_info_t *gr_info, const float *scf, int layer3gr_limit) +{ + static const int16_t tabs[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 785,785,785,785,784,784,784,784,513,513,513,513,513,513,513,513,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256, + -255,1313,1298,1282,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,290,288, + -255,1313,1298,1282,769,769,769,769,529,529,529,529,529,529,529,529,528,528,528,528,528,528,528,528,512,512,512,512,512,512,512,512,290,288, + -253,-318,-351,-367,785,785,785,785,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,819,818,547,547,275,275,275,275,561,560,515,546,289,274,288,258, + -254,-287,1329,1299,1314,1312,1057,1057,1042,1042,1026,1026,784,784,784,784,529,529,529,529,529,529,529,529,769,769,769,769,768,768,768,768,563,560,306,306,291,259, + -252,-413,-477,-542,1298,-575,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-383,-399,1107,1092,1106,1061,849,849,789,789,1104,1091,773,773,1076,1075,341,340,325,309,834,804,577,577,532,532,516,516,832,818,803,816,561,561,531,531,515,546,289,289,288,258, + -252,-429,-493,-559,1057,1057,1042,1042,529,529,529,529,529,529,529,529,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,-382,1077,-415,1106,1061,1104,849,849,789,789,1091,1076,1029,1075,834,834,597,581,340,340,339,324,804,833,532,532,832,772,818,803,817,787,816,771,290,290,290,290,288,258, + -253,-349,-414,-447,-463,1329,1299,-479,1314,1312,1057,1057,1042,1042,1026,1026,785,785,785,785,784,784,784,784,769,769,769,769,768,768,768,768,-319,851,821,-335,836,850,805,849,341,340,325,336,533,533,579,579,564,564,773,832,578,548,563,516,321,276,306,291,304,259, + -251,-572,-733,-830,-863,-879,1041,1041,784,784,784,784,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,1396,1351,1381,1366,1395,1335,1380,-559,1334,1138,1138,1063,1063,1350,1392,1031,1031,1062,1062,1364,1363,1120,1120,1333,1348,881,881,881,881,375,374,359,373,343,358,341,325,791,791,1123,1122,-703,1105,1045,-719,865,865,790,790,774,774,1104,1029,338,293,323,308,-799,-815,833,788,772,818,803,816,322,292,307,320,561,531,515,546,289,274,288,258, + -251,-525,-605,-685,-765,-831,-846,1298,1057,1057,1312,1282,785,785,785,785,784,784,784,784,769,769,769,769,512,512,512,512,512,512,512,512,1399,1398,1383,1367,1382,1396,1351,-511,1381,1366,1139,1139,1079,1079,1124,1124,1364,1349,1363,1333,882,882,882,882,807,807,807,807,1094,1094,1136,1136,373,341,535,535,881,775,867,822,774,-591,324,338,-671,849,550,550,866,864,609,609,293,336,534,534,789,835,773,-751,834,804,308,307,833,788,832,772,562,562,547,547,305,275,560,515,290,290, + -252,-397,-477,-557,-622,-653,-719,-735,-750,1329,1299,1314,1057,1057,1042,1042,1312,1282,1024,1024,785,785,785,785,784,784,784,784,769,769,769,769,-383,1127,1141,1111,1126,1140,1095,1110,869,869,883,883,1079,1109,882,882,375,374,807,868,838,881,791,-463,867,822,368,263,852,837,836,-543,610,610,550,550,352,336,534,534,865,774,851,821,850,805,593,533,579,564,773,832,578,578,548,548,577,577,307,276,306,291,516,560,259,259, + -250,-2107,-2507,-2764,-2909,-2974,-3007,-3023,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-767,-1052,-1213,-1277,-1358,-1405,-1469,-1535,-1550,-1582,-1614,-1647,-1662,-1694,-1726,-1759,-1774,-1807,-1822,-1854,-1886,1565,-1919,-1935,-1951,-1967,1731,1730,1580,1717,-1983,1729,1564,-1999,1548,-2015,-2031,1715,1595,-2047,1714,-2063,1610,-2079,1609,-2095,1323,1323,1457,1457,1307,1307,1712,1547,1641,1700,1699,1594,1685,1625,1442,1442,1322,1322,-780,-973,-910,1279,1278,1277,1262,1276,1261,1275,1215,1260,1229,-959,974,974,989,989,-943,735,478,478,495,463,506,414,-1039,1003,958,1017,927,942,987,957,431,476,1272,1167,1228,-1183,1256,-1199,895,895,941,941,1242,1227,1212,1135,1014,1014,490,489,503,487,910,1013,985,925,863,894,970,955,1012,847,-1343,831,755,755,984,909,428,366,754,559,-1391,752,486,457,924,997,698,698,983,893,740,740,908,877,739,739,667,667,953,938,497,287,271,271,683,606,590,712,726,574,302,302,738,736,481,286,526,725,605,711,636,724,696,651,589,681,666,710,364,467,573,695,466,466,301,465,379,379,709,604,665,679,316,316,634,633,436,436,464,269,424,394,452,332,438,363,347,408,393,448,331,422,362,407,392,421,346,406,391,376,375,359,1441,1306,-2367,1290,-2383,1337,-2399,-2415,1426,1321,-2431,1411,1336,-2447,-2463,-2479,1169,1169,1049,1049,1424,1289,1412,1352,1319,-2495,1154,1154,1064,1064,1153,1153,416,390,360,404,403,389,344,374,373,343,358,372,327,357,342,311,356,326,1395,1394,1137,1137,1047,1047,1365,1392,1287,1379,1334,1364,1349,1378,1318,1363,792,792,792,792,1152,1152,1032,1032,1121,1121,1046,1046,1120,1120,1030,1030,-2895,1106,1061,1104,849,849,789,789,1091,1076,1029,1090,1060,1075,833,833,309,324,532,532,832,772,818,803,561,561,531,560,515,546,289,274,288,258, + -250,-1179,-1579,-1836,-1996,-2124,-2253,-2333,-2413,-2477,-2542,-2574,-2607,-2622,-2655,1314,1313,1298,1312,1282,785,785,785,785,1040,1040,1025,1025,768,768,768,768,-766,-798,-830,-862,-895,-911,-927,-943,-959,-975,-991,-1007,-1023,-1039,-1055,-1070,1724,1647,-1103,-1119,1631,1767,1662,1738,1708,1723,-1135,1780,1615,1779,1599,1677,1646,1778,1583,-1151,1777,1567,1737,1692,1765,1722,1707,1630,1751,1661,1764,1614,1736,1676,1763,1750,1645,1598,1721,1691,1762,1706,1582,1761,1566,-1167,1749,1629,767,766,751,765,494,494,735,764,719,749,734,763,447,447,748,718,477,506,431,491,446,476,461,505,415,430,475,445,504,399,460,489,414,503,383,474,429,459,502,502,746,752,488,398,501,473,413,472,486,271,480,270,-1439,-1455,1357,-1471,-1487,-1503,1341,1325,-1519,1489,1463,1403,1309,-1535,1372,1448,1418,1476,1356,1462,1387,-1551,1475,1340,1447,1402,1386,-1567,1068,1068,1474,1461,455,380,468,440,395,425,410,454,364,467,466,464,453,269,409,448,268,432,1371,1473,1432,1417,1308,1460,1355,1446,1459,1431,1083,1083,1401,1416,1458,1445,1067,1067,1370,1457,1051,1051,1291,1430,1385,1444,1354,1415,1400,1443,1082,1082,1173,1113,1186,1066,1185,1050,-1967,1158,1128,1172,1097,1171,1081,-1983,1157,1112,416,266,375,400,1170,1142,1127,1065,793,793,1169,1033,1156,1096,1141,1111,1155,1080,1126,1140,898,898,808,808,897,897,792,792,1095,1152,1032,1125,1110,1139,1079,1124,882,807,838,881,853,791,-2319,867,368,263,822,852,837,866,806,865,-2399,851,352,262,534,534,821,836,594,594,549,549,593,593,533,533,848,773,579,579,564,578,548,563,276,276,577,576,306,291,516,560,305,305,275,259, + -251,-892,-2058,-2620,-2828,-2957,-3023,-3039,1041,1041,1040,1040,769,769,769,769,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,256,-511,-527,-543,-559,1530,-575,-591,1528,1527,1407,1526,1391,1023,1023,1023,1023,1525,1375,1268,1268,1103,1103,1087,1087,1039,1039,1523,-604,815,815,815,815,510,495,509,479,508,463,507,447,431,505,415,399,-734,-782,1262,-815,1259,1244,-831,1258,1228,-847,-863,1196,-879,1253,987,987,748,-767,493,493,462,477,414,414,686,669,478,446,461,445,474,429,487,458,412,471,1266,1264,1009,1009,799,799,-1019,-1276,-1452,-1581,-1677,-1757,-1821,-1886,-1933,-1997,1257,1257,1483,1468,1512,1422,1497,1406,1467,1496,1421,1510,1134,1134,1225,1225,1466,1451,1374,1405,1252,1252,1358,1480,1164,1164,1251,1251,1238,1238,1389,1465,-1407,1054,1101,-1423,1207,-1439,830,830,1248,1038,1237,1117,1223,1148,1236,1208,411,426,395,410,379,269,1193,1222,1132,1235,1221,1116,976,976,1192,1162,1177,1220,1131,1191,963,963,-1647,961,780,-1663,558,558,994,993,437,408,393,407,829,978,813,797,947,-1743,721,721,377,392,844,950,828,890,706,706,812,859,796,960,948,843,934,874,571,571,-1919,690,555,689,421,346,539,539,944,779,918,873,932,842,903,888,570,570,931,917,674,674,-2575,1562,-2591,1609,-2607,1654,1322,1322,1441,1441,1696,1546,1683,1593,1669,1624,1426,1426,1321,1321,1639,1680,1425,1425,1305,1305,1545,1668,1608,1623,1667,1592,1638,1666,1320,1320,1652,1607,1409,1409,1304,1304,1288,1288,1664,1637,1395,1395,1335,1335,1622,1636,1394,1394,1319,1319,1606,1621,1392,1392,1137,1137,1137,1137,345,390,360,375,404,373,1047,-2751,-2767,-2783,1062,1121,1046,-2799,1077,-2815,1106,1061,789,789,1105,1104,263,355,310,340,325,354,352,262,339,324,1091,1076,1029,1090,1060,1075,833,833,788,788,1088,1028,818,818,803,803,561,561,531,531,816,771,546,546,289,274,288,258, + -253,-317,-381,-446,-478,-509,1279,1279,-811,-1179,-1451,-1756,-1900,-2028,-2189,-2253,-2333,-2414,-2445,-2511,-2526,1313,1298,-2559,1041,1041,1040,1040,1025,1025,1024,1024,1022,1007,1021,991,1020,975,1019,959,687,687,1018,1017,671,671,655,655,1016,1015,639,639,758,758,623,623,757,607,756,591,755,575,754,559,543,543,1009,783,-575,-621,-685,-749,496,-590,750,749,734,748,974,989,1003,958,988,973,1002,942,987,957,972,1001,926,986,941,971,956,1000,910,985,925,999,894,970,-1071,-1087,-1102,1390,-1135,1436,1509,1451,1374,-1151,1405,1358,1480,1420,-1167,1507,1494,1389,1342,1465,1435,1450,1326,1505,1310,1493,1373,1479,1404,1492,1464,1419,428,443,472,397,736,526,464,464,486,457,442,471,484,482,1357,1449,1434,1478,1388,1491,1341,1490,1325,1489,1463,1403,1309,1477,1372,1448,1418,1433,1476,1356,1462,1387,-1439,1475,1340,1447,1402,1474,1324,1461,1371,1473,269,448,1432,1417,1308,1460,-1711,1459,-1727,1441,1099,1099,1446,1386,1431,1401,-1743,1289,1083,1083,1160,1160,1458,1445,1067,1067,1370,1457,1307,1430,1129,1129,1098,1098,268,432,267,416,266,400,-1887,1144,1187,1082,1173,1113,1186,1066,1050,1158,1128,1143,1172,1097,1171,1081,420,391,1157,1112,1170,1142,1127,1065,1169,1049,1156,1096,1141,1111,1155,1080,1126,1154,1064,1153,1140,1095,1048,-2159,1125,1110,1137,-2175,823,823,1139,1138,807,807,384,264,368,263,868,838,853,791,867,822,852,837,866,806,865,790,-2319,851,821,836,352,262,850,805,849,-2399,533,533,835,820,336,261,578,548,563,577,532,532,832,772,562,562,547,547,305,275,560,515,290,290,288,258 }; + static const uint8_t tab32[] = { 130,162,193,209,44,28,76,140,9,9,9,9,9,9,9,9,190,254,222,238,126,94,157,157,109,61,173,205 }; + static const uint8_t tab33[] = { 252,236,220,204,188,172,156,140,124,108,92,76,60,44,28,12 }; + static const int16_t tabindex[2*16] = { 0,32,64,98,0,132,180,218,292,364,426,538,648,746,0,1126,1460,1460,1460,1460,1460,1460,1460,1460,1842,1842,1842,1842,1842,1842,1842,1842 }; + static const uint8_t g_linbits[] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,3,4,6,8,10,13,4,5,6,7,8,9,11,13 }; + +#define PEEK_BITS(n) (bs_cache >> (32 - n)) +#define FLUSH_BITS(n) { bs_cache <<= (n); bs_sh += (n); } +#define CHECK_BITS while (bs_sh >= 0) { bs_cache |= (uint32_t)*bs_next_ptr++ << bs_sh; bs_sh -= 8; } +#define BSPOS ((bs_next_ptr - bs->buf)*8 - 24 + bs_sh) + + float one = 0.0f; + int ireg = 0, big_val_cnt = gr_info->big_values; + const uint8_t *sfb = gr_info->sfbtab; + const uint8_t *bs_next_ptr = bs->buf + bs->pos/8; + uint32_t bs_cache = (((bs_next_ptr[0]*256u + bs_next_ptr[1])*256u + bs_next_ptr[2])*256u + bs_next_ptr[3]) << (bs->pos & 7); + int pairs_to_decode, np, bs_sh = (bs->pos & 7) - 8; + bs_next_ptr += 4; + + while (big_val_cnt > 0) + { + int tab_num = gr_info->table_select[ireg]; + int sfb_cnt = gr_info->region_count[ireg++]; + const int16_t *codebook = tabs + tabindex[tab_num]; + int linbits = g_linbits[tab_num]; + if (linbits) + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + if (lsb == 15) + { + lsb += PEEK_BITS(linbits); + FLUSH_BITS(linbits); + CHECK_BITS; + *dst = one*L3_pow_43(lsb)*((int32_t)bs_cache < 0 ? -1: 1); + } else + { + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + } + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } else + { + do + { + np = *sfb++ / 2; + pairs_to_decode = MINIMP3_MIN(big_val_cnt, np); + one = *scf++; + do + { + int j, w = 5; + int leaf = codebook[PEEK_BITS(w)]; + while (leaf < 0) + { + FLUSH_BITS(w); + w = leaf & 7; + leaf = codebook[PEEK_BITS(w) - (leaf >> 3)]; + } + FLUSH_BITS(leaf >> 8); + + for (j = 0; j < 2; j++, dst++, leaf >>= 4) + { + int lsb = leaf & 0x0F; + *dst = g_pow43[16 + lsb - 16*(bs_cache >> 31)]*one; + FLUSH_BITS(lsb ? 1 : 0); + } + CHECK_BITS; + } while (--pairs_to_decode); + } while ((big_val_cnt -= np) > 0 && --sfb_cnt >= 0); + } + } + + for (np = 1 - big_val_cnt;; dst += 4) + { + const uint8_t *codebook_count1 = (gr_info->count1_table) ? tab33 : tab32; + int leaf = codebook_count1[PEEK_BITS(4)]; + if (!(leaf & 8)) + { + leaf = codebook_count1[(leaf >> 3) + (bs_cache << 4 >> (32 - (leaf & 3)))]; + } + FLUSH_BITS(leaf & 7); + if (BSPOS > layer3gr_limit) + { + break; + } +#define RELOAD_SCALEFACTOR if (!--np) { np = *sfb++/2; if (!np) break; one = *scf++; } +#define DEQ_COUNT1(s) if (leaf & (128 >> s)) { dst[s] = ((int32_t)bs_cache < 0) ? -one : one; FLUSH_BITS(1) } + RELOAD_SCALEFACTOR; + DEQ_COUNT1(0); + DEQ_COUNT1(1); + RELOAD_SCALEFACTOR; + DEQ_COUNT1(2); + DEQ_COUNT1(3); + CHECK_BITS; + } + + bs->pos = layer3gr_limit; +} + +static void L3_midside_stereo(float *left, int n) +{ + int i = 0; + float *right = left + 576; +#if HAVE_SIMD + if (have_simd()) for (; i < n - 3; i += 4) + { + f4 vl = VLD(left + i); + f4 vr = VLD(right + i); + VSTORE(left + i, VADD(vl, vr)); + VSTORE(right + i, VSUB(vl, vr)); + } +#endif /* HAVE_SIMD */ + for (; i < n; i++) + { + float a = left[i]; + float b = right[i]; + left[i] = a + b; + right[i] = a - b; + } +} + +static void L3_intensity_stereo_band(float *left, int n, float kl, float kr) +{ + int i; + for (i = 0; i < n; i++) + { + left[i + 576] = left[i]*kr; + left[i] = left[i]*kl; + } +} + +static void L3_stereo_top_band(const float *right, const uint8_t *sfb, int nbands, int max_band[3]) +{ + int i, k; + + max_band[0] = max_band[1] = max_band[2] = -1; + + for (i = 0; i < nbands; i++) + { + for (k = 0; k < sfb[i]; k += 2) + { + if (right[k] != 0 || right[k + 1] != 0) + { + max_band[i % 3] = i; + break; + } + } + right += sfb[i]; + } +} + +static void L3_stereo_process(float *left, const uint8_t *ist_pos, const uint8_t *sfb, const uint8_t *hdr, int max_band[3], int mpeg2_sh) +{ + static const float g_pan[7*2] = { 0,1,0.21132487f,0.78867513f,0.36602540f,0.63397460f,0.5f,0.5f,0.63397460f,0.36602540f,0.78867513f,0.21132487f,1,0 }; + unsigned i, max_pos = HDR_TEST_MPEG1(hdr) ? 7 : 64; + + for (i = 0; sfb[i]; i++) + { + unsigned ipos = ist_pos[i]; + if ((int)i > max_band[i % 3] && ipos < max_pos) + { + float kl, kr, s = HDR_TEST_MS_STEREO(hdr) ? 1.41421356f : 1; + if (HDR_TEST_MPEG1(hdr)) + { + kl = g_pan[2*ipos]; + kr = g_pan[2*ipos + 1]; + } else + { + kl = 1; + kr = L3_ldexp_q2(1, (ipos + 1) >> 1 << mpeg2_sh); + if (ipos & 1) + { + kl = kr; + kr = 1; + } + } + L3_intensity_stereo_band(left, sfb[i], kl*s, kr*s); + } else if (HDR_TEST_MS_STEREO(hdr)) + { + L3_midside_stereo(left, sfb[i]); + } + left += sfb[i]; + } +} + +static void L3_intensity_stereo(float *left, uint8_t *ist_pos, const L3_gr_info_t *gr, const uint8_t *hdr) +{ + int max_band[3], n_sfb = gr->n_long_sfb + gr->n_short_sfb; + int i, max_blocks = gr->n_short_sfb ? 3 : 1; + + L3_stereo_top_band(left + 576, gr->sfbtab, n_sfb, max_band); + if (gr->n_long_sfb) + { + max_band[0] = max_band[1] = max_band[2] = MINIMP3_MAX(MINIMP3_MAX(max_band[0], max_band[1]), max_band[2]); + } + for (i = 0; i < max_blocks; i++) + { + int default_pos = HDR_TEST_MPEG1(hdr) ? 3 : 0; + int itop = n_sfb - max_blocks + i; + int prev = itop - max_blocks; + ist_pos[itop] = max_band[i] >= prev ? default_pos : ist_pos[prev]; + } + L3_stereo_process(left, ist_pos, gr->sfbtab, hdr, max_band, gr[1].scalefac_compress & 1); +} + +static void L3_reorder(float *grbuf, float *scratch, const uint8_t *sfb) +{ + int i, len; + float *src = grbuf, *dst = scratch; + + for (;0 != (len = *sfb); sfb += 3, src += 2*len) + { + for (i = 0; i < len; i++, src++) + { + *dst++ = src[0*len]; + *dst++ = src[1*len]; + *dst++ = src[2*len]; + } + } + memcpy(grbuf, scratch, (dst - scratch)*sizeof(float)); +} + +static void L3_antialias(float *grbuf, int nbands) +{ + static const float g_aa[2][8] = { + {0.85749293f,0.88174200f,0.94962865f,0.98331459f,0.99551782f,0.99916056f,0.99989920f,0.99999316f}, + {0.51449576f,0.47173197f,0.31337745f,0.18191320f,0.09457419f,0.04096558f,0.01419856f,0.00369997f} + }; + + for (; nbands > 0; nbands--, grbuf += 18) + { + int i = 0; +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vu = VLD(grbuf + 18 + i); + f4 vd = VLD(grbuf + 14 - i); + f4 vc0 = VLD(g_aa[0] + i); + f4 vc1 = VLD(g_aa[1] + i); + vd = VREV(vd); + VSTORE(grbuf + 18 + i, VSUB(VMUL(vu, vc0), VMUL(vd, vc1))); + vd = VADD(VMUL(vu, vc1), VMUL(vd, vc0)); + VSTORE(grbuf + 14 - i, VREV(vd)); + } +#endif /* HAVE_SIMD */ +#ifndef MINIMP3_ONLY_SIMD + for(; i < 8; i++) + { + float u = grbuf[18 + i]; + float d = grbuf[17 - i]; + grbuf[18 + i] = u*g_aa[0][i] - d*g_aa[1][i]; + grbuf[17 - i] = u*g_aa[1][i] + d*g_aa[0][i]; + } +#endif /* MINIMP3_ONLY_SIMD */ + } +} + +static void L3_dct3_9(float *y) +{ + float s0, s1, s2, s3, s4, s5, s6, s7, s8, t0, t2, t4; + + s0 = y[0]; s2 = y[2]; s4 = y[4]; s6 = y[6]; s8 = y[8]; + t0 = s0 + s6*0.5f; + s0 -= s6; + t4 = (s4 + s2)*0.93969262f; + t2 = (s8 + s2)*0.76604444f; + s6 = (s4 - s8)*0.17364818f; + s4 += s8 - s2; + + s2 = s0 - s4*0.5f; + y[4] = s4 + s0; + s8 = t0 - t2 + s6; + s0 = t0 - t4 + t2; + s4 = t0 + t4 - s6; + + s1 = y[1]; s3 = y[3]; s5 = y[5]; s7 = y[7]; + + s3 *= 0.86602540f; + t0 = (s5 + s1)*0.98480775f; + t4 = (s5 - s7)*0.34202014f; + t2 = (s1 + s7)*0.64278761f; + s1 = (s1 - s5 - s7)*0.86602540f; + + s5 = t0 - s3 - t2; + s7 = t4 - s3 - t0; + s3 = t4 + s3 - t2; + + y[0] = s4 - s7; + y[1] = s2 + s1; + y[2] = s0 - s3; + y[3] = s8 + s5; + y[5] = s8 - s5; + y[6] = s0 + s3; + y[7] = s2 - s1; + y[8] = s4 + s7; +} + +static void L3_imdct36(float *grbuf, float *overlap, const float *window, int nbands) +{ + int i, j; + static const float g_twid9[18] = { + 0.73727734f,0.79335334f,0.84339145f,0.88701083f,0.92387953f,0.95371695f,0.97629601f,0.99144486f,0.99904822f,0.67559021f,0.60876143f,0.53729961f,0.46174861f,0.38268343f,0.30070580f,0.21643961f,0.13052619f,0.04361938f + }; + + for (j = 0; j < nbands; j++, grbuf += 18, overlap += 9) + { + float co[9], si[9]; + co[0] = -grbuf[0]; + si[0] = grbuf[17]; + for (i = 0; i < 4; i++) + { + si[8 - 2*i] = grbuf[4*i + 1] - grbuf[4*i + 2]; + co[1 + 2*i] = grbuf[4*i + 1] + grbuf[4*i + 2]; + si[7 - 2*i] = grbuf[4*i + 4] - grbuf[4*i + 3]; + co[2 + 2*i] = -(grbuf[4*i + 3] + grbuf[4*i + 4]); + } + L3_dct3_9(co); + L3_dct3_9(si); + + si[1] = -si[1]; + si[3] = -si[3]; + si[5] = -si[5]; + si[7] = -si[7]; + + i = 0; + +#if HAVE_SIMD + if (have_simd()) for (; i < 8; i += 4) + { + f4 vovl = VLD(overlap + i); + f4 vc = VLD(co + i); + f4 vs = VLD(si + i); + f4 vr0 = VLD(g_twid9 + i); + f4 vr1 = VLD(g_twid9 + 9 + i); + f4 vw0 = VLD(window + i); + f4 vw1 = VLD(window + 9 + i); + f4 vsum = VADD(VMUL(vc, vr1), VMUL(vs, vr0)); + VSTORE(overlap + i, VSUB(VMUL(vc, vr0), VMUL(vs, vr1))); + VSTORE(grbuf + i, VSUB(VMUL(vovl, vw0), VMUL(vsum, vw1))); + vsum = VADD(VMUL(vovl, vw1), VMUL(vsum, vw0)); + VSTORE(grbuf + 14 - i, VREV(vsum)); + } +#endif /* HAVE_SIMD */ + for (; i < 9; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid9[9 + i] + si[i]*g_twid9[0 + i]; + overlap[i] = co[i]*g_twid9[0 + i] - si[i]*g_twid9[9 + i]; + grbuf[i] = ovl*window[0 + i] - sum*window[9 + i]; + grbuf[17 - i] = ovl*window[9 + i] + sum*window[0 + i]; + } + } +} + +static void L3_idct3(float x0, float x1, float x2, float *dst) +{ + float m1 = x1*0.86602540f; + float a1 = x0 - x2*0.5f; + dst[1] = x0 + x2; + dst[0] = a1 + m1; + dst[2] = a1 - m1; +} + +static void L3_imdct12(float *x, float *dst, float *overlap) +{ + static const float g_twid3[6] = { 0.79335334f,0.92387953f,0.99144486f, 0.60876143f,0.38268343f,0.13052619f }; + float co[3], si[3]; + int i; + + L3_idct3(-x[0], x[6] + x[3], x[12] + x[9], co); + L3_idct3(x[15], x[12] - x[9], x[6] - x[3], si); + si[1] = -si[1]; + + for (i = 0; i < 3; i++) + { + float ovl = overlap[i]; + float sum = co[i]*g_twid3[3 + i] + si[i]*g_twid3[0 + i]; + overlap[i] = co[i]*g_twid3[0 + i] - si[i]*g_twid3[3 + i]; + dst[i] = ovl*g_twid3[2 - i] - sum*g_twid3[5 - i]; + dst[5 - i] = ovl*g_twid3[5 - i] + sum*g_twid3[2 - i]; + } +} + +static void L3_imdct_short(float *grbuf, float *overlap, int nbands) +{ + for (;nbands > 0; nbands--, overlap += 9, grbuf += 18) + { + float tmp[18]; + memcpy(tmp, grbuf, sizeof(tmp)); + memcpy(grbuf, overlap, 6*sizeof(float)); + L3_imdct12(tmp, grbuf + 6, overlap + 6); + L3_imdct12(tmp + 1, grbuf + 12, overlap + 6); + L3_imdct12(tmp + 2, overlap, overlap + 6); + } +} + +static void L3_change_sign(float *grbuf) +{ + int b, i; + for (b = 0, grbuf += 18; b < 32; b += 2, grbuf += 36) + for (i = 1; i < 18; i += 2) + grbuf[i] = -grbuf[i]; +} + +static void L3_imdct_gr(float *grbuf, float *overlap, unsigned block_type, unsigned n_long_bands) +{ + static const float g_mdct_window[2][18] = { + { 0.99904822f,0.99144486f,0.97629601f,0.95371695f,0.92387953f,0.88701083f,0.84339145f,0.79335334f,0.73727734f,0.04361938f,0.13052619f,0.21643961f,0.30070580f,0.38268343f,0.46174861f,0.53729961f,0.60876143f,0.67559021f }, + { 1,1,1,1,1,1,0.99144486f,0.92387953f,0.79335334f,0,0,0,0,0,0,0.13052619f,0.38268343f,0.60876143f } + }; + if (n_long_bands) + { + L3_imdct36(grbuf, overlap, g_mdct_window[0], n_long_bands); + grbuf += 18*n_long_bands; + overlap += 9*n_long_bands; + } + if (block_type == SHORT_BLOCK_TYPE) + L3_imdct_short(grbuf, overlap, 32 - n_long_bands); + else + L3_imdct36(grbuf, overlap, g_mdct_window[block_type == STOP_BLOCK_TYPE], 32 - n_long_bands); +} + +static void L3_save_reservoir(mp3dec_t *h, mp3dec_scratch_t *s) +{ + int pos = (s->bs.pos + 7)/8u; + int remains = s->bs.limit/8u - pos; + if (remains > MAX_BITRESERVOIR_BYTES) + { + pos += remains - MAX_BITRESERVOIR_BYTES; + remains = MAX_BITRESERVOIR_BYTES; + } + if (remains > 0) + { + memmove(h->reserv_buf, s->maindata + pos, remains); + } + h->reserv = remains; +} + +static int L3_restore_reservoir(mp3dec_t *h, bs_t *bs, mp3dec_scratch_t *s, int main_data_begin) +{ + int frame_bytes = (bs->limit - bs->pos)/8; + int bytes_have = MINIMP3_MIN(h->reserv, main_data_begin); + memcpy(s->maindata, h->reserv_buf + MINIMP3_MAX(0, h->reserv - main_data_begin), MINIMP3_MIN(h->reserv, main_data_begin)); + memcpy(s->maindata + bytes_have, bs->buf + bs->pos/8, frame_bytes); + bs_init(&s->bs, s->maindata, bytes_have + frame_bytes); + return h->reserv >= main_data_begin; +} + +static void L3_decode(mp3dec_t *h, mp3dec_scratch_t *s, L3_gr_info_t *gr_info, int nch) +{ + int ch; + + for (ch = 0; ch < nch; ch++) + { + int layer3gr_limit = s->bs.pos + gr_info[ch].part_23_length; + L3_decode_scalefactors(h->header, s->ist_pos[ch], &s->bs, gr_info + ch, s->scf, ch); + L3_huffman(s->grbuf[ch], &s->bs, gr_info + ch, s->scf, layer3gr_limit); + } + + if (HDR_TEST_I_STEREO(h->header)) + { + L3_intensity_stereo(s->grbuf[0], s->ist_pos[1], gr_info, h->header); + } else if (HDR_IS_MS_STEREO(h->header)) + { + L3_midside_stereo(s->grbuf[0], 576); + } + + for (ch = 0; ch < nch; ch++, gr_info++) + { + int aa_bands = 31; + int n_long_bands = (gr_info->mixed_block_flag ? 2 : 0) << (int)(HDR_GET_MY_SAMPLE_RATE(h->header) == 2); + + if (gr_info->n_short_sfb) + { + aa_bands = n_long_bands - 1; + L3_reorder(s->grbuf[ch] + n_long_bands*18, s->syn[0], gr_info->sfbtab + gr_info->n_long_sfb); + } + + L3_antialias(s->grbuf[ch], aa_bands); + L3_imdct_gr(s->grbuf[ch], h->mdct_overlap[ch], gr_info->block_type, n_long_bands); + L3_change_sign(s->grbuf[ch]); + } +} + +static void mp3d_DCT_II(float *grbuf, int n) +{ + static const float g_sec[24] = { + 10.19000816f,0.50060302f,0.50241929f,3.40760851f,0.50547093f,0.52249861f,2.05778098f,0.51544732f,0.56694406f,1.48416460f,0.53104258f,0.64682180f,1.16943991f,0.55310392f,0.78815460f,0.97256821f,0.58293498f,1.06067765f,0.83934963f,0.62250412f,1.72244716f,0.74453628f,0.67480832f,5.10114861f + }; + int i, k = 0; +#if HAVE_SIMD + if (have_simd()) for (; k < n; k += 4) + { + f4 t[4][8], *x; + float *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + f4 x0 = VLD(&y[i*18]); + f4 x1 = VLD(&y[(15 - i)*18]); + f4 x2 = VLD(&y[(16 + i)*18]); + f4 x3 = VLD(&y[(31 - i)*18]); + f4 t0 = VADD(x0, x3); + f4 t1 = VADD(x1, x2); + f4 t2 = VMUL_S(VSUB(x1, x2), g_sec[3*i + 0]); + f4 t3 = VMUL_S(VSUB(x0, x3), g_sec[3*i + 1]); + x[0] = VADD(t0, t1); + x[8] = VMUL_S(VSUB(t0, t1), g_sec[3*i + 2]); + x[16] = VADD(t3, t2); + x[24] = VMUL_S(VSUB(t3, t2), g_sec[3*i + 2]); + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + f4 x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = VSUB(x0, x7); x0 = VADD(x0, x7); + x7 = VSUB(x1, x6); x1 = VADD(x1, x6); + x6 = VSUB(x2, x5); x2 = VADD(x2, x5); + x5 = VSUB(x3, x4); x3 = VADD(x3, x4); + x4 = VSUB(x0, x3); x0 = VADD(x0, x3); + x3 = VSUB(x1, x2); x1 = VADD(x1, x2); + x[0] = VADD(x0, x1); + x[4] = VMUL_S(VSUB(x0, x1), 0.70710677f); + x5 = VADD(x5, x6); + x6 = VMUL_S(VADD(x6, x7), 0.70710677f); + x7 = VADD(x7, xt); + x3 = VMUL_S(VADD(x3, x4), 0.70710677f); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); /* rotate by PI/8 */ + x7 = VADD(x7, VMUL_S(x5, 0.382683432f)); + x5 = VSUB(x5, VMUL_S(x7, 0.198912367f)); + x0 = VSUB(xt, x6); xt = VADD(xt, x6); + x[1] = VMUL_S(VADD(xt, x7), 0.50979561f); + x[2] = VMUL_S(VADD(x4, x3), 0.54119611f); + x[3] = VMUL_S(VSUB(x0, x5), 0.60134488f); + x[5] = VMUL_S(VADD(x0, x5), 0.89997619f); + x[6] = VMUL_S(VSUB(x4, x3), 1.30656302f); + x[7] = VMUL_S(VSUB(xt, x7), 2.56291556f); + } + + if (k > n - 3) + { +#if HAVE_SSE +#define VSAVE2(i, v) _mm_storel_pi((__m64 *)(void*)&y[i*18], v) +#else /* HAVE_SSE */ +#define VSAVE2(i, v) vst1_f32((float32_t *)&y[i*18], vget_low_f32(v)) +#endif /* HAVE_SSE */ + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE2(0, t[0][i]); + VSAVE2(1, VADD(t[2][i], s)); + VSAVE2(2, VADD(t[1][i], t[1][i + 1])); + VSAVE2(3, VADD(t[2][1 + i], s)); + } + VSAVE2(0, t[0][7]); + VSAVE2(1, VADD(t[2][7], t[3][7])); + VSAVE2(2, t[1][7]); + VSAVE2(3, t[3][7]); + } else + { +#define VSAVE4(i, v) VSTORE(&y[i*18], v) + for (i = 0; i < 7; i++, y += 4*18) + { + f4 s = VADD(t[3][i], t[3][i + 1]); + VSAVE4(0, t[0][i]); + VSAVE4(1, VADD(t[2][i], s)); + VSAVE4(2, VADD(t[1][i], t[1][i + 1])); + VSAVE4(3, VADD(t[2][1 + i], s)); + } + VSAVE4(0, t[0][7]); + VSAVE4(1, VADD(t[2][7], t[3][7])); + VSAVE4(2, t[1][7]); + VSAVE4(3, t[3][7]); + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} +#else /* MINIMP3_ONLY_SIMD */ + for (; k < n; k++) + { + float t[4][8], *x, *y = grbuf + k; + + for (x = t[0], i = 0; i < 8; i++, x++) + { + float x0 = y[i*18]; + float x1 = y[(15 - i)*18]; + float x2 = y[(16 + i)*18]; + float x3 = y[(31 - i)*18]; + float t0 = x0 + x3; + float t1 = x1 + x2; + float t2 = (x1 - x2)*g_sec[3*i + 0]; + float t3 = (x0 - x3)*g_sec[3*i + 1]; + x[0] = t0 + t1; + x[8] = (t0 - t1)*g_sec[3*i + 2]; + x[16] = t3 + t2; + x[24] = (t3 - t2)*g_sec[3*i + 2]; + } + for (x = t[0], i = 0; i < 4; i++, x += 8) + { + float x0 = x[0], x1 = x[1], x2 = x[2], x3 = x[3], x4 = x[4], x5 = x[5], x6 = x[6], x7 = x[7], xt; + xt = x0 - x7; x0 += x7; + x7 = x1 - x6; x1 += x6; + x6 = x2 - x5; x2 += x5; + x5 = x3 - x4; x3 += x4; + x4 = x0 - x3; x0 += x3; + x3 = x1 - x2; x1 += x2; + x[0] = x0 + x1; + x[4] = (x0 - x1)*0.70710677f; + x5 = x5 + x6; + x6 = (x6 + x7)*0.70710677f; + x7 = x7 + xt; + x3 = (x3 + x4)*0.70710677f; + x5 -= x7*0.198912367f; /* rotate by PI/8 */ + x7 += x5*0.382683432f; + x5 -= x7*0.198912367f; + x0 = xt - x6; xt += x6; + x[1] = (xt + x7)*0.50979561f; + x[2] = (x4 + x3)*0.54119611f; + x[3] = (x0 - x5)*0.60134488f; + x[5] = (x0 + x5)*0.89997619f; + x[6] = (x4 - x3)*1.30656302f; + x[7] = (xt - x7)*2.56291556f; + + } + for (i = 0; i < 7; i++, y += 4*18) + { + y[0*18] = t[0][i]; + y[1*18] = t[2][i] + t[3][i] + t[3][i + 1]; + y[2*18] = t[1][i] + t[1][i + 1]; + y[3*18] = t[2][i + 1] + t[3][i] + t[3][i + 1]; + } + y[0*18] = t[0][7]; + y[1*18] = t[2][7] + t[3][7]; + y[2*18] = t[1][7]; + y[3*18] = t[3][7]; + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +#ifndef MINIMP3_FLOAT_OUTPUT +static int16_t mp3d_scale_pcm(float sample) +{ +#if HAVE_ARMV6 + int32_t s32 = (int32_t)(sample + .5f); + s32 -= (s32 < 0); + int16_t s = (int16_t)minimp3_clip_int16_arm(s32); +#else + if (sample >= 32766.5) return (int16_t) 32767; + if (sample <= -32767.5) return (int16_t)-32768; + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ +#endif + return s; +} +#else /* MINIMP3_FLOAT_OUTPUT */ +static float mp3d_scale_pcm(float sample) +{ + return sample*(1.f/32768.f); +} +#endif /* MINIMP3_FLOAT_OUTPUT */ + +static void mp3d_synth_pair(mp3d_sample_t *pcm, int nch, const float *z) +{ + float a; + a = (z[14*64] - z[ 0]) * 29; + a += (z[ 1*64] + z[13*64]) * 213; + a += (z[12*64] - z[ 2*64]) * 459; + a += (z[ 3*64] + z[11*64]) * 2037; + a += (z[10*64] - z[ 4*64]) * 5153; + a += (z[ 5*64] + z[ 9*64]) * 6574; + a += (z[ 8*64] - z[ 6*64]) * 37489; + a += z[ 7*64] * 75038; + pcm[0] = mp3d_scale_pcm(a); + + z += 2; + a = z[14*64] * 104; + a += z[12*64] * 1567; + a += z[10*64] * 9727; + a += z[ 8*64] * 64019; + a += z[ 6*64] * -9975; + a += z[ 4*64] * -45; + a += z[ 2*64] * 146; + a += z[ 0*64] * -5; + pcm[16*nch] = mp3d_scale_pcm(a); +} + +static void mp3d_synth(float *xl, mp3d_sample_t *dstl, int nch, float *lins) +{ + int i; + float *xr = xl + 576*(nch - 1); + mp3d_sample_t *dstr = dstl + (nch - 1); + + static const float g_win[] = { + -1,26,-31,208,218,401,-519,2063,2000,4788,-5517,7134,5959,35640,-39336,74992, + -1,24,-35,202,222,347,-581,2080,1952,4425,-5879,7640,5288,33791,-41176,74856, + -1,21,-38,196,225,294,-645,2087,1893,4063,-6237,8092,4561,31947,-43006,74630, + -1,19,-41,190,227,244,-711,2085,1822,3705,-6589,8492,3776,30112,-44821,74313, + -1,17,-45,183,228,197,-779,2075,1739,3351,-6935,8840,2935,28289,-46617,73908, + -1,16,-49,176,228,153,-848,2057,1644,3004,-7271,9139,2037,26482,-48390,73415, + -2,14,-53,169,227,111,-919,2032,1535,2663,-7597,9389,1082,24694,-50137,72835, + -2,13,-58,161,224,72,-991,2001,1414,2330,-7910,9592,70,22929,-51853,72169, + -2,11,-63,154,221,36,-1064,1962,1280,2006,-8209,9750,-998,21189,-53534,71420, + -2,10,-68,147,215,2,-1137,1919,1131,1692,-8491,9863,-2122,19478,-55178,70590, + -3,9,-73,139,208,-29,-1210,1870,970,1388,-8755,9935,-3300,17799,-56778,69679, + -3,8,-79,132,200,-57,-1283,1817,794,1095,-8998,9966,-4533,16155,-58333,68692, + -4,7,-85,125,189,-83,-1356,1759,605,814,-9219,9959,-5818,14548,-59838,67629, + -4,7,-91,117,177,-106,-1428,1698,402,545,-9416,9916,-7154,12980,-61289,66494, + -5,6,-97,111,163,-127,-1498,1634,185,288,-9585,9838,-8540,11455,-62684,65290 + }; + float *zlin = lins + 15*64; + const float *w = g_win; + + zlin[4*15] = xl[18*16]; + zlin[4*15 + 1] = xr[18*16]; + zlin[4*15 + 2] = xl[0]; + zlin[4*15 + 3] = xr[0]; + + zlin[4*31] = xl[1 + 18*16]; + zlin[4*31 + 1] = xr[1 + 18*16]; + zlin[4*31 + 2] = xl[1]; + zlin[4*31 + 3] = xr[1]; + + mp3d_synth_pair(dstr, nch, lins + 4*15 + 1); + mp3d_synth_pair(dstr + 32*nch, nch, lins + 4*15 + 64 + 1); + mp3d_synth_pair(dstl, nch, lins + 4*15); + mp3d_synth_pair(dstl + 32*nch, nch, lins + 4*15 + 64); + +#if HAVE_SIMD + if (have_simd()) for (i = 14; i >= 0; i--) + { +#define VLOAD(k) f4 w0 = VSET(*w++); f4 w1 = VSET(*w++); f4 vz = VLD(&zlin[4*i - 64*k]); f4 vy = VLD(&zlin[4*i - 64*(15 - k)]); +#define V0(k) { VLOAD(k) b = VADD(VMUL(vz, w1), VMUL(vy, w0)) ; a = VSUB(VMUL(vz, w0), VMUL(vy, w1)); } +#define V1(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vz, w0), VMUL(vy, w1))); } +#define V2(k) { VLOAD(k) b = VADD(b, VADD(VMUL(vz, w1), VMUL(vy, w0))); a = VADD(a, VSUB(VMUL(vy, w1), VMUL(vz, w0))); } + f4 a, b; + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*i + 64] = xl[1 + 18*(1 + i)]; + zlin[4*i + 64 + 1] = xr[1 + 18*(1 + i)]; + zlin[4*i - 64 + 2] = xl[18*(1 + i)]; + zlin[4*i - 64 + 3] = xr[18*(1 + i)]; + + V0(0) V2(1) V1(2) V2(3) V1(4) V2(5) V1(6) V2(7) + + { +#ifndef MINIMP3_FLOAT_OUTPUT +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + dstr[(15 - i)*nch] = _mm_extract_epi16(pcm8, 1); + dstr[(17 + i)*nch] = _mm_extract_epi16(pcm8, 5); + dstl[(15 - i)*nch] = _mm_extract_epi16(pcm8, 0); + dstl[(17 + i)*nch] = _mm_extract_epi16(pcm8, 4); + dstr[(47 - i)*nch] = _mm_extract_epi16(pcm8, 3); + dstr[(49 + i)*nch] = _mm_extract_epi16(pcm8, 7); + dstl[(47 - i)*nch] = _mm_extract_epi16(pcm8, 2); + dstl[(49 + i)*nch] = _mm_extract_epi16(pcm8, 6); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(dstr + (15 - i)*nch, pcma, 1); + vst1_lane_s16(dstr + (17 + i)*nch, pcmb, 1); + vst1_lane_s16(dstl + (15 - i)*nch, pcma, 0); + vst1_lane_s16(dstl + (17 + i)*nch, pcmb, 0); + vst1_lane_s16(dstr + (47 - i)*nch, pcma, 3); + vst1_lane_s16(dstr + (49 + i)*nch, pcmb, 3); + vst1_lane_s16(dstl + (47 - i)*nch, pcma, 2); + vst1_lane_s16(dstl + (49 + i)*nch, pcmb, 2); +#endif /* HAVE_SSE */ + +#else /* MINIMP3_FLOAT_OUTPUT */ + + static const f4 g_scale = { 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f, 1.0f/32768.0f }; + a = VMUL(a, g_scale); + b = VMUL(b, g_scale); +#if HAVE_SSE + _mm_store_ss(dstr + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstr + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(1, 1, 1, 1))); + _mm_store_ss(dstl + (15 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstl + (17 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(0, 0, 0, 0))); + _mm_store_ss(dstr + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstr + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(3, 3, 3, 3))); + _mm_store_ss(dstl + (47 - i)*nch, _mm_shuffle_ps(a, a, _MM_SHUFFLE(2, 2, 2, 2))); + _mm_store_ss(dstl + (49 + i)*nch, _mm_shuffle_ps(b, b, _MM_SHUFFLE(2, 2, 2, 2))); +#else /* HAVE_SSE */ + vst1q_lane_f32(dstr + (15 - i)*nch, a, 1); + vst1q_lane_f32(dstr + (17 + i)*nch, b, 1); + vst1q_lane_f32(dstl + (15 - i)*nch, a, 0); + vst1q_lane_f32(dstl + (17 + i)*nch, b, 0); + vst1q_lane_f32(dstr + (47 - i)*nch, a, 3); + vst1q_lane_f32(dstr + (49 + i)*nch, b, 3); + vst1q_lane_f32(dstl + (47 - i)*nch, a, 2); + vst1q_lane_f32(dstl + (49 + i)*nch, b, 2); +#endif /* HAVE_SSE */ +#endif /* MINIMP3_FLOAT_OUTPUT */ + } + } else +#endif /* HAVE_SIMD */ +#ifdef MINIMP3_ONLY_SIMD + {} +#else /* MINIMP3_ONLY_SIMD */ + for (i = 14; i >= 0; i--) + { +#define LOAD(k) float w0 = *w++; float w1 = *w++; float *vz = &zlin[4*i - k*64]; float *vy = &zlin[4*i - (15 - k)*64]; +#define S0(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] = vz[j]*w1 + vy[j]*w0, a[j] = vz[j]*w0 - vy[j]*w1; } +#define S1(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vz[j]*w0 - vy[j]*w1; } +#define S2(k) { int j; LOAD(k); for (j = 0; j < 4; j++) b[j] += vz[j]*w1 + vy[j]*w0, a[j] += vy[j]*w1 - vz[j]*w0; } + float a[4], b[4]; + + zlin[4*i] = xl[18*(31 - i)]; + zlin[4*i + 1] = xr[18*(31 - i)]; + zlin[4*i + 2] = xl[1 + 18*(31 - i)]; + zlin[4*i + 3] = xr[1 + 18*(31 - i)]; + zlin[4*(i + 16)] = xl[1 + 18*(1 + i)]; + zlin[4*(i + 16) + 1] = xr[1 + 18*(1 + i)]; + zlin[4*(i - 16) + 2] = xl[18*(1 + i)]; + zlin[4*(i - 16) + 3] = xr[18*(1 + i)]; + + S0(0) S2(1) S1(2) S2(3) S1(4) S2(5) S1(6) S2(7) + + dstr[(15 - i)*nch] = mp3d_scale_pcm(a[1]); + dstr[(17 + i)*nch] = mp3d_scale_pcm(b[1]); + dstl[(15 - i)*nch] = mp3d_scale_pcm(a[0]); + dstl[(17 + i)*nch] = mp3d_scale_pcm(b[0]); + dstr[(47 - i)*nch] = mp3d_scale_pcm(a[3]); + dstr[(49 + i)*nch] = mp3d_scale_pcm(b[3]); + dstl[(47 - i)*nch] = mp3d_scale_pcm(a[2]); + dstl[(49 + i)*nch] = mp3d_scale_pcm(b[2]); + } +#endif /* MINIMP3_ONLY_SIMD */ +} + +static void mp3d_synth_granule(float *qmf_state, float *grbuf, int nbands, int nch, mp3d_sample_t *pcm, float *lins) +{ + int i; + for (i = 0; i < nch; i++) + { + mp3d_DCT_II(grbuf + 576*i, nbands); + } + + memcpy(lins, qmf_state, sizeof(float)*15*64); + + for (i = 0; i < nbands; i += 2) + { + mp3d_synth(grbuf + i, pcm + 32*nch*i, nch, lins + i*64); + } +#ifndef MINIMP3_NONSTANDARD_BUT_LOGICAL + if (nch == 1) + { + for (i = 0; i < 15*64; i += 2) + { + qmf_state[i] = lins[nbands*64 + i]; + } + } else +#endif /* MINIMP3_NONSTANDARD_BUT_LOGICAL */ + { + memcpy(qmf_state, lins + nbands*64, sizeof(float)*15*64); + } +} + +static int mp3d_match_frame(const uint8_t *hdr, int mp3_bytes, int frame_bytes) +{ + int i, nmatch; + for (i = 0, nmatch = 0; nmatch < MAX_FRAME_SYNC_MATCHES; nmatch++) + { + i += hdr_frame_bytes(hdr + i, frame_bytes) + hdr_padding(hdr + i); + if (i + HDR_SIZE > mp3_bytes) + return nmatch > 0; + if (!hdr_compare(hdr, hdr + i)) + return 0; + } + return 1; +} + +static int mp3d_find_frame(const uint8_t *mp3, int mp3_bytes, int *free_format_bytes, int *ptr_frame_bytes) +{ + int i, k; + for (i = 0; i < mp3_bytes - HDR_SIZE; i++, mp3++) + { + if (hdr_valid(mp3)) + { + int frame_bytes = hdr_frame_bytes(mp3, *free_format_bytes); + int frame_and_padding = frame_bytes + hdr_padding(mp3); + + for (k = HDR_SIZE; !frame_bytes && k < MAX_FREE_FORMAT_FRAME_SIZE && i + 2*k < mp3_bytes - HDR_SIZE; k++) + { + if (hdr_compare(mp3, mp3 + k)) + { + int fb = k - hdr_padding(mp3); + int nextfb = fb + hdr_padding(mp3 + k); + if (i + k + nextfb + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + k + nextfb)) + continue; + frame_and_padding = k; + frame_bytes = fb; + *free_format_bytes = fb; + } + } + if ((frame_bytes && i + frame_and_padding <= mp3_bytes && + mp3d_match_frame(mp3, mp3_bytes - i, frame_bytes)) || + (!i && frame_and_padding == mp3_bytes)) + { + *ptr_frame_bytes = frame_and_padding; + return i; + } + *free_format_bytes = 0; + } + } + *ptr_frame_bytes = 0; + return mp3_bytes; +} + +void mp3dec_init(mp3dec_t *dec) +{ + dec->header[0] = 0; +} + +int mp3dec_decode_frame(mp3dec_t *dec, const uint8_t *mp3, int mp3_bytes, mp3d_sample_t *pcm, mp3dec_frame_info_t *info) +{ + int i = 0, igr, frame_size = 0, success = 1; + const uint8_t *hdr; + bs_t bs_frame[1]; + mp3dec_scratch_t scratch; + + if (mp3_bytes > 4 && dec->header[0] == 0xff && hdr_compare(dec->header, mp3)) + { + frame_size = hdr_frame_bytes(mp3, dec->free_format_bytes) + hdr_padding(mp3); + if (frame_size != mp3_bytes && (frame_size + HDR_SIZE > mp3_bytes || !hdr_compare(mp3, mp3 + frame_size))) + { + frame_size = 0; + } + } + if (!frame_size) + { + memset(dec, 0, sizeof(mp3dec_t)); + i = mp3d_find_frame(mp3, mp3_bytes, &dec->free_format_bytes, &frame_size); + if (!frame_size || i + frame_size > mp3_bytes) + { + info->frame_bytes = i; + return 0; + } + } + + hdr = mp3 + i; + memcpy(dec->header, hdr, HDR_SIZE); + info->frame_bytes = i + frame_size; + info->frame_offset = i; + info->channels = HDR_IS_MONO(hdr) ? 1 : 2; + info->hz = hdr_sample_rate_hz(hdr); + info->layer = 4 - HDR_GET_LAYER(hdr); + info->bitrate_kbps = hdr_bitrate_kbps(hdr); + + if (!pcm) + { + return hdr_frame_samples(hdr); + } + + bs_init(bs_frame, hdr + HDR_SIZE, frame_size - HDR_SIZE); + if (HDR_IS_CRC(hdr)) + { + get_bits(bs_frame, 16); + } + + if (info->layer == 3) + { + int main_data_begin = L3_read_side_info(bs_frame, scratch.gr_info, hdr); + if (main_data_begin < 0 || bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + success = L3_restore_reservoir(dec, bs_frame, &scratch, main_data_begin); + if (success) + { + for (igr = 0; igr < (HDR_TEST_MPEG1(hdr) ? 2 : 1); igr++, pcm += 576*info->channels) + { + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + L3_decode(dec, &scratch, scratch.gr_info + igr*info->channels, info->channels); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 18, info->channels, pcm, scratch.syn[0]); + } + } + L3_save_reservoir(dec, &scratch); + } else + { +#ifdef MINIMP3_ONLY_MP3 + return 0; +#else /* MINIMP3_ONLY_MP3 */ + L12_scale_info sci[1]; + L12_read_scale_info(hdr, bs_frame, sci); + + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + for (i = 0, igr = 0; igr < 3; igr++) + { + if (12 == (i += L12_dequantize_granule(scratch.grbuf[0] + i, bs_frame, sci, info->layer | 1))) + { + i = 0; + L12_apply_scf_384(sci, sci->scf + igr, scratch.grbuf[0]); + mp3d_synth_granule(dec->qmf_state, scratch.grbuf[0], 12, info->channels, pcm, scratch.syn[0]); + memset(scratch.grbuf[0], 0, 576*2*sizeof(float)); + pcm += 384*info->channels; + } + if (bs_frame->pos > bs_frame->limit) + { + mp3dec_init(dec); + return 0; + } + } +#endif /* MINIMP3_ONLY_MP3 */ + } + return success*hdr_frame_samples(dec->header); +} + +#ifdef MINIMP3_FLOAT_OUTPUT +void mp3dec_f32_to_s16(const float *in, int16_t *out, int num_samples) +{ + int i = 0; +#if HAVE_SIMD + int aligned_count = num_samples & ~7; + for(; i < aligned_count; i += 8) + { + static const f4 g_scale = { 32768.0f, 32768.0f, 32768.0f, 32768.0f }; + f4 a = VMUL(VLD(&in[i ]), g_scale); + f4 b = VMUL(VLD(&in[i+4]), g_scale); +#if HAVE_SSE + static const f4 g_max = { 32767.0f, 32767.0f, 32767.0f, 32767.0f }; + static const f4 g_min = { -32768.0f, -32768.0f, -32768.0f, -32768.0f }; + __m128i pcm8 = _mm_packs_epi32(_mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(a, g_max), g_min)), + _mm_cvtps_epi32(_mm_max_ps(_mm_min_ps(b, g_max), g_min))); + out[i ] = _mm_extract_epi16(pcm8, 0); + out[i+1] = _mm_extract_epi16(pcm8, 1); + out[i+2] = _mm_extract_epi16(pcm8, 2); + out[i+3] = _mm_extract_epi16(pcm8, 3); + out[i+4] = _mm_extract_epi16(pcm8, 4); + out[i+5] = _mm_extract_epi16(pcm8, 5); + out[i+6] = _mm_extract_epi16(pcm8, 6); + out[i+7] = _mm_extract_epi16(pcm8, 7); +#else /* HAVE_SSE */ + int16x4_t pcma, pcmb; + a = VADD(a, VSET(0.5f)); + b = VADD(b, VSET(0.5f)); + pcma = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(a), vreinterpretq_s32_u32(vcltq_f32(a, VSET(0))))); + pcmb = vqmovn_s32(vqaddq_s32(vcvtq_s32_f32(b), vreinterpretq_s32_u32(vcltq_f32(b, VSET(0))))); + vst1_lane_s16(out+i , pcma, 0); + vst1_lane_s16(out+i+1, pcma, 1); + vst1_lane_s16(out+i+2, pcma, 2); + vst1_lane_s16(out+i+3, pcma, 3); + vst1_lane_s16(out+i+4, pcmb, 0); + vst1_lane_s16(out+i+5, pcmb, 1); + vst1_lane_s16(out+i+6, pcmb, 2); + vst1_lane_s16(out+i+7, pcmb, 3); +#endif /* HAVE_SSE */ + } +#endif /* HAVE_SIMD */ + for(; i < num_samples; i++) + { + float sample = in[i] * 32768.0f; + if (sample >= 32766.5) + out[i] = (int16_t) 32767; + else if (sample <= -32767.5) + out[i] = (int16_t)-32768; + else + { + int16_t s = (int16_t)(sample + .5f); + s -= (s < 0); /* away from zero, to be compliant */ + out[i] = s; + } + } +} +#endif /* MINIMP3_FLOAT_OUTPUT */ +#endif /* MINIMP3_IMPLEMENTATION && !_MINIMP3_IMPLEMENTATION_GUARD */ diff --git a/src/third_party/minimp3/minimp3_ex.h b/src/third_party/minimp3/minimp3_ex.h new file mode 100644 index 0000000..df6a85e --- /dev/null +++ b/src/third_party/minimp3/minimp3_ex.h @@ -0,0 +1,1363 @@ +#ifndef MINIMP3_EXT_H +#define MINIMP3_EXT_H +/* + https://github.com/lieff/minimp3 + To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. + This software is distributed without any warranty. + See . +*/ +#include "minimp3.h" + +/* flags for mp3dec_ex_open_* functions */ +#define MP3D_SEEK_TO_BYTE 0 /* mp3dec_ex_seek seeks to byte in stream */ +#define MP3D_SEEK_TO_SAMPLE 1 /* mp3dec_ex_seek precisely seeks to sample using index (created during duration calculation scan or when mp3dec_ex_seek called) */ +#define MP3D_DO_NOT_SCAN 2 /* do not scan whole stream for duration if vbrtag not found, mp3dec_ex_t::samples will be filled only if mp3dec_ex_t::vbr_tag_found == 1 */ + +/* compile-time config */ +#define MINIMP3_PREDECODE_FRAMES 2 /* frames to pre-decode and skip after seek (to fill internal structures) */ +/*#define MINIMP3_SEEK_IDX_LINEAR_SEARCH*/ /* define to use linear index search instead of binary search on seek */ +#define MINIMP3_IO_SIZE (128*1024) /* io buffer size for streaming functions, must be greater than MINIMP3_BUF_SIZE */ +#define MINIMP3_BUF_SIZE (16*1024) /* buffer which can hold minimum 10 consecutive mp3 frames (~16KB) worst case */ +#define MINIMP3_ENABLE_RING 0 /* WIP enable hardware magic ring buffer if available, to make less input buffer memmove(s) in callback IO mode */ + +/* return error codes */ +#define MP3D_E_PARAM -1 +#define MP3D_E_MEMORY -2 +#define MP3D_E_IOERROR -3 +#define MP3D_E_USER -4 /* can be used to stop processing from callbacks without indicating specific error */ +#define MP3D_E_DECODE -5 /* decode error which can't be safely skipped, such as sample rate, layer and channels change */ + +typedef struct +{ + mp3d_sample_t *buffer; + size_t samples; /* channels included, byte size = samples*sizeof(mp3d_sample_t) */ + int channels, hz, layer, avg_bitrate_kbps; +} mp3dec_file_info_t; + +typedef struct +{ + const uint8_t *buffer; + size_t size; +} mp3dec_map_info_t; + +typedef struct +{ + uint64_t sample; + uint64_t offset; +} mp3dec_frame_t; + +typedef struct +{ + mp3dec_frame_t *frames; + size_t num_frames, capacity; +} mp3dec_index_t; + +typedef size_t (*MP3D_READ_CB)(void *buf, size_t size, void *user_data); +typedef int (*MP3D_SEEK_CB)(uint64_t position, void *user_data); + +typedef struct +{ + MP3D_READ_CB read; + void *read_data; + MP3D_SEEK_CB seek; + void *seek_data; +} mp3dec_io_t; + +typedef struct mp3dec_ex +{ + mp3dec_t mp3d; + mp3dec_map_info_t file; + mp3dec_io_t *io; + mp3dec_index_t index; + uint64_t offset, samples, detected_samples, cur_sample, start_offset, end_offset; + mp3dec_frame_info_t info; + mp3d_sample_t buffer[MINIMP3_MAX_SAMPLES_PER_FRAME]; + size_t input_consumed, input_filled; + int is_file, flags, vbr_tag_found, indexes_built; + int free_format_bytes; + int buffer_samples, buffer_consumed, to_skip, start_delay; + int last_error; +} mp3dec_ex_t; + +typedef int (*MP3D_ITERATE_CB)(void *user_data, const uint8_t *frame, int frame_size, int free_format_bytes, size_t buf_size, uint64_t offset, mp3dec_frame_info_t *info); +typedef int (*MP3D_PROGRESS_CB)(void *user_data, size_t file_size, uint64_t offset, mp3dec_frame_info_t *info); + +#ifdef __cplusplus +extern "C" { +#endif + +/* detect mp3/mpa format */ +int mp3dec_detect_buf(const uint8_t *buf, size_t buf_size); +int mp3dec_detect_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size); +/* decode whole buffer block */ +int mp3dec_load_buf(mp3dec_t *dec, const uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +int mp3dec_load_cb(mp3dec_t *dec, mp3dec_io_t *io, uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +/* iterate through frames */ +int mp3dec_iterate_buf(const uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data); +int mp3dec_iterate_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data); +/* streaming decoder with seeking capability */ +int mp3dec_ex_open_buf(mp3dec_ex_t *dec, const uint8_t *buf, size_t buf_size, int flags); +int mp3dec_ex_open_cb(mp3dec_ex_t *dec, mp3dec_io_t *io, int flags); +void mp3dec_ex_close(mp3dec_ex_t *dec); +int mp3dec_ex_seek(mp3dec_ex_t *dec, uint64_t position); +size_t mp3dec_ex_read(mp3dec_ex_t *dec, mp3d_sample_t *buf, size_t samples); +#ifndef MINIMP3_NO_STDIO +/* stdio versions of file detect, load, iterate and stream */ +int mp3dec_detect(const char *file_name); +int mp3dec_load(mp3dec_t *dec, const char *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +int mp3dec_iterate(const char *file_name, MP3D_ITERATE_CB callback, void *user_data); +int mp3dec_ex_open(mp3dec_ex_t *dec, const char *file_name, int flags); +#ifdef _WIN32 +int mp3dec_detect_w(const wchar_t *file_name); +int mp3dec_load_w(mp3dec_t *dec, const wchar_t *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data); +int mp3dec_iterate_w(const wchar_t *file_name, MP3D_ITERATE_CB callback, void *user_data); +int mp3dec_ex_open_w(mp3dec_ex_t *dec, const wchar_t *file_name, int flags); +#endif +#endif + +#ifdef __cplusplus +} +#endif +#endif /*MINIMP3_EXT_H*/ + +#ifdef MINIMP3_IMPLEMENTATION +#include + +static void mp3dec_skip_id3v1(const uint8_t *buf, size_t *pbuf_size) +{ + size_t buf_size = *pbuf_size; +#ifndef MINIMP3_NOSKIP_ID3V1 + if (buf_size >= 128 && !memcmp(buf + buf_size - 128, "TAG", 3)) + { + buf_size -= 128; + if (buf_size >= 227 && !memcmp(buf + buf_size - 227, "TAG+", 4)) + buf_size -= 227; + } +#endif +#ifndef MINIMP3_NOSKIP_APEV2 + if (buf_size > 32 && !memcmp(buf + buf_size - 32, "APETAGEX", 8)) + { + buf_size -= 32; + const uint8_t *tag = buf + buf_size + 8 + 4; + uint32_t tag_size = (uint32_t)(tag[3] << 24) | (tag[2] << 16) | (tag[1] << 8) | tag[0]; + if (buf_size >= tag_size) + buf_size -= tag_size; + } +#endif + *pbuf_size = buf_size; +} + +static size_t mp3dec_skip_id3v2(const uint8_t *buf, size_t buf_size) +{ +#define MINIMP3_ID3_DETECT_SIZE 10 +#ifndef MINIMP3_NOSKIP_ID3V2 + if (buf_size >= MINIMP3_ID3_DETECT_SIZE && !memcmp(buf, "ID3", 3) && !((buf[5] & 15) || (buf[6] & 0x80) || (buf[7] & 0x80) || (buf[8] & 0x80) || (buf[9] & 0x80))) + { + size_t id3v2size = (((buf[6] & 0x7f) << 21) | ((buf[7] & 0x7f) << 14) | ((buf[8] & 0x7f) << 7) | (buf[9] & 0x7f)) + 10; + if ((buf[5] & 16)) + id3v2size += 10; /* footer */ + return id3v2size; + } +#endif + return 0; +} + +static void mp3dec_skip_id3(const uint8_t **pbuf, size_t *pbuf_size) +{ + uint8_t *buf = (uint8_t *)(*pbuf); + size_t buf_size = *pbuf_size; + size_t id3v2size = mp3dec_skip_id3v2(buf, buf_size); + if (id3v2size) + { + if (id3v2size >= buf_size) + id3v2size = buf_size; + buf += id3v2size; + buf_size -= id3v2size; + } + mp3dec_skip_id3v1(buf, &buf_size); + *pbuf = (const uint8_t *)buf; + *pbuf_size = buf_size; +} + +static int mp3dec_check_vbrtag(const uint8_t *frame, int frame_size, uint32_t *frames, int *delay, int *padding) +{ + static const char g_xing_tag[4] = { 'X', 'i', 'n', 'g' }; + static const char g_info_tag[4] = { 'I', 'n', 'f', 'o' }; +#define FRAMES_FLAG 1 +#define BYTES_FLAG 2 +#define TOC_FLAG 4 +#define VBR_SCALE_FLAG 8 + /* Side info offsets after header: + / Mono Stereo + / MPEG1 17 32 + / MPEG2 & 2.5 9 17*/ + bs_t bs[1]; + L3_gr_info_t gr_info[4]; + bs_init(bs, frame + HDR_SIZE, frame_size - HDR_SIZE); + if (HDR_IS_CRC(frame)) + get_bits(bs, 16); + if (L3_read_side_info(bs, gr_info, frame) < 0) + return 0; /* side info corrupted */ + + const uint8_t *tag = frame + HDR_SIZE + bs->pos/8; + if (memcmp(g_xing_tag, tag, 4) && memcmp(g_info_tag, tag, 4)) + return 0; + int flags = tag[7]; + if (!((flags & FRAMES_FLAG))) + return -1; + tag += 8; + *frames = (uint32_t)(tag[0] << 24) | (tag[1] << 16) | (tag[2] << 8) | tag[3]; + tag += 4; + if (flags & BYTES_FLAG) + tag += 4; + if (flags & TOC_FLAG) + tag += 100; + if (flags & VBR_SCALE_FLAG) + tag += 4; + *delay = *padding = 0; + if (*tag) + { /* extension, LAME, Lavc, etc. Should be the same structure. */ + tag += 21; + if (tag - frame + 14 >= frame_size) + return 0; + *delay = ((tag[0] << 4) | (tag[1] >> 4)) + (528 + 1); + *padding = (((tag[1] & 0xF) << 8) | tag[2]) - (528 + 1); + } + return 1; +} + +int mp3dec_detect_buf(const uint8_t *buf, size_t buf_size) +{ + return mp3dec_detect_cb(0, (uint8_t *)buf, buf_size); +} + +int mp3dec_detect_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size) +{ + if (!buf || (size_t)-1 == buf_size || (io && buf_size < MINIMP3_BUF_SIZE)) + return MP3D_E_PARAM; + size_t filled = buf_size; + if (io) + { + if (io->seek(0, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (filled > MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_IOERROR; + } + if (filled < MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_USER; /* too small, can't be mp3/mpa */ + if (mp3dec_skip_id3v2(buf, filled)) + return 0; /* id3v2 tag is enough evidence */ + if (io) + { + size_t readed = io->read(buf + MINIMP3_ID3_DETECT_SIZE, buf_size - MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (readed > (buf_size - MINIMP3_ID3_DETECT_SIZE)) + return MP3D_E_IOERROR; + filled += readed; + if (filled < MINIMP3_BUF_SIZE) + mp3dec_skip_id3v1(buf, &filled); + } else + { + mp3dec_skip_id3v1(buf, &filled); + if (filled > MINIMP3_BUF_SIZE) + filled = MINIMP3_BUF_SIZE; + } + int free_format_bytes, frame_size; + mp3d_find_frame(buf, filled, &free_format_bytes, &frame_size); + if (frame_size) + return 0; /* MAX_FRAME_SYNC_MATCHES consecutive frames found */ + return MP3D_E_USER; +} + +int mp3dec_load_buf(mp3dec_t *dec, const uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + return mp3dec_load_cb(dec, 0, (uint8_t *)buf, buf_size, info, progress_cb, user_data); +} + +int mp3dec_load_cb(mp3dec_t *dec, mp3dec_io_t *io, uint8_t *buf, size_t buf_size, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + if (!dec || !buf || !info || (size_t)-1 == buf_size || (io && buf_size < MINIMP3_BUF_SIZE)) + return MP3D_E_PARAM; + uint64_t detected_samples = 0; + size_t orig_buf_size = buf_size; + int to_skip = 0; + mp3dec_frame_info_t frame_info; + memset(info, 0, sizeof(*info)); + memset(&frame_info, 0, sizeof(frame_info)); + + /* skip id3 */ + size_t filled = 0, consumed = 0; + int eof = 0, ret = 0; + if (io) + { + if (io->seek(0, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (filled > MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_IOERROR; + if (MINIMP3_ID3_DETECT_SIZE != filled) + return 0; + size_t id3v2size = mp3dec_skip_id3v2(buf, filled); + if (id3v2size) + { + if (io->seek(id3v2size, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, buf_size, io->read_data); + if (filled > buf_size) + return MP3D_E_IOERROR; + } else + { + size_t readed = io->read(buf + MINIMP3_ID3_DETECT_SIZE, buf_size - MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (readed > (buf_size - MINIMP3_ID3_DETECT_SIZE)) + return MP3D_E_IOERROR; + filled += readed; + } + if (filled < MINIMP3_BUF_SIZE) + mp3dec_skip_id3v1(buf, &filled); + } else + { + mp3dec_skip_id3((const uint8_t **)&buf, &buf_size); + if (!buf_size) + return 0; + } + /* try to make allocation size assumption by first frame or vbr tag */ + mp3dec_init(dec); + int samples; + do + { + uint32_t frames; + int i, delay, padding, free_format_bytes = 0, frame_size = 0; + const uint8_t *hdr; + if (io) + { + if (!eof && filled - consumed < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove(buf, buf + consumed, filled - consumed); + filled -= consumed; + consumed = 0; + size_t readed = io->read(buf + filled, buf_size - filled, io->read_data); + if (readed > (buf_size - filled)) + return MP3D_E_IOERROR; + if (readed != (buf_size - filled)) + eof = 1; + filled += readed; + if (eof) + mp3dec_skip_id3v1(buf, &filled); + } + i = mp3d_find_frame(buf + consumed, filled - consumed, &free_format_bytes, &frame_size); + consumed += i; + hdr = buf + consumed; + } else + { + i = mp3d_find_frame(buf, buf_size, &free_format_bytes, &frame_size); + buf += i; + buf_size -= i; + hdr = buf; + } + if (i && !frame_size) + continue; + if (!frame_size) + return 0; + frame_info.channels = HDR_IS_MONO(hdr) ? 1 : 2; + frame_info.hz = hdr_sample_rate_hz(hdr); + frame_info.layer = 4 - HDR_GET_LAYER(hdr); + frame_info.bitrate_kbps = hdr_bitrate_kbps(hdr); + frame_info.frame_bytes = frame_size; + samples = hdr_frame_samples(hdr)*frame_info.channels; + if (3 != frame_info.layer) + break; + int ret = mp3dec_check_vbrtag(hdr, frame_size, &frames, &delay, &padding); + if (ret > 0) + { + padding *= frame_info.channels; + to_skip = delay*frame_info.channels; + detected_samples = samples*(uint64_t)frames; + if (detected_samples >= (uint64_t)to_skip) + detected_samples -= to_skip; + if (padding > 0 && detected_samples >= (uint64_t)padding) + detected_samples -= padding; + if (!detected_samples) + return 0; + } + if (ret) + { + if (io) + { + consumed += frame_size; + } else + { + buf += frame_size; + buf_size -= frame_size; + } + } + break; + } while(1); + size_t allocated = MINIMP3_MAX_SAMPLES_PER_FRAME*sizeof(mp3d_sample_t); + if (detected_samples) + allocated += detected_samples*sizeof(mp3d_sample_t); + else + allocated += (buf_size/frame_info.frame_bytes)*samples*sizeof(mp3d_sample_t); + info->buffer = (mp3d_sample_t*)malloc(allocated); + if (!info->buffer) + return MP3D_E_MEMORY; + /* save info */ + info->channels = frame_info.channels; + info->hz = frame_info.hz; + info->layer = frame_info.layer; + /* decode all frames */ + size_t avg_bitrate_kbps = 0, frames = 0; + do + { + if ((allocated - info->samples*sizeof(mp3d_sample_t)) < MINIMP3_MAX_SAMPLES_PER_FRAME*sizeof(mp3d_sample_t)) + { + allocated *= 2; + mp3d_sample_t *alloc_buf = (mp3d_sample_t*)realloc(info->buffer, allocated); + if (!alloc_buf) + return MP3D_E_MEMORY; + info->buffer = alloc_buf; + } + if (io) + { + if (!eof && filled - consumed < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove(buf, buf + consumed, filled - consumed); + filled -= consumed; + consumed = 0; + size_t readed = io->read(buf + filled, buf_size - filled, io->read_data); + if (readed != (buf_size - filled)) + eof = 1; + filled += readed; + if (eof) + mp3dec_skip_id3v1(buf, &filled); + } + samples = mp3dec_decode_frame(dec, buf + consumed, filled - consumed, info->buffer + info->samples, &frame_info); + consumed += frame_info.frame_bytes; + } else + { + samples = mp3dec_decode_frame(dec, buf, MINIMP3_MIN(buf_size, (size_t)INT_MAX), info->buffer + info->samples, &frame_info); + buf += frame_info.frame_bytes; + buf_size -= frame_info.frame_bytes; + } + if (samples) + { + if (info->hz != frame_info.hz || info->layer != frame_info.layer) + { + ret = MP3D_E_DECODE; + break; + } + if (info->channels && info->channels != frame_info.channels) + { +#ifdef MINIMP3_ALLOW_MONO_STEREO_TRANSITION + info->channels = 0; /* mark file with mono-stereo transition */ +#else + ret = MP3D_E_DECODE; + break; +#endif + } + samples *= frame_info.channels; + if (to_skip) + { + size_t skip = MINIMP3_MIN(samples, to_skip); + to_skip -= skip; + samples -= skip; + memmove(info->buffer, info->buffer + skip, samples); + } + info->samples += samples; + avg_bitrate_kbps += frame_info.bitrate_kbps; + frames++; + if (progress_cb) + { + ret = progress_cb(user_data, orig_buf_size, orig_buf_size - buf_size, &frame_info); + if (ret) + break; + } + } + } while (frame_info.frame_bytes); + if (detected_samples && info->samples > detected_samples) + info->samples = detected_samples; /* cut padding */ + /* reallocate to normal buffer size */ + if (allocated != info->samples*sizeof(mp3d_sample_t)) + { + mp3d_sample_t *alloc_buf = (mp3d_sample_t*)realloc(info->buffer, info->samples*sizeof(mp3d_sample_t)); + if (!alloc_buf && info->samples) + return MP3D_E_MEMORY; + info->buffer = alloc_buf; + } + if (frames) + info->avg_bitrate_kbps = avg_bitrate_kbps/frames; + return ret; +} + +int mp3dec_iterate_buf(const uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data) +{ + const uint8_t *orig_buf = buf; + if (!buf || (size_t)-1 == buf_size || !callback) + return MP3D_E_PARAM; + /* skip id3 */ + mp3dec_skip_id3(&buf, &buf_size); + if (!buf_size) + return 0; + mp3dec_frame_info_t frame_info; + memset(&frame_info, 0, sizeof(frame_info)); + do + { + int free_format_bytes = 0, frame_size = 0, ret; + int i = mp3d_find_frame(buf, buf_size, &free_format_bytes, &frame_size); + buf += i; + buf_size -= i; + if (i && !frame_size) + continue; + if (!frame_size) + break; + const uint8_t *hdr = buf; + frame_info.channels = HDR_IS_MONO(hdr) ? 1 : 2; + frame_info.hz = hdr_sample_rate_hz(hdr); + frame_info.layer = 4 - HDR_GET_LAYER(hdr); + frame_info.bitrate_kbps = hdr_bitrate_kbps(hdr); + frame_info.frame_bytes = frame_size; + + if (callback) + { + if ((ret = callback(user_data, hdr, frame_size, free_format_bytes, buf_size, hdr - orig_buf, &frame_info))) + return ret; + } + buf += frame_size; + buf_size -= frame_size; + } while (1); + return 0; +} + +int mp3dec_iterate_cb(mp3dec_io_t *io, uint8_t *buf, size_t buf_size, MP3D_ITERATE_CB callback, void *user_data) +{ + if (!io || !buf || (size_t)-1 == buf_size || buf_size < MINIMP3_BUF_SIZE || !callback) + return MP3D_E_PARAM; + size_t filled = io->read(buf, MINIMP3_ID3_DETECT_SIZE, io->read_data), consumed = 0; + uint64_t readed = 0; + mp3dec_frame_info_t frame_info; + int eof = 0; + memset(&frame_info, 0, sizeof(frame_info)); + if (filled > MINIMP3_ID3_DETECT_SIZE) + return MP3D_E_IOERROR; + if (MINIMP3_ID3_DETECT_SIZE != filled) + return 0; + size_t id3v2size = mp3dec_skip_id3v2(buf, filled); + if (id3v2size) + { + if (io->seek(id3v2size, io->seek_data)) + return MP3D_E_IOERROR; + filled = io->read(buf, buf_size, io->read_data); + if (filled > buf_size) + return MP3D_E_IOERROR; + readed += id3v2size; + } else + { + size_t readed = io->read(buf + MINIMP3_ID3_DETECT_SIZE, buf_size - MINIMP3_ID3_DETECT_SIZE, io->read_data); + if (readed > (buf_size - MINIMP3_ID3_DETECT_SIZE)) + return MP3D_E_IOERROR; + filled += readed; + } + if (filled < MINIMP3_BUF_SIZE) + mp3dec_skip_id3v1(buf, &filled); + do + { + int free_format_bytes = 0, frame_size = 0, ret; + int i = mp3d_find_frame(buf + consumed, filled - consumed, &free_format_bytes, &frame_size); + if (i && !frame_size) + { + consumed += i; + continue; + } + if (!frame_size) + break; + const uint8_t *hdr = buf + consumed + i; + frame_info.channels = HDR_IS_MONO(hdr) ? 1 : 2; + frame_info.hz = hdr_sample_rate_hz(hdr); + frame_info.layer = 4 - HDR_GET_LAYER(hdr); + frame_info.bitrate_kbps = hdr_bitrate_kbps(hdr); + frame_info.frame_bytes = frame_size; + + readed += i; + if (callback) + { + if ((ret = callback(user_data, hdr, frame_size, free_format_bytes, filled - consumed, readed, &frame_info))) + return ret; + } + readed += frame_size; + consumed += i + frame_size; + if (!eof && filled - consumed < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove(buf, buf + consumed, filled - consumed); + filled -= consumed; + consumed = 0; + size_t readed = io->read(buf + filled, buf_size - filled, io->read_data); + if (readed > (buf_size - filled)) + return MP3D_E_IOERROR; + if (readed != (buf_size - filled)) + eof = 1; + filled += readed; + if (eof) + mp3dec_skip_id3v1(buf, &filled); + } + } while (1); + return 0; +} + +static int mp3dec_load_index(void *user_data, const uint8_t *frame, int frame_size, int free_format_bytes, size_t buf_size, uint64_t offset, mp3dec_frame_info_t *info) +{ + mp3dec_frame_t *idx_frame; + mp3dec_ex_t *dec = (mp3dec_ex_t *)user_data; + if (!dec->index.frames && !dec->start_offset) + { /* detect VBR tag and try to avoid full scan */ + uint32_t frames; + int delay, padding; + dec->info = *info; + dec->start_offset = dec->offset = offset; + dec->end_offset = offset + buf_size; + dec->free_format_bytes = free_format_bytes; /* should not change */ + if (3 == dec->info.layer) + { + int ret = mp3dec_check_vbrtag(frame, frame_size, &frames, &delay, &padding); + if (ret) + dec->start_offset = dec->offset = offset + frame_size; + if (ret > 0) + { + padding *= info->channels; + dec->start_delay = dec->to_skip = delay*info->channels; + dec->samples = hdr_frame_samples(frame)*info->channels*(uint64_t)frames; + if (dec->samples >= (uint64_t)dec->start_delay) + dec->samples -= dec->start_delay; + if (padding > 0 && dec->samples >= (uint64_t)padding) + dec->samples -= padding; + dec->detected_samples = dec->samples; + dec->vbr_tag_found = 1; + return MP3D_E_USER; + } else if (ret < 0) + return 0; + } + } + if (dec->flags & MP3D_DO_NOT_SCAN) + return MP3D_E_USER; + if (dec->index.num_frames + 1 > dec->index.capacity) + { + if (!dec->index.capacity) + dec->index.capacity = 4096; + else + dec->index.capacity *= 2; + mp3dec_frame_t *alloc_buf = (mp3dec_frame_t *)realloc((void*)dec->index.frames, sizeof(mp3dec_frame_t)*dec->index.capacity); + if (!alloc_buf) + return MP3D_E_MEMORY; + dec->index.frames = alloc_buf; + } + idx_frame = &dec->index.frames[dec->index.num_frames++]; + idx_frame->offset = offset; + idx_frame->sample = dec->samples; + if (!dec->buffer_samples && dec->index.num_frames < 256) + { /* for some cutted mp3 frames, bit-reservoir not filled and decoding can't be started from first frames */ + /* try to decode up to 255 first frames till samples starts to decode */ + dec->buffer_samples = mp3dec_decode_frame(&dec->mp3d, frame, MINIMP3_MIN(buf_size, (size_t)INT_MAX), dec->buffer, info); + dec->samples += dec->buffer_samples*info->channels; + } else + dec->samples += hdr_frame_samples(frame)*info->channels; + return 0; +} + +int mp3dec_ex_open_buf(mp3dec_ex_t *dec, const uint8_t *buf, size_t buf_size, int flags) +{ + if (!dec || !buf || (size_t)-1 == buf_size || (flags & (~3))) + return MP3D_E_PARAM; + memset(dec, 0, sizeof(*dec)); + dec->file.buffer = buf; + dec->file.size = buf_size; + dec->flags = flags; + mp3dec_init(&dec->mp3d); + int ret = mp3dec_iterate_buf(dec->file.buffer, dec->file.size, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + mp3dec_init(&dec->mp3d); + dec->buffer_samples = 0; + dec->indexes_built = !(dec->vbr_tag_found || (flags & MP3D_DO_NOT_SCAN)); + dec->flags &= (~MP3D_DO_NOT_SCAN); + return 0; +} + +#ifndef MINIMP3_SEEK_IDX_LINEAR_SEARCH +static size_t mp3dec_idx_binary_search(mp3dec_index_t *idx, uint64_t position) +{ + size_t end = idx->num_frames, start = 0, index = 0; + while (start <= end) + { + size_t mid = (start + end) / 2; + if (idx->frames[mid].sample >= position) + { /* move left side. */ + if (idx->frames[mid].sample == position) + return mid; + end = mid - 1; + } else + { /* move to right side */ + index = mid; + start = mid + 1; + if (start == idx->num_frames) + break; + } + } + return index; +} +#endif + +int mp3dec_ex_seek(mp3dec_ex_t *dec, uint64_t position) +{ + size_t i; + if (!dec) + return MP3D_E_PARAM; + if (!(dec->flags & MP3D_SEEK_TO_SAMPLE)) + { + if (dec->io) + { + dec->offset = position; + } else + { + dec->offset = MINIMP3_MIN(position, dec->file.size); + } + dec->cur_sample = 0; + goto do_exit; + } + dec->cur_sample = position; + position += dec->start_delay; + if (0 == position) + { /* optimize seek to zero, no index needed */ +seek_zero: + dec->offset = dec->start_offset; + dec->to_skip = 0; + goto do_exit; + } + if (!dec->indexes_built) + { /* no index created yet (vbr tag used to calculate track length or MP3D_DO_NOT_SCAN open flag used) */ + dec->indexes_built = 1; + dec->samples = 0; + dec->buffer_samples = 0; + if (dec->io) + { + if (dec->io->seek(dec->start_offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + int ret = mp3dec_iterate_cb(dec->io, (uint8_t *)dec->file.buffer, dec->file.size, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + } else + { + int ret = mp3dec_iterate_buf(dec->file.buffer + dec->start_offset, dec->file.size - dec->start_offset, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + } + for (i = 0; i < dec->index.num_frames; i++) + dec->index.frames[i].offset += dec->start_offset; + dec->samples = dec->detected_samples; + } + if (!dec->index.frames) + goto seek_zero; /* no frames in file - seek to zero */ +#ifdef MINIMP3_SEEK_IDX_LINEAR_SEARCH + for (i = 0; i < dec->index.num_frames; i++) + { + if (dec->index.frames[i].sample >= position) + break; + } +#else + i = mp3dec_idx_binary_search(&dec->index, position); +#endif + if (i) + { + int to_fill_bytes = 511; + int skip_frames = MINIMP3_PREDECODE_FRAMES +#ifdef MINIMP3_SEEK_IDX_LINEAR_SEARCH + + ((dec->index.frames[i].sample == position) ? 0 : 1) +#endif + ; + i -= MINIMP3_MIN(i, (size_t)skip_frames); + if (3 == dec->info.layer) + { + while (i && to_fill_bytes) + { /* make sure bit-reservoir is filled when we start decoding */ + bs_t bs[1]; + L3_gr_info_t gr_info[4]; + int frame_bytes, frame_size; + const uint8_t *hdr; + if (dec->io) + { + hdr = dec->file.buffer; + if (dec->io->seek(dec->index.frames[i - 1].offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + size_t readed = dec->io->read((uint8_t *)hdr, HDR_SIZE, dec->io->read_data); + if (readed != HDR_SIZE) + return MP3D_E_IOERROR; + frame_size = hdr_frame_bytes(hdr, dec->free_format_bytes) + hdr_padding(hdr); + readed = dec->io->read((uint8_t *)hdr + HDR_SIZE, frame_size - HDR_SIZE, dec->io->read_data); + if (readed != (size_t)(frame_size - HDR_SIZE)) + return MP3D_E_IOERROR; + bs_init(bs, hdr + HDR_SIZE, frame_size - HDR_SIZE); + } else + { + hdr = dec->file.buffer + dec->index.frames[i - 1].offset; + frame_size = hdr_frame_bytes(hdr, dec->free_format_bytes) + hdr_padding(hdr); + bs_init(bs, hdr + HDR_SIZE, frame_size - HDR_SIZE); + } + if (HDR_IS_CRC(hdr)) + get_bits(bs, 16); + i--; + if (L3_read_side_info(bs, gr_info, hdr) < 0) + break; /* frame not decodable, we can start from here */ + frame_bytes = (bs->limit - bs->pos)/8; + to_fill_bytes -= MINIMP3_MIN(to_fill_bytes, frame_bytes); + } + } + } + dec->offset = dec->index.frames[i].offset; + dec->to_skip = position - dec->index.frames[i].sample; + while ((i + 1) < dec->index.num_frames && !dec->index.frames[i].sample && !dec->index.frames[i + 1].sample) + { /* skip not decodable first frames */ + const uint8_t *hdr; + if (dec->io) + { + hdr = dec->file.buffer; + if (dec->io->seek(dec->index.frames[i].offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + size_t readed = dec->io->read((uint8_t *)hdr, HDR_SIZE, dec->io->read_data); + if (readed != HDR_SIZE) + return MP3D_E_IOERROR; + } else + hdr = dec->file.buffer + dec->index.frames[i].offset; + dec->to_skip += hdr_frame_samples(hdr)*dec->info.channels; + i++; + } +do_exit: + if (dec->io) + { + if (dec->io->seek(dec->offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + } + dec->buffer_samples = 0; + dec->buffer_consumed = 0; + dec->input_consumed = 0; + dec->input_filled = 0; + dec->last_error = 0; + mp3dec_init(&dec->mp3d); + return 0; +} + +size_t mp3dec_ex_read(mp3dec_ex_t *dec, mp3d_sample_t *buf, size_t samples) +{ + if (!dec || !buf) + return MP3D_E_PARAM; + uint64_t end_offset = dec->end_offset ? dec->end_offset : dec->file.size; + size_t samples_requested = samples; + int eof = 0; + mp3dec_frame_info_t frame_info; + memset(&frame_info, 0, sizeof(frame_info)); + if (dec->detected_samples && dec->cur_sample >= dec->detected_samples) + return 0; /* at end of stream */ + if (dec->last_error) + return 0; /* error eof state, seek can reset it */ + if (dec->buffer_consumed < dec->buffer_samples) + { + size_t to_copy = MINIMP3_MIN((size_t)(dec->buffer_samples - dec->buffer_consumed), samples); + if (dec->detected_samples) + { /* count decoded samples to properly cut padding */ + if (dec->cur_sample + to_copy >= dec->detected_samples) + to_copy = dec->detected_samples - dec->cur_sample; + } + dec->cur_sample += to_copy; + memcpy(buf, dec->buffer + dec->buffer_consumed, to_copy*sizeof(mp3d_sample_t)); + buf += to_copy; + dec->buffer_consumed += to_copy; + samples -= to_copy; + } + while (samples) + { + if (dec->detected_samples && dec->cur_sample >= dec->detected_samples) + break; + const uint8_t *dec_buf; + if (dec->io) + { + if (!eof && (dec->input_filled - dec->input_consumed) < MINIMP3_BUF_SIZE) + { /* keep minimum 10 consecutive mp3 frames (~16KB) worst case */ + memmove((uint8_t*)dec->file.buffer, (uint8_t*)dec->file.buffer + dec->input_consumed, dec->input_filled - dec->input_consumed); + dec->input_filled -= dec->input_consumed; + dec->input_consumed = 0; + size_t readed = dec->io->read((uint8_t*)dec->file.buffer + dec->input_filled, dec->file.size - dec->input_filled, dec->io->read_data); + if (readed > (dec->file.size - dec->input_filled)) + { + dec->last_error = MP3D_E_IOERROR; + readed = 0; + } + if (readed != (dec->file.size - dec->input_filled)) + eof = 1; + dec->input_filled += readed; + if (eof) + mp3dec_skip_id3v1((uint8_t*)dec->file.buffer, &dec->input_filled); + } + dec_buf = dec->file.buffer + dec->input_consumed; + if (!(dec->input_filled - dec->input_consumed)) + break; + dec->buffer_samples = mp3dec_decode_frame(&dec->mp3d, dec_buf, dec->input_filled - dec->input_consumed, dec->buffer, &frame_info); + dec->input_consumed += frame_info.frame_bytes; + } else + { + dec_buf = dec->file.buffer + dec->offset; + uint64_t buf_size = end_offset - dec->offset; + if (!buf_size) + break; + dec->buffer_samples = mp3dec_decode_frame(&dec->mp3d, dec_buf, MINIMP3_MIN(buf_size, (uint64_t)INT_MAX), dec->buffer, &frame_info); + } + dec->buffer_consumed = 0; + if (dec->info.hz != frame_info.hz || dec->info.layer != frame_info.layer +#ifndef MINIMP3_ALLOW_MONO_STEREO_TRANSITION + || dec->info.channels != frame_info.channels +#endif + ) + { + dec->last_error = MP3D_E_DECODE; + break; + } + if (dec->buffer_samples) + { + dec->buffer_samples *= frame_info.channels; + if (dec->to_skip) + { + size_t skip = MINIMP3_MIN(dec->buffer_samples, dec->to_skip); + dec->buffer_consumed += skip; + dec->to_skip -= skip; + } + size_t to_copy = MINIMP3_MIN((size_t)(dec->buffer_samples - dec->buffer_consumed), samples); + if (dec->detected_samples) + { /* ^ handle padding */ + if (dec->cur_sample + to_copy >= dec->detected_samples) + to_copy = dec->detected_samples - dec->cur_sample; + } + dec->cur_sample += to_copy; + memcpy(buf, dec->buffer + dec->buffer_consumed, to_copy*sizeof(mp3d_sample_t)); + buf += to_copy; + dec->buffer_consumed += to_copy; + samples -= to_copy; + } else if (dec->to_skip) + { /* In mp3 decoding not always can start decode from any frame because of bit reservoir, + count skip samples for such frames */ + int frame_samples = hdr_frame_samples(dec_buf)*frame_info.channels; + dec->to_skip -= MINIMP3_MIN(frame_samples, dec->to_skip); + } + dec->offset += frame_info.frame_bytes; + } + return samples_requested - samples; +} + +#ifndef MINIMP3_NO_STDIO + +#if defined(__linux__) || defined(__FreeBSD__) +#include +#include +#include +#include +#include +#include +#if !defined(_GNU_SOURCE) +#include +#include +#endif +#if !defined(MAP_POPULATE) && defined(__linux__) +#define MAP_POPULATE 0x08000 +#elif !defined(MAP_POPULATE) +#define MAP_POPULATE 0 +#endif + +static void mp3dec_close_file(mp3dec_map_info_t *map_info) +{ + if (map_info->buffer && MAP_FAILED != map_info->buffer) + munmap((void *)map_info->buffer, map_info->size); + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_file(const char *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + int file; + struct stat st; + memset(map_info, 0, sizeof(*map_info)); +retry_open: + file = open(file_name, O_RDONLY); + if (file < 0 && (errno == EAGAIN || errno == EINTR)) + goto retry_open; + if (file < 0 || fstat(file, &st) < 0) + { + close(file); + return MP3D_E_IOERROR; + } + + map_info->size = st.st_size; +retry_mmap: + map_info->buffer = (const uint8_t *)mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE | MAP_POPULATE, file, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap; + close(file); + if (MAP_FAILED == map_info->buffer) + return MP3D_E_IOERROR; + return 0; +} + +#if MINIMP3_ENABLE_RING && defined(__linux__) && defined(_GNU_SOURCE) +#define MINIMP3_HAVE_RING +static void mp3dec_close_ring(mp3dec_map_info_t *map_info) +{ +#if defined(__linux__) && defined(_GNU_SOURCE) + if (map_info->buffer && MAP_FAILED != map_info->buffer) + munmap((void *)map_info->buffer, map_info->size*2); +#else + if (map_info->buffer) + { + shmdt(map_info->buffer); + shmdt(map_info->buffer + map_info->size); + } +#endif + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_ring(mp3dec_map_info_t *map_info, size_t size) +{ + int memfd, page_size; +#if defined(__linux__) && defined(_GNU_SOURCE) + void *buffer; + int res; +#endif + memset(map_info, 0, sizeof(*map_info)); + +#ifdef _SC_PAGESIZE + page_size = sysconf(_SC_PAGESIZE); +#else + page_size = getpagesize(); +#endif + map_info->size = (size + page_size - 1)/page_size*page_size; + +#if defined(__linux__) && defined(_GNU_SOURCE) + memfd = memfd_create("mp3_ring", 0); + if (memfd < 0) + return MP3D_E_MEMORY; + +retry_ftruncate: + res = ftruncate(memfd, map_info->size); + if (res && (errno == EAGAIN || errno == EINTR)) + goto retry_ftruncate; + if (res) + goto error; + +retry_mmap: + map_info->buffer = (const uint8_t *)mmap(NULL, map_info->size*2, PROT_NONE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap; + if (MAP_FAILED == map_info->buffer || !map_info->buffer) + goto error; +retry_mmap2: + buffer = mmap((void *)map_info->buffer, map_info->size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, memfd, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap2; + if (MAP_FAILED == map_info->buffer || buffer != (void *)map_info->buffer) + goto error; +retry_mmap3: + buffer = mmap((void *)map_info->buffer + map_info->size, map_info->size, PROT_READ | PROT_WRITE, MAP_FIXED | MAP_SHARED, memfd, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap3; + if (MAP_FAILED == map_info->buffer || buffer != (void *)(map_info->buffer + map_info->size)) + goto error; + + close(memfd); + return 0; +error: + close(memfd); + mp3dec_close_ring(map_info); + return MP3D_E_MEMORY; +#else + memfd = shmget(IPC_PRIVATE, map_info->size, IPC_CREAT | 0700); + if (memfd < 0) + return MP3D_E_MEMORY; +retry_mmap: + map_info->buffer = (const uint8_t *)mmap(NULL, map_info->size*2, PROT_NONE, MAP_PRIVATE, -1, 0); + if (MAP_FAILED == map_info->buffer && (errno == EAGAIN || errno == EINTR)) + goto retry_mmap; + if (MAP_FAILED == map_info->buffer) + goto error; + if (map_info->buffer != shmat(memfd, map_info->buffer, 0)) + goto error; + if ((map_info->buffer + map_info->size) != shmat(memfd, map_info->buffer + map_info->size, 0)) + goto error; + if (shmctl(memfd, IPC_RMID, NULL) < 0) + return MP3D_E_MEMORY; + return 0; +error: + shmctl(memfd, IPC_RMID, NULL); + mp3dec_close_ring(map_info); + return MP3D_E_MEMORY; +#endif +} +#endif /*MINIMP3_ENABLE_RING*/ +#elif defined(_WIN32) +#include + +static void mp3dec_close_file(mp3dec_map_info_t *map_info) +{ + if (map_info->buffer) + UnmapViewOfFile(map_info->buffer); + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_file_h(HANDLE file, mp3dec_map_info_t *map_info) +{ + memset(map_info, 0, sizeof(*map_info)); + + HANDLE mapping = NULL; + LARGE_INTEGER s; + s.LowPart = GetFileSize(file, (DWORD*)&s.HighPart); + if (s.LowPart == INVALID_FILE_SIZE && GetLastError() != NO_ERROR) + goto error; + map_info->size = s.QuadPart; + + mapping = CreateFileMapping(file, NULL, PAGE_READONLY, 0, 0, NULL); + if (!mapping) + goto error; + map_info->buffer = (const uint8_t*)MapViewOfFile(mapping, FILE_MAP_READ, 0, 0, s.QuadPart); + CloseHandle(mapping); + if (!map_info->buffer) + goto error; + + CloseHandle(file); + return 0; +error: + mp3dec_close_file(map_info); + CloseHandle(file); + return MP3D_E_IOERROR; +} + +static int mp3dec_open_file(const char *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + HANDLE file = CreateFileA(file_name, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (INVALID_HANDLE_VALUE == file) + return MP3D_E_IOERROR; + return mp3dec_open_file_h(file, map_info); +} + +static int mp3dec_open_file_w(const wchar_t *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + HANDLE file = CreateFileW(file_name, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); + if (INVALID_HANDLE_VALUE == file) + return MP3D_E_IOERROR; + return mp3dec_open_file_h(file, map_info); +} +#else +#include + +static void mp3dec_close_file(mp3dec_map_info_t *map_info) +{ + if (map_info->buffer) + free((void *)map_info->buffer); + map_info->buffer = 0; + map_info->size = 0; +} + +static int mp3dec_open_file(const char *file_name, mp3dec_map_info_t *map_info) +{ + if (!file_name) + return MP3D_E_PARAM; + memset(map_info, 0, sizeof(*map_info)); + FILE *file = fopen(file_name, "rb"); + if (!file) + return MP3D_E_IOERROR; + int res = MP3D_E_IOERROR; + long size = -1; + if (fseek(file, 0, SEEK_END)) + goto error; + size = ftell(file); + if (size < 0) + goto error; + map_info->size = (size_t)size; + if (fseek(file, 0, SEEK_SET)) + goto error; + map_info->buffer = (uint8_t *)malloc(map_info->size); + if (!map_info->buffer) + { + res = MP3D_E_MEMORY; + goto error; + } + if (fread((void *)map_info->buffer, 1, map_info->size, file) != map_info->size) + goto error; + fclose(file); + return 0; +error: + mp3dec_close_file(map_info); + fclose(file); + return res; +} +#endif + +static int mp3dec_detect_mapinfo(mp3dec_map_info_t *map_info) +{ + int ret = mp3dec_detect_buf(map_info->buffer, map_info->size); + mp3dec_close_file(map_info); + return ret; +} + +static int mp3dec_load_mapinfo(mp3dec_t *dec, mp3dec_map_info_t *map_info, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + int ret = mp3dec_load_buf(dec, map_info->buffer, map_info->size, info, progress_cb, user_data); + mp3dec_close_file(map_info); + return ret; +} + +static int mp3dec_iterate_mapinfo(mp3dec_map_info_t *map_info, MP3D_ITERATE_CB callback, void *user_data) +{ + int ret = mp3dec_iterate_buf(map_info->buffer, map_info->size, callback, user_data); + mp3dec_close_file(map_info); + return ret; +} + +static int mp3dec_ex_open_mapinfo(mp3dec_ex_t *dec, int flags) +{ + int ret = mp3dec_ex_open_buf(dec, dec->file.buffer, dec->file.size, flags); + dec->is_file = 1; + if (ret) + mp3dec_ex_close(dec); + return ret; +} + +int mp3dec_detect(const char *file_name) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file(file_name, &map_info))) + return ret; + return mp3dec_detect_mapinfo(&map_info); +} + +int mp3dec_load(mp3dec_t *dec, const char *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file(file_name, &map_info))) + return ret; + return mp3dec_load_mapinfo(dec, &map_info, info, progress_cb, user_data); +} + +int mp3dec_iterate(const char *file_name, MP3D_ITERATE_CB callback, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file(file_name, &map_info))) + return ret; + return mp3dec_iterate_mapinfo(&map_info, callback, user_data); +} + +int mp3dec_ex_open(mp3dec_ex_t *dec, const char *file_name, int flags) +{ + int ret; + if (!dec) + return MP3D_E_PARAM; + if ((ret = mp3dec_open_file(file_name, &dec->file))) + return ret; + return mp3dec_ex_open_mapinfo(dec, flags); +} + +int mp3dec_ex_open_cb(mp3dec_ex_t *dec, mp3dec_io_t *io, int flags) +{ + if (!dec || !io || (flags & (~3))) + return MP3D_E_PARAM; + memset(dec, 0, sizeof(*dec)); +#ifdef MINIMP3_HAVE_RING + int ret; + if (ret = mp3dec_open_ring(&dec->file, MINIMP3_IO_SIZE)) + return ret; +#else + dec->file.size = MINIMP3_IO_SIZE; + dec->file.buffer = (const uint8_t*)malloc(dec->file.size); + if (!dec->file.buffer) + return MP3D_E_MEMORY; +#endif + dec->flags = flags; + dec->io = io; + mp3dec_init(&dec->mp3d); + if (io->seek(0, io->seek_data)) + return MP3D_E_IOERROR; + int ret = mp3dec_iterate_cb(io, (uint8_t *)dec->file.buffer, dec->file.size, mp3dec_load_index, dec); + if (ret && MP3D_E_USER != ret) + return ret; + if (dec->io->seek(dec->start_offset, dec->io->seek_data)) + return MP3D_E_IOERROR; + mp3dec_init(&dec->mp3d); + dec->buffer_samples = 0; + dec->indexes_built = !(dec->vbr_tag_found || (flags & MP3D_DO_NOT_SCAN)); + dec->flags &= (~MP3D_DO_NOT_SCAN); + return 0; +} + +void mp3dec_ex_close(mp3dec_ex_t *dec) +{ +#ifdef MINIMP3_HAVE_RING + if (dec->io) + mp3dec_close_ring(&dec->file); +#else + if (dec->io && dec->file.buffer) + free((void*)dec->file.buffer); +#endif + if (dec->is_file) + mp3dec_close_file(&dec->file); + if (dec->index.frames) + free(dec->index.frames); + memset(dec, 0, sizeof(*dec)); +} + +#ifdef _WIN32 +int mp3dec_detect_w(const wchar_t *file_name) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file_w(file_name, &map_info))) + return ret; + return mp3dec_detect_mapinfo(&map_info); +} + +int mp3dec_load_w(mp3dec_t *dec, const wchar_t *file_name, mp3dec_file_info_t *info, MP3D_PROGRESS_CB progress_cb, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file_w(file_name, &map_info))) + return ret; + return mp3dec_load_mapinfo(dec, &map_info, info, progress_cb, user_data); +} + +int mp3dec_iterate_w(const wchar_t *file_name, MP3D_ITERATE_CB callback, void *user_data) +{ + int ret; + mp3dec_map_info_t map_info; + if ((ret = mp3dec_open_file_w(file_name, &map_info))) + return ret; + return mp3dec_iterate_mapinfo(&map_info, callback, user_data); +} + +int mp3dec_ex_open_w(mp3dec_ex_t *dec, const wchar_t *file_name, int flags) +{ + int ret; + if ((ret = mp3dec_open_file_w(file_name, &dec->file))) + return ret; + return mp3dec_ex_open_mapinfo(dec, flags); +} +#endif +#else /* MINIMP3_NO_STDIO */ +void mp3dec_ex_close(mp3dec_ex_t *dec) +{ + if (dec->index.frames) + free(dec->index.frames); + memset(dec, 0, sizeof(*dec)); +} +#endif + +#endif /*MINIMP3_IMPLEMENTATION*/ diff --git a/src/third_party/minizip/ioapi.c b/src/third_party/minizip/ioapi.c new file mode 100644 index 0000000..81a64dc --- /dev/null +++ b/src/third_party/minizip/ioapi.c @@ -0,0 +1,247 @@ +/* ioapi.h -- IO base function header for compress/uncompress .zip + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + +*/ + +#if defined(_WIN32) && (!(defined(_CRT_SECURE_NO_WARNINGS))) + #define _CRT_SECURE_NO_WARNINGS +#endif + +#if defined(__APPLE__) || defined(IOAPI_NO_64) || defined(__ANDROID__) +// In darwin and perhaps other BSD variants off_t is a 64 bit value, hence no need for specific 64 bit functions +#define FOPEN_FUNC(filename, mode) fopen(filename, mode) +#define FTELLO_FUNC(stream) ftello(stream) +#define FSEEKO_FUNC(stream, offset, origin) fseeko(stream, offset, origin) +#else +#define FOPEN_FUNC(filename, mode) fopen64(filename, mode) +#define FTELLO_FUNC(stream) ftello64(stream) +#define FSEEKO_FUNC(stream, offset, origin) fseeko64(stream, offset, origin) +#endif + + +#include "ioapi.h" + +voidpf call_zopen64 (const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode) +{ + if (pfilefunc->zfile_func64.zopen64_file != NULL) + return (*(pfilefunc->zfile_func64.zopen64_file)) (pfilefunc->zfile_func64.opaque,filename,mode); + else + { + return (*(pfilefunc->zopen32_file))(pfilefunc->zfile_func64.opaque,(const char*)filename,mode); + } +} + +long call_zseek64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin) +{ + if (pfilefunc->zfile_func64.zseek64_file != NULL) + return (*(pfilefunc->zfile_func64.zseek64_file)) (pfilefunc->zfile_func64.opaque,filestream,offset,origin); + else + { + uLong offsetTruncated = (uLong)offset; + if (offsetTruncated != offset) + return -1; + else + return (*(pfilefunc->zseek32_file))(pfilefunc->zfile_func64.opaque,filestream,offsetTruncated,origin); + } +} + +ZPOS64_T call_ztell64 (const zlib_filefunc64_32_def* pfilefunc,voidpf filestream) +{ + if (pfilefunc->zfile_func64.zseek64_file != NULL) + return (*(pfilefunc->zfile_func64.ztell64_file)) (pfilefunc->zfile_func64.opaque,filestream); + else + { + uLong tell_uLong = (*(pfilefunc->ztell32_file))(pfilefunc->zfile_func64.opaque,filestream); + if ((tell_uLong) == MAXU32) + return (ZPOS64_T)-1; + else + return tell_uLong; + } +} + +void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32) +{ + p_filefunc64_32->zfile_func64.zopen64_file = NULL; + p_filefunc64_32->zopen32_file = p_filefunc32->zopen_file; + p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file; + p_filefunc64_32->zfile_func64.zread_file = p_filefunc32->zread_file; + p_filefunc64_32->zfile_func64.zwrite_file = p_filefunc32->zwrite_file; + p_filefunc64_32->zfile_func64.ztell64_file = NULL; + p_filefunc64_32->zfile_func64.zseek64_file = NULL; + p_filefunc64_32->zfile_func64.zclose_file = p_filefunc32->zclose_file; + p_filefunc64_32->zfile_func64.zerror_file = p_filefunc32->zerror_file; + p_filefunc64_32->zfile_func64.opaque = p_filefunc32->opaque; + p_filefunc64_32->zseek32_file = p_filefunc32->zseek_file; + p_filefunc64_32->ztell32_file = p_filefunc32->ztell_file; +} + + + +static voidpf ZCALLBACK fopen_file_func OF((voidpf opaque, const char* filename, int mode)); +static uLong ZCALLBACK fread_file_func OF((voidpf opaque, voidpf stream, void* buf, uLong size)); +static uLong ZCALLBACK fwrite_file_func OF((voidpf opaque, voidpf stream, const void* buf,uLong size)); +static ZPOS64_T ZCALLBACK ftell64_file_func OF((voidpf opaque, voidpf stream)); +static long ZCALLBACK fseek64_file_func OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)); +static int ZCALLBACK fclose_file_func OF((voidpf opaque, voidpf stream)); +static int ZCALLBACK ferror_file_func OF((voidpf opaque, voidpf stream)); + +static voidpf ZCALLBACK fopen_file_func (voidpf opaque, const char* filename, int mode) +{ + FILE* file = NULL; + const char* mode_fopen = NULL; + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) + mode_fopen = "rb"; + else + if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + mode_fopen = "r+b"; + else + if (mode & ZLIB_FILEFUNC_MODE_CREATE) + mode_fopen = "wb"; + + if ((filename!=NULL) && (mode_fopen != NULL)) + file = fopen(filename, mode_fopen); + return file; +} + +static voidpf ZCALLBACK fopen64_file_func (voidpf opaque, const void* filename, int mode) +{ + FILE* file = NULL; + const char* mode_fopen = NULL; + if ((mode & ZLIB_FILEFUNC_MODE_READWRITEFILTER)==ZLIB_FILEFUNC_MODE_READ) + mode_fopen = "rb"; + else + if (mode & ZLIB_FILEFUNC_MODE_EXISTING) + mode_fopen = "r+b"; + else + if (mode & ZLIB_FILEFUNC_MODE_CREATE) + mode_fopen = "wb"; + + if ((filename!=NULL) && (mode_fopen != NULL)) + file = FOPEN_FUNC((const char*)filename, mode_fopen); + return file; +} + + +static uLong ZCALLBACK fread_file_func (voidpf opaque, voidpf stream, void* buf, uLong size) +{ + uLong ret; + ret = (uLong)fread(buf, 1, (size_t)size, (FILE *)stream); + return ret; +} + +static uLong ZCALLBACK fwrite_file_func (voidpf opaque, voidpf stream, const void* buf, uLong size) +{ + uLong ret; + ret = (uLong)fwrite(buf, 1, (size_t)size, (FILE *)stream); + return ret; +} + +static long ZCALLBACK ftell_file_func (voidpf opaque, voidpf stream) +{ + long ret; + ret = ftell((FILE *)stream); + return ret; +} + + +static ZPOS64_T ZCALLBACK ftell64_file_func (voidpf opaque, voidpf stream) +{ + ZPOS64_T ret; + ret = FTELLO_FUNC((FILE *)stream); + return ret; +} + +static long ZCALLBACK fseek_file_func (voidpf opaque, voidpf stream, uLong offset, int origin) +{ + int fseek_origin=0; + long ret; + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + fseek_origin = SEEK_CUR; + break; + case ZLIB_FILEFUNC_SEEK_END : + fseek_origin = SEEK_END; + break; + case ZLIB_FILEFUNC_SEEK_SET : + fseek_origin = SEEK_SET; + break; + default: return -1; + } + ret = 0; + if (fseek((FILE *)stream, offset, fseek_origin) != 0) + ret = -1; + return ret; +} + +static long ZCALLBACK fseek64_file_func (voidpf opaque, voidpf stream, ZPOS64_T offset, int origin) +{ + int fseek_origin=0; + long ret; + switch (origin) + { + case ZLIB_FILEFUNC_SEEK_CUR : + fseek_origin = SEEK_CUR; + break; + case ZLIB_FILEFUNC_SEEK_END : + fseek_origin = SEEK_END; + break; + case ZLIB_FILEFUNC_SEEK_SET : + fseek_origin = SEEK_SET; + break; + default: return -1; + } + ret = 0; + + if(FSEEKO_FUNC((FILE *)stream, offset, fseek_origin) != 0) + ret = -1; + + return ret; +} + + +static int ZCALLBACK fclose_file_func (voidpf opaque, voidpf stream) +{ + int ret; + ret = fclose((FILE *)stream); + return ret; +} + +static int ZCALLBACK ferror_file_func (voidpf opaque, voidpf stream) +{ + int ret; + ret = ferror((FILE *)stream); + return ret; +} + +void fill_fopen_filefunc (pzlib_filefunc_def) + zlib_filefunc_def* pzlib_filefunc_def; +{ + pzlib_filefunc_def->zopen_file = fopen_file_func; + pzlib_filefunc_def->zread_file = fread_file_func; + pzlib_filefunc_def->zwrite_file = fwrite_file_func; + pzlib_filefunc_def->ztell_file = ftell_file_func; + pzlib_filefunc_def->zseek_file = fseek_file_func; + pzlib_filefunc_def->zclose_file = fclose_file_func; + pzlib_filefunc_def->zerror_file = ferror_file_func; + pzlib_filefunc_def->opaque = NULL; +} + +void fill_fopen64_filefunc (zlib_filefunc64_def* pzlib_filefunc_def) +{ + pzlib_filefunc_def->zopen64_file = fopen64_file_func; + pzlib_filefunc_def->zread_file = fread_file_func; + pzlib_filefunc_def->zwrite_file = fwrite_file_func; + pzlib_filefunc_def->ztell64_file = ftell64_file_func; + pzlib_filefunc_def->zseek64_file = fseek64_file_func; + pzlib_filefunc_def->zclose_file = fclose_file_func; + pzlib_filefunc_def->zerror_file = ferror_file_func; + pzlib_filefunc_def->opaque = NULL; +} diff --git a/src/third_party/minizip/ioapi.h b/src/third_party/minizip/ioapi.h new file mode 100644 index 0000000..3b7e4ee --- /dev/null +++ b/src/third_party/minizip/ioapi.h @@ -0,0 +1,208 @@ +/* ioapi.h -- IO base function header for compress/uncompress .zip + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications for Zip64 support + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + Changes + + Oct-2009 - Defined ZPOS64_T to fpos_t on windows and u_int64_t on linux. (might need to find a better why for this) + Oct-2009 - Change to fseeko64, ftello64 and fopen64 so large files would work on linux. + More if/def section may be needed to support other platforms + Oct-2009 - Defined fxxxx64 calls to normal fopen/ftell/fseek so they would compile on windows. + (but you should use iowin32.c for windows instead) + +*/ + +#ifndef _ZLIBIOAPI64_H +#define _ZLIBIOAPI64_H + +#if (!defined(_WIN32)) && (!defined(WIN32)) && (!defined(__APPLE__)) && (!defined(__ANDROID__)) + + // Linux needs this to support file operation on files larger then 4+GB + // But might need better if/def to select just the platforms that needs them. + + #ifndef __USE_FILE_OFFSET64 + #define __USE_FILE_OFFSET64 + #endif + #ifndef __USE_LARGEFILE64 + #define __USE_LARGEFILE64 + #endif + #ifndef _LARGEFILE64_SOURCE + #define _LARGEFILE64_SOURCE + #endif + #ifndef _FILE_OFFSET_BIT + #define _FILE_OFFSET_BIT 64 + #endif + +#endif + +#include +#include +#include "zlib.h" + +#if defined(USE_FILE32API) +#define fopen64 fopen +#define ftello64 ftell +#define fseeko64 fseek +#else +#ifdef __FreeBSD__ +#define fopen64 fopen +#define ftello64 ftello +#define fseeko64 fseeko +#endif +#ifdef _MSC_VER + #define fopen64 fopen + #if (_MSC_VER >= 1400) && (!(defined(NO_MSCVER_FILE64_FUNC))) + #define ftello64 _ftelli64 + #define fseeko64 _fseeki64 + #else // old MSC + #define ftello64 ftell + #define fseeko64 fseek + #endif +#endif +#endif + +/* +#ifndef ZPOS64_T + #ifdef _WIN32 + #define ZPOS64_T fpos_t + #else + #include + #define ZPOS64_T uint64_t + #endif +#endif +*/ + +#ifdef HAVE_MINIZIP64_CONF_H +#include "mz64conf.h" +#endif + +/* a type choosen by DEFINE */ +#ifdef HAVE_64BIT_INT_CUSTOM +typedef 64BIT_INT_CUSTOM_TYPE ZPOS64_T; +#else +#ifdef HAS_STDINT_H +#include "stdint.h" +typedef uint64_t ZPOS64_T; +#else + +/* Maximum unsigned 32-bit value used as placeholder for zip64 */ +#define MAXU32 0xffffffff + +#if defined(_MSC_VER) || defined(__BORLANDC__) +typedef unsigned __int64 ZPOS64_T; +#else +typedef unsigned long long int ZPOS64_T; +#endif +#endif +#endif + + + +#ifdef __cplusplus +extern "C" { +#endif + + +#define ZLIB_FILEFUNC_SEEK_CUR (1) +#define ZLIB_FILEFUNC_SEEK_END (2) +#define ZLIB_FILEFUNC_SEEK_SET (0) + +#define ZLIB_FILEFUNC_MODE_READ (1) +#define ZLIB_FILEFUNC_MODE_WRITE (2) +#define ZLIB_FILEFUNC_MODE_READWRITEFILTER (3) + +#define ZLIB_FILEFUNC_MODE_EXISTING (4) +#define ZLIB_FILEFUNC_MODE_CREATE (8) + + +#ifndef ZCALLBACK + #if (defined(WIN32) || defined(_WIN32) || defined (WINDOWS) || defined (_WINDOWS)) && defined(CALLBACK) && defined (USEWINDOWS_CALLBACK) + #define ZCALLBACK CALLBACK + #else + #define ZCALLBACK + #endif +#endif + + + + +typedef voidpf (ZCALLBACK *open_file_func) OF((voidpf opaque, const char* filename, int mode)); +typedef uLong (ZCALLBACK *read_file_func) OF((voidpf opaque, voidpf stream, void* buf, uLong size)); +typedef uLong (ZCALLBACK *write_file_func) OF((voidpf opaque, voidpf stream, const void* buf, uLong size)); +typedef int (ZCALLBACK *close_file_func) OF((voidpf opaque, voidpf stream)); +typedef int (ZCALLBACK *testerror_file_func) OF((voidpf opaque, voidpf stream)); + +typedef long (ZCALLBACK *tell_file_func) OF((voidpf opaque, voidpf stream)); +typedef long (ZCALLBACK *seek_file_func) OF((voidpf opaque, voidpf stream, uLong offset, int origin)); + + +/* here is the "old" 32 bits structure structure */ +typedef struct zlib_filefunc_def_s +{ + open_file_func zopen_file; + read_file_func zread_file; + write_file_func zwrite_file; + tell_file_func ztell_file; + seek_file_func zseek_file; + close_file_func zclose_file; + testerror_file_func zerror_file; + voidpf opaque; +} zlib_filefunc_def; + +typedef ZPOS64_T (ZCALLBACK *tell64_file_func) OF((voidpf opaque, voidpf stream)); +typedef long (ZCALLBACK *seek64_file_func) OF((voidpf opaque, voidpf stream, ZPOS64_T offset, int origin)); +typedef voidpf (ZCALLBACK *open64_file_func) OF((voidpf opaque, const void* filename, int mode)); + +typedef struct zlib_filefunc64_def_s +{ + open64_file_func zopen64_file; + read_file_func zread_file; + write_file_func zwrite_file; + tell64_file_func ztell64_file; + seek64_file_func zseek64_file; + close_file_func zclose_file; + testerror_file_func zerror_file; + voidpf opaque; +} zlib_filefunc64_def; + +void fill_fopen64_filefunc OF((zlib_filefunc64_def* pzlib_filefunc_def)); +void fill_fopen_filefunc OF((zlib_filefunc_def* pzlib_filefunc_def)); + +/* now internal definition, only for zip.c and unzip.h */ +typedef struct zlib_filefunc64_32_def_s +{ + zlib_filefunc64_def zfile_func64; + open_file_func zopen32_file; + tell_file_func ztell32_file; + seek_file_func zseek32_file; +} zlib_filefunc64_32_def; + + +#define ZREAD64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zread_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size)) +#define ZWRITE64(filefunc,filestream,buf,size) ((*((filefunc).zfile_func64.zwrite_file)) ((filefunc).zfile_func64.opaque,filestream,buf,size)) +//#define ZTELL64(filefunc,filestream) ((*((filefunc).ztell64_file)) ((filefunc).opaque,filestream)) +//#define ZSEEK64(filefunc,filestream,pos,mode) ((*((filefunc).zseek64_file)) ((filefunc).opaque,filestream,pos,mode)) +#define ZCLOSE64(filefunc,filestream) ((*((filefunc).zfile_func64.zclose_file)) ((filefunc).zfile_func64.opaque,filestream)) +#define ZERROR64(filefunc,filestream) ((*((filefunc).zfile_func64.zerror_file)) ((filefunc).zfile_func64.opaque,filestream)) + +voidpf call_zopen64 OF((const zlib_filefunc64_32_def* pfilefunc,const void*filename,int mode)); +long call_zseek64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream, ZPOS64_T offset, int origin)); +ZPOS64_T call_ztell64 OF((const zlib_filefunc64_32_def* pfilefunc,voidpf filestream)); + +void fill_zlib_filefunc64_32_def_from_filefunc32(zlib_filefunc64_32_def* p_filefunc64_32,const zlib_filefunc_def* p_filefunc32); + +#define ZOPEN64(filefunc,filename,mode) (call_zopen64((&(filefunc)),(filename),(mode))) +#define ZTELL64(filefunc,filestream) (call_ztell64((&(filefunc)),(filestream))) +#define ZSEEK64(filefunc,filestream,pos,mode) (call_zseek64((&(filefunc)),(filestream),(pos),(mode))) + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/third_party/minizip/unzip.c b/src/third_party/minizip/unzip.c new file mode 100644 index 0000000..affad4b --- /dev/null +++ b/src/third_party/minizip/unzip.c @@ -0,0 +1,2125 @@ +/* unzip.c -- IO for uncompress .zip files using zlib + Version 1.1, February 14h, 2010 + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications of Unzip for Zip64 + Copyright (C) 2007-2008 Even Rouault + + Modifications for Zip64 support on both zip and unzip + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + + ------------------------------------------------------------------------------------ + Decryption code comes from crypt.c by Info-ZIP but has been greatly reduced in terms of + compatibility with older software. The following is from the original crypt.c. + Code woven in by Terry Thorsen 1/2003. + + Copyright (c) 1990-2000 Info-ZIP. All rights reserved. + + See the accompanying file LICENSE, version 2000-Apr-09 or later + (the contents of which are also included in zip.h) for terms of use. + If, for some reason, all these files are missing, the Info-ZIP license + also may be found at: ftp://ftp.info-zip.org/pub/infozip/license.html + + crypt.c (full version) by Info-ZIP. Last revised: [see crypt.h] + + The encryption/decryption parts of this source code (as opposed to the + non-echoing password parts) were originally written in Europe. The + whole source package can be freely distributed, including from the USA. + (Prior to January 2000, re-export from the US was a violation of US law.) + + This encryption code is a direct transcription of the algorithm from + Roger Schlafly, described by Phil Katz in the file appnote.txt. This + file (appnote.txt) is distributed with the PKZIP program (even in the + version without encryption capabilities). + + ------------------------------------------------------------------------------------ + + Changes in unzip.c + + 2007-2008 - Even Rouault - Addition of cpl_unzGetCurrentFileZStreamPos + 2007-2008 - Even Rouault - Decoration of symbol names unz* -> cpl_unz* + 2007-2008 - Even Rouault - Remove old C style function prototypes + 2007-2008 - Even Rouault - Add unzip support for ZIP64 + + Copyright (C) 2007-2008 Even Rouault + + + Oct-2009 - Mathias Svensson - Removed cpl_* from symbol names (Even Rouault added them but since this is now moved to a new project (minizip64) I renamed them again). + Oct-2009 - Mathias Svensson - Fixed problem if uncompressed size was > 4G and compressed size was <4G + should only read the compressed/uncompressed size from the Zip64 format if + the size from normal header was 0xFFFFFFFF + Oct-2009 - Mathias Svensson - Applied some bug fixes from paches recived from Gilles Vollant + Oct-2009 - Mathias Svensson - Applied support to unzip files with compression mathod BZIP2 (bzip2 lib is required) + Patch created by Daniel Borca + + Jan-2010 - back to unzip and minizip 1.0 name scheme, with compatibility layer + + Copyright (C) 1998 - 2010 Gilles Vollant, Even Rouault, Mathias Svensson + +*/ + + +#include +#include +#include + +#ifndef NOUNCRYPT + #define NOUNCRYPT +#endif + +#include "zlib.h" +#include "unzip.h" + +#ifdef STDC +# include +# include +# include +#endif +#ifdef NO_ERRNO_H + extern int errno; +#else +# include +#endif + + +#ifndef local +# define local static +#endif +/* compile with -Dlocal if your debugger can't find static symbols */ + + +#ifndef CASESENSITIVITYDEFAULT_NO +# if !defined(unix) && !defined(CASESENSITIVITYDEFAULT_YES) +# define CASESENSITIVITYDEFAULT_NO +# endif +#endif + + +#ifndef UNZ_BUFSIZE +#define UNZ_BUFSIZE (16384) +#endif + +#ifndef UNZ_MAXFILENAMEINZIP +#define UNZ_MAXFILENAMEINZIP (256) +#endif + +#ifndef ALLOC +# define ALLOC(size) (malloc(size)) +#endif +#ifndef TRYFREE +# define TRYFREE(p) {if (p) free(p);} +#endif + +#define SIZECENTRALDIRITEM (0x2e) +#define SIZEZIPLOCALHEADER (0x1e) + + +const char unz_copyright[] = + " unzip 1.01 Copyright 1998-2004 Gilles Vollant - http://www.winimage.com/zLibDll"; + +/* unz_file_info_interntal contain internal info about a file in zipfile*/ +typedef struct unz_file_info64_internal_s +{ + ZPOS64_T offset_curfile;/* relative offset of local header 8 bytes */ +} unz_file_info64_internal; + + +/* file_in_zip_read_info_s contain internal information about a file in zipfile, + when reading and decompress it */ +typedef struct +{ + char *read_buffer; /* internal buffer for compressed data */ + z_stream stream; /* zLib stream structure for inflate */ + +#ifdef HAVE_BZIP2 + bz_stream bstream; /* bzLib stream structure for bziped */ +#endif + + ZPOS64_T pos_in_zipfile; /* position in byte on the zipfile, for fseek*/ + uLong stream_initialised; /* flag set if stream structure is initialised*/ + + ZPOS64_T offset_local_extrafield;/* offset of the local extra field */ + uInt size_local_extrafield;/* size of the local extra field */ + ZPOS64_T pos_local_extrafield; /* position in the local extra field in read*/ + ZPOS64_T total_out_64; + + uLong crc32; /* crc32 of all data uncompressed */ + uLong crc32_wait; /* crc32 we must obtain after decompress all */ + ZPOS64_T rest_read_compressed; /* number of byte to be decompressed */ + ZPOS64_T rest_read_uncompressed;/*number of byte to be obtained after decomp*/ + zlib_filefunc64_32_def z_filefunc; + voidpf filestream; /* io structore of the zipfile */ + uLong compression_method; /* compression method (0==store) */ + ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ + int raw; +} file_in_zip64_read_info_s; + + +/* unz64_s contain internal information about the zipfile +*/ +typedef struct +{ + zlib_filefunc64_32_def z_filefunc; + int is64bitOpenFunction; + voidpf filestream; /* io structore of the zipfile */ + unz_global_info64 gi; /* public global information */ + ZPOS64_T byte_before_the_zipfile;/* byte before the zipfile, (>0 for sfx)*/ + ZPOS64_T num_file; /* number of the current file in the zipfile*/ + ZPOS64_T pos_in_central_dir; /* pos of the current file in the central dir*/ + ZPOS64_T current_file_ok; /* flag about the usability of the current file*/ + ZPOS64_T central_pos; /* position of the beginning of the central dir*/ + + ZPOS64_T size_central_dir; /* size of the central directory */ + ZPOS64_T offset_central_dir; /* offset of start of central directory with + respect to the starting disk number */ + + unz_file_info64 cur_file_info; /* public info about the current file in zip*/ + unz_file_info64_internal cur_file_info_internal; /* private info about it*/ + file_in_zip64_read_info_s* pfile_in_zip_read; /* structure about the current + file if we are decompressing it */ + int encrypted; + + int isZip64; + +# ifndef NOUNCRYPT + unsigned long keys[3]; /* keys defining the pseudo-random sequence */ + const unsigned long* pcrc_32_tab; +# endif +} unz64_s; + + +#ifndef NOUNCRYPT +#include "crypt.h" +#endif + +/* =========================================================================== + Read a byte from a gz_stream; update next_in and avail_in. Return EOF + for end of file. + IN assertion: the stream s has been sucessfully opened for reading. +*/ + + +local int unz64local_getByte OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + int *pi)); + +local int unz64local_getByte(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream, int *pi) +{ + unsigned char c; + int err = (int)ZREAD64(*pzlib_filefunc_def,filestream,&c,1); + if (err==1) + { + *pi = (int)c; + return UNZ_OK; + } + else + { + if (ZERROR64(*pzlib_filefunc_def,filestream)) + return UNZ_ERRNO; + else + return UNZ_EOF; + } +} + + +/* =========================================================================== + Reads a long in LSB order from the given gz_stream. Sets +*/ +local int unz64local_getShort OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX)); + +local int unz64local_getShort (const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX) +{ + uLong x ; + int i = 0; + int err; + + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (uLong)i; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((uLong)i)<<8; + + if (err==UNZ_OK) + *pX = x; + else + *pX = 0; + return err; +} + +local int unz64local_getLong OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX)); + +local int unz64local_getLong (const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + uLong *pX) +{ + uLong x ; + int i = 0; + int err; + + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (uLong)i; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((uLong)i)<<8; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((uLong)i)<<16; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x += ((uLong)i)<<24; + + if (err==UNZ_OK) + *pX = x; + else + *pX = 0; + return err; +} + +local int unz64local_getLong64 OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + ZPOS64_T *pX)); + + +local int unz64local_getLong64 (const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream, + ZPOS64_T *pX) +{ + ZPOS64_T x ; + int i = 0; + int err; + + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x = (ZPOS64_T)i; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<8; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<16; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<24; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<32; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<40; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<48; + + if (err==UNZ_OK) + err = unz64local_getByte(pzlib_filefunc_def,filestream,&i); + x |= ((ZPOS64_T)i)<<56; + + if (err==UNZ_OK) + *pX = x; + else + *pX = 0; + return err; +} + +/* My own strcmpi / strcasecmp */ +local int strcmpcasenosensitive_internal (const char* fileName1, const char* fileName2) +{ + for (;;) + { + char c1=*(fileName1++); + char c2=*(fileName2++); + if ((c1>='a') && (c1<='z')) + c1 -= 0x20; + if ((c2>='a') && (c2<='z')) + c2 -= 0x20; + if (c1=='\0') + return ((c2=='\0') ? 0 : -1); + if (c2=='\0') + return 1; + if (c1c2) + return 1; + } +} + + +#ifdef CASESENSITIVITYDEFAULT_NO +#define CASESENSITIVITYDEFAULTVALUE 2 +#else +#define CASESENSITIVITYDEFAULTVALUE 1 +#endif + +#ifndef STRCMPCASENOSENTIVEFUNCTION +#define STRCMPCASENOSENTIVEFUNCTION strcmpcasenosensitive_internal +#endif + +/* + Compare two filename (fileName1,fileName2). + If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) + If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi + or strcasecmp) + If iCaseSenisivity = 0, case sensitivity is defaut of your operating system + (like 1 on Unix, 2 on Windows) + +*/ +extern int ZEXPORT unzStringFileNameCompare (const char* fileName1, + const char* fileName2, + int iCaseSensitivity) + +{ + if (iCaseSensitivity==0) + iCaseSensitivity=CASESENSITIVITYDEFAULTVALUE; + + if (iCaseSensitivity==1) + return strcmp(fileName1,fileName2); + + return STRCMPCASENOSENTIVEFUNCTION(fileName1,fileName2); +} + +#ifndef BUFREADCOMMENT +#define BUFREADCOMMENT (0x400) +#endif + +/* + Locate the Central directory of a zipfile (at the end, just before + the global comment) +*/ +local ZPOS64_T unz64local_SearchCentralDir OF((const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream)); +local ZPOS64_T unz64local_SearchCentralDir(const zlib_filefunc64_32_def* pzlib_filefunc_def, voidpf filestream) +{ + unsigned char* buf; + ZPOS64_T uSizeFile; + ZPOS64_T uBackRead; + ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */ + ZPOS64_T uPosFound=0; + + if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) + return 0; + + + uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream); + + if (uMaxBack>uSizeFile) + uMaxBack = uSizeFile; + + buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); + if (buf==NULL) + return 0; + + uBackRead = 4; + while (uBackReaduMaxBack) + uBackRead = uMaxBack; + else + uBackRead+=BUFREADCOMMENT; + uReadPos = uSizeFile-uBackRead ; + + uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? + (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos); + if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) + break; + + if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) + break; + + for (i=(int)uReadSize-3; (i--)>0;) + if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && + ((*(buf+i+2))==0x05) && ((*(buf+i+3))==0x06)) + { + uPosFound = uReadPos+i; + break; + } + + if (uPosFound!=0) + break; + } + TRYFREE(buf); + return uPosFound; +} + + +/* + Locate the Central directory 64 of a zipfile (at the end, just before + the global comment) +*/ +local ZPOS64_T unz64local_SearchCentralDir64 OF(( + const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream)); + +local ZPOS64_T unz64local_SearchCentralDir64(const zlib_filefunc64_32_def* pzlib_filefunc_def, + voidpf filestream) +{ + unsigned char* buf; + ZPOS64_T uSizeFile; + ZPOS64_T uBackRead; + ZPOS64_T uMaxBack=0xffff; /* maximum size of global comment */ + ZPOS64_T uPosFound=0; + uLong uL; + ZPOS64_T relativeOffset; + + if (ZSEEK64(*pzlib_filefunc_def,filestream,0,ZLIB_FILEFUNC_SEEK_END) != 0) + return 0; + + + uSizeFile = ZTELL64(*pzlib_filefunc_def,filestream); + + if (uMaxBack>uSizeFile) + uMaxBack = uSizeFile; + + buf = (unsigned char*)ALLOC(BUFREADCOMMENT+4); + if (buf==NULL) + return 0; + + uBackRead = 4; + while (uBackReaduMaxBack) + uBackRead = uMaxBack; + else + uBackRead+=BUFREADCOMMENT; + uReadPos = uSizeFile-uBackRead ; + + uReadSize = ((BUFREADCOMMENT+4) < (uSizeFile-uReadPos)) ? + (BUFREADCOMMENT+4) : (uLong)(uSizeFile-uReadPos); + if (ZSEEK64(*pzlib_filefunc_def,filestream,uReadPos,ZLIB_FILEFUNC_SEEK_SET)!=0) + break; + + if (ZREAD64(*pzlib_filefunc_def,filestream,buf,uReadSize)!=uReadSize) + break; + + for (i=(int)uReadSize-3; (i--)>0;) + if (((*(buf+i))==0x50) && ((*(buf+i+1))==0x4b) && + ((*(buf+i+2))==0x06) && ((*(buf+i+3))==0x07)) + { + uPosFound = uReadPos+i; + break; + } + + if (uPosFound!=0) + break; + } + TRYFREE(buf); + if (uPosFound == 0) + return 0; + + /* Zip64 end of central directory locator */ + if (ZSEEK64(*pzlib_filefunc_def,filestream, uPosFound,ZLIB_FILEFUNC_SEEK_SET)!=0) + return 0; + + /* the signature, already checked */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + + /* number of the disk with the start of the zip64 end of central directory */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + if (uL != 0) + return 0; + + /* relative offset of the zip64 end of central directory record */ + if (unz64local_getLong64(pzlib_filefunc_def,filestream,&relativeOffset)!=UNZ_OK) + return 0; + + /* total number of disks */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + if (uL != 1) + return 0; + + /* Goto end of central directory record */ + if (ZSEEK64(*pzlib_filefunc_def,filestream, relativeOffset,ZLIB_FILEFUNC_SEEK_SET)!=0) + return 0; + + /* the signature */ + if (unz64local_getLong(pzlib_filefunc_def,filestream,&uL)!=UNZ_OK) + return 0; + + if (uL != 0x06064b50) + return 0; + + return relativeOffset; +} + +/* + Open a Zip file. path contain the full pathname (by example, + on a Windows NT computer "c:\\test\\zlib114.zip" or on an Unix computer + "zlib/zlib114.zip". + If the zipfile cannot be opened (file doesn't exist or in not valid), the + return value is NULL. + Else, the return value is a unzFile Handle, usable with other function + of this unzip package. +*/ +local unzFile unzOpenInternal (const void *path, + zlib_filefunc64_32_def* pzlib_filefunc64_32_def, + int is64bitOpenFunction) +{ + unz64_s us; + unz64_s *s; + ZPOS64_T central_pos; + uLong uL; + + uLong number_disk; /* number of the current dist, used for + spaning ZIP, unsupported, always 0*/ + uLong number_disk_with_CD; /* number the the disk with central dir, used + for spaning ZIP, unsupported, always 0*/ + ZPOS64_T number_entry_CD; /* total number of entries in + the central dir + (same than number_entry on nospan) */ + + int err=UNZ_OK; + + if (unz_copyright[0]!=' ') + return NULL; + + us.z_filefunc.zseek32_file = NULL; + us.z_filefunc.ztell32_file = NULL; + if (pzlib_filefunc64_32_def==NULL) + fill_fopen64_filefunc(&us.z_filefunc.zfile_func64); + else + us.z_filefunc = *pzlib_filefunc64_32_def; + us.is64bitOpenFunction = is64bitOpenFunction; + + + + us.filestream = ZOPEN64(us.z_filefunc, + path, + ZLIB_FILEFUNC_MODE_READ | + ZLIB_FILEFUNC_MODE_EXISTING); + if (us.filestream==NULL) + return NULL; + + central_pos = unz64local_SearchCentralDir64(&us.z_filefunc,us.filestream); + if (central_pos) + { + uLong uS; + ZPOS64_T uL64; + + us.isZip64 = 1; + + if (ZSEEK64(us.z_filefunc, us.filestream, + central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0) + err=UNZ_ERRNO; + + /* the signature, already checked */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + + /* size of zip64 end of central directory record */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&uL64)!=UNZ_OK) + err=UNZ_ERRNO; + + /* version made by */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK) + err=UNZ_ERRNO; + + /* version needed to extract */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uS)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of this disk */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of the disk with the start of the central directory */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK) + err=UNZ_ERRNO; + + /* total number of entries in the central directory on this disk */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.gi.number_entry)!=UNZ_OK) + err=UNZ_ERRNO; + + /* total number of entries in the central directory */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&number_entry_CD)!=UNZ_OK) + err=UNZ_ERRNO; + + if ((number_entry_CD!=us.gi.number_entry) || + (number_disk_with_CD!=0) || + (number_disk!=0)) + err=UNZ_BADZIPFILE; + + /* size of the central directory */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.size_central_dir)!=UNZ_OK) + err=UNZ_ERRNO; + + /* offset of start of central directory with respect to the + starting disk number */ + if (unz64local_getLong64(&us.z_filefunc, us.filestream,&us.offset_central_dir)!=UNZ_OK) + err=UNZ_ERRNO; + + us.gi.size_comment = 0; + } + else + { + central_pos = unz64local_SearchCentralDir(&us.z_filefunc,us.filestream); + if (central_pos==0) + err=UNZ_ERRNO; + + us.isZip64 = 0; + + if (ZSEEK64(us.z_filefunc, us.filestream, + central_pos,ZLIB_FILEFUNC_SEEK_SET)!=0) + err=UNZ_ERRNO; + + /* the signature, already checked */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of this disk */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk)!=UNZ_OK) + err=UNZ_ERRNO; + + /* number of the disk with the start of the central directory */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&number_disk_with_CD)!=UNZ_OK) + err=UNZ_ERRNO; + + /* total number of entries in the central dir on this disk */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + us.gi.number_entry = uL; + + /* total number of entries in the central dir */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + number_entry_CD = uL; + + if ((number_entry_CD!=us.gi.number_entry) || + (number_disk_with_CD!=0) || + (number_disk!=0)) + err=UNZ_BADZIPFILE; + + /* size of the central directory */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + us.size_central_dir = uL; + + /* offset of start of central directory with respect to the + starting disk number */ + if (unz64local_getLong(&us.z_filefunc, us.filestream,&uL)!=UNZ_OK) + err=UNZ_ERRNO; + us.offset_central_dir = uL; + + /* zipfile comment length */ + if (unz64local_getShort(&us.z_filefunc, us.filestream,&us.gi.size_comment)!=UNZ_OK) + err=UNZ_ERRNO; + } + + if ((central_pospfile_in_zip_read!=NULL) + unzCloseCurrentFile(file); + + ZCLOSE64(s->z_filefunc, s->filestream); + TRYFREE(s); + return UNZ_OK; +} + + +/* + Write info about the ZipFile in the *pglobal_info structure. + No preparation of the structure is needed + return UNZ_OK if there is no problem. */ +extern int ZEXPORT unzGetGlobalInfo64 (unzFile file, unz_global_info64* pglobal_info) +{ + unz64_s* s; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + *pglobal_info=s->gi; + return UNZ_OK; +} + +extern int ZEXPORT unzGetGlobalInfo (unzFile file, unz_global_info* pglobal_info32) +{ + unz64_s* s; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + /* to do : check if number_entry is not truncated */ + pglobal_info32->number_entry = (uLong)s->gi.number_entry; + pglobal_info32->size_comment = s->gi.size_comment; + return UNZ_OK; +} +/* + Translate date/time from Dos format to tm_unz (readable more easilty) +*/ +local void unz64local_DosDateToTmuDate (ZPOS64_T ulDosDate, tm_unz* ptm) +{ + ZPOS64_T uDate; + uDate = (ZPOS64_T)(ulDosDate>>16); + ptm->tm_mday = (uInt)(uDate&0x1f) ; + ptm->tm_mon = (uInt)((((uDate)&0x1E0)/0x20)-1) ; + ptm->tm_year = (uInt)(((uDate&0x0FE00)/0x0200)+1980) ; + + ptm->tm_hour = (uInt) ((ulDosDate &0xF800)/0x800); + ptm->tm_min = (uInt) ((ulDosDate&0x7E0)/0x20) ; + ptm->tm_sec = (uInt) (2*(ulDosDate&0x1f)) ; +} + +/* + Get Info about the current file in the zipfile, with internal only info +*/ +local int unz64local_GetCurrentFileInfoInternal OF((unzFile file, + unz_file_info64 *pfile_info, + unz_file_info64_internal + *pfile_info_internal, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize)); + +local int unz64local_GetCurrentFileInfoInternal (unzFile file, + unz_file_info64 *pfile_info, + unz_file_info64_internal + *pfile_info_internal, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize) +{ + unz64_s* s; + unz_file_info64 file_info; + unz_file_info64_internal file_info_internal; + int err=UNZ_OK; + uLong uMagic; + long lSeek=0; + uLong uL; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (ZSEEK64(s->z_filefunc, s->filestream, + s->pos_in_central_dir+s->byte_before_the_zipfile, + ZLIB_FILEFUNC_SEEK_SET)!=0) + err=UNZ_ERRNO; + + + /* we check the magic */ + if (err==UNZ_OK) + { + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK) + err=UNZ_ERRNO; + else if (uMagic!=0x02014b50) + err=UNZ_BADZIPFILE; + } + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.version_needed) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.flag) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.compression_method) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.dosDate) != UNZ_OK) + err=UNZ_ERRNO; + + unz64local_DosDateToTmuDate(file_info.dosDate,&file_info.tmu_date); + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.crc) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + file_info.compressed_size = uL; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + file_info.uncompressed_size = uL; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_filename) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_extra) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.size_file_comment) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.disk_num_start) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&file_info.internal_fa) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&file_info.external_fa) != UNZ_OK) + err=UNZ_ERRNO; + + // relative offset of local header + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + file_info_internal.offset_curfile = uL; + + lSeek+=file_info.size_filename; + if ((err==UNZ_OK) && (szFileName!=NULL)) + { + uLong uSizeRead ; + if (file_info.size_filename0) && (fileNameBufferSize>0)) + if (ZREAD64(s->z_filefunc, s->filestream,szFileName,uSizeRead)!=uSizeRead) + err=UNZ_ERRNO; + lSeek -= uSizeRead; + } + + // Read extrafield + if ((err==UNZ_OK) && (extraField!=NULL)) + { + ZPOS64_T uSizeRead ; + if (file_info.size_file_extraz_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) + lSeek=0; + else + err=UNZ_ERRNO; + } + + if ((file_info.size_file_extra>0) && (extraFieldBufferSize>0)) + if (ZREAD64(s->z_filefunc, s->filestream,extraField,(uLong)uSizeRead)!=uSizeRead) + err=UNZ_ERRNO; + + lSeek += file_info.size_file_extra - (uLong)uSizeRead; + } + else + lSeek += file_info.size_file_extra; + + + if ((err==UNZ_OK) && (file_info.size_file_extra != 0)) + { + uLong acc = 0; + + // since lSeek now points to after the extra field we need to move back + lSeek -= file_info.size_file_extra; + + if (lSeek!=0) + { + if (ZSEEK64(s->z_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) + lSeek=0; + else + err=UNZ_ERRNO; + } + + while(acc < file_info.size_file_extra) + { + uLong headerId; + uLong dataSize; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&headerId) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&dataSize) != UNZ_OK) + err=UNZ_ERRNO; + + /* ZIP64 extra fields */ + if (headerId == 0x0001) + { + uLong uL; + + if(file_info.uncompressed_size == MAXU32) + { + if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.uncompressed_size) != UNZ_OK) + err=UNZ_ERRNO; + } + + if(file_info.compressed_size == MAXU32) + { + if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info.compressed_size) != UNZ_OK) + err=UNZ_ERRNO; + } + + if(file_info_internal.offset_curfile == MAXU32) + { + /* Relative Header offset */ + if (unz64local_getLong64(&s->z_filefunc, s->filestream,&file_info_internal.offset_curfile) != UNZ_OK) + err=UNZ_ERRNO; + } + + if(file_info.disk_num_start == MAXU32) + { + /* Disk Start Number */ + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uL) != UNZ_OK) + err=UNZ_ERRNO; + } + + } + else + { + if (ZSEEK64(s->z_filefunc, s->filestream,dataSize,ZLIB_FILEFUNC_SEEK_CUR)!=0) + err=UNZ_ERRNO; + } + + acc += 2 + 2 + dataSize; + } + } + + if ((err==UNZ_OK) && (szComment!=NULL)) + { + uLong uSizeRead ; + if (file_info.size_file_commentz_filefunc, s->filestream,lSeek,ZLIB_FILEFUNC_SEEK_CUR)==0) + lSeek=0; + else + err=UNZ_ERRNO; + } + + if ((file_info.size_file_comment>0) && (commentBufferSize>0)) + if (ZREAD64(s->z_filefunc, s->filestream,szComment,uSizeRead)!=uSizeRead) + err=UNZ_ERRNO; + lSeek+=file_info.size_file_comment - uSizeRead; + } + else + lSeek+=file_info.size_file_comment; + + + if ((err==UNZ_OK) && (pfile_info!=NULL)) + *pfile_info=file_info; + + if ((err==UNZ_OK) && (pfile_info_internal!=NULL)) + *pfile_info_internal=file_info_internal; + + return err; +} + + + +/* + Write info about the ZipFile in the *pglobal_info structure. + No preparation of the structure is needed + return UNZ_OK if there is no problem. +*/ +extern int ZEXPORT unzGetCurrentFileInfo64 (unzFile file, + unz_file_info64 * pfile_info, + char * szFileName, uLong fileNameBufferSize, + void *extraField, uLong extraFieldBufferSize, + char* szComment, uLong commentBufferSize) +{ + return unz64local_GetCurrentFileInfoInternal(file,pfile_info,NULL, + szFileName,fileNameBufferSize, + extraField,extraFieldBufferSize, + szComment,commentBufferSize); +} + +extern int ZEXPORT unzGetCurrentFileInfo (unzFile file, + unz_file_info * pfile_info, + char * szFileName, uLong fileNameBufferSize, + void *extraField, uLong extraFieldBufferSize, + char* szComment, uLong commentBufferSize) +{ + int err; + unz_file_info64 file_info64; + err = unz64local_GetCurrentFileInfoInternal(file,&file_info64,NULL, + szFileName,fileNameBufferSize, + extraField,extraFieldBufferSize, + szComment,commentBufferSize); + if ((err==UNZ_OK) && (pfile_info != NULL)) + { + pfile_info->version = file_info64.version; + pfile_info->version_needed = file_info64.version_needed; + pfile_info->flag = file_info64.flag; + pfile_info->compression_method = file_info64.compression_method; + pfile_info->dosDate = file_info64.dosDate; + pfile_info->crc = file_info64.crc; + + pfile_info->size_filename = file_info64.size_filename; + pfile_info->size_file_extra = file_info64.size_file_extra; + pfile_info->size_file_comment = file_info64.size_file_comment; + + pfile_info->disk_num_start = file_info64.disk_num_start; + pfile_info->internal_fa = file_info64.internal_fa; + pfile_info->external_fa = file_info64.external_fa; + + pfile_info->tmu_date = file_info64.tmu_date, + + + pfile_info->compressed_size = (uLong)file_info64.compressed_size; + pfile_info->uncompressed_size = (uLong)file_info64.uncompressed_size; + + } + return err; +} +/* + Set the current file of the zipfile to the first file. + return UNZ_OK if there is no problem +*/ +extern int ZEXPORT unzGoToFirstFile (unzFile file) +{ + int err=UNZ_OK; + unz64_s* s; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + s->pos_in_central_dir=s->offset_central_dir; + s->num_file=0; + err=unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + s->current_file_ok = (err == UNZ_OK); + return err; +} + +/* + Set the current file of the zipfile to the next file. + return UNZ_OK if there is no problem + return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest. +*/ +extern int ZEXPORT unzGoToNextFile (unzFile file) +{ + unz64_s* s; + int err; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_END_OF_LIST_OF_FILE; + if (s->gi.number_entry != 0xffff) /* 2^16 files overflow hack */ + if (s->num_file+1==s->gi.number_entry) + return UNZ_END_OF_LIST_OF_FILE; + + s->pos_in_central_dir += SIZECENTRALDIRITEM + s->cur_file_info.size_filename + + s->cur_file_info.size_file_extra + s->cur_file_info.size_file_comment ; + s->num_file++; + err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + s->current_file_ok = (err == UNZ_OK); + return err; +} + + +/* + Try locate the file szFileName in the zipfile. + For the iCaseSensitivity signification, see unzipStringFileNameCompare + + return value : + UNZ_OK if the file is found. It becomes the current file. + UNZ_END_OF_LIST_OF_FILE if the file is not found +*/ +extern int ZEXPORT unzLocateFile (unzFile file, const char *szFileName, int iCaseSensitivity) +{ + unz64_s* s; + int err; + + /* We remember the 'current' position in the file so that we can jump + * back there if we fail. + */ + unz_file_info64 cur_file_infoSaved; + unz_file_info64_internal cur_file_info_internalSaved; + ZPOS64_T num_fileSaved; + ZPOS64_T pos_in_central_dirSaved; + + + if (file==NULL) + return UNZ_PARAMERROR; + + if (strlen(szFileName)>=UNZ_MAXFILENAMEINZIP) + return UNZ_PARAMERROR; + + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_END_OF_LIST_OF_FILE; + + /* Save the current state */ + num_fileSaved = s->num_file; + pos_in_central_dirSaved = s->pos_in_central_dir; + cur_file_infoSaved = s->cur_file_info; + cur_file_info_internalSaved = s->cur_file_info_internal; + + err = unzGoToFirstFile(file); + + while (err == UNZ_OK) + { + char szCurrentFileName[UNZ_MAXFILENAMEINZIP+1]; + err = unzGetCurrentFileInfo64(file,NULL, + szCurrentFileName,sizeof(szCurrentFileName)-1, + NULL,0,NULL,0); + if (err == UNZ_OK) + { + if (unzStringFileNameCompare(szCurrentFileName, + szFileName,iCaseSensitivity)==0) + return UNZ_OK; + err = unzGoToNextFile(file); + } + } + + /* We failed, so restore the state of the 'current file' to where we + * were. + */ + s->num_file = num_fileSaved ; + s->pos_in_central_dir = pos_in_central_dirSaved ; + s->cur_file_info = cur_file_infoSaved; + s->cur_file_info_internal = cur_file_info_internalSaved; + return err; +} + + +/* +/////////////////////////////////////////// +// Contributed by Ryan Haksi (mailto://cryogen@infoserve.net) +// I need random access +// +// Further optimization could be realized by adding an ability +// to cache the directory in memory. The goal being a single +// comprehensive file read to put the file I need in a memory. +*/ + +/* +typedef struct unz_file_pos_s +{ + ZPOS64_T pos_in_zip_directory; // offset in file + ZPOS64_T num_of_file; // # of file +} unz_file_pos; +*/ + +extern int ZEXPORT unzGetFilePos64(unzFile file, unz64_file_pos* file_pos) +{ + unz64_s* s; + + if (file==NULL || file_pos==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_END_OF_LIST_OF_FILE; + + file_pos->pos_in_zip_directory = s->pos_in_central_dir; + file_pos->num_of_file = s->num_file; + + return UNZ_OK; +} + +extern int ZEXPORT unzGetFilePos( + unzFile file, + unz_file_pos* file_pos) +{ + unz64_file_pos file_pos64; + int err = unzGetFilePos64(file,&file_pos64); + if (err==UNZ_OK) + { + file_pos->pos_in_zip_directory = (uLong)file_pos64.pos_in_zip_directory; + file_pos->num_of_file = (uLong)file_pos64.num_of_file; + } + return err; +} + +extern int ZEXPORT unzGoToFilePos64(unzFile file, const unz64_file_pos* file_pos) +{ + unz64_s* s; + int err; + + if (file==NULL || file_pos==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + + /* jump to the right spot */ + s->pos_in_central_dir = file_pos->pos_in_zip_directory; + s->num_file = file_pos->num_of_file; + + /* set the current file */ + err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + /* return results */ + s->current_file_ok = (err == UNZ_OK); + return err; +} + +extern int ZEXPORT unzGoToFilePos( + unzFile file, + unz_file_pos* file_pos) +{ + unz64_file_pos file_pos64; + if (file_pos == NULL) + return UNZ_PARAMERROR; + + file_pos64.pos_in_zip_directory = file_pos->pos_in_zip_directory; + file_pos64.num_of_file = file_pos->num_of_file; + return unzGoToFilePos64(file,&file_pos64); +} + +/* +// Unzip Helper Functions - should be here? +/////////////////////////////////////////// +*/ + +/* + Read the local header of the current zipfile + Check the coherency of the local header and info in the end of central + directory about this file + store in *piSizeVar the size of extra info in local header + (filename and size of extra field data) +*/ +local int unz64local_CheckCurrentFileCoherencyHeader (unz64_s* s, uInt* piSizeVar, + ZPOS64_T * poffset_local_extrafield, + uInt * psize_local_extrafield) +{ + uLong uMagic,uData,uFlags; + uLong size_filename; + uLong size_extra_field; + int err=UNZ_OK; + + *piSizeVar = 0; + *poffset_local_extrafield = 0; + *psize_local_extrafield = 0; + + if (ZSEEK64(s->z_filefunc, s->filestream,s->cur_file_info_internal.offset_curfile + + s->byte_before_the_zipfile,ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + + + if (err==UNZ_OK) + { + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uMagic) != UNZ_OK) + err=UNZ_ERRNO; + else if (uMagic!=0x04034b50) + err=UNZ_BADZIPFILE; + } + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) + err=UNZ_ERRNO; +/* + else if ((err==UNZ_OK) && (uData!=s->cur_file_info.wVersion)) + err=UNZ_BADZIPFILE; +*/ + if (unz64local_getShort(&s->z_filefunc, s->filestream,&uFlags) != UNZ_OK) + err=UNZ_ERRNO; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) + err=UNZ_ERRNO; + else if ((err==UNZ_OK) && (uData!=s->cur_file_info.compression_method)) + err=UNZ_BADZIPFILE; + + if ((err==UNZ_OK) && (s->cur_file_info.compression_method!=0) && +/* #ifdef HAVE_BZIP2 */ + (s->cur_file_info.compression_method!=Z_BZIP2ED) && +/* #endif */ + (s->cur_file_info.compression_method!=Z_DEFLATED)) + err=UNZ_BADZIPFILE; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* date/time */ + err=UNZ_ERRNO; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* crc */ + err=UNZ_ERRNO; + else if ((err==UNZ_OK) && (uData!=s->cur_file_info.crc) && ((uFlags & 8)==0)) + err=UNZ_BADZIPFILE; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size compr */ + err=UNZ_ERRNO; + else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.compressed_size) && ((uFlags & 8)==0)) + err=UNZ_BADZIPFILE; + + if (unz64local_getLong(&s->z_filefunc, s->filestream,&uData) != UNZ_OK) /* size uncompr */ + err=UNZ_ERRNO; + else if (uData != 0xFFFFFFFF && (err==UNZ_OK) && (uData!=s->cur_file_info.uncompressed_size) && ((uFlags & 8)==0)) + err=UNZ_BADZIPFILE; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_filename) != UNZ_OK) + err=UNZ_ERRNO; + else if ((err==UNZ_OK) && (size_filename!=s->cur_file_info.size_filename)) + err=UNZ_BADZIPFILE; + + *piSizeVar += (uInt)size_filename; + + if (unz64local_getShort(&s->z_filefunc, s->filestream,&size_extra_field) != UNZ_OK) + err=UNZ_ERRNO; + *poffset_local_extrafield= s->cur_file_info_internal.offset_curfile + + SIZEZIPLOCALHEADER + size_filename; + *psize_local_extrafield = (uInt)size_extra_field; + + *piSizeVar += (uInt)size_extra_field; + + return err; +} + +/* + Open for reading data the current file in the zipfile. + If there is no error and the file is opened, the return value is UNZ_OK. +*/ +extern int ZEXPORT unzOpenCurrentFile3 (unzFile file, int* method, + int* level, int raw, const char* password) +{ + int err=UNZ_OK; + uInt iSizeVar; + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + ZPOS64_T offset_local_extrafield; /* offset of the local extra field */ + uInt size_local_extrafield; /* size of the local extra field */ +# ifndef NOUNCRYPT + char source[12]; +# else + if (password != NULL) + return UNZ_PARAMERROR; +# endif + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return UNZ_PARAMERROR; + + if (s->pfile_in_zip_read != NULL) + unzCloseCurrentFile(file); + + if (unz64local_CheckCurrentFileCoherencyHeader(s,&iSizeVar, &offset_local_extrafield,&size_local_extrafield)!=UNZ_OK) + return UNZ_BADZIPFILE; + + pfile_in_zip_read_info = (file_in_zip64_read_info_s*)ALLOC(sizeof(file_in_zip64_read_info_s)); + if (pfile_in_zip_read_info==NULL) + return UNZ_INTERNALERROR; + + pfile_in_zip_read_info->read_buffer=(char*)ALLOC(UNZ_BUFSIZE); + pfile_in_zip_read_info->offset_local_extrafield = offset_local_extrafield; + pfile_in_zip_read_info->size_local_extrafield = size_local_extrafield; + pfile_in_zip_read_info->pos_local_extrafield=0; + pfile_in_zip_read_info->raw=raw; + + if (pfile_in_zip_read_info->read_buffer==NULL) + { + TRYFREE(pfile_in_zip_read_info); + return UNZ_INTERNALERROR; + } + + pfile_in_zip_read_info->stream_initialised=0; + + if (method!=NULL) + *method = (int)s->cur_file_info.compression_method; + + if (level!=NULL) + { + *level = 6; + switch (s->cur_file_info.flag & 0x06) + { + case 6 : *level = 1; break; + case 4 : *level = 2; break; + case 2 : *level = 9; break; + } + } + + if ((s->cur_file_info.compression_method!=0) && +/* #ifdef HAVE_BZIP2 */ + (s->cur_file_info.compression_method!=Z_BZIP2ED) && +/* #endif */ + (s->cur_file_info.compression_method!=Z_DEFLATED)) + + err=UNZ_BADZIPFILE; + + pfile_in_zip_read_info->crc32_wait=s->cur_file_info.crc; + pfile_in_zip_read_info->crc32=0; + pfile_in_zip_read_info->total_out_64=0; + pfile_in_zip_read_info->compression_method = s->cur_file_info.compression_method; + pfile_in_zip_read_info->filestream=s->filestream; + pfile_in_zip_read_info->z_filefunc=s->z_filefunc; + pfile_in_zip_read_info->byte_before_the_zipfile=s->byte_before_the_zipfile; + + pfile_in_zip_read_info->stream.total_out = 0; + + if ((s->cur_file_info.compression_method==Z_BZIP2ED) && (!raw)) + { +#ifdef HAVE_BZIP2 + pfile_in_zip_read_info->bstream.bzalloc = (void *(*) (void *, int, int))0; + pfile_in_zip_read_info->bstream.bzfree = (free_func)0; + pfile_in_zip_read_info->bstream.opaque = (voidpf)0; + pfile_in_zip_read_info->bstream.state = (voidpf)0; + + pfile_in_zip_read_info->stream.zalloc = (alloc_func)0; + pfile_in_zip_read_info->stream.zfree = (free_func)0; + pfile_in_zip_read_info->stream.opaque = (voidpf)0; + pfile_in_zip_read_info->stream.next_in = (voidpf)0; + pfile_in_zip_read_info->stream.avail_in = 0; + + err=BZ2_bzDecompressInit(&pfile_in_zip_read_info->bstream, 0, 0); + if (err == Z_OK) + pfile_in_zip_read_info->stream_initialised=Z_BZIP2ED; + else + { + TRYFREE(pfile_in_zip_read_info); + return err; + } +#else + pfile_in_zip_read_info->raw=1; +#endif + } + else if ((s->cur_file_info.compression_method==Z_DEFLATED) && (!raw)) + { + pfile_in_zip_read_info->stream.zalloc = (alloc_func)0; + pfile_in_zip_read_info->stream.zfree = (free_func)0; + pfile_in_zip_read_info->stream.opaque = (voidpf)0; + pfile_in_zip_read_info->stream.next_in = 0; + pfile_in_zip_read_info->stream.avail_in = 0; + + err=inflateInit2(&pfile_in_zip_read_info->stream, -MAX_WBITS); + if (err == Z_OK) + pfile_in_zip_read_info->stream_initialised=Z_DEFLATED; + else + { + TRYFREE(pfile_in_zip_read_info); + return err; + } + /* windowBits is passed < 0 to tell that there is no zlib header. + * Note that in this case inflate *requires* an extra "dummy" byte + * after the compressed stream in order to complete decompression and + * return Z_STREAM_END. + * In unzip, i don't wait absolutely Z_STREAM_END because I known the + * size of both compressed and uncompressed data + */ + } + pfile_in_zip_read_info->rest_read_compressed = + s->cur_file_info.compressed_size ; + pfile_in_zip_read_info->rest_read_uncompressed = + s->cur_file_info.uncompressed_size ; + + + pfile_in_zip_read_info->pos_in_zipfile = + s->cur_file_info_internal.offset_curfile + SIZEZIPLOCALHEADER + + iSizeVar; + + pfile_in_zip_read_info->stream.avail_in = (uInt)0; + + s->pfile_in_zip_read = pfile_in_zip_read_info; + s->encrypted = 0; + +# ifndef NOUNCRYPT + if (password != NULL) + { + int i; + s->pcrc_32_tab = get_crc_table(); + init_keys(password,s->keys,s->pcrc_32_tab); + if (ZSEEK64(s->z_filefunc, s->filestream, + s->pfile_in_zip_read->pos_in_zipfile + + s->pfile_in_zip_read->byte_before_the_zipfile, + SEEK_SET)!=0) + return UNZ_INTERNALERROR; + if(ZREAD64(s->z_filefunc, s->filestream,source, 12)<12) + return UNZ_INTERNALERROR; + + for (i = 0; i<12; i++) + zdecode(s->keys,s->pcrc_32_tab,source[i]); + + s->pfile_in_zip_read->pos_in_zipfile+=12; + s->encrypted=1; + } +# endif + + + return UNZ_OK; +} + +extern int ZEXPORT unzOpenCurrentFile (unzFile file) +{ + return unzOpenCurrentFile3(file, NULL, NULL, 0, NULL); +} + +extern int ZEXPORT unzOpenCurrentFilePassword (unzFile file, const char* password) +{ + return unzOpenCurrentFile3(file, NULL, NULL, 0, password); +} + +extern int ZEXPORT unzOpenCurrentFile2 (unzFile file, int* method, int* level, int raw) +{ + return unzOpenCurrentFile3(file, method, level, raw, NULL); +} + +/** Addition for GDAL : START */ + +extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64( unzFile file) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + s=(unz64_s*)file; + if (file==NULL) + return 0; //UNZ_PARAMERROR; + pfile_in_zip_read_info=s->pfile_in_zip_read; + if (pfile_in_zip_read_info==NULL) + return 0; //UNZ_PARAMERROR; + return pfile_in_zip_read_info->pos_in_zipfile + + pfile_in_zip_read_info->byte_before_the_zipfile; +} + +/** Addition for GDAL : END */ + +/* + Read bytes from the current file. + buf contain buffer where data must be copied + len the size of buf. + + return the number of byte copied if somes bytes are copied + return 0 if the end of file was reached + return <0 with error code if there is an error + (UNZ_ERRNO for IO error, or zLib error for uncompress error) +*/ +extern int ZEXPORT unzReadCurrentFile (unzFile file, voidp buf, unsigned len) +{ + int err=UNZ_OK; + uInt iRead = 0; + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + + if (pfile_in_zip_read_info->read_buffer == NULL) + return UNZ_END_OF_LIST_OF_FILE; + if (len==0) + return 0; + + pfile_in_zip_read_info->stream.next_out = (Bytef*)buf; + + pfile_in_zip_read_info->stream.avail_out = (uInt)len; + + if ((len>pfile_in_zip_read_info->rest_read_uncompressed) && + (!(pfile_in_zip_read_info->raw))) + pfile_in_zip_read_info->stream.avail_out = + (uInt)pfile_in_zip_read_info->rest_read_uncompressed; + + if ((len>pfile_in_zip_read_info->rest_read_compressed+ + pfile_in_zip_read_info->stream.avail_in) && + (pfile_in_zip_read_info->raw)) + pfile_in_zip_read_info->stream.avail_out = + (uInt)pfile_in_zip_read_info->rest_read_compressed+ + pfile_in_zip_read_info->stream.avail_in; + + while (pfile_in_zip_read_info->stream.avail_out>0) + { + if ((pfile_in_zip_read_info->stream.avail_in==0) && + (pfile_in_zip_read_info->rest_read_compressed>0)) + { + uInt uReadThis = UNZ_BUFSIZE; + if (pfile_in_zip_read_info->rest_read_compressedrest_read_compressed; + if (uReadThis == 0) + return UNZ_EOF; + if (ZSEEK64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + pfile_in_zip_read_info->pos_in_zipfile + + pfile_in_zip_read_info->byte_before_the_zipfile, + ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + if (ZREAD64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + pfile_in_zip_read_info->read_buffer, + uReadThis)!=uReadThis) + return UNZ_ERRNO; + + +# ifndef NOUNCRYPT + if(s->encrypted) + { + uInt i; + for(i=0;iread_buffer[i] = + zdecode(s->keys,s->pcrc_32_tab, + pfile_in_zip_read_info->read_buffer[i]); + } +# endif + + + pfile_in_zip_read_info->pos_in_zipfile += uReadThis; + + pfile_in_zip_read_info->rest_read_compressed-=uReadThis; + + pfile_in_zip_read_info->stream.next_in = + (Bytef*)pfile_in_zip_read_info->read_buffer; + pfile_in_zip_read_info->stream.avail_in = (uInt)uReadThis; + } + + if ((pfile_in_zip_read_info->compression_method==0) || (pfile_in_zip_read_info->raw)) + { + uInt uDoCopy,i ; + + if ((pfile_in_zip_read_info->stream.avail_in == 0) && + (pfile_in_zip_read_info->rest_read_compressed == 0)) + return (iRead==0) ? UNZ_EOF : iRead; + + if (pfile_in_zip_read_info->stream.avail_out < + pfile_in_zip_read_info->stream.avail_in) + uDoCopy = pfile_in_zip_read_info->stream.avail_out ; + else + uDoCopy = pfile_in_zip_read_info->stream.avail_in ; + + for (i=0;istream.next_out+i) = + *(pfile_in_zip_read_info->stream.next_in+i); + + pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uDoCopy; + + pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32, + pfile_in_zip_read_info->stream.next_out, + uDoCopy); + pfile_in_zip_read_info->rest_read_uncompressed-=uDoCopy; + pfile_in_zip_read_info->stream.avail_in -= uDoCopy; + pfile_in_zip_read_info->stream.avail_out -= uDoCopy; + pfile_in_zip_read_info->stream.next_out += uDoCopy; + pfile_in_zip_read_info->stream.next_in += uDoCopy; + pfile_in_zip_read_info->stream.total_out += uDoCopy; + iRead += uDoCopy; + } + else if (pfile_in_zip_read_info->compression_method==Z_BZIP2ED) + { +#ifdef HAVE_BZIP2 + uLong uTotalOutBefore,uTotalOutAfter; + const Bytef *bufBefore; + uLong uOutThis; + + pfile_in_zip_read_info->bstream.next_in = (char*)pfile_in_zip_read_info->stream.next_in; + pfile_in_zip_read_info->bstream.avail_in = pfile_in_zip_read_info->stream.avail_in; + pfile_in_zip_read_info->bstream.total_in_lo32 = pfile_in_zip_read_info->stream.total_in; + pfile_in_zip_read_info->bstream.total_in_hi32 = 0; + pfile_in_zip_read_info->bstream.next_out = (char*)pfile_in_zip_read_info->stream.next_out; + pfile_in_zip_read_info->bstream.avail_out = pfile_in_zip_read_info->stream.avail_out; + pfile_in_zip_read_info->bstream.total_out_lo32 = pfile_in_zip_read_info->stream.total_out; + pfile_in_zip_read_info->bstream.total_out_hi32 = 0; + + uTotalOutBefore = pfile_in_zip_read_info->bstream.total_out_lo32; + bufBefore = (const Bytef *)pfile_in_zip_read_info->bstream.next_out; + + err=BZ2_bzDecompress(&pfile_in_zip_read_info->bstream); + + uTotalOutAfter = pfile_in_zip_read_info->bstream.total_out_lo32; + uOutThis = uTotalOutAfter-uTotalOutBefore; + + pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis; + + pfile_in_zip_read_info->crc32 = crc32(pfile_in_zip_read_info->crc32,bufBefore, (uInt)(uOutThis)); + pfile_in_zip_read_info->rest_read_uncompressed -= uOutThis; + iRead += (uInt)(uTotalOutAfter - uTotalOutBefore); + + pfile_in_zip_read_info->stream.next_in = (Bytef*)pfile_in_zip_read_info->bstream.next_in; + pfile_in_zip_read_info->stream.avail_in = pfile_in_zip_read_info->bstream.avail_in; + pfile_in_zip_read_info->stream.total_in = pfile_in_zip_read_info->bstream.total_in_lo32; + pfile_in_zip_read_info->stream.next_out = (Bytef*)pfile_in_zip_read_info->bstream.next_out; + pfile_in_zip_read_info->stream.avail_out = pfile_in_zip_read_info->bstream.avail_out; + pfile_in_zip_read_info->stream.total_out = pfile_in_zip_read_info->bstream.total_out_lo32; + + if (err==BZ_STREAM_END) + return (iRead==0) ? UNZ_EOF : iRead; + if (err!=BZ_OK) + break; +#endif + } // end Z_BZIP2ED + else + { + ZPOS64_T uTotalOutBefore,uTotalOutAfter; + const Bytef *bufBefore; + ZPOS64_T uOutThis; + int flush=Z_SYNC_FLUSH; + + uTotalOutBefore = pfile_in_zip_read_info->stream.total_out; + bufBefore = pfile_in_zip_read_info->stream.next_out; + + /* + if ((pfile_in_zip_read_info->rest_read_uncompressed == + pfile_in_zip_read_info->stream.avail_out) && + (pfile_in_zip_read_info->rest_read_compressed == 0)) + flush = Z_FINISH; + */ + err=inflate(&pfile_in_zip_read_info->stream,flush); + + if ((err>=0) && (pfile_in_zip_read_info->stream.msg!=NULL)) + err = Z_DATA_ERROR; + + uTotalOutAfter = pfile_in_zip_read_info->stream.total_out; + uOutThis = uTotalOutAfter-uTotalOutBefore; + + pfile_in_zip_read_info->total_out_64 = pfile_in_zip_read_info->total_out_64 + uOutThis; + + pfile_in_zip_read_info->crc32 = + crc32(pfile_in_zip_read_info->crc32,bufBefore, + (uInt)(uOutThis)); + + pfile_in_zip_read_info->rest_read_uncompressed -= + uOutThis; + + iRead += (uInt)(uTotalOutAfter - uTotalOutBefore); + + if (err==Z_STREAM_END) + return (iRead==0) ? UNZ_EOF : iRead; + if (err!=Z_OK) + break; + } + } + + if (err==Z_OK) + return iRead; + return err; +} + + +/* + Give the current position in uncompressed data +*/ +extern z_off_t ZEXPORT unztell (unzFile file) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + return (z_off_t)pfile_in_zip_read_info->stream.total_out; +} + +extern ZPOS64_T ZEXPORT unztell64 (unzFile file) +{ + + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return (ZPOS64_T)-1; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return (ZPOS64_T)-1; + + return pfile_in_zip_read_info->total_out_64; +} + + +/* + return 1 if the end of file was reached, 0 elsewhere +*/ +extern int ZEXPORT unzeof (unzFile file) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + if (pfile_in_zip_read_info->rest_read_uncompressed == 0) + return 1; + else + return 0; +} + + + +/* +Read extra field from the current file (opened by unzOpenCurrentFile) +This is the local-header version of the extra field (sometimes, there is +more info in the local-header version than in the central-header) + + if buf==NULL, it return the size of the local extra field that can be read + + if buf!=NULL, len is the size of the buffer, the extra header is copied in + buf. + the return value is the number of bytes copied in buf, or (if <0) + the error code +*/ +extern int ZEXPORT unzGetLocalExtrafield (unzFile file, voidp buf, unsigned len) +{ + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + uInt read_now; + ZPOS64_T size_to_read; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + size_to_read = (pfile_in_zip_read_info->size_local_extrafield - + pfile_in_zip_read_info->pos_local_extrafield); + + if (buf==NULL) + return (int)size_to_read; + + if (len>size_to_read) + read_now = (uInt)size_to_read; + else + read_now = (uInt)len ; + + if (read_now==0) + return 0; + + if (ZSEEK64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + pfile_in_zip_read_info->offset_local_extrafield + + pfile_in_zip_read_info->pos_local_extrafield, + ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + + if (ZREAD64(pfile_in_zip_read_info->z_filefunc, + pfile_in_zip_read_info->filestream, + buf,read_now)!=read_now) + return UNZ_ERRNO; + + return (int)read_now; +} + +/* + Close the file in zip opened with unzipOpenCurrentFile + Return UNZ_CRCERROR if all the file was read but the CRC is not good +*/ +extern int ZEXPORT unzCloseCurrentFile (unzFile file) +{ + int err=UNZ_OK; + + unz64_s* s; + file_in_zip64_read_info_s* pfile_in_zip_read_info; + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + pfile_in_zip_read_info=s->pfile_in_zip_read; + + if (pfile_in_zip_read_info==NULL) + return UNZ_PARAMERROR; + + + if ((pfile_in_zip_read_info->rest_read_uncompressed == 0) && + (!pfile_in_zip_read_info->raw)) + { + if (pfile_in_zip_read_info->crc32 != pfile_in_zip_read_info->crc32_wait) + err=UNZ_CRCERROR; + } + + + TRYFREE(pfile_in_zip_read_info->read_buffer); + pfile_in_zip_read_info->read_buffer = NULL; + if (pfile_in_zip_read_info->stream_initialised == Z_DEFLATED) + inflateEnd(&pfile_in_zip_read_info->stream); +#ifdef HAVE_BZIP2 + else if (pfile_in_zip_read_info->stream_initialised == Z_BZIP2ED) + BZ2_bzDecompressEnd(&pfile_in_zip_read_info->bstream); +#endif + + + pfile_in_zip_read_info->stream_initialised = 0; + TRYFREE(pfile_in_zip_read_info); + + s->pfile_in_zip_read=NULL; + + return err; +} + + +/* + Get the global comment string of the ZipFile, in the szComment buffer. + uSizeBuf is the size of the szComment buffer. + return the number of byte copied or an error code <0 +*/ +extern int ZEXPORT unzGetGlobalComment (unzFile file, char * szComment, uLong uSizeBuf) +{ + unz64_s* s; + uLong uReadThis ; + if (file==NULL) + return (int)UNZ_PARAMERROR; + s=(unz64_s*)file; + + uReadThis = uSizeBuf; + if (uReadThis>s->gi.size_comment) + uReadThis = s->gi.size_comment; + + if (ZSEEK64(s->z_filefunc,s->filestream,s->central_pos+22,ZLIB_FILEFUNC_SEEK_SET)!=0) + return UNZ_ERRNO; + + if (uReadThis>0) + { + *szComment='\0'; + if (ZREAD64(s->z_filefunc,s->filestream,szComment,uReadThis)!=uReadThis) + return UNZ_ERRNO; + } + + if ((szComment != NULL) && (uSizeBuf > s->gi.size_comment)) + *(szComment+s->gi.size_comment)='\0'; + return (int)uReadThis; +} + +/* Additions by RX '2004 */ +extern ZPOS64_T ZEXPORT unzGetOffset64(unzFile file) +{ + unz64_s* s; + + if (file==NULL) + return 0; //UNZ_PARAMERROR; + s=(unz64_s*)file; + if (!s->current_file_ok) + return 0; + if (s->gi.number_entry != 0 && s->gi.number_entry != 0xffff) + if (s->num_file==s->gi.number_entry) + return 0; + return s->pos_in_central_dir; +} + +extern uLong ZEXPORT unzGetOffset (unzFile file) +{ + ZPOS64_T offset64; + + if (file==NULL) + return 0; //UNZ_PARAMERROR; + offset64 = unzGetOffset64(file); + return (uLong)offset64; +} + +extern int ZEXPORT unzSetOffset64(unzFile file, ZPOS64_T pos) +{ + unz64_s* s; + int err; + + if (file==NULL) + return UNZ_PARAMERROR; + s=(unz64_s*)file; + + s->pos_in_central_dir = pos; + s->num_file = s->gi.number_entry; /* hack */ + err = unz64local_GetCurrentFileInfoInternal(file,&s->cur_file_info, + &s->cur_file_info_internal, + NULL,0,NULL,0,NULL,0); + s->current_file_ok = (err == UNZ_OK); + return err; +} + +extern int ZEXPORT unzSetOffset (unzFile file, uLong pos) +{ + return unzSetOffset64(file,pos); +} diff --git a/src/third_party/minizip/unzip.h b/src/third_party/minizip/unzip.h new file mode 100644 index 0000000..3183968 --- /dev/null +++ b/src/third_party/minizip/unzip.h @@ -0,0 +1,437 @@ +/* unzip.h -- IO for uncompress .zip files using zlib + Version 1.1, February 14h, 2010 + part of the MiniZip project - ( http://www.winimage.com/zLibDll/minizip.html ) + + Copyright (C) 1998-2010 Gilles Vollant (minizip) ( http://www.winimage.com/zLibDll/minizip.html ) + + Modifications of Unzip for Zip64 + Copyright (C) 2007-2008 Even Rouault + + Modifications for Zip64 support on both zip and unzip + Copyright (C) 2009-2010 Mathias Svensson ( http://result42.com ) + + For more info read MiniZip_info.txt + + --------------------------------------------------------------------------------- + + Condition of use and distribution are the same than zlib : + + This software is provided 'as-is', without any express or implied + warranty. In no event will the authors be held liable for any damages + arising from the use of this software. + + Permission is granted to anyone to use this software for any purpose, + including commercial applications, and to alter it and redistribute it + freely, subject to the following restrictions: + + 1. The origin of this software must not be misrepresented; you must not + claim that you wrote the original software. If you use this software + in a product, an acknowledgment in the product documentation would be + appreciated but is not required. + 2. Altered source versions must be plainly marked as such, and must not be + misrepresented as being the original software. + 3. This notice may not be removed or altered from any source distribution. + + --------------------------------------------------------------------------------- + + Changes + + See header of unzip64.c + +*/ + +#ifndef _unz64_H +#define _unz64_H + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef _ZLIB_H +#include "zlib.h" +#endif + +#ifndef _ZLIBIOAPI_H +#include "ioapi.h" +#endif + +#ifdef HAVE_BZIP2 +#include "bzlib.h" +#endif + +#define Z_BZIP2ED 12 + +#if defined(STRICTUNZIP) || defined(STRICTZIPUNZIP) +/* like the STRICT of WIN32, we define a pointer that cannot be converted + from (void*) without cast */ +typedef struct TagunzFile__ { int unused; } unzFile__; +typedef unzFile__ *unzFile; +#else +typedef voidp unzFile; +#endif + + +#define UNZ_OK (0) +#define UNZ_END_OF_LIST_OF_FILE (-100) +#define UNZ_ERRNO (Z_ERRNO) +#define UNZ_EOF (0) +#define UNZ_PARAMERROR (-102) +#define UNZ_BADZIPFILE (-103) +#define UNZ_INTERNALERROR (-104) +#define UNZ_CRCERROR (-105) + +/* tm_unz contain date/time info */ +typedef struct tm_unz_s +{ + uInt tm_sec; /* seconds after the minute - [0,59] */ + uInt tm_min; /* minutes after the hour - [0,59] */ + uInt tm_hour; /* hours since midnight - [0,23] */ + uInt tm_mday; /* day of the month - [1,31] */ + uInt tm_mon; /* months since January - [0,11] */ + uInt tm_year; /* years - [1980..2044] */ +} tm_unz; + +/* unz_global_info structure contain global data about the ZIPfile + These data comes from the end of central dir */ +typedef struct unz_global_info64_s +{ + ZPOS64_T number_entry; /* total number of entries in + the central dir on this disk */ + uLong size_comment; /* size of the global comment of the zipfile */ +} unz_global_info64; + +typedef struct unz_global_info_s +{ + uLong number_entry; /* total number of entries in + the central dir on this disk */ + uLong size_comment; /* size of the global comment of the zipfile */ +} unz_global_info; + +/* unz_file_info contain information about a file in the zipfile */ +typedef struct unz_file_info64_s +{ + uLong version; /* version made by 2 bytes */ + uLong version_needed; /* version needed to extract 2 bytes */ + uLong flag; /* general purpose bit flag 2 bytes */ + uLong compression_method; /* compression method 2 bytes */ + uLong dosDate; /* last mod file date in Dos fmt 4 bytes */ + uLong crc; /* crc-32 4 bytes */ + ZPOS64_T compressed_size; /* compressed size 8 bytes */ + ZPOS64_T uncompressed_size; /* uncompressed size 8 bytes */ + uLong size_filename; /* filename length 2 bytes */ + uLong size_file_extra; /* extra field length 2 bytes */ + uLong size_file_comment; /* file comment length 2 bytes */ + + uLong disk_num_start; /* disk number start 2 bytes */ + uLong internal_fa; /* internal file attributes 2 bytes */ + uLong external_fa; /* external file attributes 4 bytes */ + + tm_unz tmu_date; +} unz_file_info64; + +typedef struct unz_file_info_s +{ + uLong version; /* version made by 2 bytes */ + uLong version_needed; /* version needed to extract 2 bytes */ + uLong flag; /* general purpose bit flag 2 bytes */ + uLong compression_method; /* compression method 2 bytes */ + uLong dosDate; /* last mod file date in Dos fmt 4 bytes */ + uLong crc; /* crc-32 4 bytes */ + uLong compressed_size; /* compressed size 4 bytes */ + uLong uncompressed_size; /* uncompressed size 4 bytes */ + uLong size_filename; /* filename length 2 bytes */ + uLong size_file_extra; /* extra field length 2 bytes */ + uLong size_file_comment; /* file comment length 2 bytes */ + + uLong disk_num_start; /* disk number start 2 bytes */ + uLong internal_fa; /* internal file attributes 2 bytes */ + uLong external_fa; /* external file attributes 4 bytes */ + + tm_unz tmu_date; +} unz_file_info; + +extern int ZEXPORT unzStringFileNameCompare OF ((const char* fileName1, + const char* fileName2, + int iCaseSensitivity)); +/* + Compare two filename (fileName1,fileName2). + If iCaseSenisivity = 1, comparision is case sensitivity (like strcmp) + If iCaseSenisivity = 2, comparision is not case sensitivity (like strcmpi + or strcasecmp) + If iCaseSenisivity = 0, case sensitivity is defaut of your operating system + (like 1 on Unix, 2 on Windows) +*/ + + +extern unzFile ZEXPORT unzOpen OF((const char *path)); +extern unzFile ZEXPORT unzOpen64 OF((const void *path)); +/* + Open a Zip file. path contain the full pathname (by example, + on a Windows XP computer "c:\\zlib\\zlib113.zip" or on an Unix computer + "zlib/zlib113.zip". + If the zipfile cannot be opened (file don't exist or in not valid), the + return value is NULL. + Else, the return value is a unzFile Handle, usable with other function + of this unzip package. + the "64" function take a const void* pointer, because the path is just the + value passed to the open64_file_func callback. + Under Windows, if UNICODE is defined, using fill_fopen64_filefunc, the path + is a pointer to a wide unicode string (LPCTSTR is LPCWSTR), so const char* + does not describe the reality +*/ + + +extern unzFile ZEXPORT unzOpen2 OF((const char *path, + zlib_filefunc_def* pzlib_filefunc_def)); +/* + Open a Zip file, like unzOpen, but provide a set of file low level API + for read/write the zip file (see ioapi.h) +*/ + +extern unzFile ZEXPORT unzOpen2_64 OF((const void *path, + zlib_filefunc64_def* pzlib_filefunc_def)); +/* + Open a Zip file, like unz64Open, but provide a set of file low level API + for read/write the zip file (see ioapi.h) +*/ + +extern int ZEXPORT unzClose OF((unzFile file)); +/* + Close a ZipFile opened with unzipOpen. + If there is files inside the .Zip opened with unzOpenCurrentFile (see later), + these files MUST be closed with unzipCloseCurrentFile before call unzipClose. + return UNZ_OK if there is no problem. */ + +extern int ZEXPORT unzGetGlobalInfo OF((unzFile file, + unz_global_info *pglobal_info)); + +extern int ZEXPORT unzGetGlobalInfo64 OF((unzFile file, + unz_global_info64 *pglobal_info)); +/* + Write info about the ZipFile in the *pglobal_info structure. + No preparation of the structure is needed + return UNZ_OK if there is no problem. */ + + +extern int ZEXPORT unzGetGlobalComment OF((unzFile file, + char *szComment, + uLong uSizeBuf)); +/* + Get the global comment string of the ZipFile, in the szComment buffer. + uSizeBuf is the size of the szComment buffer. + return the number of byte copied or an error code <0 +*/ + + +/***************************************************************************/ +/* Unzip package allow you browse the directory of the zipfile */ + +extern int ZEXPORT unzGoToFirstFile OF((unzFile file)); +/* + Set the current file of the zipfile to the first file. + return UNZ_OK if there is no problem +*/ + +extern int ZEXPORT unzGoToNextFile OF((unzFile file)); +/* + Set the current file of the zipfile to the next file. + return UNZ_OK if there is no problem + return UNZ_END_OF_LIST_OF_FILE if the actual file was the latest. +*/ + +extern int ZEXPORT unzLocateFile OF((unzFile file, + const char *szFileName, + int iCaseSensitivity)); +/* + Try locate the file szFileName in the zipfile. + For the iCaseSensitivity signification, see unzStringFileNameCompare + + return value : + UNZ_OK if the file is found. It becomes the current file. + UNZ_END_OF_LIST_OF_FILE if the file is not found +*/ + + +/* ****************************************** */ +/* Ryan supplied functions */ +/* unz_file_info contain information about a file in the zipfile */ +typedef struct unz_file_pos_s +{ + uLong pos_in_zip_directory; /* offset in zip file directory */ + uLong num_of_file; /* # of file */ +} unz_file_pos; + +extern int ZEXPORT unzGetFilePos( + unzFile file, + unz_file_pos* file_pos); + +extern int ZEXPORT unzGoToFilePos( + unzFile file, + unz_file_pos* file_pos); + +typedef struct unz64_file_pos_s +{ + ZPOS64_T pos_in_zip_directory; /* offset in zip file directory */ + ZPOS64_T num_of_file; /* # of file */ +} unz64_file_pos; + +extern int ZEXPORT unzGetFilePos64( + unzFile file, + unz64_file_pos* file_pos); + +extern int ZEXPORT unzGoToFilePos64( + unzFile file, + const unz64_file_pos* file_pos); + +/* ****************************************** */ + +extern int ZEXPORT unzGetCurrentFileInfo64 OF((unzFile file, + unz_file_info64 *pfile_info, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize)); + +extern int ZEXPORT unzGetCurrentFileInfo OF((unzFile file, + unz_file_info *pfile_info, + char *szFileName, + uLong fileNameBufferSize, + void *extraField, + uLong extraFieldBufferSize, + char *szComment, + uLong commentBufferSize)); +/* + Get Info about the current file + if pfile_info!=NULL, the *pfile_info structure will contain somes info about + the current file + if szFileName!=NULL, the filemane string will be copied in szFileName + (fileNameBufferSize is the size of the buffer) + if extraField!=NULL, the extra field information will be copied in extraField + (extraFieldBufferSize is the size of the buffer). + This is the Central-header version of the extra field + if szComment!=NULL, the comment string of the file will be copied in szComment + (commentBufferSize is the size of the buffer) +*/ + + +/** Addition for GDAL : START */ + +extern ZPOS64_T ZEXPORT unzGetCurrentFileZStreamPos64 OF((unzFile file)); + +/** Addition for GDAL : END */ + + +/***************************************************************************/ +/* for reading the content of the current zipfile, you can open it, read data + from it, and close it (you can close it before reading all the file) + */ + +extern int ZEXPORT unzOpenCurrentFile OF((unzFile file)); +/* + Open for reading data the current file in the zipfile. + If there is no error, the return value is UNZ_OK. +*/ + +extern int ZEXPORT unzOpenCurrentFilePassword OF((unzFile file, + const char* password)); +/* + Open for reading data the current file in the zipfile. + password is a crypting password + If there is no error, the return value is UNZ_OK. +*/ + +extern int ZEXPORT unzOpenCurrentFile2 OF((unzFile file, + int* method, + int* level, + int raw)); +/* + Same than unzOpenCurrentFile, but open for read raw the file (not uncompress) + if raw==1 + *method will receive method of compression, *level will receive level of + compression + note : you can set level parameter as NULL (if you did not want known level, + but you CANNOT set method parameter as NULL +*/ + +extern int ZEXPORT unzOpenCurrentFile3 OF((unzFile file, + int* method, + int* level, + int raw, + const char* password)); +/* + Same than unzOpenCurrentFile, but open for read raw the file (not uncompress) + if raw==1 + *method will receive method of compression, *level will receive level of + compression + note : you can set level parameter as NULL (if you did not want known level, + but you CANNOT set method parameter as NULL +*/ + + +extern int ZEXPORT unzCloseCurrentFile OF((unzFile file)); +/* + Close the file in zip opened with unzOpenCurrentFile + Return UNZ_CRCERROR if all the file was read but the CRC is not good +*/ + +extern int ZEXPORT unzReadCurrentFile OF((unzFile file, + voidp buf, + unsigned len)); +/* + Read bytes from the current file (opened by unzOpenCurrentFile) + buf contain buffer where data must be copied + len the size of buf. + + return the number of byte copied if somes bytes are copied + return 0 if the end of file was reached + return <0 with error code if there is an error + (UNZ_ERRNO for IO error, or zLib error for uncompress error) +*/ + +extern z_off_t ZEXPORT unztell OF((unzFile file)); + +extern ZPOS64_T ZEXPORT unztell64 OF((unzFile file)); +/* + Give the current position in uncompressed data +*/ + +extern int ZEXPORT unzeof OF((unzFile file)); +/* + return 1 if the end of file was reached, 0 elsewhere +*/ + +extern int ZEXPORT unzGetLocalExtrafield OF((unzFile file, + voidp buf, + unsigned len)); +/* + Read extra field from the current file (opened by unzOpenCurrentFile) + This is the local-header version of the extra field (sometimes, there is + more info in the local-header version than in the central-header) + + if buf==NULL, it return the size of the local extra field + + if buf!=NULL, len is the size of the buffer, the extra header is copied in + buf. + the return value is the number of bytes copied in buf, or (if <0) + the error code +*/ + +/***************************************************************************/ + +/* Get the current file offset */ +extern ZPOS64_T ZEXPORT unzGetOffset64 (unzFile file); +extern uLong ZEXPORT unzGetOffset (unzFile file); + +/* Set the current file offset */ +extern int ZEXPORT unzSetOffset64 (unzFile file, ZPOS64_T pos); +extern int ZEXPORT unzSetOffset (unzFile file, uLong pos); + + + +#ifdef __cplusplus +} +#endif + +#endif /* _unz64_H */ diff --git a/src/third_party/oboe/CMakeLists.txt b/src/third_party/oboe/CMakeLists.txt new file mode 100644 index 0000000..8a6b632 --- /dev/null +++ b/src/third_party/oboe/CMakeLists.txt @@ -0,0 +1,92 @@ +cmake_minimum_required(VERSION 3.4.1) + +# Set the name of the project and store it in PROJECT_NAME. Also set the following variables: +# PROJECT_SOURCE_DIR (usually the root directory where Oboe has been cloned e.g.) +# PROJECT_BINARY_DIR (usually the containing project's binary directory, +# e.g. ${OBOE_HOME}/samples/RhythmGame/.externalNativeBuild/cmake/ndkExtractorDebug/x86/oboe-bin) +project(oboe) + +set (oboe_sources + src/aaudio/AAudioLoader.cpp + src/aaudio/AudioStreamAAudio.cpp + src/common/AudioSourceCaller.cpp + src/common/AudioStream.cpp + src/common/AudioStreamBuilder.cpp + src/common/DataConversionFlowGraph.cpp + src/common/FilterAudioStream.cpp + src/common/FixedBlockAdapter.cpp + src/common/FixedBlockReader.cpp + src/common/FixedBlockWriter.cpp + src/common/LatencyTuner.cpp + src/common/SourceFloatCaller.cpp + src/common/SourceI16Caller.cpp + src/common/Utilities.cpp + src/common/QuirksManager.cpp + src/fifo/FifoBuffer.cpp + src/fifo/FifoController.cpp + src/fifo/FifoControllerBase.cpp + src/fifo/FifoControllerIndirect.cpp + src/flowgraph/FlowGraphNode.cpp + src/flowgraph/ClipToRange.cpp + src/flowgraph/ManyToMultiConverter.cpp + src/flowgraph/MonoToMultiConverter.cpp + src/flowgraph/RampLinear.cpp + src/flowgraph/SampleRateConverter.cpp + src/flowgraph/SinkFloat.cpp + src/flowgraph/SinkI16.cpp + src/flowgraph/SinkI24.cpp + src/flowgraph/SourceFloat.cpp + src/flowgraph/SourceI16.cpp + src/flowgraph/SourceI24.cpp + src/flowgraph/resampler/IntegerRatio.cpp + src/flowgraph/resampler/LinearResampler.cpp + src/flowgraph/resampler/MultiChannelResampler.cpp + src/flowgraph/resampler/PolyphaseResampler.cpp + src/flowgraph/resampler/PolyphaseResamplerMono.cpp + src/flowgraph/resampler/PolyphaseResamplerStereo.cpp + src/flowgraph/resampler/SincResampler.cpp + src/flowgraph/resampler/SincResamplerStereo.cpp + src/opensles/AudioInputStreamOpenSLES.cpp + src/opensles/AudioOutputStreamOpenSLES.cpp + src/opensles/AudioStreamBuffered.cpp + src/opensles/AudioStreamOpenSLES.cpp + src/opensles/EngineOpenSLES.cpp + src/opensles/OpenSLESUtilities.cpp + src/opensles/OutputMixerOpenSLES.cpp + src/common/StabilizedCallback.cpp + src/common/Trace.cpp + src/common/Version.cpp + ) + +add_library(oboe ${oboe_sources}) + +# Specify directories which the compiler should look for headers +target_include_directories(oboe + PRIVATE src + PUBLIC include) + +# Compile Flags: +# Enable -Werror when building debug config +# Enable -Ofast +target_compile_options(oboe + PRIVATE + -std=c++14 + -Wall + -Wextra-semi + -Wshadow + -Wshadow-field + -Ofast + "$<$:-Werror>") + +# Enable logging of D,V for debug builds +target_compile_definitions(oboe PUBLIC $<$:OBOE_ENABLE_LOGGING=1>) + +target_link_libraries(oboe PRIVATE log OpenSLES) + +# When installing oboe put the libraries in the lib/ folder e.g. lib/arm64-v8a +install(TARGETS oboe + LIBRARY DESTINATION lib/${ANDROID_ABI} + ARCHIVE DESTINATION lib/${ANDROID_ABI}) + +# Also install the headers +install(DIRECTORY include/oboe DESTINATION include) \ No newline at end of file diff --git a/src/third_party/oboe/include/oboe/AudioStream.h b/src/third_party/oboe/include/oboe/AudioStream.h new file mode 100644 index 0000000..a158aaf --- /dev/null +++ b/src/third_party/oboe/include/oboe/AudioStream.h @@ -0,0 +1,543 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_H_ +#define OBOE_STREAM_H_ + +#include +#include +#include +#include +#include "oboe/Definitions.h" +#include "oboe/ResultWithValue.h" +#include "oboe/AudioStreamBuilder.h" +#include "oboe/AudioStreamBase.h" + +/** WARNING - UNDER CONSTRUCTION - THIS API WILL CHANGE. */ + +namespace oboe { + +/** + * The default number of nanoseconds to wait for when performing state change operations on the + * stream, such as `start` and `stop`. + * + * @see oboe::AudioStream::start + */ +constexpr int64_t kDefaultTimeoutNanos = (2000 * kNanosPerMillisecond); + +/** + * Base class for Oboe C++ audio stream. + */ +class AudioStream : public AudioStreamBase { + friend class AudioStreamBuilder; // allow access to setWeakThis() and lockWeakThis() +public: + + AudioStream() {} + + /** + * Construct an `AudioStream` using the given `AudioStreamBuilder` + * + * @param builder containing all the stream's attributes + */ + explicit AudioStream(const AudioStreamBuilder &builder); + + virtual ~AudioStream() = default; + + /** + * Open a stream based on the current settings. + * + * Note that we do not recommend re-opening a stream that has been closed. + * TODO Should we prevent re-opening? + * + * @return + */ + virtual Result open() { + return Result::OK; // Called by subclasses. Might do more in the future. + } + + /** + * Close the stream and deallocate any resources from the open() call. + */ + virtual Result close(); + + /** + * Start the stream. This will block until the stream has been started, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result start(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /** + * Pause the stream. This will block until the stream has been paused, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result pause(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /** + * Flush the stream. This will block until the stream has been flushed, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result flush(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /** + * Stop the stream. This will block until the stream has been stopped, an error occurs + * or `timeoutNanoseconds` has been reached. + */ + virtual Result stop(int64_t timeoutNanoseconds = kDefaultTimeoutNanos); + + /* Asynchronous requests. + * Use waitForStateChange() if you need to wait for completion. + */ + + /** + * Start the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `start(0)`. + */ + virtual Result requestStart() = 0; + + /** + * Pause the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `pause(0)`. + */ + virtual Result requestPause() = 0; + + /** + * Flush the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `flush(0)`. + */ + virtual Result requestFlush() = 0; + + /** + * Stop the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `stop(0)`. + */ + virtual Result requestStop() = 0; + + /** + * Query the current state, eg. StreamState::Pausing + * + * @return state or a negative error. + */ + virtual StreamState getState() const = 0; + + /** + * Wait until the stream's current state no longer matches the input state. + * The input state is passed to avoid race conditions caused by the state + * changing between calls. + * + * Note that generally applications do not need to call this. It is considered + * an advanced technique and is mostly used for testing. + * + *


+     * int64_t timeoutNanos = 500 * kNanosPerMillisecond; // arbitrary 1/2 second
+     * StreamState currentState = stream->getState();
+     * StreamState nextState = StreamState::Unknown;
+     * while (result == Result::OK && currentState != StreamState::Paused) {
+     *     result = stream->waitForStateChange(
+     *                                   currentState, &nextState, timeoutNanos);
+     *     currentState = nextState;
+     * }
+     * 
+ * + * If the state does not change within the timeout period then it will + * return ErrorTimeout. This is true even if timeoutNanoseconds is zero. + * + * @param inputState The state we want to change away from. + * @param nextState Pointer to a variable that will be set to the new state. + * @param timeoutNanoseconds The maximum time to wait in nanoseconds. + * @return Result::OK or a Result::Error. + */ + virtual Result waitForStateChange(StreamState inputState, + StreamState *nextState, + int64_t timeoutNanoseconds) = 0; + + /** + * This can be used to adjust the latency of the buffer by changing + * the threshold where blocking will occur. + * By combining this with getXRunCount(), the latency can be tuned + * at run-time for each device. + * + * This cannot be set higher than getBufferCapacity(). + * + * @param requestedFrames requested number of frames that can be filled without blocking + * @return the resulting buffer size in frames (obtained using value()) or an error (obtained + * using error()) + */ + virtual ResultWithValue setBufferSizeInFrames(int32_t /* requestedFrames */) { + return Result::ErrorUnimplemented; + } + + /** + * An XRun is an Underrun or an Overrun. + * During playing, an underrun will occur if the stream is not written in time + * and the system runs out of valid data. + * During recording, an overrun will occur if the stream is not read in time + * and there is no place to put the incoming data so it is discarded. + * + * An underrun or overrun can cause an audible "pop" or "glitch". + * + * @return a result which is either Result::OK with the xRun count as the value, or a + * Result::Error* code + */ + virtual ResultWithValue getXRunCount() const { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * @return true if XRun counts are supported on the stream + */ + virtual bool isXRunCountSupported() const = 0; + + /** + * Query the number of frames that are read or written by the endpoint at one time. + * + * @return burst size + */ + virtual int32_t getFramesPerBurst() = 0; + + /** + * Get the number of bytes in each audio frame. This is calculated using the channel count + * and the sample format. For example, a 2 channel floating point stream will have + * 2 * 4 = 8 bytes per frame. + * + * @return number of bytes in each audio frame. + */ + int32_t getBytesPerFrame() const { return mChannelCount * getBytesPerSample(); } + + /** + * Get the number of bytes per sample. This is calculated using the sample format. For example, + * a stream using 16-bit integer samples will have 2 bytes per sample. + * + * @return the number of bytes per sample. + */ + int32_t getBytesPerSample() const; + + /** + * The number of audio frames written into the stream. + * This monotonic counter will never get reset. + * + * @return the number of frames written so far + */ + virtual int64_t getFramesWritten(); + + /** + * The number of audio frames read from the stream. + * This monotonic counter will never get reset. + * + * @return the number of frames read so far + */ + virtual int64_t getFramesRead(); + + /** + * Calculate the latency of a stream based on getTimestamp(). + * + * Output latency is the time it takes for a given frame to travel from the + * app to some type of digital-to-analog converter. If the DAC is external, for example + * in a USB interface or a TV connected by HDMI, then there may be additional latency + * that the Android device is unaware of. + * + * Input latency is the time it takes to a given frame to travel from an analog-to-digital + * converter (ADC) to the app. + * + * Note that the latency of an OUTPUT stream will increase abruptly when you write data to it + * and then decrease slowly over time as the data is consumed. + * + * The latency of an INPUT stream will decrease abruptly when you read data from it + * and then increase slowly over time as more data arrives. + * + * The latency of an OUTPUT stream is generally higher than the INPUT latency + * because an app generally tries to keep the OUTPUT buffer full and the INPUT buffer empty. + * + * @return a ResultWithValue which has a result of Result::OK and a value containing the latency + * in milliseconds, or a result of Result::Error*. + */ + virtual ResultWithValue calculateLatencyMillis() { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * Get the estimated time that the frame at `framePosition` entered or left the audio processing + * pipeline. + * + * This can be used to coordinate events and interactions with the external environment, and to + * estimate the latency of an audio stream. An example of usage can be found in the hello-oboe + * sample (search for "calculateCurrentOutputLatencyMillis"). + * + * The time is based on the implementation's best effort, using whatever knowledge is available + * to the system, but cannot account for any delay unknown to the implementation. + * + * @deprecated since 1.0, use AudioStream::getTimestamp(clockid_t clockId) instead, which + * returns ResultWithValue + * @param clockId the type of clock to use e.g. CLOCK_MONOTONIC + * @param framePosition the frame number to query + * @param timeNanoseconds an output parameter which will contain the presentation timestamp + */ + virtual Result getTimestamp(clockid_t /* clockId */, + int64_t* /* framePosition */, + int64_t* /* timeNanoseconds */) { + return Result::ErrorUnimplemented; + } + + /** + * Get the estimated time that the frame at `framePosition` entered or left the audio processing + * pipeline. + * + * This can be used to coordinate events and interactions with the external environment, and to + * estimate the latency of an audio stream. An example of usage can be found in the hello-oboe + * sample (search for "calculateCurrentOutputLatencyMillis"). + * + * The time is based on the implementation's best effort, using whatever knowledge is available + * to the system, but cannot account for any delay unknown to the implementation. + * + * @param clockId the type of clock to use e.g. CLOCK_MONOTONIC + * @return a FrameTimestamp containing the position and time at which a particular audio frame + * entered or left the audio processing pipeline, or an error if the operation failed. + */ + virtual ResultWithValue getTimestamp(clockid_t /* clockId */); + + // ============== I/O =========================== + /** + * Write data from the supplied buffer into the stream. This method will block until the write + * is complete or it runs out of time. + * + * If `timeoutNanoseconds` is zero then this call will not wait. + * + * @param buffer The address of the first sample. + * @param numFrames Number of frames to write. Only complete frames will be written. + * @param timeoutNanoseconds Maximum number of nanoseconds to wait for completion. + * @return a ResultWithValue which has a result of Result::OK and a value containing the number + * of frames actually written, or result of Result::Error*. + */ + virtual ResultWithValue write(const void* /* buffer */, + int32_t /* numFrames */, + int64_t /* timeoutNanoseconds */ ) { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * Read data into the supplied buffer from the stream. This method will block until the read + * is complete or it runs out of time. + * + * If `timeoutNanoseconds` is zero then this call will not wait. + * + * @param buffer The address of the first sample. + * @param numFrames Number of frames to read. Only complete frames will be read. + * @param timeoutNanoseconds Maximum number of nanoseconds to wait for completion. + * @return a ResultWithValue which has a result of Result::OK and a value containing the number + * of frames actually read, or result of Result::Error*. + */ + virtual ResultWithValue read(void* /* buffer */, + int32_t /* numFrames */, + int64_t /* timeoutNanoseconds */) { + return ResultWithValue(Result::ErrorUnimplemented); + } + + /** + * Get the underlying audio API which the stream uses. + * + * @return the API that this stream uses. + */ + virtual AudioApi getAudioApi() const = 0; + + /** + * Returns true if the underlying audio API is AAudio. + * + * @return true if this stream is implemented using the AAudio API. + */ + bool usesAAudio() const { + return getAudioApi() == AudioApi::AAudio; + } + + /** + * Only for debugging. Do not use in production. + * If you need to call this method something is wrong. + * If you think you need it for production then please let us know + * so we can modify Oboe so that you don't need this. + * + * @return nullptr or a pointer to a stream from the system API + */ + virtual void *getUnderlyingStream() const { + return nullptr; + } + + /** + * Launch a thread that will stop the stream. + */ + void launchStopThread(); + + /** + * Update mFramesWritten. + * For internal use only. + */ + virtual void updateFramesWritten() = 0; + + /** + * Update mFramesRead. + * For internal use only. + */ + virtual void updateFramesRead() = 0; + + /* + * Swap old callback for new callback. + * This not atomic. + * This should only be used internally. + * @param streamCallback + * @return previous streamCallback + */ + AudioStreamCallback *swapCallback(AudioStreamCallback *streamCallback) { + AudioStreamCallback *previousCallback = mStreamCallback; + mStreamCallback = streamCallback; + return previousCallback; + } + + /** + * @return number of frames of data currently in the buffer + */ + ResultWithValue getAvailableFrames(); + + /** + * Wait until the stream has a minimum amount of data available in its buffer. + * This can be used with an EXCLUSIVE MMAP input stream to avoid reading data too close to + * the DSP write position, which may cause glitches. + * + * @param numFrames minimum frames available + * @param timeoutNanoseconds + * @return number of frames available, ErrorTimeout + */ + ResultWithValue waitForAvailableFrames(int32_t numFrames, + int64_t timeoutNanoseconds); + +protected: + + /** + * This is used to detect more than one error callback from a stream. + * These were bugs in some versions of Android that caused multiple error callbacks. + * Internal bug b/63087953 + * + * Calling this sets an atomic true and returns the previous value. + * + * @return false on first call, true on subsequent calls + */ + bool wasErrorCallbackCalled() { + return mErrorCallbackCalled.exchange(true); + } + + /** + * Wait for a transition from one state to another. + * @return OK if the endingState was observed, or ErrorUnexpectedState + * if any state that was not the startingState or endingState was observed + * or ErrorTimeout. + */ + virtual Result waitForStateTransition(StreamState startingState, + StreamState endingState, + int64_t timeoutNanoseconds); + + /** + * Override this to provide a default for when the application did not specify a callback. + * + * @param audioData + * @param numFrames + * @return result + */ + virtual DataCallbackResult onDefaultCallback(void* /* audioData */, int /* numFrames */) { + return DataCallbackResult::Stop; + } + + /** + * Override this to provide your own behaviour for the audio callback + * + * @param audioData container array which audio frames will be written into or read from + * @param numFrames number of frames which were read/written + * @return the result of the callback: stop or continue + * + */ + DataCallbackResult fireDataCallback(void *audioData, int numFrames); + + /** + * @return true if callbacks may be called + */ + bool isDataCallbackEnabled() { + return mDataCallbackEnabled; + } + + /** + * This can be set false internally to prevent callbacks + * after DataCallbackResult::Stop has been returned. + */ + void setDataCallbackEnabled(bool enabled) { + mDataCallbackEnabled = enabled; + } + + /* + * Set a weak_ptr to this stream from the shared_ptr so that we can + * later use a shared_ptr in the error callback. + */ + void setWeakThis(std::shared_ptr &sharedStream) { + mWeakThis = sharedStream; + } + + /* + * Make a shared_ptr that will prevent this stream from being deleted. + */ + std::shared_ptr lockWeakThis() { + return mWeakThis.lock(); + } + + std::weak_ptr mWeakThis; // weak pointer to this object + + /** + * Number of frames which have been written into the stream + * + * This is signed integer to match the counters in AAudio. + * At audio rates, the counter will overflow in about six million years. + */ + std::atomic mFramesWritten{}; + + /** + * Number of frames which have been read from the stream. + * + * This is signed integer to match the counters in AAudio. + * At audio rates, the counter will overflow in about six million years. + */ + std::atomic mFramesRead{}; + + std::mutex mLock; // for synchronizing start/stop/close + + +private: + // Log the scheduler if it changes. + void checkScheduler(); + int mPreviousScheduler = -1; + + std::atomic mDataCallbackEnabled{false}; + std::atomic mErrorCallbackCalled{false}; + +}; + +/** + * This struct is a stateless functor which closes an AudioStream prior to its deletion. + * This means it can be used to safely delete a smart pointer referring to an open stream. + */ + struct StreamDeleterFunctor { + void operator()(AudioStream *audioStream) { + if (audioStream) { + audioStream->close(); + } + delete audioStream; + } + }; +} // namespace oboe + +#endif /* OBOE_STREAM_H_ */ diff --git a/src/third_party/oboe/include/oboe/AudioStreamBase.h b/src/third_party/oboe/include/oboe/AudioStreamBase.h new file mode 100644 index 0000000..4a6237d --- /dev/null +++ b/src/third_party/oboe/include/oboe/AudioStreamBase.h @@ -0,0 +1,202 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_BASE_H_ +#define OBOE_STREAM_BASE_H_ + +#include +#include "oboe/AudioStreamCallback.h" +#include "oboe/Definitions.h" + +namespace oboe { + +/** + * Base class containing parameters for audio streams and builders. + **/ +class AudioStreamBase { + +public: + + AudioStreamBase() {} + + virtual ~AudioStreamBase() = default; + + // This class only contains primitives so we can use default constructor and copy methods. + + /** + * Default copy constructor + */ + AudioStreamBase(const AudioStreamBase&) = default; + + /** + * Default assignment operator + */ + AudioStreamBase& operator=(const AudioStreamBase&) = default; + + /** + * @return number of channels, for example 2 for stereo, or kUnspecified + */ + int32_t getChannelCount() const { return mChannelCount; } + + /** + * @return Direction::Input or Direction::Output + */ + Direction getDirection() const { return mDirection; } + + /** + * @return sample rate for the stream or kUnspecified + */ + int32_t getSampleRate() const { return mSampleRate; } + + /** + * @return the number of frames in each callback or kUnspecified. + */ + int32_t getFramesPerCallback() const { return mFramesPerCallback; } + + /** + * @return the audio sample format (e.g. Float or I16) + */ + AudioFormat getFormat() const { return mFormat; } + + /** + * Query the maximum number of frames that can be filled without blocking. + * If the stream has been closed the last known value will be returned. + * + * @return buffer size + */ + virtual int32_t getBufferSizeInFrames() { return mBufferSizeInFrames; } + + /** + * @return capacityInFrames or kUnspecified + */ + virtual int32_t getBufferCapacityInFrames() const { return mBufferCapacityInFrames; } + + /** + * @return the sharing mode of the stream. + */ + SharingMode getSharingMode() const { return mSharingMode; } + + /** + * @return the performance mode of the stream. + */ + PerformanceMode getPerformanceMode() const { return mPerformanceMode; } + + /** + * @return the device ID of the stream. + */ + int32_t getDeviceId() const { return mDeviceId; } + + /** + * @return the callback object for this stream, if set. + */ + AudioStreamCallback* getCallback() const { + return mStreamCallback; + } + + /** + * @return the usage for this stream. + */ + Usage getUsage() const { return mUsage; } + + /** + * @return the stream's content type. + */ + ContentType getContentType() const { return mContentType; } + + /** + * @return the stream's input preset. + */ + InputPreset getInputPreset() const { return mInputPreset; } + + /** + * @return the stream's session ID allocation strategy (None or Allocate). + */ + SessionId getSessionId() const { return mSessionId; } + + /** + * @return true if Oboe can convert channel counts to achieve optimal results. + */ + bool isChannelConversionAllowed() const { + return mChannelConversionAllowed; + } + + /** + * @return true if Oboe can convert data formats to achieve optimal results. + */ + bool isFormatConversionAllowed() const { + return mFormatConversionAllowed; + } + + /** + * @return whether and how Oboe can convert sample rates to achieve optimal results. + */ + SampleRateConversionQuality getSampleRateConversionQuality() const { + return mSampleRateConversionQuality; + } + +protected: + + /** The callback which will be fired when new data is ready to be read/written **/ + AudioStreamCallback *mStreamCallback = nullptr; + /** Number of audio frames which will be requested in each callback */ + int32_t mFramesPerCallback = kUnspecified; + /** Stream channel count */ + int32_t mChannelCount = kUnspecified; + /** Stream sample rate */ + int32_t mSampleRate = kUnspecified; + /** Stream audio device ID */ + int32_t mDeviceId = kUnspecified; + /** Stream buffer capacity specified as a number of audio frames */ + int32_t mBufferCapacityInFrames = kUnspecified; + /** Stream buffer size specified as a number of audio frames */ + int32_t mBufferSizeInFrames = kUnspecified; + /** + * Number of frames which will be copied to/from the audio device in a single read/write + * operation + */ + int32_t mFramesPerBurst = kUnspecified; + + /** Stream sharing mode */ + SharingMode mSharingMode = SharingMode::Shared; + /** Format of audio frames */ + AudioFormat mFormat = AudioFormat::Unspecified; + /** Stream direction */ + Direction mDirection = Direction::Output; + /** Stream performance mode */ + PerformanceMode mPerformanceMode = PerformanceMode::None; + + /** Stream usage. Only active on Android 28+ */ + Usage mUsage = Usage::Media; + /** Stream content type. Only active on Android 28+ */ + ContentType mContentType = ContentType::Music; + /** Stream input preset. Only active on Android 28+ + * TODO InputPreset::Unspecified should be considered as a possible default alternative. + */ + InputPreset mInputPreset = InputPreset::VoiceRecognition; + /** Stream session ID allocation strategy. Only active on Android 28+ */ + SessionId mSessionId = SessionId::None; + + // Control whether Oboe can convert channel counts to achieve optimal results. + bool mChannelConversionAllowed = false; + // Control whether Oboe can convert data formats to achieve optimal results. + bool mFormatConversionAllowed = false; + // Control whether and how Oboe can convert sample rates to achieve optimal results. + SampleRateConversionQuality mSampleRateConversionQuality = SampleRateConversionQuality::None; +}; + +} // namespace oboe + +#endif /* OBOE_STREAM_BASE_H_ */ diff --git a/src/third_party/oboe/include/oboe/AudioStreamBuilder.h b/src/third_party/oboe/include/oboe/AudioStreamBuilder.h new file mode 100644 index 0000000..a1ea0a8 --- /dev/null +++ b/src/third_party/oboe/include/oboe/AudioStreamBuilder.h @@ -0,0 +1,438 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_BUILDER_H_ +#define OBOE_STREAM_BUILDER_H_ + +#include "oboe/Definitions.h" +#include "oboe/AudioStreamBase.h" +#include "ResultWithValue.h" + +namespace oboe { + + // This depends on AudioStream, so we use forward declaration, it will close and delete the stream + struct StreamDeleterFunctor; + using ManagedStream = std::unique_ptr; + +/** + * Factory class for an audio Stream. + */ +class AudioStreamBuilder : public AudioStreamBase { +public: + + AudioStreamBuilder() : AudioStreamBase() {} + + AudioStreamBuilder(const AudioStreamBase &audioStreamBase): AudioStreamBase(audioStreamBase) {} + + /** + * Request a specific number of channels. + * + * Default is kUnspecified. If the value is unspecified then + * the application should query for the actual value after the stream is opened. + */ + AudioStreamBuilder *setChannelCount(int channelCount) { + mChannelCount = channelCount; + return this; + } + + /** + * Request the direction for a stream. The default is Direction::Output. + * + * @param direction Direction::Output or Direction::Input + */ + AudioStreamBuilder *setDirection(Direction direction) { + mDirection = direction; + return this; + } + + /** + * Request a specific sample rate in Hz. + * + * Default is kUnspecified. If the value is unspecified then + * the application should query for the actual value after the stream is opened. + * + * Technically, this should be called the "frame rate" or "frames per second", + * because it refers to the number of complete frames transferred per second. + * But it is traditionally called "sample rate". Se we use that term. + * + */ + AudioStreamBuilder *setSampleRate(int32_t sampleRate) { + mSampleRate = sampleRate; + return this; + } + + /** + * Request a specific number of frames for the data callback. + * + * Default is kUnspecified. If the value is unspecified then + * the actual number may vary from callback to callback. + * + * If an application can handle a varying number of frames then we recommend + * leaving this unspecified. This allow the underlying API to optimize + * the callbacks. But if your application is, for example, doing FFTs or other block + * oriented operations, then call this function to get the sizes you need. + * + * @param framesPerCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setFramesPerCallback(int framesPerCallback) { + mFramesPerCallback = framesPerCallback; + return this; + } + + /** + * Request a sample data format, for example Format::Float. + * + * Default is Format::Unspecified. If the value is unspecified then + * the application should query for the actual value after the stream is opened. + */ + AudioStreamBuilder *setFormat(AudioFormat format) { + mFormat = format; + return this; + } + + /** + * Set the requested buffer capacity in frames. + * BufferCapacityInFrames is the maximum possible BufferSizeInFrames. + * + * The final stream capacity may differ. For AAudio it should be at least this big. + * For OpenSL ES, it could be smaller. + * + * Default is kUnspecified. + * + * @param bufferCapacityInFrames the desired buffer capacity in frames or kUnspecified + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setBufferCapacityInFrames(int32_t bufferCapacityInFrames) { + mBufferCapacityInFrames = bufferCapacityInFrames; + return this; + } + + /** + * Get the audio API which will be requested when opening the stream. No guarantees that this is + * the API which will actually be used. Query the stream itself to find out the API which is + * being used. + * + * If you do not specify the API, then AAudio will be used if isAAudioRecommended() + * returns true. Otherwise OpenSL ES will be used. + * + * @return the requested audio API + */ + AudioApi getAudioApi() const { return mAudioApi; } + + /** + * If you leave this unspecified then Oboe will choose the best API + * for the device and SDK version at runtime. + * + * This should almost always be left unspecified, except for debugging purposes. + * Specifying AAudio will force Oboe to use AAudio on 8.0, which is extremely risky. + * Specifying OpenSLES should mainly be used to test legacy performance/functionality. + * + * If the caller requests AAudio and it is supported then AAudio will be used. + * + * @param audioApi Must be AudioApi::Unspecified, AudioApi::OpenSLES or AudioApi::AAudio. + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setAudioApi(AudioApi audioApi) { + mAudioApi = audioApi; + return this; + } + + /** + * Is the AAudio API supported on this device? + * + * AAudio was introduced in the Oreo 8.0 release. + * + * @return true if supported + */ + static bool isAAudioSupported(); + + /** + * Is the AAudio API recommended this device? + * + * AAudio may be supported but not recommended because of version specific issues. + * AAudio is not recommended for Android 8.0 or earlier versions. + * + * @return true if recommended + */ + static bool isAAudioRecommended(); + + /** + * Request a mode for sharing the device. + * The requested sharing mode may not be available. + * So the application should query for the actual mode after the stream is opened. + * + * @param sharingMode SharingMode::Shared or SharingMode::Exclusive + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setSharingMode(SharingMode sharingMode) { + mSharingMode = sharingMode; + return this; + } + + /** + * Request a performance level for the stream. + * This will determine the latency, the power consumption, and the level of + * protection from glitches. + * + * @param performanceMode for example, PerformanceMode::LowLatency + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setPerformanceMode(PerformanceMode performanceMode) { + mPerformanceMode = performanceMode; + return this; + } + + + /** + * Set the intended use case for the stream. + * + * The system will use this information to optimize the behavior of the stream. + * This could, for example, affect how volume and focus is handled for the stream. + * + * The default, if you do not call this function, is Usage::Media. + * + * Added in API level 28. + * + * @param usage the desired usage, eg. Usage::Game + */ + AudioStreamBuilder *setUsage(Usage usage) { + mUsage = usage; + return this; + } + + /** + * Set the type of audio data that the stream will carry. + * + * The system will use this information to optimize the behavior of the stream. + * This could, for example, affect whether a stream is paused when a notification occurs. + * + * The default, if you do not call this function, is ContentType::Music. + * + * Added in API level 28. + * + * @param contentType the type of audio data, eg. ContentType::Speech + */ + AudioStreamBuilder *setContentType(ContentType contentType) { + mContentType = contentType; + return this; + } + + /** + * Set the input (capture) preset for the stream. + * + * The system will use this information to optimize the behavior of the stream. + * This could, for example, affect which microphones are used and how the + * recorded data is processed. + * + * The default, if you do not call this function, is InputPreset::VoiceRecognition. + * That is because VoiceRecognition is the preset with the lowest latency + * on many platforms. + * + * Added in API level 28. + * + * @param inputPreset the desired configuration for recording + */ + AudioStreamBuilder *setInputPreset(InputPreset inputPreset) { + mInputPreset = inputPreset; + return this; + } + + /** Set the requested session ID. + * + * The session ID can be used to associate a stream with effects processors. + * The effects are controlled using the Android AudioEffect Java API. + * + * The default, if you do not call this function, is SessionId::None. + * + * If set to SessionId::Allocate then a session ID will be allocated + * when the stream is opened. + * + * The allocated session ID can be obtained by calling AudioStream::getSessionId() + * and then used with this function when opening another stream. + * This allows effects to be shared between streams. + * + * Session IDs from Oboe can be used the Android Java APIs and vice versa. + * So a session ID from an Oboe stream can be passed to Java + * and effects applied using the Java AudioEffect API. + * + * Allocated session IDs will always be positive and nonzero. + * + * Added in API level 28. + * + * @param sessionId an allocated sessionID or SessionId::Allocate + */ + AudioStreamBuilder *setSessionId(SessionId sessionId) { + mSessionId = sessionId; + return this; + } + + /** + * Request a stream to a specific audio input/output device given an audio device ID. + * + * In most cases, the primary device will be the appropriate device to use, and the + * deviceId can be left kUnspecified. + * + * On Android, for example, the ID could be obtained from the Java AudioManager. + * AudioManager.getDevices() returns an array of AudioDeviceInfo[], which contains + * a getId() method (as well as other type information), that should be passed + * to this method. + * + * + * Note that when using OpenSL ES, this will be ignored and the created + * stream will have deviceId kUnspecified. + * + * @param deviceId device identifier or kUnspecified + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setDeviceId(int32_t deviceId) { + mDeviceId = deviceId; + return this; + } + + /** + * Specifies an object to handle data or error related callbacks from the underlying API. + * + * Important: See AudioStreamCallback for restrictions on what may be called + * from the callback methods. + * + * When an error callback occurs, the associated stream will be stopped and closed in a separate thread. + * + * A note on why the streamCallback parameter is a raw pointer rather than a smart pointer: + * + * The caller should retain ownership of the object streamCallback points to. At first glance weak_ptr may seem like + * a good candidate for streamCallback as this implies temporary ownership. However, a weak_ptr can only be created + * from a shared_ptr. A shared_ptr incurs some performance overhead. The callback object is likely to be accessed + * every few milliseconds when the stream requires new data so this overhead is something we want to avoid. + * + * This leaves a raw pointer as the logical type choice. The only caveat being that the caller must not destroy + * the callback before the stream has been closed. + * + * @param streamCallback + * @return pointer to the builder so calls can be chained + */ + AudioStreamBuilder *setCallback(AudioStreamCallback *streamCallback) { + mStreamCallback = streamCallback; + return this; + } + + /** + * If true then Oboe might convert channel counts to achieve optimal results. + * On some versions of Android for example, stereo streams could not use a FAST track. + * So a mono stream might be used instead and duplicated to two channels. + * On some devices, mono streams might be broken, so a stereo stream might be opened + * and converted to mono. + * + * Default is true. + */ + AudioStreamBuilder *setChannelConversionAllowed(bool allowed) { + mChannelConversionAllowed = allowed; + return this; + } + + /** + * If true then Oboe might convert data formats to achieve optimal results. + * On some versions of Android, for example, a float stream could not get a + * low latency data path. So an I16 stream might be opened and converted to float. + * + * Default is true. + */ + AudioStreamBuilder *setFormatConversionAllowed(bool allowed) { + mFormatConversionAllowed = allowed; + return this; + } + + /** + * Specify the quality of the sample rate converter in Oboe. + * + * If set to None then Oboe will not do sample rate conversion. But the underlying APIs might + * still do sample rate conversion if you specify a sample rate. + * That can prevent you from getting a low latency stream. + * + * If you do the conversion in Oboe then you might still get a low latency stream. + * + * Default is SampleRateConversionQuality::None + */ + AudioStreamBuilder *setSampleRateConversionQuality(SampleRateConversionQuality quality) { + mSampleRateConversionQuality = quality; + return this; + } + + /** + * @return true if AAudio will be used based on the current settings. + */ + bool willUseAAudio() const { + return (mAudioApi == AudioApi::AAudio && isAAudioSupported()) + || (mAudioApi == AudioApi::Unspecified && isAAudioRecommended()); + } + + /** + * Create and open a stream object based on the current settings. + * + * The caller owns the pointer to the AudioStream object. + * + * @deprecated Use openStream(std::shared_ptr &stream) instead. + * @param stream pointer to a variable to receive the stream address + * @return OBOE_OK if successful or a negative error code + */ + Result openStream(AudioStream **stream); + + /** + * Create and open a stream object based on the current settings. + * + * The caller shares the pointer to the AudioStream object. + * The shared_ptr is used internally by Oboe to prevent the stream from being + * deleted while it is being used by callbacks. + * + * @param stream reference to a shared_ptr to receive the stream address + * @return OBOE_OK if successful or a negative error code + */ + Result openStream(std::shared_ptr &stream); + + /** + * Create and open a ManagedStream object based on the current builder state. + * + * The caller must create a unique ptr, and pass by reference so it can be + * modified to point to an opened stream. The caller owns the unique ptr, + * and it will be automatically closed and deleted when going out of scope. + * @param stream Reference to the ManagedStream (uniqueptr) used to keep track of stream + * @return OBOE_OK if successful or a negative error code. + */ + Result openManagedStream(ManagedStream &stream); + +private: + + /** + * @param other + * @return true if channels, format and sample rate match + */ + bool isCompatible(AudioStreamBase &other); + + /** + * Create an AudioStream object. The AudioStream must be opened before use. + * + * The caller owns the pointer. + * + * @return pointer to an AudioStream object or nullptr. + */ + oboe::AudioStream *build(); + + AudioApi mAudioApi = AudioApi::Unspecified; +}; + +} // namespace oboe + +#endif /* OBOE_STREAM_BUILDER_H_ */ diff --git a/src/third_party/oboe/include/oboe/AudioStreamCallback.h b/src/third_party/oboe/include/oboe/AudioStreamCallback.h new file mode 100644 index 0000000..f427693 --- /dev/null +++ b/src/third_party/oboe/include/oboe/AudioStreamCallback.h @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_CALLBACK_H +#define OBOE_STREAM_CALLBACK_H + +#include "oboe/Definitions.h" + +namespace oboe { + +class AudioStream; + +/** + * AudioStreamCallback defines a callback interface for: + * + * 1) moving data to/from an audio stream using `onAudioReady` + * 2) being alerted when a stream has an error using `onError*` methods + * + */ +class AudioStreamCallback { +public: + virtual ~AudioStreamCallback() = default; + + /** + * A buffer is ready for processing. + * + * For an output stream, this function should render and write numFrames of data + * in the stream's current data format to the audioData buffer. + * + * For an input stream, this function should read and process numFrames of data + * from the audioData buffer. + * + * The audio data is passed through the buffer. So do NOT call read() or + * write() on the stream that is making the callback. + * + * Note that numFrames can vary unless AudioStreamBuilder::setFramesPerCallback() + * is called. + * + * Also note that this callback function should be considered a "real-time" function. + * It must not do anything that could cause an unbounded delay because that can cause the + * audio to glitch or pop. + * + * These are things the function should NOT do: + *
    + *
  • allocate memory using, for example, malloc() or new
  • + *
  • any file operations such as opening, closing, reading or writing
  • + *
  • any network operations such as streaming
  • + *
  • use any mutexes or other synchronization primitives
  • + *
  • sleep
  • + *
  • oboeStream->stop(), pause(), flush() or close()
  • + *
  • oboeStream->read()
  • + *
  • oboeStream->write()
  • + *
+ * + * The following are OK to call from the data callback: + *
    + *
  • oboeStream->get*()
  • + *
  • oboe::convertToText()
  • + *
  • oboeStream->setBufferSizeInFrames()
  • + *
+ * + * If you need to move data, eg. MIDI commands, in or out of the callback function then + * we recommend the use of non-blocking techniques such as an atomic FIFO. + * + * @param oboeStream pointer to the associated stream + * @param audioData buffer containing input data or a place to put output data + * @param numFrames number of frames to be processed + * @return DataCallbackResult::Continue or DataCallbackResult::Stop + */ + virtual DataCallbackResult onAudioReady( + AudioStream *oboeStream, + void *audioData, + int32_t numFrames) = 0; + + /** + * This will be called when an error occurs on a stream or when the stream is disconnected. + * + * Note that this will be called on a different thread than the onAudioReady() thread. + * This thread will be created by Oboe. + * + * The underlying stream will already be stopped by Oboe but not yet closed. + * So the stream can be queried. + * + * Do not close or delete the stream in this method because it will be + * closed after this method returns. + * + * @param oboeStream pointer to the associated stream + * @param error + */ + virtual void onErrorBeforeClose(AudioStream* /* oboeStream */, Result /* error */) {} + + /** + * This will be called when an error occurs on a stream or when the stream is disconnected. + * The underlying AAudio or OpenSL ES stream will already be stopped AND closed by Oboe. + * So the underlying stream cannot be referenced. + * But you can still query most parameters. + * + * This callback could be used to reopen a new stream on another device. + * You can safely delete the old AudioStream in this method. + * + * @param oboeStream pointer to the associated stream + * @param error + */ + virtual void onErrorAfterClose(AudioStream* /* oboeStream */, Result /* error */) {} + +}; + +} // namespace oboe + +#endif //OBOE_STREAM_CALLBACK_H diff --git a/src/third_party/oboe/include/oboe/Definitions.h b/src/third_party/oboe/include/oboe/Definitions.h new file mode 100644 index 0000000..8063a83 --- /dev/null +++ b/src/third_party/oboe/include/oboe/Definitions.h @@ -0,0 +1,519 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_DEFINITIONS_H +#define OBOE_DEFINITIONS_H + + +#include +#include + +// Oboe needs to be able to build on old NDKs so we use hard coded constants. +// The correctness of these constants is verified in "aaudio/AAudioLoader.cpp". + +namespace oboe { + + /** + * Represents any attribute, property or value which hasn't been specified. + */ + constexpr int32_t kUnspecified = 0; + + // TODO: Investigate using std::chrono + /** + * The number of nanoseconds in a microsecond. 1,000. + */ + constexpr int64_t kNanosPerMicrosecond = 1000; + + /** + * The number of nanoseconds in a millisecond. 1,000,000. + */ + constexpr int64_t kNanosPerMillisecond = kNanosPerMicrosecond * 1000; + + /** + * The number of milliseconds in a second. 1,000. + */ + constexpr int64_t kMillisPerSecond = 1000; + + /** + * The number of nanoseconds in a second. 1,000,000,000. + */ + constexpr int64_t kNanosPerSecond = kNanosPerMillisecond * kMillisPerSecond; + + /** + * The state of the audio stream. + */ + enum class StreamState : int32_t { // aaudio_stream_state_t + Uninitialized = 0, // AAUDIO_STREAM_STATE_UNINITIALIZED, + Unknown = 1, // AAUDIO_STREAM_STATE_UNKNOWN, + Open = 2, // AAUDIO_STREAM_STATE_OPEN, + Starting = 3, // AAUDIO_STREAM_STATE_STARTING, + Started = 4, // AAUDIO_STREAM_STATE_STARTED, + Pausing = 5, // AAUDIO_STREAM_STATE_PAUSING, + Paused = 6, // AAUDIO_STREAM_STATE_PAUSED, + Flushing = 7, // AAUDIO_STREAM_STATE_FLUSHING, + Flushed = 8, // AAUDIO_STREAM_STATE_FLUSHED, + Stopping = 9, // AAUDIO_STREAM_STATE_STOPPING, + Stopped = 10, // AAUDIO_STREAM_STATE_STOPPED, + Closing = 11, // AAUDIO_STREAM_STATE_CLOSING, + Closed = 12, // AAUDIO_STREAM_STATE_CLOSED, + Disconnected = 13, // AAUDIO_STREAM_STATE_DISCONNECTED, + }; + + /** + * The direction of the stream. + */ + enum class Direction : int32_t { // aaudio_direction_t + + /** + * Used for playback. + */ + Output = 0, // AAUDIO_DIRECTION_OUTPUT, + + /** + * Used for recording. + */ + Input = 1, // AAUDIO_DIRECTION_INPUT, + }; + + /** + * The format of audio samples. + */ + enum class AudioFormat : int32_t { // aaudio_format_t + /** + * Invalid format. + */ + Invalid = -1, // AAUDIO_FORMAT_INVALID, + + /** + * Unspecified format. Format will be decided by Oboe. + */ + Unspecified = 0, // AAUDIO_FORMAT_UNSPECIFIED, + + /** + * Signed 16-bit integers. + */ + I16 = 1, // AAUDIO_FORMAT_PCM_I16, + + /** + * Single precision floating points. + */ + Float = 2, // AAUDIO_FORMAT_PCM_FLOAT, + }; + + /** + * The result of an audio callback. + */ + enum class DataCallbackResult : int32_t { // aaudio_data_callback_result_t + // Indicates to the caller that the callbacks should continue. + Continue = 0, // AAUDIO_CALLBACK_RESULT_CONTINUE, + + // Indicates to the caller that the callbacks should stop immediately. + Stop = 1, // AAUDIO_CALLBACK_RESULT_STOP, + }; + + /** + * The result of an operation. All except the `OK` result indicates that an error occurred. + * The `Result` can be converted into a human readable string using `convertToText`. + */ + enum class Result : int32_t { // aaudio_result_t + OK = 0, // AAUDIO_OK + ErrorBase = -900, // AAUDIO_ERROR_BASE, + ErrorDisconnected = -899, // AAUDIO_ERROR_DISCONNECTED, + ErrorIllegalArgument = -898, // AAUDIO_ERROR_ILLEGAL_ARGUMENT, + ErrorInternal = -896, // AAUDIO_ERROR_INTERNAL, + ErrorInvalidState = -895, // AAUDIO_ERROR_INVALID_STATE, + ErrorInvalidHandle = -892, // AAUDIO_ERROR_INVALID_HANDLE, + ErrorUnimplemented = -890, // AAUDIO_ERROR_UNIMPLEMENTED, + ErrorUnavailable = -889, // AAUDIO_ERROR_UNAVAILABLE, + ErrorNoFreeHandles = -888, // AAUDIO_ERROR_NO_FREE_HANDLES, + ErrorNoMemory = -887, // AAUDIO_ERROR_NO_MEMORY, + ErrorNull = -886, // AAUDIO_ERROR_NULL, + ErrorTimeout = -885, // AAUDIO_ERROR_TIMEOUT, + ErrorWouldBlock = -884, // AAUDIO_ERROR_WOULD_BLOCK, + ErrorInvalidFormat = -883, // AAUDIO_ERROR_INVALID_FORMAT, + ErrorOutOfRange = -882, // AAUDIO_ERROR_OUT_OF_RANGE, + ErrorNoService = -881, // AAUDIO_ERROR_NO_SERVICE, + ErrorInvalidRate = -880, // AAUDIO_ERROR_INVALID_RATE, + // Reserved for future AAudio result types + Reserved1, + Reserved2, + Reserved3, + Reserved4, + Reserved5, + Reserved6, + Reserved7, + Reserved8, + Reserved9, + Reserved10, + ErrorClosed, + }; + + /** + * The sharing mode of the audio stream. + */ + enum class SharingMode : int32_t { // aaudio_sharing_mode_t + + /** + * This will be the only stream using a particular source or sink. + * This mode will provide the lowest possible latency. + * You should close EXCLUSIVE streams immediately when you are not using them. + * + * If you do not need the lowest possible latency then we recommend using Shared, + * which is the default. + */ + Exclusive = 0, // AAUDIO_SHARING_MODE_EXCLUSIVE, + + /** + * Multiple applications can share the same device. + * The data from output streams will be mixed by the audio service. + * The data for input streams will be distributed by the audio service. + * + * This will have higher latency than the EXCLUSIVE mode. + */ + Shared = 1, // AAUDIO_SHARING_MODE_SHARED, + }; + + /** + * The performance mode of the audio stream. + */ + enum class PerformanceMode : int32_t { // aaudio_performance_mode_t + + /** + * No particular performance needs. Default. + */ + None = 10, // AAUDIO_PERFORMANCE_MODE_NONE, + + /** + * Extending battery life is most important. + */ + PowerSaving = 11, // AAUDIO_PERFORMANCE_MODE_POWER_SAVING, + + /** + * Reducing latency is most important. + */ + LowLatency = 12, // AAUDIO_PERFORMANCE_MODE_LOW_LATENCY + }; + + /** + * The underlying audio API used by the audio stream. + */ + enum class AudioApi : int32_t { + /** + * Try to use AAudio. If not available then use OpenSL ES. + */ + Unspecified = kUnspecified, + + /** + * Use OpenSL ES. + */ + OpenSLES, + + /** + * Try to use AAudio. Fail if unavailable. + */ + AAudio + }; + + /** + * Specifies the quality of the sample rate conversion performed by Oboe. + * Higher quality will require more CPU load. + * Higher quality conversion will probably be implemented using a sinc based resampler. + */ + enum class SampleRateConversionQuality : int32_t { + /** + * No conversion by Oboe. Underlying APIs may still do conversion. + */ + None, + /** + * Fastest conversion but may not sound great. + * This may be implemented using bilinear interpolation. + */ + Fastest, + Low, + Medium, + High, + /** + * Highest quality conversion, which may be expensive in terms of CPU. + */ + Best, + }; + + /** + * The Usage attribute expresses *why* you are playing a sound, what is this sound used for. + * This information is used by certain platforms or routing policies + * to make more refined volume or routing decisions. + * + * Note that these match the equivalent values in AudioAttributes in the Android Java API. + * + * This attribute only has an effect on Android API 28+. + */ + enum class Usage : int32_t { // aaudio_usage_t + /** + * Use this for streaming media, music performance, video, podcasts, etcetera. + */ + Media = 1, // AAUDIO_USAGE_MEDIA + + /** + * Use this for voice over IP, telephony, etcetera. + */ + VoiceCommunication = 2, // AAUDIO_USAGE_VOICE_COMMUNICATION + + /** + * Use this for sounds associated with telephony such as busy tones, DTMF, etcetera. + */ + VoiceCommunicationSignalling = 3, // AAUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING + + /** + * Use this to demand the users attention. + */ + Alarm = 4, // AAUDIO_USAGE_ALARM + + /** + * Use this for notifying the user when a message has arrived or some + * other background event has occured. + */ + Notification = 5, // AAUDIO_USAGE_NOTIFICATION + + /** + * Use this when the phone rings. + */ + NotificationRingtone = 6, // AAUDIO_USAGE_NOTIFICATION_RINGTONE + + /** + * Use this to attract the users attention when, for example, the battery is low. + */ + NotificationEvent = 10, // AAUDIO_USAGE_NOTIFICATION_EVENT + + /** + * Use this for screen readers, etcetera. + */ + AssistanceAccessibility = 11, // AAUDIO_USAGE_ASSISTANCE_ACCESSIBILITY + + /** + * Use this for driving or navigation directions. + */ + AssistanceNavigationGuidance = 12, // AAUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE + + /** + * Use this for user interface sounds, beeps, etcetera. + */ + AssistanceSonification = 13, // AAUDIO_USAGE_ASSISTANCE_SONIFICATION + + /** + * Use this for game audio and sound effects. + */ + Game = 14, // AAUDIO_USAGE_GAME + + /** + * Use this for audio responses to user queries, audio instructions or help utterances. + */ + Assistant = 16, // AAUDIO_USAGE_ASSISTANT + }; + + + /** + * The ContentType attribute describes *what* you are playing. + * It expresses the general category of the content. This information is optional. + * But in case it is known (for instance {@link Movie} for a + * movie streaming service or {@link Speech} for + * an audio book application) this information might be used by the audio framework to + * enforce audio focus. + * + * Note that these match the equivalent values in AudioAttributes in the Android Java API. + * + * This attribute only has an effect on Android API 28+. + */ + enum ContentType : int32_t { // aaudio_content_type_t + + /** + * Use this for spoken voice, audio books, etcetera. + */ + Speech = 1, // AAUDIO_CONTENT_TYPE_SPEECH + + /** + * Use this for pre-recorded or live music. + */ + Music = 2, // AAUDIO_CONTENT_TYPE_MUSIC + + /** + * Use this for a movie or video soundtrack. + */ + Movie = 3, // AAUDIO_CONTENT_TYPE_MOVIE + + /** + * Use this for sound is designed to accompany a user action, + * such as a click or beep sound made when the user presses a button. + */ + Sonification = 4, // AAUDIO_CONTENT_TYPE_SONIFICATION + }; + + /** + * Defines the audio source. + * An audio source defines both a default physical source of audio signal, and a recording + * configuration. + * + * Note that these match the equivalent values in MediaRecorder.AudioSource in the Android Java API. + * + * This attribute only has an effect on Android API 28+. + */ + enum InputPreset : int32_t { // aaudio_input_preset_t + /** + * Use this preset when other presets do not apply. + */ + Generic = 1, // AAUDIO_INPUT_PRESET_GENERIC + + /** + * Use this preset when recording video. + */ + Camcorder = 5, // AAUDIO_INPUT_PRESET_CAMCORDER + + /** + * Use this preset when doing speech recognition. + */ + VoiceRecognition = 6, // AAUDIO_INPUT_PRESET_VOICE_RECOGNITION + + /** + * Use this preset when doing telephony or voice messaging. + */ + VoiceCommunication = 7, // AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION + + /** + * Use this preset to obtain an input with no effects. + * Note that this input will not have automatic gain control + * so the recorded volume may be very low. + */ + Unprocessed = 9, // AAUDIO_INPUT_PRESET_UNPROCESSED + + /** + * Use this preset for capturing audio meant to be processed in real time + * and played back for live performance (e.g karaoke). + * The capture path will minimize latency and coupling with playback path. + */ + VoicePerformance = 10, // AAUDIO_INPUT_PRESET_VOICE_PERFORMANCE + + }; + + /** + * This attribute can be used to allocate a session ID to the audio stream. + * + * This attribute only has an effect on Android API 28+. + */ + enum SessionId { + /** + * Do not allocate a session ID. + * Effects cannot be used with this stream. + * Default. + */ + None = -1, // AAUDIO_SESSION_ID_NONE + + /** + * Allocate a session ID that can be used to attach and control + * effects using the Java AudioEffects API. + * Note that the use of this flag may result in higher latency. + * + * Note that this matches the value of AudioManager.AUDIO_SESSION_ID_GENERATE. + */ + Allocate = 0, // AAUDIO_SESSION_ID_ALLOCATE + }; + + /** + * The channel count of the audio stream. The underlying type is `int32_t`. + * Use of this enum is convenient to avoid "magic" + * numbers when specifying the channel count. + * + * For example, you can write + * `builder.setChannelCount(ChannelCount::Stereo)` + * rather than `builder.setChannelCount(2)` + * + */ + enum ChannelCount : int32_t { + /** + * Audio channel count definition, use Mono or Stereo + */ + Unspecified = kUnspecified, + + /** + * Use this for mono audio + */ + Mono = 1, + + /** + * Use this for stereo audio. + */ + Stereo = 2, + }; + + /** + * On API 16 to 26 OpenSL ES will be used. When using OpenSL ES the optimal values for sampleRate and + * framesPerBurst are not known by the native code. + * On API 17+ these values should be obtained from the AudioManager using this code: + * + *

+     * // Note that this technique only works for built-in speakers and headphones.
+     * AudioManager myAudioMgr = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
+     * String sampleRateStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_SAMPLE_RATE);
+     * int defaultSampleRate = Integer.parseInt(sampleRateStr);
+     * String framesPerBurstStr = myAudioMgr.getProperty(AudioManager.PROPERTY_OUTPUT_FRAMES_PER_BUFFER);
+     * int defaultFramesPerBurst = Integer.parseInt(framesPerBurstStr);
+     * 
+ * + * It can then be passed down to Oboe through JNI. + * + * AAudio will get the optimal framesPerBurst from the HAL and will ignore this value. + */ + class DefaultStreamValues { + + public: + + /** The default sample rate to use when opening new audio streams */ + static int32_t SampleRate; + /** The default frames per burst to use when opening new audio streams */ + static int32_t FramesPerBurst; + /** The default channel count to use when opening new audio streams */ + static int32_t ChannelCount; + + }; + + /** + * The time at which the frame at `position` was presented + */ + struct FrameTimestamp { + int64_t position; // in frames + int64_t timestamp; // in nanoseconds + }; + + class OboeGlobals { + public: + + static bool areWorkaroundsEnabled() { + return mWorkaroundsEnabled; + } + + /** + * Disable this when writing tests to reproduce bugs in AAudio or OpenSL ES + * that have workarounds in Oboe. + * @param enabled + */ + static void setWorkaroundsEnabled(bool enabled) { + mWorkaroundsEnabled = enabled; + } + + private: + static bool mWorkaroundsEnabled; + }; +} // namespace oboe + +#endif // OBOE_DEFINITIONS_H diff --git a/src/third_party/oboe/include/oboe/LatencyTuner.h b/src/third_party/oboe/include/oboe/LatencyTuner.h new file mode 100644 index 0000000..8b8404d --- /dev/null +++ b/src/third_party/oboe/include/oboe/LatencyTuner.h @@ -0,0 +1,150 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_LATENCY_TUNER_ +#define OBOE_LATENCY_TUNER_ + +#include +#include +#include "oboe/Definitions.h" +#include "oboe/AudioStream.h" + +namespace oboe { + +/** + * LatencyTuner can be used to dynamically tune the latency of an output stream. + * It adjusts the stream's bufferSize by monitoring the number of underruns. + * + * This only affects the latency associated with the first level of buffering that is closest + * to the application. It does not affect low latency in the HAL, or touch latency in the UI. + * + * Call tune() right before returning from your data callback function if using callbacks. + * Call tune() right before calling write() if using blocking writes. + * + * If you want to see the ongoing results of this tuning process then call + * stream->getBufferSize() periodically. + * + */ +class LatencyTuner { +public: + + /** + * Construct a new LatencyTuner object which will act on the given audio stream + * + * @param stream the stream who's latency will be tuned + */ + explicit LatencyTuner(AudioStream &stream); + + /** + * Construct a new LatencyTuner object which will act on the given audio stream. + * + * @param stream the stream who's latency will be tuned + * @param the maximum buffer size which the tune() operation will set the buffer size to + */ + explicit LatencyTuner(AudioStream &stream, int32_t maximumBufferSize); + + /** + * Adjust the bufferSizeInFrames to optimize latency. + * It will start with a low latency and then raise it if an underrun occurs. + * + * Latency tuning is only supported for AAudio. + * + * @return OK or negative error, ErrorUnimplemented for OpenSL ES + */ + Result tune(); + + /** + * This may be called from another thread. Then tune() will call reset(), + * which will lower the latency to the minimum and then allow it to rise back up + * if there are glitches. + * + * This is typically called in response to a user decision to minimize latency. In other words, + * call this from a button handler. + */ + void requestReset(); + + /** + * @return true if the audio stream's buffer size is at the maximum value. If no maximum value + * was specified when constructing the LatencyTuner then the value of + * stream->getBufferCapacityInFrames is used + */ + bool isAtMaximumBufferSize(); + + /** + * Set the minimum bufferSize in frames that is used when the tuner is reset. + * You may wish to call requestReset() after calling this. + * @param bufferSize + */ + void setMinimumBufferSize(int32_t bufferSize) { + mMinimumBufferSize = bufferSize; + } + + int32_t getMinimumBufferSize() const { + return mMinimumBufferSize; + } + + /** + * Set the amount the bufferSize will be incremented while tuning. + * By default, this will be one burst. + * + * Note that AAudio will quantize the buffer size to a multiple of the burstSize. + * So the final buffer sizes may not be a multiple of this increment. + * + * @param sizeIncrement + */ + void setBufferSizeIncrement(int32_t sizeIncrement) { + mBufferSizeIncrement = sizeIncrement; + } + + int32_t getBufferSizeIncrement() const { + return mBufferSizeIncrement; + } + +private: + + /** + * Drop the latency down to the minimum and then let it rise back up. + * This is useful if a glitch caused the latency to increase and it hasn't gone back down. + * + * This should only be called in the same thread as tune(). + */ + void reset(); + + enum class State { + Idle, + Active, + AtMax, + Unsupported + } ; + + // arbitrary number of calls to wait before bumping up the latency + static constexpr int32_t kIdleCount = 8; + static constexpr int32_t kDefaultNumBursts = 2; + + AudioStream &mStream; + State mState = State::Idle; + int32_t mMaxBufferSize = 0; + int32_t mPreviousXRuns = 0; + int32_t mIdleCountDown = 0; + int32_t mMinimumBufferSize; + int32_t mBufferSizeIncrement; + std::atomic mLatencyTriggerRequests{0}; // TODO user atomic requester from AAudio + std::atomic mLatencyTriggerResponses{0}; +}; + +} // namespace oboe + +#endif // OBOE_LATENCY_TUNER_ diff --git a/src/third_party/oboe/include/oboe/Oboe.h b/src/third_party/oboe/include/oboe/Oboe.h new file mode 100644 index 0000000..c3a0bca --- /dev/null +++ b/src/third_party/oboe/include/oboe/Oboe.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OBOE_H +#define OBOE_OBOE_H + +/** + * \mainpage API reference + * + * All documentation is found in the oboe namespace section + * + */ + +#include "oboe/Definitions.h" +#include "oboe/ResultWithValue.h" +#include "oboe/LatencyTuner.h" +#include "oboe/AudioStream.h" +#include "oboe/AudioStreamBase.h" +#include "oboe/AudioStreamBuilder.h" +#include "oboe/Utilities.h" +#include "oboe/Version.h" +#include "oboe/StabilizedCallback.h" + +#endif //OBOE_OBOE_H diff --git a/src/third_party/oboe/include/oboe/ResultWithValue.h b/src/third_party/oboe/include/oboe/ResultWithValue.h new file mode 100644 index 0000000..fcb1fac --- /dev/null +++ b/src/third_party/oboe/include/oboe/ResultWithValue.h @@ -0,0 +1,155 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_RESULT_WITH_VALUE_H +#define OBOE_RESULT_WITH_VALUE_H + +#include "oboe/Definitions.h" +#include +#include + +namespace oboe { + +/** + * A ResultWithValue can store both the result of an operation (either OK or an error) and a value. + * + * It has been designed for cases where the caller needs to know whether an operation succeeded and, + * if it did, a value which was obtained during the operation. + * + * For example, when reading from a stream the caller needs to know the result of the read operation + * and, if it was successful, how many frames were read. Note that ResultWithValue can be evaluated + * as a boolean so it's simple to check whether the result is OK. + * + * + * ResultWithValue resultOfRead = myStream.read(&buffer, numFrames, timeoutNanoseconds); + * + * if (resultOfRead) { + * LOGD("Frames read: %d", resultOfRead.value()); + * } else { + * LOGD("Error reading from stream: %s", resultOfRead.error()); + * } + * + */ +template +class ResultWithValue { +public: + + /** + * Construct a ResultWithValue containing an error result. + * + * @param error The error + */ + ResultWithValue(oboe::Result error) + : mValue{} + , mError(error) {} + + /** + * Construct a ResultWithValue containing an OK result and a value. + * + * @param value the value to store + */ + explicit ResultWithValue(T value) + : mValue(value) + , mError(oboe::Result::OK) {} + + /** + * Get the result. + * + * @return the result + */ + oboe::Result error() const { + return mError; + } + + /** + * Get the value + * @return + */ + T value() const { + return mValue; + } + + /** + * @return true if OK + */ + explicit operator bool() const { return mError == oboe::Result::OK; } + + /** + * Quick way to check for an error. + * + * The caller could write something like this: + * + * if (!result) { printf("Got error %s\n", convertToText(result.error())); } + * + * + * @return true if an error occurred + */ + bool operator !() const { return mError != oboe::Result::OK; } + + /** + * Implicitly convert to a Result. This enables easy comparison with Result values. Example: + * + * + * ResultWithValue result = openStream(); + * if (result == Result::ErrorNoMemory){ // tell user they're out of memory } + * + */ + operator Result() const { + return mError; + } + + /** + * Create a ResultWithValue from a number. If the number is positive the ResultWithValue will + * have a result of Result::OK and the value will contain the number. If the number is negative + * the result will be obtained from the negative number (numeric error codes can be found in + * AAudio.h) and the value will be null. + * + */ + static ResultWithValue createBasedOnSign(T numericResult){ + + // Ensure that the type is either an integer or float + static_assert(std::is_arithmetic::value, + "createBasedOnSign can only be called for numeric types (int or float)"); + + if (numericResult >= 0){ + return ResultWithValue(numericResult); + } else { + return ResultWithValue(static_cast(numericResult)); + } + } + +private: + const T mValue; + const oboe::Result mError; +}; + +/** + * If the result is `OK` then return the value, otherwise return a human-readable error message. + */ +template +std::ostream& operator<<(std::ostream &strm, const ResultWithValue &result) { + if (!result) { + strm << convertToText(result.error()); + } else { + strm << result.value(); + } + return strm; +} + +} // namespace oboe + + +#endif //OBOE_RESULT_WITH_VALUE_H diff --git a/src/third_party/oboe/include/oboe/StabilizedCallback.h b/src/third_party/oboe/include/oboe/StabilizedCallback.h new file mode 100644 index 0000000..3f1a689 --- /dev/null +++ b/src/third_party/oboe/include/oboe/StabilizedCallback.h @@ -0,0 +1,75 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STABILIZEDCALLBACK_H +#define OBOE_STABILIZEDCALLBACK_H + +#include +#include "oboe/AudioStream.h" + +namespace oboe { + +class StabilizedCallback : public AudioStreamCallback { + +public: + explicit StabilizedCallback(AudioStreamCallback *callback); + + DataCallbackResult + onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) override; + + void onErrorBeforeClose(AudioStream *oboeStream, Result error) override { + return mCallback->onErrorBeforeClose(oboeStream, error); + } + + void onErrorAfterClose(AudioStream *oboeStream, Result error) override { + + // Reset all fields now that the stream has been closed + mFrameCount = 0; + mEpochTimeNanos = 0; + mOpsPerNano = 1; + return mCallback->onErrorAfterClose(oboeStream, error); + } + +private: + + AudioStreamCallback *mCallback = nullptr; + int64_t mFrameCount = 0; + int64_t mEpochTimeNanos = 0; + double mOpsPerNano = 1; + + void generateLoad(int64_t durationNanos); +}; + +/** + * cpu_relax is an architecture specific method of telling the CPU that you don't want it to + * do much work. asm volatile keeps the compiler from optimising these instructions out. + */ +#if defined(__i386__) || defined(__x86_64__) +#define cpu_relax() asm volatile("rep; nop" ::: "memory"); + +#elif defined(__arm__) || defined(__mips__) + #define cpu_relax() asm volatile("":::"memory") + +#elif defined(__aarch64__) +#define cpu_relax() asm volatile("yield" ::: "memory") + +#else +#error "cpu_relax is not defined for this architecture" +#endif + +} + +#endif //OBOE_STABILIZEDCALLBACK_H diff --git a/src/third_party/oboe/include/oboe/Utilities.h b/src/third_party/oboe/include/oboe/Utilities.h new file mode 100644 index 0000000..2c27088 --- /dev/null +++ b/src/third_party/oboe/include/oboe/Utilities.h @@ -0,0 +1,87 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_UTILITIES_H +#define OBOE_UTILITIES_H + +#include +#include +#include +#include "oboe/Definitions.h" + +namespace oboe { + +/** + * Convert an array of floats to an array of 16-bit integers. + * + * @param source the input array. + * @param destination the output array. + * @param numSamples the number of values to convert. + */ +void convertFloatToPcm16(const float *source, int16_t *destination, int32_t numSamples); + +/** + * Convert an array of 16-bit integers to an array of floats. + * + * @param source the input array. + * @param destination the output array. + * @param numSamples the number of values to convert. + */ +void convertPcm16ToFloat(const int16_t *source, float *destination, int32_t numSamples); + +/** + * @return the size of a sample of the given format in bytes or 0 if format is invalid + */ +int32_t convertFormatToSizeInBytes(AudioFormat format); + +/** + * The text is the ASCII symbol corresponding to the supplied Oboe enum value, + * or an English message saying the value is unrecognized. + * This is intended for developers to use when debugging. + * It is not for displaying to users. + * + * @param input object to convert from. @see common/Utilities.cpp for concrete implementations + * @return text representation of an Oboe enum value. There is no need to call free on this. + */ +template +const char * convertToText(FromType input); + +/** + * @param name + * @return the value of a named system property in a string or empty string + */ +std::string getPropertyString(const char * name); + +/** + * @param name + * @param defaultValue + * @return integer value associated with a property or the default value + */ +int getPropertyInteger(const char * name, int defaultValue); + +/** + * Return the version of the SDK that is currently running. + * + * For example, on Android, this would return 27 for Oreo 8.1. + * If the version number cannot be determined then this will return -1. + * + * @return version number or -1 + */ +int getSdkVersion(); + +} // namespace oboe + +#endif //OBOE_UTILITIES_H diff --git a/src/third_party/oboe/include/oboe/Version.h b/src/third_party/oboe/include/oboe/Version.h new file mode 100644 index 0000000..bdc12e4 --- /dev/null +++ b/src/third_party/oboe/include/oboe/Version.h @@ -0,0 +1,92 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_VERSIONINFO_H +#define OBOE_VERSIONINFO_H + +#include + +/** + * A note on use of preprocessor defines: + * + * This is one of the few times when it's suitable to use preprocessor defines rather than constexpr + * Why? Because C++11 requires a lot of boilerplate code to convert integers into compile-time + * string literals. The preprocessor, despite it's lack of type checking, is more suited to the task + * + * See: https://stackoverflow.com/questions/6713420/c-convert-integer-to-string-at-compile-time/26824971#26824971 + * + */ + +// Type: 8-bit unsigned int. Min value: 0 Max value: 255. See below for description. +#define OBOE_VERSION_MAJOR 1 + +// Type: 8-bit unsigned int. Min value: 0 Max value: 255. See below for description. +#define OBOE_VERSION_MINOR 4 + +// Type: 16-bit unsigned int. Min value: 0 Max value: 65535. See below for description. +#define OBOE_VERSION_PATCH 2 + +#define OBOE_STRINGIFY(x) #x +#define OBOE_TOSTRING(x) OBOE_STRINGIFY(x) + +// Type: String literal. See below for description. +#define OBOE_VERSION_TEXT \ + OBOE_TOSTRING(OBOE_VERSION_MAJOR) "." \ + OBOE_TOSTRING(OBOE_VERSION_MINOR) "." \ + OBOE_TOSTRING(OBOE_VERSION_PATCH) + +// Type: 32-bit unsigned int. See below for description. +#define OBOE_VERSION_NUMBER ((OBOE_VERSION_MAJOR << 24) | (OBOE_VERSION_MINOR << 16) | OBOE_VERSION_PATCH) + +namespace oboe { + +const char * getVersionText(); + +/** + * Oboe versioning object + */ +struct Version { + /** + * This is incremented when we make breaking API changes. Based loosely on https://semver.org/. + */ + static constexpr uint8_t Major = OBOE_VERSION_MAJOR; + + /** + * This is incremented when we add backwards compatible functionality. Or set to zero when MAJOR is + * incremented. + */ + static constexpr uint8_t Minor = OBOE_VERSION_MINOR; + + /** + * This is incremented when we make backwards compatible bug fixes. Or set to zero when MINOR is + * incremented. + */ + static constexpr uint16_t Patch = OBOE_VERSION_PATCH; + + /** + * Version string in the form MAJOR.MINOR.PATCH. + */ + static constexpr const char * Text = OBOE_VERSION_TEXT; + + /** + * Integer representation of the current Oboe library version. This will always increase when the + * version number changes so can be compared using integer comparison. + */ + static constexpr uint32_t Number = OBOE_VERSION_NUMBER; +}; + +} // namespace oboe +#endif //OBOE_VERSIONINFO_H diff --git a/src/third_party/oboe/src/aaudio/AAudioLoader.cpp b/src/third_party/oboe/src/aaudio/AAudioLoader.cpp new file mode 100644 index 0000000..064633c --- /dev/null +++ b/src/third_party/oboe/src/aaudio/AAudioLoader.cpp @@ -0,0 +1,348 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "common/OboeDebug.h" +#include "AAudioLoader.h" + +#define LIB_AAUDIO_NAME "libaaudio.so" + +namespace oboe { + +AAudioLoader::~AAudioLoader() { + if (mLibHandle != nullptr) { + dlclose(mLibHandle); + mLibHandle = nullptr; + } +} + +AAudioLoader* AAudioLoader::getInstance() { + static AAudioLoader instance; + return &instance; +} + +int AAudioLoader::open() { + if (mLibHandle != nullptr) { + return 0; + } + + // Use RTLD_NOW to avoid the unpredictable behavior that RTLD_LAZY can cause. + // Also resolving all the links now will prevent a run-time penalty later. + mLibHandle = dlopen(LIB_AAUDIO_NAME, RTLD_NOW); + if (mLibHandle == nullptr) { + LOGI("AAudioLoader::open() could not find " LIB_AAUDIO_NAME); + return -1; // TODO review return code + } else { + LOGD("AAudioLoader(): dlopen(%s) returned %p", LIB_AAUDIO_NAME, mLibHandle); + } + + // Load all the function pointers. + createStreamBuilder = load_I_PPB("AAudio_createStreamBuilder"); + builder_openStream = load_I_PBPPS("AAudioStreamBuilder_openStream"); + + builder_setChannelCount = load_V_PBI("AAudioStreamBuilder_setChannelCount"); + if (builder_setChannelCount == nullptr) { + // Use old deprecated alias if needed. + builder_setChannelCount = load_V_PBI("AAudioStreamBuilder_setSamplesPerFrame"); + } + + builder_setBufferCapacityInFrames = load_V_PBI("AAudioStreamBuilder_setBufferCapacityInFrames"); + builder_setDeviceId = load_V_PBI("AAudioStreamBuilder_setDeviceId"); + builder_setDirection = load_V_PBI("AAudioStreamBuilder_setDirection"); + builder_setFormat = load_V_PBI("AAudioStreamBuilder_setFormat"); + builder_setFramesPerDataCallback = load_V_PBI("AAudioStreamBuilder_setFramesPerDataCallback"); + builder_setSharingMode = load_V_PBI("AAudioStreamBuilder_setSharingMode"); + builder_setPerformanceMode = load_V_PBI("AAudioStreamBuilder_setPerformanceMode"); + builder_setSampleRate = load_V_PBI("AAudioStreamBuilder_setSampleRate"); + + if (getSdkVersion() >= __ANDROID_API_P__){ + builder_setUsage = load_V_PBI("AAudioStreamBuilder_setUsage"); + builder_setContentType = load_V_PBI("AAudioStreamBuilder_setContentType"); + builder_setInputPreset = load_V_PBI("AAudioStreamBuilder_setInputPreset"); + builder_setSessionId = load_V_PBI("AAudioStreamBuilder_setSessionId"); + } + + builder_delete = load_I_PB("AAudioStreamBuilder_delete"); + + + builder_setDataCallback = load_V_PBPDPV("AAudioStreamBuilder_setDataCallback"); + builder_setErrorCallback = load_V_PBPEPV("AAudioStreamBuilder_setErrorCallback"); + + stream_read = load_I_PSPVIL("AAudioStream_read"); + + stream_write = load_I_PSCPVIL("AAudioStream_write"); + + stream_waitForStateChange = load_I_PSTPTL("AAudioStream_waitForStateChange"); + + stream_getTimestamp = load_I_PSKPLPL("AAudioStream_getTimestamp"); + + stream_isMMapUsed = load_B_PS("AAudioStream_isMMapUsed"); + + stream_getChannelCount = load_I_PS("AAudioStream_getChannelCount"); + if (stream_getChannelCount == nullptr) { + // Use old alias if needed. + stream_getChannelCount = load_I_PS("AAudioStream_getSamplesPerFrame"); + } + + stream_close = load_I_PS("AAudioStream_close"); + + stream_getBufferSize = load_I_PS("AAudioStream_getBufferSizeInFrames"); + stream_getDeviceId = load_I_PS("AAudioStream_getDeviceId"); + stream_getBufferCapacity = load_I_PS("AAudioStream_getBufferCapacityInFrames"); + stream_getFormat = load_F_PS("AAudioStream_getFormat"); + stream_getFramesPerBurst = load_I_PS("AAudioStream_getFramesPerBurst"); + stream_getFramesRead = load_L_PS("AAudioStream_getFramesRead"); + stream_getFramesWritten = load_L_PS("AAudioStream_getFramesWritten"); + stream_getPerformanceMode = load_I_PS("AAudioStream_getPerformanceMode"); + stream_getSampleRate = load_I_PS("AAudioStream_getSampleRate"); + stream_getSharingMode = load_I_PS("AAudioStream_getSharingMode"); + stream_getState = load_I_PS("AAudioStream_getState"); + stream_getXRunCount = load_I_PS("AAudioStream_getXRunCount"); + + stream_requestStart = load_I_PS("AAudioStream_requestStart"); + stream_requestPause = load_I_PS("AAudioStream_requestPause"); + stream_requestFlush = load_I_PS("AAudioStream_requestFlush"); + stream_requestStop = load_I_PS("AAudioStream_requestStop"); + + stream_setBufferSize = load_I_PSI("AAudioStream_setBufferSizeInFrames"); + + convertResultToText = load_CPH_I("AAudio_convertResultToText"); + + if (getSdkVersion() >= __ANDROID_API_P__){ + stream_getUsage = load_I_PS("AAudioStream_getUsage"); + stream_getContentType = load_I_PS("AAudioStream_getContentType"); + stream_getInputPreset = load_I_PS("AAudioStream_getInputPreset"); + stream_getSessionId = load_I_PS("AAudioStream_getSessionId"); + } + return 0; +} + +static void AAudioLoader_check(void *proc, const char *functionName) { + if (proc == nullptr) { + LOGW("AAudioLoader could not find %s", functionName); + } +} + +AAudioLoader::signature_I_PPB AAudioLoader::load_I_PPB(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_CPH_I AAudioLoader::load_CPH_I(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBI AAudioLoader::load_V_PBI(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBPDPV AAudioLoader::load_V_PBPDPV(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_V_PBPEPV AAudioLoader::load_V_PBPEPV(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSI AAudioLoader::load_I_PSI(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PS AAudioLoader::load_I_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_L_PS AAudioLoader::load_L_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_F_PS AAudioLoader::load_F_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_B_PS AAudioLoader::load_B_PS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PB AAudioLoader::load_I_PB(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PBPPS AAudioLoader::load_I_PBPPS(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSCPVIL AAudioLoader::load_I_PSCPVIL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSPVIL AAudioLoader::load_I_PSPVIL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSTPTL AAudioLoader::load_I_PSTPTL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +AAudioLoader::signature_I_PSKPLPL AAudioLoader::load_I_PSKPLPL(const char *functionName) { + void *proc = dlsym(mLibHandle, functionName); + AAudioLoader_check(proc, functionName); + return reinterpret_cast(proc); +} + +// Ensure that all AAudio primitive data types are int32_t +#define ASSERT_INT32(type) static_assert(std::is_same::value, \ +#type" must be int32_t") + +#define ERRMSG "Oboe constants must match AAudio constants." + +// These asserts help verify that the Oboe definitions match the equivalent AAudio definitions. +// This code is in this .cpp file so it only gets tested once. +#ifdef AAUDIO_AAUDIO_H + + ASSERT_INT32(aaudio_stream_state_t); + ASSERT_INT32(aaudio_direction_t); + ASSERT_INT32(aaudio_format_t); + ASSERT_INT32(aaudio_data_callback_result_t); + ASSERT_INT32(aaudio_result_t); + ASSERT_INT32(aaudio_sharing_mode_t); + ASSERT_INT32(aaudio_performance_mode_t); + + static_assert((int32_t)StreamState::Uninitialized == AAUDIO_STREAM_STATE_UNINITIALIZED, ERRMSG); + static_assert((int32_t)StreamState::Unknown == AAUDIO_STREAM_STATE_UNKNOWN, ERRMSG); + static_assert((int32_t)StreamState::Open == AAUDIO_STREAM_STATE_OPEN, ERRMSG); + static_assert((int32_t)StreamState::Starting == AAUDIO_STREAM_STATE_STARTING, ERRMSG); + static_assert((int32_t)StreamState::Started == AAUDIO_STREAM_STATE_STARTED, ERRMSG); + static_assert((int32_t)StreamState::Pausing == AAUDIO_STREAM_STATE_PAUSING, ERRMSG); + static_assert((int32_t)StreamState::Paused == AAUDIO_STREAM_STATE_PAUSED, ERRMSG); + static_assert((int32_t)StreamState::Flushing == AAUDIO_STREAM_STATE_FLUSHING, ERRMSG); + static_assert((int32_t)StreamState::Flushed == AAUDIO_STREAM_STATE_FLUSHED, ERRMSG); + static_assert((int32_t)StreamState::Stopping == AAUDIO_STREAM_STATE_STOPPING, ERRMSG); + static_assert((int32_t)StreamState::Stopped == AAUDIO_STREAM_STATE_STOPPED, ERRMSG); + static_assert((int32_t)StreamState::Closing == AAUDIO_STREAM_STATE_CLOSING, ERRMSG); + static_assert((int32_t)StreamState::Closed == AAUDIO_STREAM_STATE_CLOSED, ERRMSG); + static_assert((int32_t)StreamState::Disconnected == AAUDIO_STREAM_STATE_DISCONNECTED, ERRMSG); + + static_assert((int32_t)Direction::Output == AAUDIO_DIRECTION_OUTPUT, ERRMSG); + static_assert((int32_t)Direction::Input == AAUDIO_DIRECTION_INPUT, ERRMSG); + + static_assert((int32_t)AudioFormat::Invalid == AAUDIO_FORMAT_INVALID, ERRMSG); + static_assert((int32_t)AudioFormat::Unspecified == AAUDIO_FORMAT_UNSPECIFIED, ERRMSG); + static_assert((int32_t)AudioFormat::I16 == AAUDIO_FORMAT_PCM_I16, ERRMSG); + static_assert((int32_t)AudioFormat::Float == AAUDIO_FORMAT_PCM_FLOAT, ERRMSG); + + static_assert((int32_t)DataCallbackResult::Continue == AAUDIO_CALLBACK_RESULT_CONTINUE, ERRMSG); + static_assert((int32_t)DataCallbackResult::Stop == AAUDIO_CALLBACK_RESULT_STOP, ERRMSG); + + static_assert((int32_t)Result::OK == AAUDIO_OK, ERRMSG); + static_assert((int32_t)Result::ErrorBase == AAUDIO_ERROR_BASE, ERRMSG); + static_assert((int32_t)Result::ErrorDisconnected == AAUDIO_ERROR_DISCONNECTED, ERRMSG); + static_assert((int32_t)Result::ErrorIllegalArgument == AAUDIO_ERROR_ILLEGAL_ARGUMENT, ERRMSG); + static_assert((int32_t)Result::ErrorInternal == AAUDIO_ERROR_INTERNAL, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidState == AAUDIO_ERROR_INVALID_STATE, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidHandle == AAUDIO_ERROR_INVALID_HANDLE, ERRMSG); + static_assert((int32_t)Result::ErrorUnimplemented == AAUDIO_ERROR_UNIMPLEMENTED, ERRMSG); + static_assert((int32_t)Result::ErrorUnavailable == AAUDIO_ERROR_UNAVAILABLE, ERRMSG); + static_assert((int32_t)Result::ErrorNoFreeHandles == AAUDIO_ERROR_NO_FREE_HANDLES, ERRMSG); + static_assert((int32_t)Result::ErrorNoMemory == AAUDIO_ERROR_NO_MEMORY, ERRMSG); + static_assert((int32_t)Result::ErrorNull == AAUDIO_ERROR_NULL, ERRMSG); + static_assert((int32_t)Result::ErrorTimeout == AAUDIO_ERROR_TIMEOUT, ERRMSG); + static_assert((int32_t)Result::ErrorWouldBlock == AAUDIO_ERROR_WOULD_BLOCK, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidFormat == AAUDIO_ERROR_INVALID_FORMAT, ERRMSG); + static_assert((int32_t)Result::ErrorOutOfRange == AAUDIO_ERROR_OUT_OF_RANGE, ERRMSG); + static_assert((int32_t)Result::ErrorNoService == AAUDIO_ERROR_NO_SERVICE, ERRMSG); + static_assert((int32_t)Result::ErrorInvalidRate == AAUDIO_ERROR_INVALID_RATE, ERRMSG); + + static_assert((int32_t)SharingMode::Exclusive == AAUDIO_SHARING_MODE_EXCLUSIVE, ERRMSG); + static_assert((int32_t)SharingMode::Shared == AAUDIO_SHARING_MODE_SHARED, ERRMSG); + + static_assert((int32_t)PerformanceMode::None == AAUDIO_PERFORMANCE_MODE_NONE, ERRMSG); + static_assert((int32_t)PerformanceMode::PowerSaving + == AAUDIO_PERFORMANCE_MODE_POWER_SAVING, ERRMSG); + static_assert((int32_t)PerformanceMode::LowLatency + == AAUDIO_PERFORMANCE_MODE_LOW_LATENCY, ERRMSG); +#endif + +// The aaudio_ usage, content and input_preset types were added in NDK 17, +// which is the first version to support Android Pie (API 28). +#if __NDK_MAJOR__ >= 17 + + ASSERT_INT32(aaudio_usage_t); + ASSERT_INT32(aaudio_content_type_t); + ASSERT_INT32(aaudio_input_preset_t); + + static_assert((int32_t)Usage::Media == AAUDIO_USAGE_MEDIA, ERRMSG); + static_assert((int32_t)Usage::VoiceCommunication == AAUDIO_USAGE_VOICE_COMMUNICATION, ERRMSG); + static_assert((int32_t)Usage::VoiceCommunicationSignalling + == AAUDIO_USAGE_VOICE_COMMUNICATION_SIGNALLING, ERRMSG); + static_assert((int32_t)Usage::Alarm == AAUDIO_USAGE_ALARM, ERRMSG); + static_assert((int32_t)Usage::Notification == AAUDIO_USAGE_NOTIFICATION, ERRMSG); + static_assert((int32_t)Usage::NotificationRingtone == AAUDIO_USAGE_NOTIFICATION_RINGTONE, ERRMSG); + static_assert((int32_t)Usage::NotificationEvent == AAUDIO_USAGE_NOTIFICATION_EVENT, ERRMSG); + static_assert((int32_t)Usage::AssistanceAccessibility == AAUDIO_USAGE_ASSISTANCE_ACCESSIBILITY, ERRMSG); + static_assert((int32_t)Usage::AssistanceNavigationGuidance + == AAUDIO_USAGE_ASSISTANCE_NAVIGATION_GUIDANCE, ERRMSG); + static_assert((int32_t)Usage::AssistanceSonification == AAUDIO_USAGE_ASSISTANCE_SONIFICATION, ERRMSG); + static_assert((int32_t)Usage::Game == AAUDIO_USAGE_GAME, ERRMSG); + static_assert((int32_t)Usage::Assistant == AAUDIO_USAGE_ASSISTANT, ERRMSG); + + static_assert((int32_t)ContentType::Speech == AAUDIO_CONTENT_TYPE_SPEECH, ERRMSG); + static_assert((int32_t)ContentType::Music == AAUDIO_CONTENT_TYPE_MUSIC, ERRMSG); + static_assert((int32_t)ContentType::Movie == AAUDIO_CONTENT_TYPE_MOVIE, ERRMSG); + static_assert((int32_t)ContentType::Sonification == AAUDIO_CONTENT_TYPE_SONIFICATION, ERRMSG); + + static_assert((int32_t)InputPreset::Generic == AAUDIO_INPUT_PRESET_GENERIC, ERRMSG); + static_assert((int32_t)InputPreset::Camcorder == AAUDIO_INPUT_PRESET_CAMCORDER, ERRMSG); + static_assert((int32_t)InputPreset::VoiceRecognition == AAUDIO_INPUT_PRESET_VOICE_RECOGNITION, ERRMSG); + static_assert((int32_t)InputPreset::VoiceCommunication + == AAUDIO_INPUT_PRESET_VOICE_COMMUNICATION, ERRMSG); + static_assert((int32_t)InputPreset::Unprocessed == AAUDIO_INPUT_PRESET_UNPROCESSED, ERRMSG); + + static_assert((int32_t)SessionId::None == AAUDIO_SESSION_ID_NONE, ERRMSG); + static_assert((int32_t)SessionId::Allocate == AAUDIO_SESSION_ID_ALLOCATE, ERRMSG); +#endif + +} // namespace oboe diff --git a/src/third_party/oboe/src/aaudio/AAudioLoader.h b/src/third_party/oboe/src/aaudio/AAudioLoader.h new file mode 100644 index 0000000..6f5bb95 --- /dev/null +++ b/src/third_party/oboe/src/aaudio/AAudioLoader.h @@ -0,0 +1,229 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AAUDIO_LOADER_H_ +#define OBOE_AAUDIO_LOADER_H_ + +#include +#include "oboe/Definitions.h" + +// If the NDK is before O then define this in your build +// so that AAudio.h will not be included. +#ifdef OBOE_NO_INCLUDE_AAUDIO + +// Define missing types from AAudio.h +typedef int32_t aaudio_stream_state_t; +typedef int32_t aaudio_direction_t; +typedef int32_t aaudio_format_t; +typedef int32_t aaudio_data_callback_result_t; +typedef int32_t aaudio_result_t; +typedef int32_t aaudio_sharing_mode_t; +typedef int32_t aaudio_performance_mode_t; + +typedef struct AAudioStreamStruct AAudioStream; +typedef struct AAudioStreamBuilderStruct AAudioStreamBuilder; + +typedef aaudio_data_callback_result_t (*AAudioStream_dataCallback)( + AAudioStream *stream, + void *userData, + void *audioData, + int32_t numFrames); + +typedef void (*AAudioStream_errorCallback)( + AAudioStream *stream, + void *userData, + aaudio_result_t error); + +// These were defined in P +typedef int32_t aaudio_usage_t; +typedef int32_t aaudio_content_type_t; +typedef int32_t aaudio_input_preset_t; +typedef int32_t aaudio_session_id_t; +#else +#include +#include +#endif + +#ifndef __NDK_MAJOR__ +#define __NDK_MAJOR__ 0 +#endif + +namespace oboe { + + +/** + * The AAudio API was not available in early versions of Android. + * To avoid linker errors, we dynamically link with the functions by name using dlsym(). + * On older versions this linkage will safely fail. + */ +class AAudioLoader { + public: + // Use signatures for common functions. + // Key to letter abbreviations. + // S = Stream + // B = Builder + // I = int32_t + // L = int64_t + // T = sTate + // K = clocKid_t + // P = Pointer to following data type + // C = Const prefix + // H = cHar + typedef int32_t (*signature_I_PPB)(AAudioStreamBuilder **builder); + + typedef const char * (*signature_CPH_I)(int32_t); + + typedef int32_t (*signature_I_PBPPS)(AAudioStreamBuilder *, + AAudioStream **stream); // AAudioStreamBuilder_open() + + typedef int32_t (*signature_I_PB)(AAudioStreamBuilder *); // AAudioStreamBuilder_delete() + // AAudioStreamBuilder_setSampleRate() + typedef void (*signature_V_PBI)(AAudioStreamBuilder *, int32_t); + + typedef int32_t (*signature_I_PS)(AAudioStream *); // AAudioStream_getSampleRate() + typedef int64_t (*signature_L_PS)(AAudioStream *); // AAudioStream_getFramesRead() + // AAudioStream_setBufferSizeInFrames() + typedef int32_t (*signature_I_PSI)(AAudioStream *, int32_t); + + typedef void (*signature_V_PBPDPV)(AAudioStreamBuilder *, + AAudioStream_dataCallback, + void *); + + typedef void (*signature_V_PBPEPV)(AAudioStreamBuilder *, + AAudioStream_errorCallback, + void *); + + typedef aaudio_format_t (*signature_F_PS)(AAudioStream *stream); + + typedef int32_t (*signature_I_PSPVIL)(AAudioStream *, void *, int32_t, int64_t); + typedef int32_t (*signature_I_PSCPVIL)(AAudioStream *, const void *, int32_t, int64_t); + + typedef int32_t (*signature_I_PSTPTL)(AAudioStream *, + aaudio_stream_state_t, + aaudio_stream_state_t *, + int64_t); + + typedef int32_t (*signature_I_PSKPLPL)(AAudioStream *, clockid_t, int64_t *, int64_t *); + + typedef bool (*signature_B_PS)(AAudioStream *); + + static AAudioLoader* getInstance(); // singleton + + /** + * Open the AAudio shared library and load the function pointers. + * This can be called multiple times. + * It should only be called from one thread. + * + * The destructor will clean up after the open. + * + * @return 0 if successful or negative error. + */ + int open(); + + // Function pointers into the AAudio shared library. + signature_I_PPB createStreamBuilder = nullptr; + + signature_I_PBPPS builder_openStream = nullptr; + + signature_V_PBI builder_setBufferCapacityInFrames = nullptr; + signature_V_PBI builder_setChannelCount = nullptr; + signature_V_PBI builder_setDeviceId = nullptr; + signature_V_PBI builder_setDirection = nullptr; + signature_V_PBI builder_setFormat = nullptr; + signature_V_PBI builder_setFramesPerDataCallback = nullptr; + signature_V_PBI builder_setPerformanceMode = nullptr; + signature_V_PBI builder_setSampleRate = nullptr; + signature_V_PBI builder_setSharingMode = nullptr; + + signature_V_PBI builder_setUsage = nullptr; + signature_V_PBI builder_setContentType = nullptr; + signature_V_PBI builder_setInputPreset = nullptr; + signature_V_PBI builder_setSessionId = nullptr; + + signature_V_PBPDPV builder_setDataCallback = nullptr; + signature_V_PBPEPV builder_setErrorCallback = nullptr; + + signature_I_PB builder_delete = nullptr; + + signature_F_PS stream_getFormat = nullptr; + + signature_I_PSPVIL stream_read = nullptr; + signature_I_PSCPVIL stream_write = nullptr; + + signature_I_PSTPTL stream_waitForStateChange = nullptr; + + signature_I_PSKPLPL stream_getTimestamp = nullptr; + + signature_B_PS stream_isMMapUsed = nullptr; + + signature_I_PS stream_close = nullptr; + + signature_I_PS stream_getChannelCount = nullptr; + signature_I_PS stream_getDeviceId = nullptr; + + signature_I_PS stream_getBufferSize = nullptr; + signature_I_PS stream_getBufferCapacity = nullptr; + signature_I_PS stream_getFramesPerBurst = nullptr; + signature_I_PS stream_getState = nullptr; + signature_I_PS stream_getPerformanceMode = nullptr; + signature_I_PS stream_getSampleRate = nullptr; + signature_I_PS stream_getSharingMode = nullptr; + signature_I_PS stream_getXRunCount = nullptr; + + signature_I_PSI stream_setBufferSize = nullptr; + signature_I_PS stream_requestStart = nullptr; + signature_I_PS stream_requestPause = nullptr; + signature_I_PS stream_requestFlush = nullptr; + signature_I_PS stream_requestStop = nullptr; + + signature_L_PS stream_getFramesRead = nullptr; + signature_L_PS stream_getFramesWritten = nullptr; + + signature_CPH_I convertResultToText = nullptr; + + signature_I_PS stream_getUsage = nullptr; + signature_I_PS stream_getContentType = nullptr; + signature_I_PS stream_getInputPreset = nullptr; + signature_I_PS stream_getSessionId = nullptr; + + private: + AAudioLoader() {} + ~AAudioLoader(); + + // Load function pointers for specific signatures. + signature_I_PPB load_I_PPB(const char *name); + signature_CPH_I load_CPH_I(const char *name); + signature_V_PBI load_V_PBI(const char *name); + signature_V_PBPDPV load_V_PBPDPV(const char *name); + signature_V_PBPEPV load_V_PBPEPV(const char *name); + signature_I_PB load_I_PB(const char *name); + signature_I_PBPPS load_I_PBPPS(const char *name); + signature_I_PS load_I_PS(const char *name); + signature_L_PS load_L_PS(const char *name); + signature_F_PS load_F_PS(const char *name); + signature_B_PS load_B_PS(const char *name); + signature_I_PSI load_I_PSI(const char *name); + signature_I_PSPVIL load_I_PSPVIL(const char *name); + signature_I_PSCPVIL load_I_PSCPVIL(const char *name); + signature_I_PSTPTL load_I_PSTPTL(const char *name); + signature_I_PSKPLPL load_I_PSKPLPL(const char *name); + + void *mLibHandle = nullptr; +}; + +} // namespace oboe + +#endif //OBOE_AAUDIO_LOADER_H_ diff --git a/src/third_party/oboe/src/aaudio/AudioStreamAAudio.cpp b/src/third_party/oboe/src/aaudio/AudioStreamAAudio.cpp new file mode 100644 index 0000000..52d85b9 --- /dev/null +++ b/src/third_party/oboe/src/aaudio/AudioStreamAAudio.cpp @@ -0,0 +1,643 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include "aaudio/AAudioLoader.h" +#include "aaudio/AudioStreamAAudio.h" +#include "common/AudioClock.h" +#include "common/OboeDebug.h" +#include "oboe/Utilities.h" + +#ifdef __ANDROID__ +#include +#include + +#endif + +#ifndef OBOE_FIX_FORCE_STARTING_TO_STARTED +// Workaround state problems in AAudio +// TODO Which versions does this occur in? Verify fixed in Q. +#define OBOE_FIX_FORCE_STARTING_TO_STARTED 1 +#endif // OBOE_FIX_FORCE_STARTING_TO_STARTED + +using namespace oboe; +AAudioLoader *AudioStreamAAudio::mLibLoader = nullptr; + +// 'C' wrapper for the data callback method +static aaudio_data_callback_result_t oboe_aaudio_data_callback_proc( + AAudioStream *stream, + void *userData, + void *audioData, + int32_t numFrames) { + + AudioStreamAAudio *oboeStream = reinterpret_cast(userData); + if (oboeStream != nullptr) { + return static_cast( + oboeStream->callOnAudioReady(stream, audioData, numFrames)); + + } else { + return static_cast(DataCallbackResult::Stop); + } +} + +// This runs in its own thread. +// Only one of these threads will be launched from internalErrorCallback(). +// It calls app error callbacks from a static function in case the stream gets deleted. +static void oboe_aaudio_error_thread_proc(AudioStreamAAudio *oboeStream, + Result error) { + LOGD("%s() - entering >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>", __func__); + oboeStream->requestStop(); + if (oboeStream->getCallback() != nullptr) { + oboeStream->getCallback()->onErrorBeforeClose(oboeStream, error); + } + oboeStream->close(); + if (oboeStream->getCallback() != nullptr) { + // Warning, oboeStream may get deleted by this callback. + oboeStream->getCallback()->onErrorAfterClose(oboeStream, error); + } + LOGD("%s() - exiting <<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<", __func__); +} + +// This runs in its own thread. +// Only one of these threads will be launched from internalErrorCallback(). +// Prevents deletion of the stream if the app is using AudioStreamBuilder::openSharedStream() +static void oboe_aaudio_error_thread_proc_shared(std::shared_ptr sharedStream, + Result error) { + AudioStreamAAudio *oboeStream = reinterpret_cast(sharedStream.get()); + oboe_aaudio_error_thread_proc(oboeStream, error); +} + +namespace oboe { + +/* + * Create a stream that uses Oboe Audio API. + */ +AudioStreamAAudio::AudioStreamAAudio(const AudioStreamBuilder &builder) + : AudioStream(builder) + , mAAudioStream(nullptr) { + mCallbackThreadEnabled.store(false); + isSupported(); +} + +bool AudioStreamAAudio::isSupported() { + mLibLoader = AAudioLoader::getInstance(); + int openResult = mLibLoader->open(); + return openResult == 0; +} + +// Static 'C' wrapper for the error callback method. +// Launch a thread to handle the error. +// That other thread can safely stop, close and delete the stream. +void AudioStreamAAudio::internalErrorCallback( + AAudioStream *stream, + void *userData, + aaudio_result_t error) { + AudioStreamAAudio *oboeStream = reinterpret_cast(userData); + + // Prevents deletion of the stream if the app is using AudioStreamBuilder::openSharedStream() + std::shared_ptr sharedStream = oboeStream->lockWeakThis(); + + // These checks should be enough because we assume that the stream close() + // will join() any active callback threads and will not allow new callbacks. + if (oboeStream->wasErrorCallbackCalled()) { // block extra error callbacks + LOGE("%s() multiple error callbacks called!", __func__); + } else if (stream != oboeStream->getUnderlyingStream()) { + LOGW("%s() stream already closed", __func__); // can happen if there are bugs + } else if (sharedStream) { + // Handle error on a separate thread using shared pointer. + std::thread t(oboe_aaudio_error_thread_proc_shared, sharedStream, + static_cast(error)); + t.detach(); + } else { + // Handle error on a separate thread. + std::thread t(oboe_aaudio_error_thread_proc, oboeStream, + static_cast(error)); + t.detach(); + } +} + +void AudioStreamAAudio::logUnsupportedAttributes() { + int sdkVersion = getSdkVersion(); + + // These attributes are not supported pre Android "P" + if (sdkVersion < __ANDROID_API_P__) { + if (mUsage != Usage::Media) { + LOGW("Usage [AudioStreamBuilder::setUsage()] " + "is not supported on AAudio streams running on pre-Android P versions."); + } + + if (mContentType != ContentType::Music) { + LOGW("ContentType [AudioStreamBuilder::setContentType()] " + "is not supported on AAudio streams running on pre-Android P versions."); + } + + if (mSessionId != SessionId::None) { + LOGW("SessionId [AudioStreamBuilder::setSessionId()] " + "is not supported on AAudio streams running on pre-Android P versions."); + } + } +} + +Result AudioStreamAAudio::open() { + Result result = Result::OK; + + if (mAAudioStream != nullptr) { + return Result::ErrorInvalidState; + } + + result = AudioStream::open(); + if (result != Result::OK) { + return result; + } + + AAudioStreamBuilder *aaudioBuilder; + result = static_cast(mLibLoader->createStreamBuilder(&aaudioBuilder)); + if (result != Result::OK) { + return result; + } + + // Do not set INPUT capacity below 4096 because that prevents us from getting a FAST track + // when using the Legacy data path. + // If the app requests > 4096 then we allow it but we are less likely to get LowLatency. + // See internal bug b/80308183 for more details. + // Fixed in Q but let's still clip the capacity because high input capacity + // does not increase latency. + int32_t capacity = mBufferCapacityInFrames; + constexpr int kCapacityRequiredForFastLegacyTrack = 4096; // matches value in AudioFinger + if (OboeGlobals::areWorkaroundsEnabled() + && mDirection == oboe::Direction::Input + && capacity != oboe::Unspecified + && capacity < kCapacityRequiredForFastLegacyTrack + && mPerformanceMode == oboe::PerformanceMode::LowLatency) { + capacity = kCapacityRequiredForFastLegacyTrack; + LOGD("AudioStreamAAudio.open() capacity changed from %d to %d for lower latency", + static_cast(mBufferCapacityInFrames), capacity); + } + mLibLoader->builder_setBufferCapacityInFrames(aaudioBuilder, capacity); + + mLibLoader->builder_setChannelCount(aaudioBuilder, mChannelCount); + mLibLoader->builder_setDeviceId(aaudioBuilder, mDeviceId); + mLibLoader->builder_setDirection(aaudioBuilder, static_cast(mDirection)); + mLibLoader->builder_setFormat(aaudioBuilder, static_cast(mFormat)); + mLibLoader->builder_setSampleRate(aaudioBuilder, mSampleRate); + mLibLoader->builder_setSharingMode(aaudioBuilder, + static_cast(mSharingMode)); + mLibLoader->builder_setPerformanceMode(aaudioBuilder, + static_cast(mPerformanceMode)); + + // These were added in P so we have to check for the function pointer. + if (mLibLoader->builder_setUsage != nullptr) { + mLibLoader->builder_setUsage(aaudioBuilder, + static_cast(mUsage)); + } + + if (mLibLoader->builder_setContentType != nullptr) { + mLibLoader->builder_setContentType(aaudioBuilder, + static_cast(mContentType)); + } + + if (mLibLoader->builder_setInputPreset != nullptr) { + mLibLoader->builder_setInputPreset(aaudioBuilder, + static_cast(mInputPreset)); + } + + if (mLibLoader->builder_setSessionId != nullptr) { + mLibLoader->builder_setSessionId(aaudioBuilder, + static_cast(mSessionId)); + } + + // TODO get more parameters from the builder? + + if (mStreamCallback != nullptr) { + mLibLoader->builder_setDataCallback(aaudioBuilder, oboe_aaudio_data_callback_proc, this); + mLibLoader->builder_setFramesPerDataCallback(aaudioBuilder, getFramesPerCallback()); + // If the data callback is not being used then the write method will return an error + // and the app can stop and close the stream. + mLibLoader->builder_setErrorCallback(aaudioBuilder, internalErrorCallback, this); + } + + // ============= OPEN THE STREAM ================ + { + AAudioStream *stream = nullptr; + result = static_cast(mLibLoader->builder_openStream(aaudioBuilder, &stream)); + mAAudioStream.store(stream); + } + if (result != Result::OK) { + goto error2; + } + + // Query and cache the stream properties + mDeviceId = mLibLoader->stream_getDeviceId(mAAudioStream); + mChannelCount = mLibLoader->stream_getChannelCount(mAAudioStream); + mSampleRate = mLibLoader->stream_getSampleRate(mAAudioStream); + mFormat = static_cast(mLibLoader->stream_getFormat(mAAudioStream)); + mSharingMode = static_cast(mLibLoader->stream_getSharingMode(mAAudioStream)); + mPerformanceMode = static_cast( + mLibLoader->stream_getPerformanceMode(mAAudioStream)); + mBufferCapacityInFrames = mLibLoader->stream_getBufferCapacity(mAAudioStream); + mBufferSizeInFrames = mLibLoader->stream_getBufferSize(mAAudioStream); + + + // These were added in P so we have to check for the function pointer. + if (mLibLoader->stream_getUsage != nullptr) { + mUsage = static_cast(mLibLoader->stream_getUsage(mAAudioStream)); + } + if (mLibLoader->stream_getContentType != nullptr) { + mContentType = static_cast(mLibLoader->stream_getContentType(mAAudioStream)); + } + if (mLibLoader->stream_getInputPreset != nullptr) { + mInputPreset = static_cast(mLibLoader->stream_getInputPreset(mAAudioStream)); + } + if (mLibLoader->stream_getSessionId != nullptr) { + mSessionId = static_cast(mLibLoader->stream_getSessionId(mAAudioStream)); + } else { + mSessionId = SessionId::None; + } + + LOGD("AudioStreamAAudio.open() format=%d, sampleRate=%d, capacity = %d", + static_cast(mFormat), static_cast(mSampleRate), + static_cast(mBufferCapacityInFrames)); + +error2: + mLibLoader->builder_delete(aaudioBuilder); + LOGD("AudioStreamAAudio.open: AAudioStream_Open() returned %s", + mLibLoader->convertResultToText(static_cast(result))); + return result; +} + +Result AudioStreamAAudio::close() { + // The main reason we have this mutex if to prevent a collision between a call + // by the application to stop a stream at the same time that an onError callback + // is being executed because of a disconnect. The close will delete the stream, + // which could otherwise cause the requestStop() to crash. + std::lock_guard lock(mLock); + + AudioStream::close(); + + // This will delete the AAudio stream object so we need to null out the pointer. + AAudioStream *stream = mAAudioStream.exchange(nullptr); + if (stream != nullptr) { + return static_cast(mLibLoader->stream_close(stream)); + } else { + return Result::ErrorClosed; + } +} + +DataCallbackResult AudioStreamAAudio::callOnAudioReady(AAudioStream *stream, + void *audioData, + int32_t numFrames) { + DataCallbackResult result = fireDataCallback(audioData, numFrames); + if (result == DataCallbackResult::Continue) { + return result; + } else { + if (result == DataCallbackResult::Stop) { + LOGD("Oboe callback returned DataCallbackResult::Stop"); + } else { + LOGE("Oboe callback returned unexpected value = %d", result); + } + + if (getSdkVersion() <= __ANDROID_API_P__) { + launchStopThread(); + if (isMMapUsed()) { + return DataCallbackResult::Stop; + } else { + // Legacy stream <= API_P cannot be restarted after returning Stop. + return DataCallbackResult::Continue; + } + } else { + return DataCallbackResult::Stop; // OK >= API_Q + } + } +} + +Result AudioStreamAAudio::requestStart() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Starting || state == StreamState::Started) { + // WARNING: On P, AAudio is returning ErrorInvalidState for Output and OK for Input. + return Result::OK; + } + } + if (mStreamCallback != nullptr) { // Was a callback requested? + setDataCallbackEnabled(true); + } + return static_cast(mLibLoader->stream_requestStart(stream)); + } else { + return Result::ErrorClosed; + } +} + +Result AudioStreamAAudio::requestPause() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Pausing || state == StreamState::Paused) { + return Result::OK; + } + } + return static_cast(mLibLoader->stream_requestPause(stream)); + } else { + return Result::ErrorClosed; + } +} + +Result AudioStreamAAudio::requestFlush() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Flushing || state == StreamState::Flushed) { + return Result::OK; + } + } + return static_cast(mLibLoader->stream_requestFlush(stream)); + } else { + return Result::ErrorClosed; + } +} + +Result AudioStreamAAudio::requestStop() { + std::lock_guard lock(mLock); + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + // Avoid state machine errors in O_MR1. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + StreamState state = static_cast(mLibLoader->stream_getState(stream)); + if (state == StreamState::Stopping || state == StreamState::Stopped) { + return Result::OK; + } + } + return static_cast(mLibLoader->stream_requestStop(stream)); + } else { + return Result::ErrorClosed; + } +} + +ResultWithValue AudioStreamAAudio::write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + int32_t result = mLibLoader->stream_write(mAAudioStream, buffer, + numFrames, timeoutNanoseconds); + return ResultWithValue::createBasedOnSign(result); + } else { + return ResultWithValue(Result::ErrorClosed); + } +} + +ResultWithValue AudioStreamAAudio::read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + int32_t result = mLibLoader->stream_read(mAAudioStream, buffer, + numFrames, timeoutNanoseconds); + return ResultWithValue::createBasedOnSign(result); + } else { + return ResultWithValue(Result::ErrorClosed); + } +} + + +// AAudioStream_waitForStateChange() can crash if it is waiting on a stream and that stream +// is closed from another thread. We do not want to lock the stream for the duration of the call. +// So we call AAudioStream_waitForStateChange() with a timeout of zero so that it will not block. +// Then we can do our own sleep with the lock unlocked. +Result AudioStreamAAudio::waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) { + Result oboeResult = Result::ErrorTimeout; + int64_t sleepTimeNanos = 20 * kNanosPerMillisecond; // arbitrary + aaudio_stream_state_t currentAAudioState = static_cast(currentState); + + aaudio_result_t result = AAUDIO_OK; + int64_t timeLeftNanos = timeoutNanoseconds; + + mLock.lock(); + while (true) { + // Do we still have an AAudio stream? If not then stream must have been closed. + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + if (nextState != nullptr) { + *nextState = StreamState::Closed; + } + oboeResult = Result::ErrorClosed; + break; + } + + // Update and query state change with no blocking. + aaudio_stream_state_t aaudioNextState; + result = mLibLoader->stream_waitForStateChange( + mAAudioStream, + currentAAudioState, + &aaudioNextState, + 0); // timeout=0 for non-blocking + // AAudio will return AAUDIO_ERROR_TIMEOUT if timeout=0 and the state does not change. + if (result != AAUDIO_OK && result != AAUDIO_ERROR_TIMEOUT) { + oboeResult = static_cast(result); + break; + } +#if OBOE_FIX_FORCE_STARTING_TO_STARTED + if (OboeGlobals::areWorkaroundsEnabled() + && aaudioNextState == static_cast(StreamState::Starting)) { + aaudioNextState = static_cast(StreamState::Started); + } +#endif // OBOE_FIX_FORCE_STARTING_TO_STARTED + if (nextState != nullptr) { + *nextState = static_cast(aaudioNextState); + } + if (currentAAudioState != aaudioNextState) { // state changed? + oboeResult = Result::OK; + break; + } + + // Did we timeout or did user ask for non-blocking? + if (timeLeftNanos <= 0) { + break; + } + + // No change yet so sleep. + mLock.unlock(); // Don't sleep while locked. + if (sleepTimeNanos > timeLeftNanos) { + sleepTimeNanos = timeLeftNanos; // last little bit + } + AudioClock::sleepForNanos(sleepTimeNanos); + timeLeftNanos -= sleepTimeNanos; + mLock.lock(); + } + + mLock.unlock(); + return oboeResult; +} + +ResultWithValue AudioStreamAAudio::setBufferSizeInFrames(int32_t requestedFrames) { + + AAudioStream *stream = mAAudioStream.load(); + + if (stream != nullptr) { + int32_t adjustedFrames = requestedFrames; + if (adjustedFrames > mBufferCapacityInFrames) { + adjustedFrames = mBufferCapacityInFrames; + } + adjustedFrames = QuirksManager::getInstance().clipBufferSize(*this, adjustedFrames); + + int32_t newBufferSize = mLibLoader->stream_setBufferSize(mAAudioStream, adjustedFrames); + + // Cache the result if it's valid + if (newBufferSize > 0) mBufferSizeInFrames = newBufferSize; + + return ResultWithValue::createBasedOnSign(newBufferSize); + + } else { + return ResultWithValue(Result::ErrorClosed); + } +} + +StreamState AudioStreamAAudio::getState() const { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + aaudio_stream_state_t aaudioState = mLibLoader->stream_getState(stream); +#if OBOE_FIX_FORCE_STARTING_TO_STARTED + if (OboeGlobals::areWorkaroundsEnabled() + && aaudioState == AAUDIO_STREAM_STATE_STARTING) { + aaudioState = AAUDIO_STREAM_STATE_STARTED; + } +#endif // OBOE_FIX_FORCE_STARTING_TO_STARTED + return static_cast(aaudioState); + } else { + return StreamState::Closed; + } +} + +int32_t AudioStreamAAudio::getBufferSizeInFrames() { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + mBufferSizeInFrames = mLibLoader->stream_getBufferSize(stream); + } + return mBufferSizeInFrames; +} + +int32_t AudioStreamAAudio::getFramesPerBurst() { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + mFramesPerBurst = mLibLoader->stream_getFramesPerBurst(stream); + } + return mFramesPerBurst; +} + +void AudioStreamAAudio::updateFramesRead() { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + mFramesRead = mLibLoader->stream_getFramesRead(stream); + } +} + +void AudioStreamAAudio::updateFramesWritten() { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + mFramesWritten = mLibLoader->stream_getFramesWritten(stream); + } +} + +ResultWithValue AudioStreamAAudio::getXRunCount() const { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + return ResultWithValue::createBasedOnSign(mLibLoader->stream_getXRunCount(stream)); + } else { + return ResultWithValue(Result::ErrorNull); + } +} + +Result AudioStreamAAudio::getTimestamp(clockid_t clockId, + int64_t *framePosition, + int64_t *timeNanoseconds) { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + if (getState() != StreamState::Started) { + return Result::ErrorInvalidState; + } + return static_cast(mLibLoader->stream_getTimestamp(stream, clockId, + framePosition, timeNanoseconds)); + } else { + return Result::ErrorNull; + } +} + +ResultWithValue AudioStreamAAudio::calculateLatencyMillis() { + AAudioStream *stream = mAAudioStream.load(); + if (stream == nullptr) { + return ResultWithValue(Result::ErrorClosed); + } + + // Get the time that a known audio frame was presented. + int64_t hardwareFrameIndex; + int64_t hardwareFrameHardwareTime; + auto result = getTimestamp(CLOCK_MONOTONIC, + &hardwareFrameIndex, + &hardwareFrameHardwareTime); + if (result != oboe::Result::OK) { + return ResultWithValue(static_cast(result)); + } + + // Get counter closest to the app. + bool isOutput = (getDirection() == oboe::Direction::Output); + int64_t appFrameIndex = isOutput ? getFramesWritten() : getFramesRead(); + + // Assume that the next frame will be processed at the current time + using namespace std::chrono; + int64_t appFrameAppTime = + duration_cast(steady_clock::now().time_since_epoch()).count(); + + // Calculate the number of frames between app and hardware + int64_t frameIndexDelta = appFrameIndex - hardwareFrameIndex; + + // Calculate the time which the next frame will be or was presented + int64_t frameTimeDelta = (frameIndexDelta * oboe::kNanosPerSecond) / getSampleRate(); + int64_t appFrameHardwareTime = hardwareFrameHardwareTime + frameTimeDelta; + + // The current latency is the difference in time between when the current frame is at + // the app and when it is at the hardware. + double latencyNanos = static_cast(isOutput + ? (appFrameHardwareTime - appFrameAppTime) // hardware is later + : (appFrameAppTime - appFrameHardwareTime)); // hardware is earlier + double latencyMillis = latencyNanos / kNanosPerMillisecond; + + return ResultWithValue(latencyMillis); +} + +bool AudioStreamAAudio::isMMapUsed() { + AAudioStream *stream = mAAudioStream.load(); + if (stream != nullptr) { + return mLibLoader->stream_isMMapUsed(stream); + } else { + return false; + } +} + +} // namespace oboe diff --git a/src/third_party/oboe/src/aaudio/AudioStreamAAudio.h b/src/third_party/oboe/src/aaudio/AudioStreamAAudio.h new file mode 100644 index 0000000..6267328 --- /dev/null +++ b/src/third_party/oboe/src/aaudio/AudioStreamAAudio.h @@ -0,0 +1,123 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_AAUDIO_H_ +#define OBOE_STREAM_AAUDIO_H_ + +#include +#include +#include + +#include "oboe/AudioStreamBuilder.h" +#include "oboe/AudioStream.h" +#include "oboe/Definitions.h" +#include "AAudioLoader.h" + +namespace oboe { + +/** + * Implementation of OboeStream that uses AAudio. + * + * Do not create this class directly. + * Use an OboeStreamBuilder to create one. + */ +class AudioStreamAAudio : public AudioStream { +public: + AudioStreamAAudio(); + explicit AudioStreamAAudio(const AudioStreamBuilder &builder); + + virtual ~AudioStreamAAudio() = default; + + /** + * + * @return true if AAudio is supported on this device. + */ + static bool isSupported(); + + // These functions override methods in AudioStream. + // See AudioStream for documentation. + Result open() override; + Result close() override; + + Result requestStart() override; + Result requestPause() override; + Result requestFlush() override; + Result requestStop() override; + + ResultWithValue write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue setBufferSizeInFrames(int32_t requestedFrames) override; + int32_t getBufferSizeInFrames() override; + int32_t getFramesPerBurst() override; + ResultWithValue getXRunCount() const override; + bool isXRunCountSupported() const override { return true; } + + ResultWithValue calculateLatencyMillis() override; + + Result waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) override; + + Result getTimestamp(clockid_t clockId, + int64_t *framePosition, + int64_t *timeNanoseconds) override; + + StreamState getState() const override; + + AudioApi getAudioApi() const override { + return AudioApi::AAudio; + } + + DataCallbackResult callOnAudioReady(AAudioStream *stream, + void *audioData, + int32_t numFrames); + + bool isMMapUsed(); + +protected: + static void internalErrorCallback( + AAudioStream *stream, + void *userData, + aaudio_result_t error); + + void *getUnderlyingStream() const override { + return mAAudioStream.load(); + } + + void updateFramesRead() override; + void updateFramesWritten() override; + + void logUnsupportedAttributes(); + +private: + + std::atomic mCallbackThreadEnabled; + + // pointer to the underlying AAudio stream, valid if open, null if closed + std::atomic mAAudioStream{nullptr}; + + static AAudioLoader *mLibLoader; +}; + +} // namespace oboe + +#endif // OBOE_STREAM_AAUDIO_H_ diff --git a/src/third_party/oboe/src/common/AudioClock.h b/src/third_party/oboe/src/common/AudioClock.h new file mode 100644 index 0000000..3fe20cb --- /dev/null +++ b/src/third_party/oboe/src/common/AudioClock.h @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AUDIO_CLOCK_H +#define OBOE_AUDIO_CLOCK_H + +#include +#include +#include "oboe/Definitions.h" + +namespace oboe { + +// TODO: Move this class into the public headers because it is useful when calculating stream latency +class AudioClock { +public: + static int64_t getNanoseconds(clockid_t clockId = CLOCK_MONOTONIC) { + struct timespec time; + int result = clock_gettime(clockId, &time); + if (result < 0) { + return result; + } + return (time.tv_sec * kNanosPerSecond) + time.tv_nsec; + } + + /** + * Sleep until the specified time. + * + * @param nanoTime time to wake up + * @param clockId CLOCK_MONOTONIC is default + * @return 0 or a negative error, eg. -EINTR + */ + + static int sleepUntilNanoTime(int64_t nanoTime, clockid_t clockId = CLOCK_MONOTONIC) { + struct timespec time; + time.tv_sec = nanoTime / kNanosPerSecond; + time.tv_nsec = nanoTime - (time.tv_sec * kNanosPerSecond); + return 0 - clock_nanosleep(clockId, TIMER_ABSTIME, &time, NULL); + } + + /** + * Sleep for the specified number of nanoseconds in real-time. + * Return immediately with 0 if a negative nanoseconds is specified. + * + * @param nanoseconds time to sleep + * @param clockId CLOCK_REALTIME is default + * @return 0 or a negative error, eg. -EINTR + */ + + static int sleepForNanos(int64_t nanoseconds, clockid_t clockId = CLOCK_REALTIME) { + if (nanoseconds > 0) { + struct timespec time; + time.tv_sec = nanoseconds / kNanosPerSecond; + time.tv_nsec = nanoseconds - (time.tv_sec * kNanosPerSecond); + return 0 - clock_nanosleep(clockId, 0, &time, NULL); + } + return 0; + } +}; + +} // namespace oboe + +#endif //OBOE_AUDIO_CLOCK_H diff --git a/src/third_party/oboe/src/common/AudioSourceCaller.cpp b/src/third_party/oboe/src/common/AudioSourceCaller.cpp new file mode 100644 index 0000000..0180c22 --- /dev/null +++ b/src/third_party/oboe/src/common/AudioSourceCaller.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "AudioSourceCaller.h" + +using namespace oboe; +using namespace flowgraph; + +int32_t AudioSourceCaller::onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) { + oboe::AudioStreamCallback *callback = mStream->getCallback(); + int32_t result = 0; + int32_t numFrames = numBytes / mStream->getBytesPerFrame(); + if (callback != nullptr) { + DataCallbackResult callbackResult = callback->onAudioReady(mStream, buffer, numFrames); + // onAudioReady() does not return the number of bytes processed so we have to assume all. + result = (callbackResult == DataCallbackResult::Continue) + ? numBytes + : -1; + } else { + auto readResult = mStream->read(buffer, numFrames, mTimeoutNanos); + if (!readResult) return (int32_t) readResult.error(); + result = readResult.value() * mStream->getBytesPerFrame(); + } + return result; +} diff --git a/src/third_party/oboe/src/common/AudioSourceCaller.h b/src/third_party/oboe/src/common/AudioSourceCaller.h new file mode 100644 index 0000000..d196d41 --- /dev/null +++ b/src/third_party/oboe/src/common/AudioSourceCaller.h @@ -0,0 +1,83 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AUDIO_SOURCE_CALLER_H +#define OBOE_AUDIO_SOURCE_CALLER_H + +#include "OboeDebug.h" +#include "oboe/Oboe.h" + +#include "flowgraph/FlowGraphNode.h" +#include "FixedBlockReader.h" + +namespace oboe { + +class AudioStreamCallback; +class AudioStream; + +/** + * For output streams that use a callback, call the application for more data. + * For input streams that do not use a callback, read from the stream. + */ +class AudioSourceCaller : public flowgraph::FlowGraphSource, public FixedBlockProcessor { +public: + AudioSourceCaller(int32_t channelCount, int32_t framesPerCallback, int32_t bytesPerSample) + : FlowGraphSource(channelCount) + , mBlockReader(*this) { + mBlockReader.open(channelCount * framesPerCallback * bytesPerSample); + } + + /** + * Set the stream to use as a source of data. + * @param stream + */ + void setStream(oboe::AudioStream *stream) { + mStream = stream; + } + + oboe::AudioStream *getStream() { + return mStream; + } + + /** + * Timeout value to use when calling audioStream->read(). + * @param timeoutNanos Zero for no timeout or time in nanoseconds. + */ + void setTimeoutNanos(int64_t timeoutNanos) { + mTimeoutNanos = timeoutNanos; + } + + int64_t getTimeoutNanos() const { + return mTimeoutNanos; + } + + /** + * Called internally for block size adaptation. + * @param buffer + * @param numBytes + * @return + */ + int32_t onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) override; + +protected: + oboe::AudioStream *mStream = nullptr; + int64_t mTimeoutNanos = 0; + + FixedBlockReader mBlockReader; +}; + +} +#endif //OBOE_AUDIO_SOURCE_CALLER_H diff --git a/src/third_party/oboe/src/common/AudioStream.cpp b/src/third_party/oboe/src/common/AudioStream.cpp new file mode 100644 index 0000000..7bcd087 --- /dev/null +++ b/src/third_party/oboe/src/common/AudioStream.cpp @@ -0,0 +1,211 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include + +#include +#include "OboeDebug.h" +#include "AudioClock.h" +#include + +namespace oboe { + +/* + * AudioStream + */ +AudioStream::AudioStream(const AudioStreamBuilder &builder) + : AudioStreamBase(builder) { +} + +Result AudioStream::close() { + // Update local counters so they can be read after the close. + updateFramesWritten(); + updateFramesRead(); + return Result::OK; +} + +// Call this from fireDataCallback() if you want to monitor CPU scheduler. +void AudioStream::checkScheduler() { + int scheduler = sched_getscheduler(0) & ~SCHED_RESET_ON_FORK; // for current thread + if (scheduler != mPreviousScheduler) { + LOGD("AudioStream::%s() scheduler = %s", __func__, + ((scheduler == SCHED_FIFO) ? "SCHED_FIFO" : + ((scheduler == SCHED_OTHER) ? "SCHED_OTHER" : + ((scheduler == SCHED_RR) ? "SCHED_RR" : "UNKNOWN"))) + ); + mPreviousScheduler = scheduler; + } +} + +DataCallbackResult AudioStream::fireDataCallback(void *audioData, int32_t numFrames) { + if (!isDataCallbackEnabled()) { + LOGW("AudioStream::%s() called with data callback disabled!", __func__); + return DataCallbackResult::Stop; // We should not be getting called any more. + } + + DataCallbackResult result; + if (mStreamCallback == nullptr) { + result = onDefaultCallback(audioData, numFrames); + } else { + result = mStreamCallback->onAudioReady(this, audioData, numFrames); + } + // On Oreo, we might get called after returning stop. + // So block that here. + setDataCallbackEnabled(result == DataCallbackResult::Continue); + + return result; +} + +Result AudioStream::waitForStateTransition(StreamState startingState, + StreamState endingState, + int64_t timeoutNanoseconds) +{ + StreamState state; + { + std::lock_guard lock(mLock); + state = getState(); + if (state == StreamState::Closed) { + return Result::ErrorClosed; + } else if (state == StreamState::Disconnected) { + return Result::ErrorDisconnected; + } + } + + StreamState nextState = state; + // TODO Should this be a while()?! + if (state == startingState && state != endingState) { + Result result = waitForStateChange(state, &nextState, timeoutNanoseconds); + if (result != Result::OK) { + return result; + } + } + + if (nextState != endingState) { + return Result::ErrorInvalidState; + } else { + return Result::OK; + } +} + +Result AudioStream::start(int64_t timeoutNanoseconds) +{ + Result result = requestStart(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + return waitForStateTransition(StreamState::Starting, + StreamState::Started, timeoutNanoseconds); +} + +Result AudioStream::pause(int64_t timeoutNanoseconds) +{ + Result result = requestPause(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + return waitForStateTransition(StreamState::Pausing, + StreamState::Paused, timeoutNanoseconds); +} + +Result AudioStream::flush(int64_t timeoutNanoseconds) +{ + Result result = requestFlush(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + return waitForStateTransition(StreamState::Flushing, + StreamState::Flushed, timeoutNanoseconds); +} + +Result AudioStream::stop(int64_t timeoutNanoseconds) +{ + Result result = requestStop(); + if (result != Result::OK) return result; + if (timeoutNanoseconds <= 0) return result; + return waitForStateTransition(StreamState::Stopping, + StreamState::Stopped, timeoutNanoseconds); +} + +int32_t AudioStream::getBytesPerSample() const { + return convertFormatToSizeInBytes(mFormat); +} + +int64_t AudioStream::getFramesRead() { + updateFramesRead(); + return mFramesRead; +} + +int64_t AudioStream::getFramesWritten() { + updateFramesWritten(); + return mFramesWritten; +} + +ResultWithValue AudioStream::getAvailableFrames() { + int64_t readCounter = getFramesRead(); + if (readCounter < 0) return ResultWithValue::createBasedOnSign(readCounter); + int64_t writeCounter = getFramesWritten(); + if (writeCounter < 0) return ResultWithValue::createBasedOnSign(writeCounter); + int32_t framesAvailable = writeCounter - readCounter; + return ResultWithValue(framesAvailable); +} + +ResultWithValue AudioStream::waitForAvailableFrames(int32_t numFrames, + int64_t timeoutNanoseconds) { + if (numFrames == 0) return Result::OK; + if (numFrames < 0) return Result::ErrorOutOfRange; + + int64_t framesAvailable = 0; + int64_t burstInNanos = getFramesPerBurst() * kNanosPerSecond / getSampleRate(); + bool ready = false; + int64_t deadline = AudioClock::getNanoseconds() + timeoutNanoseconds; + do { + ResultWithValue result = getAvailableFrames(); + if (!result) return result; + framesAvailable = result.value(); + ready = (framesAvailable >= numFrames); + if (!ready) { + int64_t now = AudioClock::getNanoseconds(); + if (now > deadline) break; + AudioClock::sleepForNanos(burstInNanos); + } + } while (!ready); + return (!ready) + ? ResultWithValue(Result::ErrorTimeout) + : ResultWithValue(framesAvailable); +} + +ResultWithValue AudioStream::getTimestamp(clockid_t clockId) { + FrameTimestamp frame; + Result result = getTimestamp(clockId, &frame.position, &frame.timestamp); + if (result == Result::OK){ + return ResultWithValue(frame); + } else { + return ResultWithValue(static_cast(result)); + } +} + +static void oboe_stop_thread_proc(AudioStream *oboeStream) { + if (oboeStream != nullptr) { + oboeStream->requestStop(); + } +} + +void AudioStream::launchStopThread() { + // Stop this stream on a separate thread + std::thread t(oboe_stop_thread_proc, this); + t.detach(); +} + +} // namespace oboe diff --git a/src/third_party/oboe/src/common/AudioStreamBuilder.cpp b/src/third_party/oboe/src/common/AudioStreamBuilder.cpp new file mode 100644 index 0000000..9f1e31b --- /dev/null +++ b/src/third_party/oboe/src/common/AudioStreamBuilder.cpp @@ -0,0 +1,201 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "aaudio/AudioStreamAAudio.h" +#include "FilterAudioStream.h" +#include "OboeDebug.h" +#include "oboe/Oboe.h" +#include "oboe/AudioStreamBuilder.h" +#include "opensles/AudioInputStreamOpenSLES.h" +#include "opensles/AudioOutputStreamOpenSLES.h" +#include "opensles/AudioStreamOpenSLES.h" +#include "QuirksManager.h" + +bool oboe::OboeGlobals::mWorkaroundsEnabled = true; + +namespace oboe { + +/** + * The following default values are used when oboe does not have any better way of determining the optimal values + * for an audio stream. This can happen when: + * + * - Client is creating a stream on API < 26 (OpenSLES) but has not supplied the optimal sample + * rate and/or frames per burst + * - Client is creating a stream on API 16 (OpenSLES) where AudioManager.PROPERTY_OUTPUT_* values + * are not available + */ +int32_t DefaultStreamValues::SampleRate = 48000; // Common rate for mobile audio and video +int32_t DefaultStreamValues::FramesPerBurst = 192; // 4 msec at 48000 Hz +int32_t DefaultStreamValues::ChannelCount = 2; // Stereo + +constexpr int kBufferSizeInBurstsForLowLatencyStreams = 2; + +#ifndef OBOE_ENABLE_AAUDIO +// Set OBOE_ENABLE_AAUDIO to 0 if you want to disable the AAudio API. +// This might be useful if you want to force all the unit tests to use OpenSL ES. +#define OBOE_ENABLE_AAUDIO 1 +#endif + +bool AudioStreamBuilder::isAAudioSupported() { + return AudioStreamAAudio::isSupported() && OBOE_ENABLE_AAUDIO; +} + +bool AudioStreamBuilder::isAAudioRecommended() { + // See https://github.com/google/oboe/issues/40, + // AAudio may not be stable on Android O, depending on how it is used. + // To be safe, use AAudio only on O_MR1 and above. + return (getSdkVersion() >= __ANDROID_API_O_MR1__) && isAAudioSupported(); +} + +AudioStream *AudioStreamBuilder::build() { + AudioStream *stream = nullptr; + if (isAAudioRecommended() && mAudioApi != AudioApi::OpenSLES) { + stream = new AudioStreamAAudio(*this); + } else if (isAAudioSupported() && mAudioApi == AudioApi::AAudio) { + stream = new AudioStreamAAudio(*this); + LOGE("Creating AAudio stream on 8.0 because it was specified. This is error prone."); + } else { + if (getDirection() == oboe::Direction::Output) { + stream = new AudioOutputStreamOpenSLES(*this); + } else if (getDirection() == oboe::Direction::Input) { + stream = new AudioInputStreamOpenSLES(*this); + } + } + return stream; +} + +bool AudioStreamBuilder::isCompatible(AudioStreamBase &other) { + return getSampleRate() == other.getSampleRate() + && getFormat() == other.getFormat() + && getChannelCount() == other.getChannelCount(); +} + +Result AudioStreamBuilder::openStream(AudioStream **streamPP) { + Result result = Result::OK; + LOGI("%s() %s -------- %s --------", + __func__, getDirection() == Direction::Input ? "INPUT" : "OUTPUT", getVersionText()); + + if (streamPP == nullptr) { + return Result::ErrorNull; + } + *streamPP = nullptr; + + AudioStream *streamP = nullptr; + + // Maybe make a FilterInputStream. + AudioStreamBuilder childBuilder(*this); + // Check need for conversion and modify childBuilder for optimal stream. + bool conversionNeeded = QuirksManager::getInstance().isConversionNeeded(*this, childBuilder); + // Do we need to make a child stream and convert. + if (conversionNeeded) { + AudioStream *tempStream; + + result = childBuilder.openStream(&tempStream); + if (result != Result::OK) { + return result; + } + + if (isCompatible(*tempStream)) { + // Everything matches so we can just use the child stream directly. + *streamPP = tempStream; + return result; + } else { + AudioStreamBuilder parentBuilder = *this; + // Build a stream that is as close as possible to the childStream. + if (getFormat() == oboe::AudioFormat::Unspecified) { + parentBuilder.setFormat(tempStream->getFormat()); + } + if (getChannelCount() == oboe::Unspecified) { + parentBuilder.setChannelCount(tempStream->getChannelCount()); + } + if (getSampleRate() == oboe::Unspecified) { + parentBuilder.setSampleRate(tempStream->getSampleRate()); + } + + // Use childStream in a FilterAudioStream. + LOGI("%s() create a FilterAudioStream for data conversion.", __func__); + FilterAudioStream *filterStream = new FilterAudioStream(parentBuilder, tempStream); + result = filterStream->configureFlowGraph(); + if (result != Result::OK) { + filterStream->close(); + delete filterStream; + // Just open streamP the old way. + } else { + streamP = static_cast(filterStream); + } + } + } + + if (streamP == nullptr) { + streamP = build(); + if (streamP == nullptr) { + return Result::ErrorNull; + } + } + + result = streamP->open(); // TODO review API + if (result == Result::OK) { + + int32_t optimalBufferSize = -1; + // Use a reasonable default buffer size. + if (streamP->getDirection() == Direction::Input) { + // For input, small size does not improve latency because the stream is usually + // run close to empty. And a low size can result in XRuns so always use the maximum. + optimalBufferSize = streamP->getBufferCapacityInFrames(); + } else if (streamP->getPerformanceMode() == PerformanceMode::LowLatency + && streamP->getDirection() == Direction::Output) { // Output check is redundant. + optimalBufferSize = streamP->getFramesPerBurst() * + kBufferSizeInBurstsForLowLatencyStreams; + } + if (optimalBufferSize >= 0) { + auto setBufferResult = streamP->setBufferSizeInFrames(optimalBufferSize); + if (!setBufferResult) { + LOGW("Failed to setBufferSizeInFrames(%d). Error was %s", + optimalBufferSize, + convertToText(setBufferResult.error())); + } + } + + *streamPP = streamP; + } else { + delete streamP; + } + return result; +} + +Result AudioStreamBuilder::openManagedStream(oboe::ManagedStream &stream) { + stream.reset(); + AudioStream *streamptr; + auto result = openStream(&streamptr); + stream.reset(streamptr); + return result; +} + +Result AudioStreamBuilder::openStream(std::shared_ptr &sharedStream) { + sharedStream.reset(); + AudioStream *streamptr; + auto result = openStream(&streamptr); + if (result == Result::OK) { + sharedStream.reset(streamptr); + // Save a weak_ptr in the stream for use with callbacks. + streamptr->setWeakThis(sharedStream); + } + return result; +} + +} // namespace oboe diff --git a/src/third_party/oboe/src/common/DataConversionFlowGraph.cpp b/src/third_party/oboe/src/common/DataConversionFlowGraph.cpp new file mode 100644 index 0000000..c15e92d --- /dev/null +++ b/src/third_party/oboe/src/common/DataConversionFlowGraph.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "OboeDebug.h" +#include "DataConversionFlowGraph.h" +#include "SourceFloatCaller.h" +#include "SourceI16Caller.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace oboe; +using namespace flowgraph; +using namespace resampler; + +void DataConversionFlowGraph::setSource(const void *buffer, int32_t numFrames) { + mSource->setData(buffer, numFrames); +} + +static MultiChannelResampler::Quality convertOboeSRQualityToMCR(SampleRateConversionQuality quality) { + switch (quality) { + case SampleRateConversionQuality::Fastest: + return MultiChannelResampler::Quality::Fastest; + case SampleRateConversionQuality::Low: + return MultiChannelResampler::Quality::Low; + default: + case SampleRateConversionQuality::Medium: + return MultiChannelResampler::Quality::Medium; + case SampleRateConversionQuality::High: + return MultiChannelResampler::Quality::High; + case SampleRateConversionQuality::Best: + return MultiChannelResampler::Quality::Best; + } +} + +// Chain together multiple processors. +// Callback Output +// Use SourceCaller that calls original app callback from the flowgraph. +// The child callback from FilteredAudioStream read()s from the flowgraph. +// Callback Input +// Child callback from FilteredAudioStream writes()s to the flowgraph. +// The output of the flowgraph goes through a BlockWriter to the app callback. +// Blocking Write +// Write buffer is set on an AudioSource. +// Data is pulled through the graph and written to the child stream. +// Blocking Read +// Reads in a loop from the flowgraph Sink to fill the read buffer. +// A SourceCaller then does a blocking read from the child Stream. +// +Result DataConversionFlowGraph::configure(AudioStream *sourceStream, AudioStream *sinkStream) { + + FlowGraphPortFloatOutput *lastOutput = nullptr; + + bool isOutput = sourceStream->getDirection() == Direction::Output; + bool isInput = !isOutput; + mFilterStream = isOutput ? sourceStream : sinkStream; + + AudioFormat sourceFormat = sourceStream->getFormat(); + int32_t sourceChannelCount = sourceStream->getChannelCount(); + int32_t sourceSampleRate = sourceStream->getSampleRate(); + + AudioFormat sinkFormat = sinkStream->getFormat(); + int32_t sinkChannelCount = sinkStream->getChannelCount(); + int32_t sinkSampleRate = sinkStream->getSampleRate(); + + LOGI("%s() flowgraph converts channels: %d to %d, format: %d to %d, rate: %d to %d, qual = %d", + __func__, + sourceChannelCount, sinkChannelCount, + sourceFormat, sinkFormat, + sourceSampleRate, sinkSampleRate, + sourceStream->getSampleRateConversionQuality()); + + int32_t framesPerCallback = (sourceStream->getFramesPerCallback() == kUnspecified) + ? sourceStream->getFramesPerBurst() + : sourceStream->getFramesPerCallback(); + // Source + // If OUTPUT and using a callback then call back to the app using a SourceCaller. + // If INPUT and NOT using a callback then read from the child stream using a SourceCaller. + if ((sourceStream->getCallback() != nullptr && isOutput) + || (sourceStream->getCallback() == nullptr && isInput)) { + switch (sourceFormat) { + case AudioFormat::Float: + mSourceCaller = std::make_unique(sourceChannelCount, + framesPerCallback); + break; + case AudioFormat::I16: + mSourceCaller = std::make_unique(sourceChannelCount, + framesPerCallback); + break; + default: + LOGE("%s() Unsupported source caller format = %d", __func__, sourceFormat); + return Result::ErrorIllegalArgument; + } + mSourceCaller->setStream(sourceStream); + lastOutput = &mSourceCaller->output; + } else { + // If OUTPUT and NOT using a callback then write to the child stream using a BlockWriter. + // If INPUT and using a callback then write to the app using a BlockWriter. + switch (sourceFormat) { + case AudioFormat::Float: + mSource = std::make_unique(sourceChannelCount); + break; + case AudioFormat::I16: + mSource = std::make_unique(sourceChannelCount); + break; + default: + LOGE("%s() Unsupported source format = %d", __func__, sourceFormat); + return Result::ErrorIllegalArgument; + } + if (isInput) { + // The BlockWriter is after the Sink so use the SinkStream size. + mBlockWriter.open(framesPerCallback * sinkStream->getBytesPerFrame()); + mAppBuffer = std::make_unique( + kDefaultBufferSize * sinkStream->getBytesPerFrame()); + } + lastOutput = &mSource->output; + } + + // Sample Rate conversion + if (sourceSampleRate != sinkSampleRate) { + mResampler.reset(MultiChannelResampler::make(sourceChannelCount, + sourceSampleRate, + sinkSampleRate, + convertOboeSRQualityToMCR( + sourceStream->getSampleRateConversionQuality()))); + mRateConverter = std::make_unique(sourceChannelCount, + *mResampler.get()); + lastOutput->connect(&mRateConverter->input); + lastOutput = &mRateConverter->output; + } + + // Expand the number of channels if required. + if (sourceChannelCount == 1 && sinkChannelCount > 1) { + mChannelConverter = std::make_unique(sinkChannelCount); + lastOutput->connect(&mChannelConverter->input); + lastOutput = &mChannelConverter->output; + } else if (sourceChannelCount != sinkChannelCount) { + LOGW("%s() Channel reduction not supported.", __func__); + return Result::ErrorUnimplemented; // TODO + } + + // Sink + switch (sinkFormat) { + case AudioFormat::Float: + mSink = std::make_unique(sinkChannelCount); + break; + case AudioFormat::I16: + mSink = std::make_unique(sinkChannelCount); + break; + default: + LOGE("%s() Unsupported sink format = %d", __func__, sinkFormat); + return Result::ErrorIllegalArgument;; + } + lastOutput->connect(&mSink->input); + + mFramePosition = 0; + + return Result::OK; +} + +int32_t DataConversionFlowGraph::read(void *buffer, int32_t numFrames, int64_t timeoutNanos) { + if (mSourceCaller) { + mSourceCaller->setTimeoutNanos(timeoutNanos); + } + int32_t numRead = mSink->read(mFramePosition, buffer, numFrames); + mFramePosition += numRead; + return numRead; +} + +// This is similar to pushing data through the flowgraph. +int32_t DataConversionFlowGraph::write(void *inputBuffer, int32_t numFrames) { + // Put the data from the input at the head of the flowgraph. + mSource->setData(inputBuffer, numFrames); + while (true) { + // Pull and read some data in app format into a small buffer. + int32_t framesRead = mSink->read(mFramePosition, mAppBuffer.get(), flowgraph::kDefaultBufferSize); + mFramePosition += framesRead; + if (framesRead <= 0) break; + // Write to a block adapter, which will call the destination whenever it has enough data. + int32_t bytesRead = mBlockWriter.write(mAppBuffer.get(), + framesRead * mFilterStream->getBytesPerFrame()); + if (bytesRead < 0) return bytesRead; // TODO review + } + return numFrames; +} + +int32_t DataConversionFlowGraph::onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) { + int32_t numFrames = numBytes / mFilterStream->getBytesPerFrame(); + mCallbackResult = mFilterStream->getCallback()->onAudioReady(mFilterStream, buffer, numFrames); + // TODO handle STOP from callback, process data remaining in the block adapter + return numBytes; +} \ No newline at end of file diff --git a/src/third_party/oboe/src/common/DataConversionFlowGraph.h b/src/third_party/oboe/src/common/DataConversionFlowGraph.h new file mode 100644 index 0000000..b1d5ebe --- /dev/null +++ b/src/third_party/oboe/src/common/DataConversionFlowGraph.h @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OBOE_FLOW_GRAPH_H +#define OBOE_OBOE_FLOW_GRAPH_H + +#include +#include +#include + +#include +#include +#include +#include "AudioSourceCaller.h" +#include "FixedBlockWriter.h" + +namespace oboe { + +class AudioStream; +class AudioSourceCaller; + +/** + * Convert PCM channels, format and sample rate for optimal latency. + */ +class DataConversionFlowGraph : public FixedBlockProcessor { +public: + + DataConversionFlowGraph() + : mBlockWriter(*this) {} + + void setSource(const void *buffer, int32_t numFrames); + + /** Connect several modules together to convert from source to sink. + * This should only be called once for each instance. + * + * @param sourceFormat + * @param sourceChannelCount + * @param sinkFormat + * @param sinkChannelCount + * @return + */ + oboe::Result configure(oboe::AudioStream *sourceStream, oboe::AudioStream *sinkStream); + + int32_t read(void *buffer, int32_t numFrames, int64_t timeoutNanos); + + int32_t write(void *buffer, int32_t numFrames); + + int32_t onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) override; + + DataCallbackResult getDataCallbackResult() { + return mCallbackResult; + } + +private: + std::unique_ptr mSource; + std::unique_ptr mSourceCaller; + std::unique_ptr mChannelConverter; + std::unique_ptr mResampler; + std::unique_ptr mRateConverter; + std::unique_ptr mSink; + + FixedBlockWriter mBlockWriter; + DataCallbackResult mCallbackResult = DataCallbackResult::Continue; + AudioStream *mFilterStream = nullptr; + std::unique_ptr mAppBuffer; + + int64_t mFramePosition = 0; +}; + +} +#endif //OBOE_OBOE_FLOW_GRAPH_H diff --git a/src/third_party/oboe/src/common/FilterAudioStream.cpp b/src/third_party/oboe/src/common/FilterAudioStream.cpp new file mode 100644 index 0000000..a471583 --- /dev/null +++ b/src/third_party/oboe/src/common/FilterAudioStream.cpp @@ -0,0 +1,92 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "FilterAudioStream.h" + +using namespace oboe; +using namespace flowgraph; + +// Output callback uses FixedBlockReader::read() +// <= SourceFloatCaller::onProcess() +// <=== DataConversionFlowGraph::read() +// <== FilterAudioStream::onAudioReady() +// +// Output blocking uses no block adapter because AAudio can accept +// writes of any size. It uses DataConversionFlowGraph::read() <== FilterAudioStream::write() <= app +// +// Input callback uses FixedBlockWriter::write() +// <= DataConversionFlowGraph::write() +// <= FilterAudioStream::onAudioReady() +// +// Input blocking uses FixedBlockReader::read() // TODO may not need block adapter +// <= SourceFloatCaller::onProcess() +// <=== SinkFloat::read() +// <= DataConversionFlowGraph::read() +// <== FilterAudioStream::read() +// <= app + +Result FilterAudioStream::configureFlowGraph() { + mFlowGraph = std::make_unique(); + bool isOutput = getDirection() == Direction::Output; + + AudioStream *sourceStream = isOutput ? this : mChildStream.get(); + AudioStream *sinkStream = isOutput ? mChildStream.get() : this; + + mRateScaler = ((double) sourceStream->getSampleRate()) / sinkStream->getSampleRate(); + + return mFlowGraph->configure(sourceStream, sinkStream); +} + +// Put the data to be written at the source end of the flowgraph. +// Then read (pull) the data from the flowgraph and write it to the +// child stream. +ResultWithValue FilterAudioStream::write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + int32_t framesWritten = 0; + mFlowGraph->setSource(buffer, numFrames); + while (true) { + int32_t numRead = mFlowGraph->read(mBlockingBuffer.get(), + getFramesPerBurst(), + timeoutNanoseconds); + if (numRead < 0) { + return ResultWithValue::createBasedOnSign(numRead); + } + if (numRead == 0) { + break; // finished processing the source buffer + } + auto writeResult = mChildStream->write(mBlockingBuffer.get(), + numRead, + timeoutNanoseconds); + if (!writeResult) { + return writeResult; + } + framesWritten += writeResult.value(); + } + return ResultWithValue::createBasedOnSign(framesWritten); +} + +// Read (pull) the data we want from the sink end of the flowgraph. +// The necessary data will be read from the child stream using a flowgraph callback. +ResultWithValue FilterAudioStream::read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + int32_t framesRead = mFlowGraph->read(buffer, numFrames, timeoutNanoseconds); + return ResultWithValue::createBasedOnSign(framesRead); +} + diff --git a/src/third_party/oboe/src/common/FilterAudioStream.h b/src/third_party/oboe/src/common/FilterAudioStream.h new file mode 100644 index 0000000..3949de7 --- /dev/null +++ b/src/third_party/oboe/src/common/FilterAudioStream.h @@ -0,0 +1,214 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_FILTER_AUDIO_STREAM_H +#define OBOE_FILTER_AUDIO_STREAM_H + +#include +#include +#include "DataConversionFlowGraph.h" + +namespace oboe { + +/** + * An AudioStream that wraps another AudioStream and provides audio data conversion. + * Operations may include channel conversion, data format conversion and/or sample rate conversion. + */ +class FilterAudioStream : public AudioStream, AudioStreamCallback { +public: + + /** + * Construct an `AudioStream` using the given `AudioStreamBuilder` and a child AudioStream. + * + * This should only be called internally by AudioStreamBuilder. + * Ownership of childStream will be passed to this object. + * + * @param builder containing all the stream's attributes + */ + FilterAudioStream(const AudioStreamBuilder &builder, AudioStream *childStream) + : AudioStream(builder) + , mChildStream(childStream) { + // Intercept the callback if used. + if (builder.getCallback() != nullptr) { + mStreamCallback = mChildStream->swapCallback(this); + } else { + const int size = childStream->getFramesPerBurst() * childStream->getBytesPerFrame(); + mBlockingBuffer = std::make_unique(size); + } + + // Copy parameters that may not match builder. + mBufferCapacityInFrames = mChildStream->getBufferCapacityInFrames(); + mPerformanceMode = mChildStream->getPerformanceMode(); + } + + virtual ~FilterAudioStream() = default; + + AudioStream *getChildStream() const { + return mChildStream.get(); + } + + Result configureFlowGraph(); + + // Close child and parent. + Result close() override { + const Result result1 = mChildStream->close(); + const Result result2 = AudioStream::close(); + return (result1 != Result::OK ? result1 : result2); + } + + /** + * Start the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `start(0)`. + */ + Result requestStart() override { + return mChildStream->requestStart(); + } + + /** + * Pause the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `pause(0)`. + */ + Result requestPause() override { + return mChildStream->requestPause(); + } + + /** + * Flush the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `flush(0)`. + */ + Result requestFlush() override { + return mChildStream->requestFlush(); + } + + /** + * Stop the stream asynchronously. Returns immediately (does not block). Equivalent to calling + * `stop(0)`. + */ + Result requestStop() override { + return mChildStream->requestStop(); + } + + ResultWithValue read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + StreamState getState() const override { + return mChildStream->getState(); + } + + Result waitForStateChange( + StreamState inputState, + StreamState *nextState, + int64_t timeoutNanoseconds) override { + return mChildStream->waitForStateChange(inputState, nextState, timeoutNanoseconds); + } + + bool isXRunCountSupported() const override { + return mChildStream->isXRunCountSupported(); + } + + int32_t getFramesPerBurst() override { + return mChildStream->getFramesPerBurst(); + } + + AudioApi getAudioApi() const override { + return mChildStream->getAudioApi(); + } + + void updateFramesWritten() override { + // TODO for output, just count local writes? + mFramesWritten = static_cast(mChildStream->getFramesWritten() * mRateScaler); + } + + void updateFramesRead() override { + // TODO for input, just count local reads? + mFramesRead = static_cast(mChildStream->getFramesRead() * mRateScaler); + } + + void *getUnderlyingStream() const override { + return mChildStream->getUnderlyingStream(); + } + + ResultWithValue setBufferSizeInFrames(int32_t requestedFrames) override { + return mChildStream->setBufferSizeInFrames(requestedFrames); + } + + int32_t getBufferSizeInFrames() override { + mBufferSizeInFrames = mChildStream->getBufferSizeInFrames(); + return mBufferSizeInFrames; + } + + ResultWithValue getXRunCount() const override { + return mChildStream->getXRunCount(); + } + + ResultWithValue calculateLatencyMillis() override { + // This will automatically include the latency of the flowgraph? + return mChildStream->calculateLatencyMillis(); + } + + Result getTimestamp(clockid_t clockId, + int64_t *framePosition, + int64_t *timeNanoseconds) override { + int64_t childPosition = 0; + Result result = mChildStream->getTimestamp(clockId, &childPosition, timeNanoseconds); + *framePosition = childPosition * mRateScaler; + return result; + } + + DataCallbackResult onAudioReady(AudioStream *oboeStream, + void *audioData, + int32_t numFrames) override { + int32_t framesProcessed; + if (oboeStream->getDirection() == Direction::Output) { + framesProcessed = mFlowGraph->read(audioData, numFrames, 0 /* timeout */); + } else { + framesProcessed = mFlowGraph->write(audioData, numFrames); + } + return (framesProcessed < numFrames) + ? DataCallbackResult::Stop + : mFlowGraph->getDataCallbackResult(); + } + + void onErrorBeforeClose(AudioStream *oboeStream, Result error) override { + if (mStreamCallback != nullptr) { + mStreamCallback->onErrorBeforeClose(this, error); + } + } + + void onErrorAfterClose(AudioStream *oboeStream, Result error) override { + // Close this parent stream because the callback will only close the child. + AudioStream::close(); + if (mStreamCallback != nullptr) { + mStreamCallback->onErrorAfterClose(this, error); + } + } + +private: + + std::unique_ptr mChildStream; // this stream wraps the child stream + std::unique_ptr mFlowGraph; // for converting data + std::unique_ptr mBlockingBuffer; // temp buffer for write() + double mRateScaler = 1.0; // ratio parent/child sample rates +}; + +} // oboe + +#endif //OBOE_FILTER_AUDIO_STREAM_H diff --git a/src/third_party/oboe/src/common/FixedBlockAdapter.cpp b/src/third_party/oboe/src/common/FixedBlockAdapter.cpp new file mode 100644 index 0000000..e14c2ca --- /dev/null +++ b/src/third_party/oboe/src/common/FixedBlockAdapter.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "FixedBlockAdapter.h" + +FixedBlockAdapter::~FixedBlockAdapter() { +} + +int32_t FixedBlockAdapter::open(int32_t bytesPerFixedBlock) +{ + mSize = bytesPerFixedBlock; + mStorage = std::make_unique(bytesPerFixedBlock); + mPosition = 0; + return 0; +} + +int32_t FixedBlockAdapter::close() +{ + mStorage.reset(nullptr); + mSize = 0; + mPosition = 0; + return 0; +} diff --git a/src/third_party/oboe/src/common/FixedBlockAdapter.h b/src/third_party/oboe/src/common/FixedBlockAdapter.h new file mode 100644 index 0000000..76e961c --- /dev/null +++ b/src/third_party/oboe/src/common/FixedBlockAdapter.h @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAUDIO_FIXED_BLOCK_ADAPTER_H +#define AAUDIO_FIXED_BLOCK_ADAPTER_H + +#include +#include +#include + +/** + * Interface for a class that needs fixed-size blocks. + */ +class FixedBlockProcessor { +public: + virtual ~FixedBlockProcessor() = default; + /** + * + * @param buffer Pointer to first byte of data. + * @param numBytes This will be a fixed size specified in FixedBlockAdapter::open(). + * @return Number of bytes processed or a negative error code. + */ + virtual int32_t onProcessFixedBlock(uint8_t *buffer, int32_t numBytes) = 0; +}; + +/** + * Base class for a variable-to-fixed-size block adapter. + */ +class FixedBlockAdapter +{ +public: + FixedBlockAdapter(FixedBlockProcessor &fixedBlockProcessor) + : mFixedBlockProcessor(fixedBlockProcessor) {} + + virtual ~FixedBlockAdapter(); + + /** + * Allocate internal resources needed for buffering data. + */ + virtual int32_t open(int32_t bytesPerFixedBlock); + + /** + * Free internal resources. + */ + int32_t close(); + +protected: + FixedBlockProcessor &mFixedBlockProcessor; + std::unique_ptr mStorage; // Store data here while assembling buffers. + int32_t mSize = 0; // Size in bytes of the fixed size buffer. + int32_t mPosition = 0; // Offset of the last byte read or written. +}; + +#endif /* AAUDIO_FIXED_BLOCK_ADAPTER_H */ diff --git a/src/third_party/oboe/src/common/FixedBlockReader.cpp b/src/third_party/oboe/src/common/FixedBlockReader.cpp new file mode 100644 index 0000000..62e9664 --- /dev/null +++ b/src/third_party/oboe/src/common/FixedBlockReader.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "FixedBlockAdapter.h" + +#include "FixedBlockReader.h" + + +FixedBlockReader::FixedBlockReader(FixedBlockProcessor &fixedBlockProcessor) + : FixedBlockAdapter(fixedBlockProcessor) { + mPosition = mSize; +} + +int32_t FixedBlockReader::open(int32_t bytesPerFixedBlock) { + int32_t result = FixedBlockAdapter::open(bytesPerFixedBlock); + mPosition = 0; + mValid = 0; + return result; +} + +int32_t FixedBlockReader::readFromStorage(uint8_t *buffer, int32_t numBytes) { + int32_t bytesToRead = numBytes; + int32_t dataAvailable = mValid - mPosition; + if (bytesToRead > dataAvailable) { + bytesToRead = dataAvailable; + } + memcpy(buffer, mStorage.get() + mPosition, bytesToRead); + mPosition += bytesToRead; + return bytesToRead; +} + +int32_t FixedBlockReader::read(uint8_t *buffer, int32_t numBytes) { + int32_t bytesRead; + int32_t bytesLeft = numBytes; + while(bytesLeft > 0) { + if (mPosition < mValid) { + // Use up bytes currently in storage. + bytesRead = readFromStorage(buffer, bytesLeft); + buffer += bytesRead; + bytesLeft -= bytesRead; + } else if (bytesLeft >= mSize) { + // Nothing in storage. Read through if enough for a complete block. + bytesRead = mFixedBlockProcessor.onProcessFixedBlock(buffer, mSize); + if (bytesRead < 0) return bytesRead; + buffer += bytesRead; + bytesLeft -= bytesRead; + } else { + // Just need a partial block so we have to reload storage. + bytesRead = mFixedBlockProcessor.onProcessFixedBlock(mStorage.get(), mSize); + if (bytesRead < 0) return bytesRead; + mPosition = 0; + mValid = bytesRead; + if (bytesRead == 0) break; + } + } + return numBytes - bytesLeft; +} diff --git a/src/third_party/oboe/src/common/FixedBlockReader.h b/src/third_party/oboe/src/common/FixedBlockReader.h new file mode 100644 index 0000000..4cea9c8 --- /dev/null +++ b/src/third_party/oboe/src/common/FixedBlockReader.h @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAUDIO_FIXED_BLOCK_READER_H +#define AAUDIO_FIXED_BLOCK_READER_H + +#include + +#include "FixedBlockAdapter.h" + +/** + * Read from a fixed-size block to a variable sized block. + * + * This can be used to convert a pull data flow from fixed sized buffers to variable sized buffers. + * An example would be an audio output callback that reads from the app. + */ +class FixedBlockReader : public FixedBlockAdapter +{ +public: + FixedBlockReader(FixedBlockProcessor &fixedBlockProcessor); + + virtual ~FixedBlockReader() = default; + + int32_t open(int32_t bytesPerFixedBlock) override; + + /** + * Read into a variable sized block. + * + * Note that if the fixed-sized blocks must be aligned, then the variable-sized blocks + * must have the same alignment. + * For example, if the fixed-size blocks must be a multiple of 8, then the variable-sized + * blocks must also be a multiple of 8. + * + * @param buffer + * @param numBytes + * @return Number of bytes read or a negative error code. + */ + int32_t read(uint8_t *buffer, int32_t numBytes); + +private: + int32_t readFromStorage(uint8_t *buffer, int32_t numBytes); + + int32_t mValid = 0; // Number of valid bytes in mStorage. +}; + + +#endif /* AAUDIO_FIXED_BLOCK_READER_H */ diff --git a/src/third_party/oboe/src/common/FixedBlockWriter.cpp b/src/third_party/oboe/src/common/FixedBlockWriter.cpp new file mode 100644 index 0000000..b315609 --- /dev/null +++ b/src/third_party/oboe/src/common/FixedBlockWriter.cpp @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "FixedBlockAdapter.h" +#include "FixedBlockWriter.h" + +FixedBlockWriter::FixedBlockWriter(FixedBlockProcessor &fixedBlockProcessor) + : FixedBlockAdapter(fixedBlockProcessor) {} + + +int32_t FixedBlockWriter::writeToStorage(uint8_t *buffer, int32_t numBytes) { + int32_t bytesToStore = numBytes; + int32_t roomAvailable = mSize - mPosition; + if (bytesToStore > roomAvailable) { + bytesToStore = roomAvailable; + } + memcpy(mStorage.get() + mPosition, buffer, bytesToStore); + mPosition += bytesToStore; + return bytesToStore; +} + +int32_t FixedBlockWriter::write(uint8_t *buffer, int32_t numBytes) { + int32_t bytesLeft = numBytes; + + // If we already have data in storage then add to it. + if (mPosition > 0) { + int32_t bytesWritten = writeToStorage(buffer, bytesLeft); + buffer += bytesWritten; + bytesLeft -= bytesWritten; + // If storage full then flush it out + if (mPosition == mSize) { + bytesWritten = mFixedBlockProcessor.onProcessFixedBlock(mStorage.get(), mSize); + if (bytesWritten < 0) return bytesWritten; + mPosition = 0; + if (bytesWritten < mSize) { + // Only some of the data was written! This should not happen. + return -1; + } + } + } + + // Write through if enough for a complete block. + while(bytesLeft > mSize) { + int32_t bytesWritten = mFixedBlockProcessor.onProcessFixedBlock(buffer, mSize); + if (bytesWritten < 0) return bytesWritten; + buffer += bytesWritten; + bytesLeft -= bytesWritten; + } + + // Save any remaining partial blocks for next time. + if (bytesLeft > 0) { + int32_t bytesWritten = writeToStorage(buffer, bytesLeft); + bytesLeft -= bytesWritten; + } + + return numBytes - bytesLeft; +} diff --git a/src/third_party/oboe/src/common/FixedBlockWriter.h b/src/third_party/oboe/src/common/FixedBlockWriter.h new file mode 100644 index 0000000..6dea6ca --- /dev/null +++ b/src/third_party/oboe/src/common/FixedBlockWriter.h @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AAUDIO_FIXED_BLOCK_WRITER_H +#define AAUDIO_FIXED_BLOCK_WRITER_H + +#include + +#include "FixedBlockAdapter.h" + +/** + * This can be used to convert a push data flow from variable sized buffers to fixed sized buffers. + * An example would be an audio input callback. + */ +class FixedBlockWriter : public FixedBlockAdapter +{ +public: + FixedBlockWriter(FixedBlockProcessor &fixedBlockProcessor); + + virtual ~FixedBlockWriter() = default; + + /** + * Write from a variable sized block. + * + * Note that if the fixed-sized blocks must be aligned, then the variable-sized blocks + * must have the same alignment. + * For example, if the fixed-size blocks must be a multiple of 8, then the variable-sized + * blocks must also be a multiple of 8. + * + * @param buffer + * @param numBytes + * @return Number of bytes written or a negative error code. + */ + int32_t write(uint8_t *buffer, int32_t numBytes); + +private: + + int32_t writeToStorage(uint8_t *buffer, int32_t numBytes); +}; + +#endif /* AAUDIO_FIXED_BLOCK_WRITER_H */ diff --git a/src/third_party/oboe/src/common/LatencyTuner.cpp b/src/third_party/oboe/src/common/LatencyTuner.cpp new file mode 100644 index 0000000..5a0d037 --- /dev/null +++ b/src/third_party/oboe/src/common/LatencyTuner.cpp @@ -0,0 +1,105 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe/LatencyTuner.h" + +using namespace oboe; + +LatencyTuner::LatencyTuner(AudioStream &stream) + : LatencyTuner(stream, stream.getBufferCapacityInFrames()) { +} + +LatencyTuner::LatencyTuner(oboe::AudioStream &stream, int32_t maximumBufferSize) + : mStream(stream) + , mMaxBufferSize(maximumBufferSize) { + int32_t burstSize = stream.getFramesPerBurst(); + setMinimumBufferSize(kDefaultNumBursts * burstSize); + setBufferSizeIncrement(burstSize); + reset(); +} + +Result LatencyTuner::tune() { + if (mState == State::Unsupported) { + return Result::ErrorUnimplemented; + } + + Result result = Result::OK; + + // Process reset requests. + int32_t numRequests = mLatencyTriggerRequests.load(); + if (numRequests != mLatencyTriggerResponses.load()) { + mLatencyTriggerResponses.store(numRequests); + reset(); + } + + // Set state to Active if the idle countdown has reached zero. + if (mState == State::Idle && --mIdleCountDown <= 0) { + mState = State::Active; + } + + // When state is Active attempt to change the buffer size if the number of xRuns has increased. + if (mState == State::Active) { + + auto xRunCountResult = mStream.getXRunCount(); + if (xRunCountResult == Result::OK) { + if ((xRunCountResult.value() - mPreviousXRuns) > 0) { + mPreviousXRuns = xRunCountResult.value(); + int32_t oldBufferSize = mStream.getBufferSizeInFrames(); + int32_t requestedBufferSize = oldBufferSize + getBufferSizeIncrement(); + + // Do not request more than the maximum buffer size (which was either user-specified + // or was from stream->getBufferCapacityInFrames()) + if (requestedBufferSize > mMaxBufferSize) requestedBufferSize = mMaxBufferSize; + + auto setBufferResult = mStream.setBufferSizeInFrames(requestedBufferSize); + if (setBufferResult != Result::OK) { + result = setBufferResult; + mState = State::Unsupported; + } else if (setBufferResult.value() == oldBufferSize) { + mState = State::AtMax; + } + } + } else { + mState = State::Unsupported; + } + } + + if (mState == State::Unsupported) { + result = Result::ErrorUnimplemented; + } + + if (mState == State::AtMax) { + result = Result::OK; + } + return result; +} + +void LatencyTuner::requestReset() { + if (mState != State::Unsupported) { + mLatencyTriggerRequests++; + } +} + +void LatencyTuner::reset() { + mState = State::Idle; + mIdleCountDown = kIdleCount; + // Set to minimal latency + mStream.setBufferSizeInFrames(getMinimumBufferSize()); +} + +bool LatencyTuner::isAtMaximumBufferSize() { + return mState == State::AtMax; +} diff --git a/src/third_party/oboe/src/common/MonotonicCounter.h b/src/third_party/oboe/src/common/MonotonicCounter.h new file mode 100644 index 0000000..00c979c --- /dev/null +++ b/src/third_party/oboe/src/common/MonotonicCounter.h @@ -0,0 +1,112 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef COMMON_MONOTONIC_COUNTER_H +#define COMMON_MONOTONIC_COUNTER_H + +#include + +/** + * Maintain a 64-bit monotonic counter. + * Can be used to track a 32-bit counter that wraps or gets reset. + * + * Note that this is not atomic and has no interior locks. + * A caller will need to provide their own exterior locking + * if they need to use it from multiple threads. + */ +class MonotonicCounter { + +public: + MonotonicCounter() {} + virtual ~MonotonicCounter() {} + + /** + * @return current value of the counter + */ + int64_t get() const { + return mCounter64; + } + + /** + * set the current value of the counter + */ + void set(int64_t counter) { + mCounter64 = counter; + } + + /** + * Advance the counter if delta is positive. + * @return current value of the counter + */ + int64_t increment(int64_t delta) { + if (delta > 0) { + mCounter64 += delta; + } + return mCounter64; + } + + /** + * Advance the 64-bit counter if (current32 - previousCurrent32) > 0. + * This can be used to convert a 32-bit counter that may be wrapping into + * a monotonic 64-bit counter. + * + * This counter32 should NOT be allowed to advance by more than 0x7FFFFFFF between calls. + * Think of the wrapping counter like a sine wave. If the frequency of the signal + * is more than half the sampling rate (Nyquist rate) then you cannot measure it properly. + * If the counter wraps around every 24 hours then we should measure it with a period + * of less than 12 hours. + * + * @return current value of the 64-bit counter + */ + int64_t update32(int32_t counter32) { + int32_t delta = counter32 - mCounter32; + // protect against the mCounter64 going backwards + if (delta > 0) { + mCounter64 += delta; + mCounter32 = counter32; + } + return mCounter64; + } + + /** + * Reset the stored value of the 32-bit counter. + * This is used if your counter32 has been reset to zero. + */ + void reset32() { + mCounter32 = 0; + } + + /** + * Round 64-bit counter up to a multiple of the period. + * + * The period must be positive. + * + * @param period might be, for example, a buffer capacity + */ + void roundUp64(int32_t period) { + if (period > 0) { + int64_t numPeriods = (mCounter64 + period - 1) / period; + mCounter64 = numPeriods * period; + } + } + +private: + int64_t mCounter64 = 0; + int32_t mCounter32 = 0; +}; + + +#endif //COMMON_MONOTONIC_COUNTER_H diff --git a/src/third_party/oboe/src/common/OboeDebug.h b/src/third_party/oboe/src/common/OboeDebug.h new file mode 100644 index 0000000..dc7434c --- /dev/null +++ b/src/third_party/oboe/src/common/OboeDebug.h @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef OBOE_DEBUG_H +#define OBOE_DEBUG_H + +#include + +#ifndef MODULE_NAME +#define MODULE_NAME "OboeAudio" +#endif + +// Always log INFO and errors. +#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, MODULE_NAME, __VA_ARGS__) +#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, MODULE_NAME, __VA_ARGS__) +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, MODULE_NAME, __VA_ARGS__) +#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, MODULE_NAME, __VA_ARGS__) + +#if OBOE_ENABLE_LOGGING +#define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, MODULE_NAME, __VA_ARGS__) +#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, MODULE_NAME, __VA_ARGS__) +#else +#define LOGV(...) +#define LOGD(...) +#endif + +#endif //OBOE_DEBUG_H diff --git a/src/third_party/oboe/src/common/QuirksManager.cpp b/src/third_party/oboe/src/common/QuirksManager.cpp new file mode 100644 index 0000000..3df1bc4 --- /dev/null +++ b/src/third_party/oboe/src/common/QuirksManager.cpp @@ -0,0 +1,138 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "QuirksManager.h" + +using namespace oboe; + +int32_t QuirksManager::DeviceQuirks::clipBufferSize(AudioStream &stream, + int32_t requestedSize) { + if (!OboeGlobals::areWorkaroundsEnabled()) { + return requestedSize; + } + int bottomMargin = kDefaultBottomMarginInBursts; + int topMargin = kDefaultTopMarginInBursts; + if (isMMapUsed(stream)) { + if (stream.getSharingMode() == SharingMode::Exclusive) { + bottomMargin = getExclusiveBottomMarginInBursts(); + topMargin = getExclusiveTopMarginInBursts(); + } + } else { + bottomMargin = kLegacyBottomMarginInBursts; + } + + int32_t burst = stream.getFramesPerBurst(); + int32_t minSize = bottomMargin * burst; + int32_t adjustedSize = requestedSize; + if (adjustedSize < minSize ) { + adjustedSize = minSize; + } else { + int32_t maxSize = stream.getBufferCapacityInFrames() - (topMargin * burst); + if (adjustedSize > maxSize ) { + adjustedSize = maxSize; + } + } + return adjustedSize; +} + +class SamsungDeviceQuirks : public QuirksManager::DeviceQuirks { +public: + SamsungDeviceQuirks() { + std::string arch = getPropertyString("ro.arch"); + isExynos = (arch.rfind("exynos", 0) == 0); // starts with? + } + + virtual ~SamsungDeviceQuirks() = default; + + int32_t getExclusiveBottomMarginInBursts() const override { + // TODO Make this conditional on build version when MMAP timing improves. + return isExynos ? kBottomMarginExynos : kBottomMarginOther; + } + + int32_t getExclusiveTopMarginInBursts() const override { + return kTopMargin; + } + +private: + // Stay farther away from DSP position on Exynos devices. + static constexpr int32_t kBottomMarginExynos = 2; + static constexpr int32_t kBottomMarginOther = 1; + static constexpr int32_t kTopMargin = 1; + bool isExynos = false; +}; + +QuirksManager::QuirksManager() { + std::string manufacturer = getPropertyString("ro.product.manufacturer"); + if (manufacturer == "samsung") { + mDeviceQuirks = std::make_unique(); + } else { + mDeviceQuirks = std::make_unique(); + } +} + +bool QuirksManager::isConversionNeeded( + const AudioStreamBuilder &builder, + AudioStreamBuilder &childBuilder) { + bool conversionNeeded = false; + const bool isLowLatency = builder.getPerformanceMode() == PerformanceMode::LowLatency; + const bool isInput = builder.getDirection() == Direction::Input; + const bool isFloat = builder.getFormat() == AudioFormat::Float; + + // If a SAMPLE RATE is specified for low latency then let the native code choose an optimal rate. + // TODO There may be a problem if the devices supports low latency + // at a higher rate than the default. + if (builder.getSampleRate() != oboe::Unspecified + && builder.getSampleRateConversionQuality() != SampleRateConversionQuality::None + && isLowLatency + ) { + childBuilder.setSampleRate(oboe::Unspecified); // native API decides the best sample rate + conversionNeeded = true; + } + + // Data Format + // OpenSL ES and AAudio before P do not support FAST path for FLOAT capture. + if (isFloat + && isInput + && builder.isFormatConversionAllowed() + && isLowLatency + && (!builder.willUseAAudio() || (getSdkVersion() < __ANDROID_API_P__)) + ) { + childBuilder.setFormat(AudioFormat::I16); // needed for FAST track + conversionNeeded = true; + } + + // Channel Count + if (builder.getChannelCount() != oboe::Unspecified + && builder.isChannelConversionAllowed()) { + if (OboeGlobals::areWorkaroundsEnabled() + && builder.getChannelCount() == 2 // stereo? + && isInput + && isLowLatency + && (!builder.willUseAAudio() && (getSdkVersion() == __ANDROID_API_O__))) { + // Workaround for heap size regression in O. + // b/66967812 AudioRecord does not allow FAST track for stereo capture in O + childBuilder.setChannelCount(1); + conversionNeeded = true; + } + // Note that MMAP does not support mono in 8.1. But that would only matter on Pixel 1 + // phones and they have almost all been updated to 9.0. + } + + return conversionNeeded; +} diff --git a/src/third_party/oboe/src/common/QuirksManager.h b/src/third_party/oboe/src/common/QuirksManager.h new file mode 100644 index 0000000..a177764 --- /dev/null +++ b/src/third_party/oboe/src/common/QuirksManager.h @@ -0,0 +1,110 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_QUIRKS_MANAGER_H +#define OBOE_QUIRKS_MANAGER_H + +#include +#include +#include + +namespace oboe { + +/** + * INTERNAL USE ONLY. + * + * Based on manufacturer, model and Android version number + * decide whether data conversion needs to occur. + * + * This also manages device and version specific workarounds. + */ + +class QuirksManager { +public: + + static QuirksManager &getInstance() { + static QuirksManager instance; // singleton + return instance; + } + + QuirksManager(); + virtual ~QuirksManager() = default; + + /** + * Do we need to do channel, format or rate conversion to provide a low latency + * stream for this builder? If so then provide a builder for the native child stream + * that will be used to get low latency. + * + * @param builder builder provided by application + * @param childBuilder modified builder appropriate for the underlying device + * @return true if conversion is needed + */ + bool isConversionNeeded(const AudioStreamBuilder &builder, AudioStreamBuilder &childBuilder); + + static bool isMMapUsed(AudioStream &stream) { + bool answer = false; + if (stream.getAudioApi() == AudioApi::AAudio) { + AudioStreamAAudio *streamAAudio = + reinterpret_cast(&stream); + answer = streamAAudio->isMMapUsed(); + } + return answer; + } + + virtual int32_t clipBufferSize(AudioStream &stream, int32_t bufferSize) { + return mDeviceQuirks->clipBufferSize(stream, bufferSize); + } + + class DeviceQuirks { + public: + virtual ~DeviceQuirks() = default; + + /** + * Restrict buffer size. This is mainly to avoid glitches caused by MMAP + * timestamp inaccuracies. + * @param stream + * @param requestedSize + * @return + */ + int32_t clipBufferSize(AudioStream &stream, int32_t requestedSize); + + // Exclusive MMAP streams can have glitches because they are using a timing + // model of the DSP to control IO instead of direct synchronization. + virtual int32_t getExclusiveBottomMarginInBursts() const { + return kDefaultBottomMarginInBursts; + } + + virtual int32_t getExclusiveTopMarginInBursts() const { + return kDefaultTopMarginInBursts; + } + + static constexpr int32_t kDefaultBottomMarginInBursts = 0; + static constexpr int32_t kDefaultTopMarginInBursts = 0; + + // For Legacy streams, do not let the buffer go below one burst. + // b/129545119 | AAudio Legacy allows setBufferSizeInFrames too low + // Fixed in Q + static constexpr int32_t kLegacyBottomMarginInBursts = 1; + }; + +private: + + std::unique_ptr mDeviceQuirks{}; + +}; + +} +#endif //OBOE_QUIRKS_MANAGER_H diff --git a/src/third_party/oboe/src/common/README.md b/src/third_party/oboe/src/common/README.md new file mode 100644 index 0000000..d397458 --- /dev/null +++ b/src/third_party/oboe/src/common/README.md @@ -0,0 +1,33 @@ +# Notes on Implementation + +## Latency from Resampling + +There are two components of the latency. The resampler itself, and a buffer that +is used to adapt the block sizes. + +1) The resampler is an FIR running at the target sample rate. So its latency is the number of taps. +From MultiChannelResampler.cpp, numTaps is + + Fastest: 2 + Low: 4 + Medium: 8 + High: 16 + Best: 32 + +For output, the device sampling rate is used, which is typically 48000.For input, the app sampling rate is used. + +2) There is a block size adapter that collects odd sized blocks into larger blocks of the correct size. + +The adapter contains one burst of frames, from getFramesPerBurst(). But if the app specifies a +particular size using setFramesPerCallback() then that size will be used. +Here is some pseudo-code to calculate the latency. + + latencyMillis = 0 + targetRate = isOutput ? deviceRate : applicationRate + // Add latency from FIR + latencyMillis += numTaps * 1000.0 / targetRate + // Add latency from block size adaptation + adapterSize = (callbackSize > 0) ? callbackSize : burstSize + if (isOutput && isCallbackUsed) latencyMillis += adapterSize * 1000.0 / deviceRate + else if (isInput && isCallbackUsed) latencyMillis += adapterSize * 1000.0 / applicationRate + else if (isInput && !isCallbackUsed) latencyMillis += adapterSize * 1000.0 / deviceRate diff --git a/src/third_party/oboe/src/common/SourceFloatCaller.cpp b/src/third_party/oboe/src/common/SourceFloatCaller.cpp new file mode 100644 index 0000000..631af85 --- /dev/null +++ b/src/third_party/oboe/src/common/SourceFloatCaller.cpp @@ -0,0 +1,30 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "flowgraph/FlowGraphNode.h" +#include "SourceFloatCaller.h" + +using namespace oboe; +using namespace flowgraph; + +int32_t SourceFloatCaller::onProcess(int32_t numFrames) { + int32_t numBytes = mStream->getBytesPerFrame() * numFrames; + int32_t bytesRead = mBlockReader.read((uint8_t *) output.getBuffer(), numBytes); + int32_t framesRead = bytesRead / mStream->getBytesPerFrame(); + return framesRead; +} diff --git a/src/third_party/oboe/src/common/SourceFloatCaller.h b/src/third_party/oboe/src/common/SourceFloatCaller.h new file mode 100644 index 0000000..85a1585 --- /dev/null +++ b/src/third_party/oboe/src/common/SourceFloatCaller.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SOURCE_FLOAT_CALLER_H +#define OBOE_SOURCE_FLOAT_CALLER_H + +#include +#include + +#include "flowgraph/FlowGraphNode.h" +#include "AudioSourceCaller.h" +#include "FixedBlockReader.h" + +namespace oboe { +/** + * AudioSource that uses callback to get more float data. + */ +class SourceFloatCaller : public AudioSourceCaller { +public: + SourceFloatCaller(int32_t channelCount, int32_t framesPerCallback) + : AudioSourceCaller(channelCount, framesPerCallback, (int32_t)sizeof(float)) {} + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceFloatCaller"; + } +}; + +} +#endif //OBOE_SOURCE_FLOAT_CALLER_H diff --git a/src/third_party/oboe/src/common/SourceI16Caller.cpp b/src/third_party/oboe/src/common/SourceI16Caller.cpp new file mode 100644 index 0000000..2ab372b --- /dev/null +++ b/src/third_party/oboe/src/common/SourceI16Caller.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "flowgraph/FlowGraphNode.h" +#include "SourceI16Caller.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace oboe; +using namespace flowgraph; + +int32_t SourceI16Caller::onProcess(int32_t numFrames) { + int32_t numBytes = mStream->getBytesPerFrame() * numFrames; + int32_t bytesRead = mBlockReader.read((uint8_t *) mConversionBuffer.get(), numBytes); + int32_t framesRead = bytesRead / mStream->getBytesPerFrame(); + + float *floatData = output.getBuffer(); + const int16_t *shortData = mConversionBuffer.get(); + int32_t numSamples = framesRead * output.getSamplesPerFrame(); + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_i16(floatData, shortData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *shortData++ * (1.0f / 32768); + } +#endif + + return framesRead; +} diff --git a/src/third_party/oboe/src/common/SourceI16Caller.h b/src/third_party/oboe/src/common/SourceI16Caller.h new file mode 100644 index 0000000..22c1b9a --- /dev/null +++ b/src/third_party/oboe/src/common/SourceI16Caller.h @@ -0,0 +1,48 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SOURCE_I16_CALLER_H +#define OBOE_SOURCE_I16_CALLER_H + +#include +#include + +#include "flowgraph/FlowGraphNode.h" +#include "AudioSourceCaller.h" +#include "FixedBlockReader.h" + +namespace oboe { +/** + * AudioSource that uses callback to get more data. + */ +class SourceI16Caller : public AudioSourceCaller { +public: + SourceI16Caller(int32_t channelCount, int32_t framesPerCallback) + : AudioSourceCaller(channelCount, framesPerCallback, sizeof(int16_t)) { + mConversionBuffer = std::make_unique(channelCount * output.getFramesPerBuffer()); + } + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI16Caller"; + } +private: + std::unique_ptr mConversionBuffer; +}; + +} +#endif //OBOE_SOURCE_I16_CALLER_H diff --git a/src/third_party/oboe/src/common/StabilizedCallback.cpp b/src/third_party/oboe/src/common/StabilizedCallback.cpp new file mode 100644 index 0000000..692db89 --- /dev/null +++ b/src/third_party/oboe/src/common/StabilizedCallback.cpp @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "oboe/StabilizedCallback.h" +#include "common/AudioClock.h" +#include "common/Trace.h" + +constexpr int32_t kLoadGenerationStepSizeNanos = 20000; +constexpr float kPercentageOfCallbackToUse = 0.8; + +using namespace oboe; + +StabilizedCallback::StabilizedCallback(AudioStreamCallback *callback) : mCallback(callback){ + Trace::initialize(); +} + +/** + * An audio callback which attempts to do work for a fixed amount of time. + * + * @param oboeStream + * @param audioData + * @param numFrames + * @return + */ +DataCallbackResult +StabilizedCallback::onAudioReady(AudioStream *oboeStream, void *audioData, int32_t numFrames) { + + int64_t startTimeNanos = AudioClock::getNanoseconds(); + + if (mFrameCount == 0){ + mEpochTimeNanos = startTimeNanos; + } + + int64_t durationSinceEpochNanos = startTimeNanos - mEpochTimeNanos; + + // In an ideal world the callback start time will be exactly the same as the duration of the + // frames already read/written into the stream. In reality the callback can start early + // or late. By finding the delta we can calculate the target duration for our stabilized + // callback. + int64_t idealStartTimeNanos = (mFrameCount * kNanosPerSecond) / oboeStream->getSampleRate(); + int64_t lateStartNanos = durationSinceEpochNanos - idealStartTimeNanos; + + if (lateStartNanos < 0){ + // This was an early start which indicates that our previous epoch was a late callback. + // Update our epoch to this more accurate time. + mEpochTimeNanos = startTimeNanos; + mFrameCount = 0; + } + + int64_t numFramesAsNanos = (numFrames * kNanosPerSecond) / oboeStream->getSampleRate(); + int64_t targetDurationNanos = static_cast( + (numFramesAsNanos * kPercentageOfCallbackToUse) - lateStartNanos); + + Trace::beginSection("Actual load"); + DataCallbackResult result = mCallback->onAudioReady(oboeStream, audioData, numFrames); + Trace::endSection(); + + int64_t executionDurationNanos = AudioClock::getNanoseconds() - startTimeNanos; + int64_t stabilizingLoadDurationNanos = targetDurationNanos - executionDurationNanos; + + Trace::beginSection("Stabilized load for %lldns", stabilizingLoadDurationNanos); + generateLoad(stabilizingLoadDurationNanos); + Trace::endSection(); + + // Wraparound: At 48000 frames per second mFrameCount wraparound will occur after 6m years, + // significantly longer than the average lifetime of an Android phone. + mFrameCount += numFrames; + return result; +} + +void StabilizedCallback::generateLoad(int64_t durationNanos) { + + int64_t currentTimeNanos = AudioClock::getNanoseconds(); + int64_t deadlineTimeNanos = currentTimeNanos + durationNanos; + + // opsPerStep gives us an estimated number of operations which need to be run to fully utilize + // the CPU for a fixed amount of time (specified by kLoadGenerationStepSizeNanos). + // After each step the opsPerStep value is re-calculated based on the actual time taken to + // execute those operations. + auto opsPerStep = (int)(mOpsPerNano * kLoadGenerationStepSizeNanos); + int64_t stepDurationNanos = 0; + int64_t previousTimeNanos = 0; + + while (currentTimeNanos <= deadlineTimeNanos){ + + for (int i = 0; i < opsPerStep; i++) cpu_relax(); + + previousTimeNanos = currentTimeNanos; + currentTimeNanos = AudioClock::getNanoseconds(); + stepDurationNanos = currentTimeNanos - previousTimeNanos; + + // Calculate exponential moving average to smooth out values, this acts as a low pass filter. + // @see https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average + static const float kFilterCoefficient = 0.1; + auto measuredOpsPerNano = (double) opsPerStep / stepDurationNanos; + mOpsPerNano = kFilterCoefficient * measuredOpsPerNano + (1.0 - kFilterCoefficient) * mOpsPerNano; + opsPerStep = (int) (mOpsPerNano * kLoadGenerationStepSizeNanos); + } +} \ No newline at end of file diff --git a/src/third_party/oboe/src/common/Trace.cpp b/src/third_party/oboe/src/common/Trace.cpp new file mode 100644 index 0000000..5ed445b --- /dev/null +++ b/src/third_party/oboe/src/common/Trace.cpp @@ -0,0 +1,75 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "Trace.h" +#include "OboeDebug.h" + +static char buffer[256]; + +// Tracing functions +static void *(*ATrace_beginSection)(const char *sectionName); + +static void *(*ATrace_endSection)(); + +typedef void *(*fp_ATrace_beginSection)(const char *sectionName); + +typedef void *(*fp_ATrace_endSection)(); + +bool Trace::mIsTracingSupported = false; + +void Trace::beginSection(const char *format, ...){ + + if (mIsTracingSupported) { + va_list va; + va_start(va, format); + vsprintf(buffer, format, va); + ATrace_beginSection(buffer); + va_end(va); + } else { + LOGE("Tracing is either not initialized (call Trace::initialize()) " + "or not supported on this device"); + } +} + +void Trace::endSection() { + + if (mIsTracingSupported) { + ATrace_endSection(); + } +} + +void Trace::initialize() { + + // Using dlsym allows us to use tracing on API 21+ without needing android/trace.h which wasn't + // published until API 23 + void *lib = dlopen("libandroid.so", RTLD_NOW | RTLD_LOCAL); + if (lib == nullptr) { + LOGE("Could not open libandroid.so to dynamically load tracing symbols"); + } else { + ATrace_beginSection = + reinterpret_cast( + dlsym(lib, "ATrace_beginSection")); + ATrace_endSection = + reinterpret_cast( + dlsym(lib, "ATrace_endSection")); + + if (ATrace_beginSection != nullptr && ATrace_endSection != nullptr){ + mIsTracingSupported = true; + } + } +} \ No newline at end of file diff --git a/src/third_party/oboe/src/common/Trace.h b/src/third_party/oboe/src/common/Trace.h new file mode 100644 index 0000000..c7965f9 --- /dev/null +++ b/src/third_party/oboe/src/common/Trace.h @@ -0,0 +1,31 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_TRACE_H +#define OBOE_TRACE_H + +class Trace { + +public: + static void beginSection(const char *format, ...); + static void endSection(); + static void initialize(); + +private: + static bool mIsTracingSupported; +}; + +#endif //OBOE_TRACE_H \ No newline at end of file diff --git a/src/third_party/oboe/src/common/Utilities.cpp b/src/third_party/oboe/src/common/Utilities.cpp new file mode 100644 index 0000000..c3acf47 --- /dev/null +++ b/src/third_party/oboe/src/common/Utilities.cpp @@ -0,0 +1,305 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include +#include +#include + +#ifdef __ANDROID__ +#include +#endif + +#include +#include "oboe/Definitions.h" +#include "oboe/Utilities.h" + +namespace oboe { + +constexpr float kScaleI16ToFloat = (1.0f / 32768.0f); + +void convertFloatToPcm16(const float *source, int16_t *destination, int32_t numSamples) { + for (int i = 0; i < numSamples; i++) { + float fval = source[i]; + fval += 1.0; // to avoid discontinuity at 0.0 caused by truncation + fval *= 32768.0f; + auto sample = static_cast(fval); + // clip to 16-bit range + if (sample < 0) sample = 0; + else if (sample > 0x0FFFF) sample = 0x0FFFF; + sample -= 32768; // center at zero + destination[i] = static_cast(sample); + } +} + +void convertPcm16ToFloat(const int16_t *source, float *destination, int32_t numSamples) { + for (int i = 0; i < numSamples; i++) { + destination[i] = source[i] * kScaleI16ToFloat; + } +} + +int32_t convertFormatToSizeInBytes(AudioFormat format) { + int32_t size = 0; + switch (format) { + case AudioFormat::I16: + size = sizeof(int16_t); + break; + case AudioFormat::Float: + size = sizeof(float); + break; + default: + break; + } + return size; +} + +template<> +const char *convertToText(Result returnCode) { + switch (returnCode) { + case Result::OK: return "OK"; + case Result::ErrorDisconnected: return "ErrorDisconnected"; + case Result::ErrorIllegalArgument: return "ErrorIllegalArgument"; + case Result::ErrorInternal: return "ErrorInternal"; + case Result::ErrorInvalidState: return "ErrorInvalidState"; + case Result::ErrorInvalidHandle: return "ErrorInvalidHandle"; + case Result::ErrorUnimplemented: return "ErrorUnimplemented"; + case Result::ErrorUnavailable: return "ErrorUnavailable"; + case Result::ErrorNoFreeHandles: return "ErrorNoFreeHandles"; + case Result::ErrorNoMemory: return "ErrorNoMemory"; + case Result::ErrorNull: return "ErrorNull"; + case Result::ErrorTimeout: return "ErrorTimeout"; + case Result::ErrorWouldBlock: return "ErrorWouldBlock"; + case Result::ErrorInvalidFormat: return "ErrorInvalidFormat"; + case Result::ErrorOutOfRange: return "ErrorOutOfRange"; + case Result::ErrorNoService: return "ErrorNoService"; + case Result::ErrorInvalidRate: return "ErrorInvalidRate"; + case Result::ErrorClosed: return "ErrorClosed"; + default: return "Unrecognized result"; + } +} + +template<> +const char *convertToText(AudioFormat format) { + switch (format) { + case AudioFormat::Invalid: return "Invalid"; + case AudioFormat::Unspecified: return "Unspecified"; + case AudioFormat::I16: return "I16"; + case AudioFormat::Float: return "Float"; + default: return "Unrecognized format"; + } +} + +template<> +const char *convertToText(PerformanceMode mode) { + switch (mode) { + case PerformanceMode::LowLatency: return "LowLatency"; + case PerformanceMode::None: return "None"; + case PerformanceMode::PowerSaving: return "PowerSaving"; + default: return "Unrecognized performance mode"; + } +} + +template<> +const char *convertToText(SharingMode mode) { + switch (mode) { + case SharingMode::Exclusive: return "Exclusive"; + case SharingMode::Shared: return "Shared"; + default: return "Unrecognized sharing mode"; + } +} + +template<> +const char *convertToText(DataCallbackResult result) { + switch (result) { + case DataCallbackResult::Continue: return "Continue"; + case DataCallbackResult::Stop: return "Stop"; + default: return "Unrecognized data callback result"; + } +} + +template<> +const char *convertToText(Direction direction) { + switch (direction) { + case Direction::Input: return "Input"; + case Direction::Output: return "Output"; + default: return "Unrecognized direction"; + } +} + +template<> +const char *convertToText(StreamState state) { + switch (state) { + case StreamState::Closed: return "Closed"; + case StreamState::Closing: return "Closing"; + case StreamState::Disconnected: return "Disconnected"; + case StreamState::Flushed: return "Flushed"; + case StreamState::Flushing: return "Flushing"; + case StreamState::Open: return "Open"; + case StreamState::Paused: return "Paused"; + case StreamState::Pausing: return "Pausing"; + case StreamState::Started: return "Started"; + case StreamState::Starting: return "Starting"; + case StreamState::Stopped: return "Stopped"; + case StreamState::Stopping: return "Stopping"; + case StreamState::Uninitialized: return "Uninitialized"; + case StreamState::Unknown: return "Unknown"; + default: return "Unrecognized stream state"; + } +} + +template<> +const char *convertToText(AudioApi audioApi) { + + switch (audioApi) { + case AudioApi::Unspecified: return "Unspecified"; + case AudioApi::OpenSLES: return "OpenSLES"; + case AudioApi::AAudio: return "AAudio"; + default: return "Unrecognized audio API"; + } +} + +template<> +const char *convertToText(AudioStream* stream) { + static std::string streamText; + std::stringstream s; + + s<<"StreamID: "<< static_cast(stream)<getDeviceId()<getDirection())<getAudioApi())<getBufferCapacityInFrames()<getBufferSizeInFrames()<getFramesPerBurst()<getFramesPerCallback()<getSampleRate()<getChannelCount()<getFormat())<getSharingMode())<getPerformanceMode()) + <getState())<getXRunCount()<getFramesRead()<getFramesWritten()< +const char *convertToText(Usage usage) { + + switch (usage) { + case Usage::Media: return "Media"; + case Usage::VoiceCommunication: return "VoiceCommunication"; + case Usage::VoiceCommunicationSignalling: return "VoiceCommunicationSignalling"; + case Usage::Alarm: return "Alarm"; + case Usage::Notification: return "Notification"; + case Usage::NotificationRingtone: return "NotificationRingtone"; + case Usage::NotificationEvent: return "NotificationEvent"; + case Usage::AssistanceAccessibility: return "AssistanceAccessibility"; + case Usage::AssistanceNavigationGuidance: return "AssistanceNavigationGuidance"; + case Usage::AssistanceSonification: return "AssistanceSonification"; + case Usage::Game: return "Game"; + case Usage::Assistant: return "Assistant"; + default: return "Unrecognized usage"; + } +} + +template<> +const char *convertToText(ContentType contentType) { + + switch (contentType) { + case ContentType::Speech: return "Speech"; + case ContentType::Music: return "Music"; + case ContentType::Movie: return "Movie"; + case ContentType::Sonification: return "Sonification"; + default: return "Unrecognized content type"; + } +} + +template<> +const char *convertToText(InputPreset inputPreset) { + + switch (inputPreset) { + case InputPreset::Generic: return "Generic"; + case InputPreset::Camcorder: return "Camcorder"; + case InputPreset::VoiceRecognition: return "VoiceRecognition"; + case InputPreset::VoiceCommunication: return "VoiceCommunication"; + case InputPreset::Unprocessed: return "Unprocessed"; + case InputPreset::VoicePerformance: return "VoicePerformance"; + default: return "Unrecognized input preset"; + } +} + +template<> +const char *convertToText(SessionId sessionId) { + + switch (sessionId) { + case SessionId::None: return "None"; + case SessionId::Allocate: return "Allocate"; + default: return "Unrecognized session id"; + } +} + +template<> +const char *convertToText(ChannelCount channelCount) { + + switch (channelCount) { + case ChannelCount::Unspecified: return "Unspecified"; + case ChannelCount::Mono: return "Mono"; + case ChannelCount::Stereo: return "Stereo"; + default: return "Unrecognized channel count"; + } +} + +std::string getPropertyString(const char * name) { + std::string result; +#ifdef __ANDROID__ + char valueText[PROP_VALUE_MAX] = {0}; + if (__system_property_get(name, valueText) != 0) { + result = valueText; + } +#else + (void) name; +#endif + return result; +} + +int getPropertyInteger(const char * name, int defaultValue) { + int result = defaultValue; +#ifdef __ANDROID__ + char valueText[PROP_VALUE_MAX] = {0}; + if (__system_property_get(name, valueText) != 0) { + result = atoi(valueText); + } +#else + (void) name; +#endif + return result; +} + +int getSdkVersion() { + static int sCachedSdkVersion = -1; +#ifdef __ANDROID__ + if (sCachedSdkVersion == -1) { + sCachedSdkVersion = getPropertyInteger("ro.build.version.sdk", -1); + } +#endif + return sCachedSdkVersion; +} + +}// namespace oboe diff --git a/src/third_party/oboe/src/common/Version.cpp b/src/third_party/oboe/src/common/Version.cpp new file mode 100644 index 0000000..481bee7 --- /dev/null +++ b/src/third_party/oboe/src/common/Version.cpp @@ -0,0 +1,28 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "oboe/Version.h" + +namespace oboe { + + // This variable enables the version information to be read from the resulting binary e.g. + // by running `objdump -s --section=.data ` + // Please do not optimize or change in any way. + char kVersionText[] = "OboeVersion" OBOE_VERSION_TEXT; + + const char * getVersionText(){ + return kVersionText; + } +} // namespace oboe diff --git a/src/third_party/oboe/src/fifo/FifoBuffer.cpp b/src/third_party/oboe/src/fifo/FifoBuffer.cpp new file mode 100644 index 0000000..8d6b03b --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoBuffer.cpp @@ -0,0 +1,182 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include +#include +#include + +#include "common/OboeDebug.h" +#include "fifo/FifoControllerBase.h" +#include "fifo/FifoController.h" +#include "fifo/FifoControllerIndirect.h" +#include "fifo/FifoBuffer.h" +#include "common/AudioClock.h" + +namespace oboe { + +FifoBuffer::FifoBuffer(uint32_t bytesPerFrame, uint32_t capacityInFrames) + : mBytesPerFrame(bytesPerFrame) + , mStorage(nullptr) + , mFramesReadCount(0) + , mFramesUnderrunCount(0) +{ + mFifo = std::make_unique(capacityInFrames); + // allocate buffer + int32_t bytesPerBuffer = bytesPerFrame * capacityInFrames; + mStorage = new uint8_t[bytesPerBuffer]; + mStorageOwned = true; +} + +FifoBuffer::FifoBuffer( uint32_t bytesPerFrame, + uint32_t capacityInFrames, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress, + uint8_t *dataStorageAddress + ) + : mBytesPerFrame(bytesPerFrame) + , mStorage(dataStorageAddress) + , mFramesReadCount(0) + , mFramesUnderrunCount(0) +{ + mFifo = std::make_unique(capacityInFrames, + readCounterAddress, + writeCounterAddress); + mStorage = dataStorageAddress; + mStorageOwned = false; +} + +FifoBuffer::~FifoBuffer() { + if (mStorageOwned) { + delete[] mStorage; + } +} + +int32_t FifoBuffer::convertFramesToBytes(int32_t frames) { + return frames * mBytesPerFrame; +} + +int32_t FifoBuffer::read(void *buffer, int32_t numFrames) { + if (numFrames <= 0) { + return 0; + } + // safe because numFrames is guaranteed positive + uint32_t framesToRead = static_cast(numFrames); + uint32_t framesAvailable = mFifo->getFullFramesAvailable(); + framesToRead = std::min(framesToRead, framesAvailable); + + uint32_t readIndex = mFifo->getReadIndex(); // ranges 0 to capacity + uint8_t *destination = reinterpret_cast(buffer); + uint8_t *source = &mStorage[convertFramesToBytes(readIndex)]; + if ((readIndex + framesToRead) > mFifo->getFrameCapacity()) { + // read in two parts, first part here is at the end of the mStorage buffer + int32_t frames1 = static_cast(mFifo->getFrameCapacity() - readIndex); + int32_t numBytes = convertFramesToBytes(frames1); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + destination += numBytes; + // read second part, which is at the beginning of mStorage + source = &mStorage[0]; + int32_t frames2 = static_cast(framesToRead - frames1); + numBytes = convertFramesToBytes(frames2); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } else { + // just read in one shot + int32_t numBytes = convertFramesToBytes(framesToRead); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } + mFifo->advanceReadIndex(framesToRead); + + return framesToRead; +} + +int32_t FifoBuffer::write(const void *buffer, int32_t numFrames) { + if (numFrames <= 0) { + return 0; + } + // Guaranteed positive. + uint32_t framesToWrite = static_cast(numFrames); + uint32_t framesAvailable = mFifo->getEmptyFramesAvailable(); + framesToWrite = std::min(framesToWrite, framesAvailable); + + uint32_t writeIndex = mFifo->getWriteIndex(); + int byteIndex = convertFramesToBytes(writeIndex); + const uint8_t *source = reinterpret_cast(buffer); + uint8_t *destination = &mStorage[byteIndex]; + if ((writeIndex + framesToWrite) > mFifo->getFrameCapacity()) { + // write in two parts, first part here + int32_t frames1 = static_cast(mFifo->getFrameCapacity() - writeIndex); + int32_t numBytes = convertFramesToBytes(frames1); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + // read second part + source += convertFramesToBytes(frames1); + destination = &mStorage[0]; + int frames2 = static_cast(framesToWrite - frames1); + numBytes = convertFramesToBytes(frames2); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } else { + // just write in one shot + int32_t numBytes = convertFramesToBytes(framesToWrite); + if (numBytes < 0) { + return static_cast(Result::ErrorOutOfRange); + } + memcpy(destination, source, static_cast(numBytes)); + } + mFifo->advanceWriteIndex(framesToWrite); + + return framesToWrite; +} + +int32_t FifoBuffer::readNow(void *buffer, int32_t numFrames) { + int32_t framesRead = read(buffer, numFrames); + if (framesRead < 0) { + return framesRead; + } + int32_t framesLeft = numFrames - framesRead; + mFramesReadCount += framesRead; + mFramesUnderrunCount += framesLeft; + // Zero out any samples we could not set. + if (framesLeft > 0) { + uint8_t *destination = reinterpret_cast(buffer); + destination += convertFramesToBytes(framesRead); // point to first byte not set + int32_t bytesToZero = convertFramesToBytes(framesLeft); + memset(destination, 0, static_cast(bytesToZero)); + } + + return framesRead; +} + + +uint32_t FifoBuffer::getBufferCapacityInFrames() const { + return mFifo->getFrameCapacity(); +} + +} // namespace oboe diff --git a/src/third_party/oboe/src/fifo/FifoBuffer.h b/src/third_party/oboe/src/fifo/FifoBuffer.h new file mode 100644 index 0000000..8018620 --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoBuffer.h @@ -0,0 +1,99 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_FIFOPROCESSOR_H +#define OBOE_FIFOPROCESSOR_H + +#include +#include + +#include "common/OboeDebug.h" +#include "FifoControllerBase.h" +#include "oboe/Definitions.h" + +namespace oboe { + +class FifoBuffer { +public: + FifoBuffer(uint32_t bytesPerFrame, uint32_t capacityInFrames); + + FifoBuffer(uint32_t bytesPerFrame, + uint32_t capacityInFrames, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress, + uint8_t *dataStorageAddress); + + ~FifoBuffer(); + + int32_t convertFramesToBytes(int32_t frames); + + /** + * Read framesToRead or, if not enough, then read as many as are available. + * @param destination + * @param framesToRead number of frames requested + * @return number of frames actually read + */ + int32_t read(void *destination, int32_t framesToRead); + + int32_t write(const void *source, int32_t framesToWrite); + + uint32_t getBufferCapacityInFrames() const; + + /** + * Calls read(). If all of the frames cannot be read then the remainder of the buffer + * is set to zero. + * + * @param destination + * @param framesToRead number of frames requested + * @return number of frames actually read + */ + int32_t readNow(void *destination, int32_t numFrames); + + uint32_t getFullFramesAvailable() { + return mFifo->getFullFramesAvailable(); + } + + uint32_t getBytesPerFrame() const { + return mBytesPerFrame; + } + + uint64_t getReadCounter() const { + return mFifo->getReadCounter(); + } + + void setReadCounter(uint64_t n) { + mFifo->setReadCounter(n); + } + + uint64_t getWriteCounter() { + return mFifo->getWriteCounter(); + } + void setWriteCounter(uint64_t n) { + mFifo->setWriteCounter(n); + } + +private: + uint32_t mBytesPerFrame; + uint8_t* mStorage; + bool mStorageOwned; // did this object allocate the storage? + std::unique_ptr mFifo; + uint64_t mFramesReadCount; + uint64_t mFramesUnderrunCount; +}; + +} // namespace oboe + +#endif //OBOE_FIFOPROCESSOR_H diff --git a/src/third_party/oboe/src/fifo/FifoController.cpp b/src/third_party/oboe/src/fifo/FifoController.cpp new file mode 100644 index 0000000..5590683 --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoController.cpp @@ -0,0 +1,31 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "FifoControllerBase.h" +#include "FifoController.h" + +namespace oboe { + +FifoController::FifoController(uint32_t numFrames) + : FifoControllerBase(numFrames) +{ + setReadCounter(0); + setWriteCounter(0); +} + +} // namespace oboe diff --git a/src/third_party/oboe/src/fifo/FifoController.h b/src/third_party/oboe/src/fifo/FifoController.h new file mode 100644 index 0000000..6562e6d --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoController.h @@ -0,0 +1,61 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVEOBOE_FIFOCONTROLLER_H +#define NATIVEOBOE_FIFOCONTROLLER_H + +#include +#include "FifoControllerBase.h" +#include + +namespace oboe { + +/** + * A FifoControllerBase with counters contained in the class. + */ +class FifoController : public FifoControllerBase +{ +public: + FifoController(uint32_t bufferSize); + virtual ~FifoController() = default; + + virtual uint64_t getReadCounter() const override { + return mReadCounter.load(std::memory_order_acquire); + } + virtual void setReadCounter(uint64_t n) override { + mReadCounter.store(n, std::memory_order_release); + } + virtual void incrementReadCounter(uint64_t n) override { + mReadCounter.fetch_add(n, std::memory_order_acq_rel); + } + virtual uint64_t getWriteCounter() const override { + return mWriteCounter.load(std::memory_order_acquire); + } + virtual void setWriteCounter(uint64_t n) override { + mWriteCounter.store(n, std::memory_order_release); + } + virtual void incrementWriteCounter(uint64_t n) override { + mWriteCounter.fetch_add(n, std::memory_order_acq_rel); + } + +private: + std::atomic mReadCounter{}; + std::atomic mWriteCounter{}; +}; + +} // namespace oboe + +#endif //NATIVEOBOE_FIFOCONTROLLER_H diff --git a/src/third_party/oboe/src/fifo/FifoControllerBase.cpp b/src/third_party/oboe/src/fifo/FifoControllerBase.cpp new file mode 100644 index 0000000..6aceea0 --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoControllerBase.cpp @@ -0,0 +1,71 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "FifoControllerBase.h" + +#include +#include +#include +#include "FifoControllerBase.h" + +#include "common/OboeDebug.h" + +namespace oboe { + +FifoControllerBase::FifoControllerBase(uint32_t capacityInFrames) + : mTotalFrames(capacityInFrames) +{ + // Avoid ridiculously large buffers and the arithmetic wraparound issues that can follow. + assert(capacityInFrames <= (UINT32_MAX / 4)); +} + +uint32_t FifoControllerBase::getFullFramesAvailable() const { + uint64_t writeCounter = getWriteCounter(); + uint64_t readCounter = getReadCounter(); + if (readCounter > writeCounter) { + return 0; + } + uint64_t delta = writeCounter - readCounter; + if (delta >= mTotalFrames) { + return mTotalFrames; + } + // delta is now guaranteed to fit within the range of a uint32_t + return static_cast(delta); +} + +uint32_t FifoControllerBase::getReadIndex() const { + // % works with non-power of two sizes + return static_cast(getReadCounter() % mTotalFrames); +} + +void FifoControllerBase::advanceReadIndex(uint32_t numFrames) { + incrementReadCounter(numFrames); +} + +uint32_t FifoControllerBase::getEmptyFramesAvailable() const { + return static_cast(mTotalFrames - getFullFramesAvailable()); +} + +uint32_t FifoControllerBase::getWriteIndex() const { + // % works with non-power of two sizes + return static_cast(getWriteCounter() % mTotalFrames); +} + +void FifoControllerBase::advanceWriteIndex(uint32_t numFrames) { + incrementWriteCounter(numFrames); +} + +} // namespace oboe diff --git a/src/third_party/oboe/src/fifo/FifoControllerBase.h b/src/third_party/oboe/src/fifo/FifoControllerBase.h new file mode 100644 index 0000000..c8041e0 --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoControllerBase.h @@ -0,0 +1,93 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVEOBOE_FIFOCONTROLLERBASE_H +#define NATIVEOBOE_FIFOCONTROLLERBASE_H + +#include +#include + +namespace oboe { + +/** + * Manage the read/write indices of a circular buffer. + * + * The caller is responsible for reading and writing the actual data. + * Note that the span of available frames may not be contiguous. They + * may wrap around from the end to the beginning of the buffer. In that + * case the data must be read or written in at least two blocks of frames. + * + */ + +class FifoControllerBase { + +public: + /** + * @param totalFrames capacity of the circular buffer in frames. + */ + FifoControllerBase(uint32_t totalFrames); + + virtual ~FifoControllerBase() = default; + + /** + * The frames available to read will be calculated from the read and write counters. + * The result will be clipped to the capacity of the buffer. + * If the buffer has underflowed then this will return zero. + * @return number of valid frames available to read. + */ + uint32_t getFullFramesAvailable() const; + + /** + * The index in a circular buffer of the next frame to read. + */ + uint32_t getReadIndex() const; + + /** + * @param numFrames number of frames to advance the read index + */ + void advanceReadIndex(uint32_t numFrames); + + /** + * @return maximum number of frames that can be written without exceeding the threshold. + */ + uint32_t getEmptyFramesAvailable() const; + + /** + * The index in a circular buffer of the next frame to write. + */ + uint32_t getWriteIndex() const; + + /** + * @param numFrames number of frames to advance the write index + */ + void advanceWriteIndex(uint32_t numFrames); + + uint32_t getFrameCapacity() const { return mTotalFrames; } + + virtual uint64_t getReadCounter() const = 0; + virtual void setReadCounter(uint64_t n) = 0; + virtual void incrementReadCounter(uint64_t n) = 0; + virtual uint64_t getWriteCounter() const = 0; + virtual void setWriteCounter(uint64_t n) = 0; + virtual void incrementWriteCounter(uint64_t n) = 0; + +private: + uint32_t mTotalFrames; +}; + +} // namespace oboe + +#endif //NATIVEOBOE_FIFOCONTROLLERBASE_H diff --git a/src/third_party/oboe/src/fifo/FifoControllerIndirect.cpp b/src/third_party/oboe/src/fifo/FifoControllerIndirect.cpp new file mode 100644 index 0000000..f596461 --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoControllerIndirect.cpp @@ -0,0 +1,31 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "FifoControllerIndirect.h" + +namespace oboe { + +FifoControllerIndirect::FifoControllerIndirect(uint32_t numFrames, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress) + : FifoControllerBase(numFrames) + , mReadCounterAddress(readCounterAddress) + , mWriteCounterAddress(writeCounterAddress) +{ +} + +} diff --git a/src/third_party/oboe/src/fifo/FifoControllerIndirect.h b/src/third_party/oboe/src/fifo/FifoControllerIndirect.h new file mode 100644 index 0000000..216a28b --- /dev/null +++ b/src/third_party/oboe/src/fifo/FifoControllerIndirect.h @@ -0,0 +1,64 @@ +/* + * Copyright 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef NATIVEOBOE_FIFOCONTROLLERINDIRECT_H +#define NATIVEOBOE_FIFOCONTROLLERINDIRECT_H + +#include "FifoControllerBase.h" +#include + +namespace oboe { + +/** + * A FifoControllerBase with counters external to the class. + */ +class FifoControllerIndirect : public FifoControllerBase { + +public: + FifoControllerIndirect(uint32_t bufferSize, + std::atomic *readCounterAddress, + std::atomic *writeCounterAddress); + virtual ~FifoControllerIndirect() = default; + + virtual uint64_t getReadCounter() const override { + return mReadCounterAddress->load(std::memory_order_acquire); + } + virtual void setReadCounter(uint64_t n) override { + mReadCounterAddress->store(n, std::memory_order_release); + } + virtual void incrementReadCounter(uint64_t n) override { + mReadCounterAddress->fetch_add(n, std::memory_order_acq_rel); + } + virtual uint64_t getWriteCounter() const override { + return mWriteCounterAddress->load(std::memory_order_acquire); + } + virtual void setWriteCounter(uint64_t n) override { + mWriteCounterAddress->store(n, std::memory_order_release); + } + virtual void incrementWriteCounter(uint64_t n) override { + mWriteCounterAddress->fetch_add(n, std::memory_order_acq_rel); + } + +private: + + std::atomic *mReadCounterAddress; + std::atomic *mWriteCounterAddress; + +}; + +} // namespace oboe + +#endif //NATIVEOBOE_FIFOCONTROLLERINDIRECT_H diff --git a/src/third_party/oboe/src/flowgraph/ClipToRange.cpp b/src/third_party/oboe/src/flowgraph/ClipToRange.cpp new file mode 100644 index 0000000..d2f8a02 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/ClipToRange.cpp @@ -0,0 +1,38 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "FlowGraphNode.h" +#include "ClipToRange.h" + +using namespace flowgraph; + +ClipToRange::ClipToRange(int32_t channelCount) + : FlowGraphFilter(channelCount) { +} + +int32_t ClipToRange::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + + int32_t numSamples = numFrames * output.getSamplesPerFrame(); + for (int32_t i = 0; i < numSamples; i++) { + *outputBuffer++ = std::min(mMaximum, std::max(mMinimum, *inputBuffer++)); + } + + return numFrames; +} diff --git a/src/third_party/oboe/src/flowgraph/ClipToRange.h b/src/third_party/oboe/src/flowgraph/ClipToRange.h new file mode 100644 index 0000000..22b7804 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/ClipToRange.h @@ -0,0 +1,68 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_CLIP_TO_RANGE_H +#define FLOWGRAPH_CLIP_TO_RANGE_H + +#include +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +// This is 3 dB, (10^(3/20)), to match the maximum headroom in AudioTrack for float data. +// It is designed to allow occasional transient peaks. +constexpr float kDefaultMaxHeadroom = 1.41253754f; +constexpr float kDefaultMinHeadroom = -kDefaultMaxHeadroom; + +class ClipToRange : public FlowGraphFilter { +public: + explicit ClipToRange(int32_t channelCount); + + virtual ~ClipToRange() = default; + + int32_t onProcess(int32_t numFrames) override; + + void setMinimum(float min) { + mMinimum = min; + } + + float getMinimum() const { + return mMinimum; + } + + void setMaximum(float min) { + mMaximum = min; + } + + float getMaximum() const { + return mMaximum; + } + + const char *getName() override { + return "ClipToRange"; + } + +private: + float mMinimum = kDefaultMinHeadroom; + float mMaximum = kDefaultMaxHeadroom; +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_CLIP_TO_RANGE_H diff --git a/src/third_party/oboe/src/flowgraph/FlowGraphNode.cpp b/src/third_party/oboe/src/flowgraph/FlowGraphNode.cpp new file mode 100644 index 0000000..bb6ecc9 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/FlowGraphNode.cpp @@ -0,0 +1,111 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "stdio.h" +#include +#include +#include "FlowGraphNode.h" + +using namespace flowgraph; + +/***************************************************************************/ +int32_t FlowGraphNode::pullData(int64_t framePosition, int32_t numFrames) { + int32_t frameCount = numFrames; + // Prevent recursion and multiple execution of nodes. + if (framePosition <= mLastFramePosition && !mBlockRecursion) { + mBlockRecursion = true; // for cyclic graphs + if (mDataPulledAutomatically) { + // Pull from all the upstream nodes. + for (auto &port : mInputPorts) { + // TODO fix bug of leaving unused data in some ports if using multiple AudioSource + frameCount = port.get().pullData(framePosition, frameCount); + } + } + if (frameCount > 0) { + frameCount = onProcess(frameCount); + } + mLastFramePosition += frameCount; + mBlockRecursion = false; + mLastFrameCount = frameCount; + } else { + frameCount = mLastFrameCount; + } + return frameCount; +} + +void FlowGraphNode::pullReset() { + if (!mBlockRecursion) { + mBlockRecursion = true; // for cyclic graphs + // Pull reset from all the upstream nodes. + for (auto &port : mInputPorts) { + port.get().pullReset(); + } + mBlockRecursion = false; + reset(); + } +} + +void FlowGraphNode::reset() { + mLastFrameCount = 0; +} + +/***************************************************************************/ +FlowGraphPortFloat::FlowGraphPortFloat(FlowGraphNode &parent, + int32_t samplesPerFrame, + int32_t framesPerBuffer) + : FlowGraphPort(parent, samplesPerFrame) + , mFramesPerBuffer(framesPerBuffer) + , mBuffer(nullptr) { + size_t numFloats = static_cast(framesPerBuffer * getSamplesPerFrame()); + mBuffer = std::make_unique(numFloats); +} + +/***************************************************************************/ +int32_t FlowGraphPortFloatOutput::pullData(int64_t framePosition, int32_t numFrames) { + numFrames = std::min(getFramesPerBuffer(), numFrames); + return mContainingNode.pullData(framePosition, numFrames); +} + +void FlowGraphPortFloatOutput::pullReset() { + mContainingNode.pullReset(); +} + +// These need to be in the .cpp file because of forward cross references. +void FlowGraphPortFloatOutput::connect(FlowGraphPortFloatInput *port) { + port->connect(this); +} + +void FlowGraphPortFloatOutput::disconnect(FlowGraphPortFloatInput *port) { + port->disconnect(this); +} + +/***************************************************************************/ +int32_t FlowGraphPortFloatInput::pullData(int64_t framePosition, int32_t numFrames) { + return (mConnected == nullptr) + ? std::min(getFramesPerBuffer(), numFrames) + : mConnected->pullData(framePosition, numFrames); +} +void FlowGraphPortFloatInput::pullReset() { + if (mConnected != nullptr) mConnected->pullReset(); +} + +float *FlowGraphPortFloatInput::getBuffer() { + if (mConnected == nullptr) { + return FlowGraphPortFloat::getBuffer(); // loaded using setValue() + } else { + return mConnected->getBuffer(); + } +} diff --git a/src/third_party/oboe/src/flowgraph/FlowGraphNode.h b/src/third_party/oboe/src/flowgraph/FlowGraphNode.h new file mode 100644 index 0000000..007131e --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/FlowGraphNode.h @@ -0,0 +1,422 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * FlowGraph.h + * + * Processing node and ports that can be used in a simple data flow graph. + * This was designed to work with audio but could be used for other + * types of data. + */ + +#ifndef FLOWGRAPH_FLOW_GRAPH_NODE_H +#define FLOWGRAPH_FLOW_GRAPH_NODE_H + +#include +#include +#include +#include +#include +#include +#include +#include + +// TODO Move these classes into separate files. +// TODO Review use of raw pointers for connect(). Maybe use smart pointers but need to avoid +// run-time deallocation in audio thread. + +// Set this to 1 if using it inside the Android framework. +// This code is kept here so that it can be moved easily between Oboe and AAudio. +#define FLOWGRAPH_ANDROID_INTERNAL 0 + +namespace flowgraph { + +// Default block size that can be overridden when the FlowGraphPortFloat is created. +// If it is too small then we will have too much overhead from switching between nodes. +// If it is too high then we will thrash the caches. +constexpr int kDefaultBufferSize = 8; // arbitrary + +class FlowGraphPort; +class FlowGraphPortFloatInput; + +/***************************************************************************/ +/** + * Base class for all nodes in the flowgraph. + */ +class FlowGraphNode { +public: + FlowGraphNode() {} + virtual ~FlowGraphNode() = default; + + /** + * Read from the input ports, + * generate multiple frames of data then write the results to the output ports. + * + * @param numFrames maximum number of frames requested for processing + * @return number of frames actually processed + */ + virtual int32_t onProcess(int32_t numFrames) = 0; + + /** + * If the framePosition is at or after the last frame position then call onProcess(). + * This prevents infinite recursion in case of cyclic graphs. + * It also prevents nodes upstream from a branch from being executed twice. + * + * @param framePosition + * @param numFrames + * @return number of frames valid + */ + int32_t pullData(int64_t framePosition, int32_t numFrames); + + /** + * Recursively reset all the nodes in the graph, starting from a Sink. + * + * This must not be called at the same time as pullData! + */ + void pullReset(); + + /** + * Reset framePosition counters. + */ + virtual void reset(); + + void addInputPort(FlowGraphPort &port) { + mInputPorts.push_back(port); + } + + bool isDataPulledAutomatically() const { + return mDataPulledAutomatically; + } + + /** + * Set true if you want the data pulled through the graph automatically. + * This is the default. + * + * Set false if you want to pull the data from the input ports in the onProcess() method. + * You might do this, for example, in a sample rate converting node. + * + * @param automatic + */ + void setDataPulledAutomatically(bool automatic) { + mDataPulledAutomatically = automatic; + } + + virtual const char *getName() { + return "FlowGraph"; + } + + int64_t getLastFramePosition() { + return mLastFramePosition; + } + +protected: + int64_t mLastFramePosition = 0; + + std::vector> mInputPorts; + +private: + bool mDataPulledAutomatically = true; + bool mBlockRecursion = false; + int32_t mLastFrameCount = 0; + +}; + +/***************************************************************************/ +/** + * This is a connector that allows data to flow between modules. + * + * The ports are the primary means of interacting with a module. + * So they are generally declared as public. + * + */ +class FlowGraphPort { +public: + FlowGraphPort(FlowGraphNode &parent, int32_t samplesPerFrame) + : mContainingNode(parent) + , mSamplesPerFrame(samplesPerFrame) { + } + + // Ports are often declared public. So let's make them non-copyable. + FlowGraphPort(const FlowGraphPort&) = delete; + FlowGraphPort& operator=(const FlowGraphPort&) = delete; + + int32_t getSamplesPerFrame() const { + return mSamplesPerFrame; + } + + virtual int32_t pullData(int64_t framePosition, int32_t numFrames) = 0; + + virtual void pullReset() {} + +protected: + FlowGraphNode &mContainingNode; + +private: + const int32_t mSamplesPerFrame = 1; +}; + +/***************************************************************************/ +/** + * This port contains a 32-bit float buffer that can contain several frames of data. + * Processing the data in a block improves performance. + * + * The size is framesPerBuffer * samplesPerFrame). + */ +class FlowGraphPortFloat : public FlowGraphPort { +public: + FlowGraphPortFloat(FlowGraphNode &parent, + int32_t samplesPerFrame, + int32_t framesPerBuffer = kDefaultBufferSize + ); + + virtual ~FlowGraphPortFloat() = default; + + int32_t getFramesPerBuffer() const { + return mFramesPerBuffer; + } + +protected: + + /** + * @return buffer internal to the port or from a connected port + */ + virtual float *getBuffer() { + return mBuffer.get(); + } + +private: + const int32_t mFramesPerBuffer = 1; + std::unique_ptr mBuffer; // allocated in constructor +}; + +/***************************************************************************/ +/** + * The results of a node's processing are stored in the buffers of the output ports. + */ +class FlowGraphPortFloatOutput : public FlowGraphPortFloat { +public: + FlowGraphPortFloatOutput(FlowGraphNode &parent, int32_t samplesPerFrame) + : FlowGraphPortFloat(parent, samplesPerFrame) { + } + + virtual ~FlowGraphPortFloatOutput() = default; + + using FlowGraphPortFloat::getBuffer; + + /** + * Connect to the input of another module. + * An input port can only have one connection. + * An output port can have multiple connections. + * If you connect a second output port to an input port + * then it overwrites the previous connection. + * + * This not thread safe. Do not modify the graph topology from another thread while running. + * Also do not delete a module while it is connected to another port if the graph is running. + */ + void connect(FlowGraphPortFloatInput *port); + + /** + * Disconnect from the input of another module. + * This not thread safe. + */ + void disconnect(FlowGraphPortFloatInput *port); + + /** + * Call the parent module's onProcess() method. + * That may pull data from its inputs and recursively + * process the entire graph. + * @return number of frames actually pulled + */ + int32_t pullData(int64_t framePosition, int32_t numFrames) override; + + + void pullReset() override; + +}; + +/***************************************************************************/ + +/** + * An input port for streaming audio data. + * You can set a value that will be used for processing. + * If you connect an output port to this port then its value will be used instead. + */ +class FlowGraphPortFloatInput : public FlowGraphPortFloat { +public: + FlowGraphPortFloatInput(FlowGraphNode &parent, int32_t samplesPerFrame) + : FlowGraphPortFloat(parent, samplesPerFrame) { + // Add to parent so it can pull data from each input. + parent.addInputPort(*this); + } + + virtual ~FlowGraphPortFloatInput() = default; + + /** + * If connected to an output port then this will return + * that output ports buffers. + * If not connected then it returns the input ports own buffer + * which can be loaded using setValue(). + */ + float *getBuffer() override; + + /** + * Write every value of the float buffer. + * This value will be ignored if an output port is connected + * to this port. + */ + void setValue(float value) { + int numFloats = kDefaultBufferSize * getSamplesPerFrame(); + float *buffer = getBuffer(); + for (int i = 0; i < numFloats; i++) { + *buffer++ = value; + } + } + + /** + * Connect to the output of another module. + * An input port can only have one connection. + * An output port can have multiple connections. + * This not thread safe. + */ + void connect(FlowGraphPortFloatOutput *port) { + assert(getSamplesPerFrame() == port->getSamplesPerFrame()); + mConnected = port; + } + + void disconnect(FlowGraphPortFloatOutput *port) { + assert(mConnected == port); + (void) port; + mConnected = nullptr; + } + + void disconnect() { + mConnected = nullptr; + } + + /** + * Pull data from any output port that is connected. + */ + int32_t pullData(int64_t framePosition, int32_t numFrames) override; + + void pullReset() override; + +private: + FlowGraphPortFloatOutput *mConnected = nullptr; +}; + +/***************************************************************************/ + +/** + * Base class for an edge node in a graph that has no upstream nodes. + * It outputs data but does not consume data. + * By default, it will read its data from an external buffer. + */ +class FlowGraphSource : public FlowGraphNode { +public: + explicit FlowGraphSource(int32_t channelCount) + : output(*this, channelCount) { + } + + virtual ~FlowGraphSource() = default; + + FlowGraphPortFloatOutput output; +}; + +/***************************************************************************/ + +/** + * Base class for an edge node in a graph that has no upstream nodes. + * It outputs data but does not consume data. + * By default, it will read its data from an external buffer. + */ +class FlowGraphSourceBuffered : public FlowGraphSource { +public: + explicit FlowGraphSourceBuffered(int32_t channelCount) + : FlowGraphSource(channelCount) {} + + virtual ~FlowGraphSourceBuffered() = default; + + /** + * Specify buffer that the node will read from. + * + * @param data TODO Consider using std::shared_ptr. + * @param numFrames + */ + void setData(const void *data, int32_t numFrames) { + mData = data; + mSizeInFrames = numFrames; + mFrameIndex = 0; + } + +protected: + const void *mData = nullptr; + int32_t mSizeInFrames = 0; // number of frames in mData + int32_t mFrameIndex = 0; // index of next frame to be processed +}; + +/***************************************************************************/ +/** + * Base class for an edge node in a graph that has no downstream nodes. + * It consumes data but does not output data. + * This graph will be executed when data is read() from this node + * by pulling data from upstream nodes. + */ +class FlowGraphSink : public FlowGraphNode { +public: + explicit FlowGraphSink(int32_t channelCount) + : input(*this, channelCount) { + } + + virtual ~FlowGraphSink() = default; + + FlowGraphPortFloatInput input; + + /** + * Dummy processor. The work happens in the read() method. + * + * @param numFrames + * @return number of frames actually processed + */ + int32_t onProcess(int32_t numFrames) override { + return numFrames; + } + + virtual int32_t read(int64_t framePosition, void *data, int32_t numFrames) = 0; + +}; + +/***************************************************************************/ +/** + * Base class for a node that has an input and an output with the same number of channels. + * This may include traditional filters, eg. FIR, but also include + * any processing node that converts input to output. + */ +class FlowGraphFilter : public FlowGraphNode { +public: + explicit FlowGraphFilter(int32_t channelCount) + : input(*this, channelCount) + , output(*this, channelCount) { + } + + virtual ~FlowGraphFilter() = default; + + FlowGraphPortFloatInput input; + FlowGraphPortFloatOutput output; +}; + +} /* namespace flowgraph */ + +#endif /* FLOWGRAPH_FLOW_GRAPH_NODE_H */ diff --git a/src/third_party/oboe/src/flowgraph/ManyToMultiConverter.cpp b/src/third_party/oboe/src/flowgraph/ManyToMultiConverter.cpp new file mode 100644 index 0000000..879685e --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/ManyToMultiConverter.cpp @@ -0,0 +1,47 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "ManyToMultiConverter.h" + +using namespace flowgraph; + +ManyToMultiConverter::ManyToMultiConverter(int32_t channelCount) + : inputs(channelCount) + , output(*this, channelCount) { + for (int i = 0; i < channelCount; i++) { + inputs[i] = std::make_unique(*this, 1); + } +} + +int32_t ManyToMultiConverter::onProcess(int32_t numFrames) { + int32_t channelCount = output.getSamplesPerFrame(); + + for (int ch = 0; ch < channelCount; ch++) { + const float *inputBuffer = inputs[ch]->getBuffer(); + float *outputBuffer = output.getBuffer() + ch; + + for (int i = 0; i < numFrames; i++) { + // read one, write into the proper interleaved output channel + float sample = *inputBuffer++; + *outputBuffer = sample; + outputBuffer += channelCount; // advance to next multichannel frame + } + } + return numFrames; +} + diff --git a/src/third_party/oboe/src/flowgraph/ManyToMultiConverter.h b/src/third_party/oboe/src/flowgraph/ManyToMultiConverter.h new file mode 100644 index 0000000..eca4a8e --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/ManyToMultiConverter.h @@ -0,0 +1,49 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_MANY_TO_MULTI_CONVERTER_H +#define FLOWGRAPH_MANY_TO_MULTI_CONVERTER_H + +#include +#include +#include + +#include "FlowGraphNode.h" + +/** + * Combine multiple mono inputs into one interleaved multi-channel output. + */ +class ManyToMultiConverter : public flowgraph::FlowGraphNode { +public: + explicit ManyToMultiConverter(int32_t channelCount); + + virtual ~ManyToMultiConverter() = default; + + int32_t onProcess(int numFrames) override; + + void setEnabled(bool enabled) {} + + std::vector> inputs; + flowgraph::FlowGraphPortFloatOutput output; + + const char *getName() override { + return "ManyToMultiConverter"; + } + +private: +}; + +#endif //FLOWGRAPH_MANY_TO_MULTI_CONVERTER_H diff --git a/src/third_party/oboe/src/flowgraph/MonoToMultiConverter.cpp b/src/third_party/oboe/src/flowgraph/MonoToMultiConverter.cpp new file mode 100644 index 0000000..11cea78 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/MonoToMultiConverter.cpp @@ -0,0 +1,43 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "FlowGraphNode.h" +#include "MonoToMultiConverter.h" + +using namespace flowgraph; + +MonoToMultiConverter::MonoToMultiConverter(int32_t channelCount) + : input(*this, 1) + , output(*this, channelCount) { +} + +MonoToMultiConverter::~MonoToMultiConverter() { } + +int32_t MonoToMultiConverter::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + for (int i = 0; i < numFrames; i++) { + // read one, write many + float sample = *inputBuffer++; + for (int channel = 0; channel < channelCount; channel++) { + *outputBuffer++ = sample; + } + } + return numFrames; +} + diff --git a/src/third_party/oboe/src/flowgraph/MonoToMultiConverter.h b/src/third_party/oboe/src/flowgraph/MonoToMultiConverter.h new file mode 100644 index 0000000..376f7a3 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/MonoToMultiConverter.h @@ -0,0 +1,49 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_MONO_TO_MULTI_CONVERTER_H +#define FLOWGRAPH_MONO_TO_MULTI_CONVERTER_H + +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +/** + * Convert a monophonic stream to a multi-channel stream + * with the same signal on each channel. + */ +class MonoToMultiConverter : public FlowGraphNode { +public: + explicit MonoToMultiConverter(int32_t channelCount); + + virtual ~MonoToMultiConverter(); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "MonoToMultiConverter"; + } + + FlowGraphPortFloatInput input; + FlowGraphPortFloatOutput output; +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_MONO_TO_MULTI_CONVERTER_H diff --git a/src/third_party/oboe/src/flowgraph/RampLinear.cpp b/src/third_party/oboe/src/flowgraph/RampLinear.cpp new file mode 100644 index 0000000..afef018 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/RampLinear.cpp @@ -0,0 +1,77 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "FlowGraphNode.h" +#include "RampLinear.h" + +using namespace flowgraph; + +RampLinear::RampLinear(int32_t channelCount) + : FlowGraphFilter(channelCount) { + mTarget.store(1.0f); +} + +void RampLinear::setLengthInFrames(int32_t frames) { + mLengthInFrames = frames; +} + +void RampLinear::setTarget(float target) { + mTarget.store(target); +} + +float RampLinear::interpolateCurrent() { + return mLevelTo - (mRemaining * mScaler); +} + +int32_t RampLinear::onProcess(int32_t numFrames) { + const float *inputBuffer = input.getBuffer(); + float *outputBuffer = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + + float target = getTarget(); + if (target != mLevelTo) { + // Start new ramp. Continue from previous level. + mLevelFrom = interpolateCurrent(); + mLevelTo = target; + mRemaining = mLengthInFrames; + mScaler = (mLevelTo - mLevelFrom) / mLengthInFrames; // for interpolation + } + + int32_t framesLeft = numFrames; + + if (mRemaining > 0) { // Ramping? This doesn't happen very often. + int32_t framesToRamp = std::min(framesLeft, mRemaining); + framesLeft -= framesToRamp; + while (framesToRamp > 0) { + float currentLevel = interpolateCurrent(); + for (int ch = 0; ch < channelCount; ch++) { + *outputBuffer++ = *inputBuffer++ * currentLevel; + } + mRemaining--; + framesToRamp--; + } + } + + // Process any frames after the ramp. + int32_t samplesLeft = framesLeft * channelCount; + for (int i = 0; i < samplesLeft; i++) { + *outputBuffer++ = *inputBuffer++ * mLevelTo; + } + + return numFrames; +} diff --git a/src/third_party/oboe/src/flowgraph/RampLinear.h b/src/third_party/oboe/src/flowgraph/RampLinear.h new file mode 100644 index 0000000..f285704 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/RampLinear.h @@ -0,0 +1,96 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_RAMP_LINEAR_H +#define FLOWGRAPH_RAMP_LINEAR_H + +#include +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +/** + * When the target is modified then the output will ramp smoothly + * between the original and the new target value. + * This can be used to smooth out control values and reduce pops. + * + * The target may be updated while a ramp is in progress, which will trigger + * a new ramp from the current value. + */ +class RampLinear : public FlowGraphFilter { +public: + explicit RampLinear(int32_t channelCount); + + virtual ~RampLinear() = default; + + int32_t onProcess(int32_t numFrames) override; + + /** + * This is used for the next ramp. + * Calling this does not affect a ramp that is in progress. + */ + void setLengthInFrames(int32_t frames); + + int32_t getLengthInFrames() const { + return mLengthInFrames; + } + + /** + * This may be safely called by another thread. + * @param target + */ + void setTarget(float target); + + float getTarget() const { + return mTarget.load(); + } + + /** + * Force the nextSegment to start from this level. + * + * WARNING: this can cause a discontinuity if called while the ramp is being used. + * Only call this when setting the initial ramp. + * + * @param level + */ + void forceCurrent(float level) { + mLevelFrom = level; + mLevelTo = level; + } + + const char *getName() override { + return "RampLinear"; + } + +private: + + float interpolateCurrent(); + + std::atomic mTarget; + + int32_t mLengthInFrames = 48000.0f / 100.0f ; // 10 msec at 48000 Hz; + int32_t mRemaining = 0; + float mScaler = 0.0f; + float mLevelFrom = 0.0f; + float mLevelTo = 0.0f; +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_RAMP_LINEAR_H diff --git a/src/third_party/oboe/src/flowgraph/SampleRateConverter.cpp b/src/third_party/oboe/src/flowgraph/SampleRateConverter.cpp new file mode 100644 index 0000000..708c684 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SampleRateConverter.cpp @@ -0,0 +1,64 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "SampleRateConverter.h" + +using namespace flowgraph; +using namespace resampler; + +SampleRateConverter::SampleRateConverter(int32_t channelCount, MultiChannelResampler &resampler) + : FlowGraphFilter(channelCount) + , mResampler(resampler) { + setDataPulledAutomatically(false); +} + +// Return true if there is a sample available. +bool SampleRateConverter::isInputAvailable() { + if (mInputCursor >= mNumValidInputFrames) { + mNumValidInputFrames = input.pullData(mInputFramePosition, input.getFramesPerBuffer()); + mInputFramePosition += mNumValidInputFrames; + mInputCursor = 0; + } + return (mInputCursor < mNumValidInputFrames); +} + +const float *SampleRateConverter::getNextInputFrame() { + const float *inputBuffer = input.getBuffer(); + return &inputBuffer[mInputCursor++ * input.getSamplesPerFrame()]; +} + +int32_t SampleRateConverter::onProcess(int32_t numFrames) { + float *outputBuffer = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + int framesLeft = numFrames; + while (framesLeft > 0) { + // Gather input samples as needed. + if(mResampler.isWriteNeeded()) { + if (isInputAvailable()) { + const float *frame = getNextInputFrame(); + mResampler.writeNextFrame(frame); + } else { + break; + } + } else { + // Output frame is interpolated from input samples. + mResampler.readNextFrame(outputBuffer); + outputBuffer += channelCount; + framesLeft--; + } + } + return numFrames - framesLeft; +} diff --git a/src/third_party/oboe/src/flowgraph/SampleRateConverter.h b/src/third_party/oboe/src/flowgraph/SampleRateConverter.h new file mode 100644 index 0000000..d940b22 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SampleRateConverter.h @@ -0,0 +1,56 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SAMPLE_RATE_CONVERTER_H +#define OBOE_SAMPLE_RATE_CONVERTER_H + +#include +#include + +#include "FlowGraphNode.h" +#include "resampler/MultiChannelResampler.h" + +namespace flowgraph { + +class SampleRateConverter : public FlowGraphFilter { +public: + explicit SampleRateConverter(int32_t channelCount, resampler::MultiChannelResampler &mResampler); + + virtual ~SampleRateConverter() = default; + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SampleRateConverter"; + } + +private: + + // Return true if there is a sample available. + bool isInputAvailable(); + + // This assumes data is available. Only call after calling isInputAvailable(). + const float *getNextInputFrame(); + + resampler::MultiChannelResampler &mResampler; + + int32_t mInputCursor = 0; + int32_t mNumValidInputFrames = 0; + int64_t mInputFramePosition = 0; // monotonic counter of input frames used for pullData + +}; +} /* namespace flowgraph */ +#endif //OBOE_SAMPLE_RATE_CONVERTER_H diff --git a/src/third_party/oboe/src/flowgraph/SinkFloat.cpp b/src/third_party/oboe/src/flowgraph/SinkFloat.cpp new file mode 100644 index 0000000..f830daf --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SinkFloat.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include +#include "FlowGraphNode.h" +#include "SinkFloat.h" + +using namespace flowgraph; + +SinkFloat::SinkFloat(int32_t channelCount) + : FlowGraphSink(channelCount) { +} + +int32_t SinkFloat::read(int64_t framePosition, void *data, int32_t numFrames) { + // printf("SinkFloat::read(,,%d)\n", numFrames); + float *floatData = (float *) data; + int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesPulled = pullData(framePosition, framesLeft); + // printf("SinkFloat::read: framesLeft = %d, framesPulled = %d\n", framesLeft, framesPulled); + if (framesPulled <= 0) { + break; + } + const float *signal = input.getBuffer(); + int32_t numSamples = framesPulled * channelCount; + memcpy(floatData, signal, numSamples * sizeof(float)); + floatData += numSamples; + framesLeft -= framesPulled; + framePosition += framesPulled; + } + // printf("SinkFloat returning %d\n", numFrames - framesLeft); + return numFrames - framesLeft; +} diff --git a/src/third_party/oboe/src/flowgraph/SinkFloat.h b/src/third_party/oboe/src/flowgraph/SinkFloat.h new file mode 100644 index 0000000..b6474d1 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SinkFloat.h @@ -0,0 +1,44 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#ifndef FLOWGRAPH_SINK_FLOAT_H +#define FLOWGRAPH_SINK_FLOAT_H + +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +/** + * AudioSink that lets you read data as 32-bit floats. + */ +class SinkFloat : public FlowGraphSink { +public: + explicit SinkFloat(int32_t channelCount); + + int32_t read(int64_t framePosition, void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkFloat"; + } +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_SINK_FLOAT_H diff --git a/src/third_party/oboe/src/flowgraph/SinkI16.cpp b/src/third_party/oboe/src/flowgraph/SinkI16.cpp new file mode 100644 index 0000000..a5904e8 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SinkI16.cpp @@ -0,0 +1,58 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "SinkI16.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace flowgraph; + +SinkI16::SinkI16(int32_t channelCount) + : FlowGraphSink(channelCount) {} + +int32_t SinkI16::read(int64_t framePosition, void *data, int32_t numFrames) { + int16_t *shortData = (int16_t *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesRead = pullData(framePosition, framesLeft); + if (framesRead <= 0) { + break; + } + const float *signal = input.getBuffer(); + int32_t numSamples = framesRead * channelCount; +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_i16_from_float(shortData, signal, numSamples); + shortData += numSamples; + signal += numSamples; +#else + for (int i = 0; i < numSamples; i++) { + int32_t n = (int32_t) (*signal++ * 32768.0f); + *shortData++ = std::min(INT16_MAX, std::max(INT16_MIN, n)); // clip + } +#endif + framesLeft -= framesRead; + framePosition += framesRead; + } + return numFrames - framesLeft; +} diff --git a/src/third_party/oboe/src/flowgraph/SinkI16.h b/src/third_party/oboe/src/flowgraph/SinkI16.h new file mode 100644 index 0000000..2cfdfb0 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SinkI16.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SINK_I16_H +#define FLOWGRAPH_SINK_I16_H + +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +/** + * AudioSink that lets you read data as 16-bit signed integers. + */ +class SinkI16 : public FlowGraphSink { +public: + explicit SinkI16(int32_t channelCount); + + int32_t read(int64_t framePosition, void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkI16"; + } +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_SINK_I16_H diff --git a/src/third_party/oboe/src/flowgraph/SinkI24.cpp b/src/third_party/oboe/src/flowgraph/SinkI24.cpp new file mode 100644 index 0000000..b944b77 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SinkI24.cpp @@ -0,0 +1,67 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + + +#include "FlowGraphNode.h" +#include "SinkI24.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace flowgraph; + +SinkI24::SinkI24(int32_t channelCount) + : FlowGraphSink(channelCount) {} + +int32_t SinkI24::read(int64_t framePosition, void *data, int32_t numFrames) { + uint8_t *byteData = (uint8_t *) data; + const int32_t channelCount = input.getSamplesPerFrame(); + + int32_t framesLeft = numFrames; + while (framesLeft > 0) { + // Run the graph and pull data through the input port. + int32_t framesRead = pullData(framePosition, framesLeft); + if (framesRead <= 0) { + break; + } + const float *floatData = input.getBuffer(); + int32_t numSamples = framesRead * channelCount; +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_p24_from_float(byteData, floatData, numSamples); + static const int kBytesPerI24Packed = 3; + byteData += numSamples * kBytesPerI24Packed; + floatData += numSamples; +#else + const int32_t kI24PackedMax = 0x007FFFFF; + const int32_t kI24PackedMin = 0xFF800000; + for (int i = 0; i < numSamples; i++) { + int32_t n = (int32_t) (*floatData++ * 0x00800000); + n = std::min(kI24PackedMax, std::max(kI24PackedMin, n)); // clip + // Write as a packed 24-bit integer in Little Endian format. + *byteData++ = (uint8_t) n; + *byteData++ = (uint8_t) (n >> 8); + *byteData++ = (uint8_t) (n >> 16); + } +#endif + framesLeft -= framesRead; + framePosition += framesRead; + } + return numFrames - framesLeft; +} diff --git a/src/third_party/oboe/src/flowgraph/SinkI24.h b/src/third_party/oboe/src/flowgraph/SinkI24.h new file mode 100644 index 0000000..7477c8d --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SinkI24.h @@ -0,0 +1,44 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SINK_I24_H +#define FLOWGRAPH_SINK_I24_H + +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +/** + * AudioSink that lets you read data as packed 24-bit signed integers. + * The sample size is 3 bytes. + */ +class SinkI24 : public FlowGraphSink { +public: + explicit SinkI24(int32_t channelCount); + + int32_t read(int64_t framePosition, void *data, int32_t numFrames) override; + + const char *getName() override { + return "SinkI24"; + } +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_SINK_I24_H diff --git a/src/third_party/oboe/src/flowgraph/SourceFloat.cpp b/src/third_party/oboe/src/flowgraph/SourceFloat.cpp new file mode 100644 index 0000000..f574d84 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SourceFloat.cpp @@ -0,0 +1,43 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common/OboeDebug.h" +#include +#include +#include "FlowGraphNode.h" +#include "SourceFloat.h" + +using namespace flowgraph; + +SourceFloat::SourceFloat(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceFloat::onProcess(int32_t numFrames) { + float *outputBuffer = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + + int32_t framesLeft = mSizeInFrames - mFrameIndex; + int32_t framesToProcess = std::min(numFrames, framesLeft); + int32_t numSamples = framesToProcess * channelCount; + + const float *floatBase = (float *) mData; + const float *floatData = &floatBase[mFrameIndex * channelCount]; + memcpy(outputBuffer, floatData, numSamples * sizeof(float)); + mFrameIndex += framesToProcess; + return framesToProcess; +} + diff --git a/src/third_party/oboe/src/flowgraph/SourceFloat.h b/src/third_party/oboe/src/flowgraph/SourceFloat.h new file mode 100644 index 0000000..4de1b41 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SourceFloat.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_FLOAT_H +#define FLOWGRAPH_SOURCE_FLOAT_H + +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +/** + * AudioSource that reads a block of pre-defined float data. + */ +class SourceFloat : public FlowGraphSourceBuffered { +public: + explicit SourceFloat(int32_t channelCount); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceFloat"; + } +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_SOURCE_FLOAT_H diff --git a/src/third_party/oboe/src/flowgraph/SourceI16.cpp b/src/third_party/oboe/src/flowgraph/SourceI16.cpp new file mode 100644 index 0000000..8813023 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SourceI16.cpp @@ -0,0 +1,54 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#include "FlowGraphNode.h" +#include "SourceI16.h" + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +using namespace flowgraph; + +SourceI16::SourceI16(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceI16::onProcess(int32_t numFrames) { + float *floatData = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + + int32_t framesLeft = mSizeInFrames - mFrameIndex; + int32_t framesToProcess = std::min(numFrames, framesLeft); + int32_t numSamples = framesToProcess * channelCount; + + const int16_t *shortBase = static_cast(mData); + const int16_t *shortData = &shortBase[mFrameIndex * channelCount]; + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_i16(floatData, shortData, numSamples); +#else + for (int i = 0; i < numSamples; i++) { + *floatData++ = *shortData++ * (1.0f / 32768); + } +#endif + + mFrameIndex += framesToProcess; + return framesToProcess; +} \ No newline at end of file diff --git a/src/third_party/oboe/src/flowgraph/SourceI16.h b/src/third_party/oboe/src/flowgraph/SourceI16.h new file mode 100644 index 0000000..fe440b2 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SourceI16.h @@ -0,0 +1,42 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_I16_H +#define FLOWGRAPH_SOURCE_I16_H + +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { +/** + * AudioSource that reads a block of pre-defined 16-bit integer data. + */ +class SourceI16 : public FlowGraphSourceBuffered { +public: + explicit SourceI16(int32_t channelCount); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI16"; + } +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_SOURCE_I16_H diff --git a/src/third_party/oboe/src/flowgraph/SourceI24.cpp b/src/third_party/oboe/src/flowgraph/SourceI24.cpp new file mode 100644 index 0000000..1975878 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SourceI24.cpp @@ -0,0 +1,65 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include + +#if FLOWGRAPH_ANDROID_INTERNAL +#include +#endif + +#include "FlowGraphNode.h" +#include "SourceI24.h" + +using namespace flowgraph; + +constexpr int kBytesPerI24Packed = 3; + +SourceI24::SourceI24(int32_t channelCount) + : FlowGraphSourceBuffered(channelCount) { +} + +int32_t SourceI24::onProcess(int32_t numFrames) { + float *floatData = output.getBuffer(); + int32_t channelCount = output.getSamplesPerFrame(); + + int32_t framesLeft = mSizeInFrames - mFrameIndex; + int32_t framesToProcess = std::min(numFrames, framesLeft); + int32_t numSamples = framesToProcess * channelCount; + + const uint8_t *byteBase = (uint8_t *) mData; + const uint8_t *byteData = &byteBase[mFrameIndex * channelCount * kBytesPerI24Packed]; + +#if FLOWGRAPH_ANDROID_INTERNAL + memcpy_to_float_from_p24(floatData, byteData, numSamples); +#else + static const float scale = 1. / (float)(1UL << 31); + for (int i = 0; i < numSamples; i++) { + // Assemble the data assuming Little Endian format. + int32_t pad = byteData[2]; + pad <<= 8; + pad |= byteData[1]; + pad <<= 8; + pad |= byteData[0]; + pad <<= 8; // Shift to 32 bit data so the sign is correct. + byteData += kBytesPerI24Packed; + *floatData++ = pad * scale; // scale to range -1.0 to 1.0 + } +#endif + + mFrameIndex += framesToProcess; + return framesToProcess; +} \ No newline at end of file diff --git a/src/third_party/oboe/src/flowgraph/SourceI24.h b/src/third_party/oboe/src/flowgraph/SourceI24.h new file mode 100644 index 0000000..3779534 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/SourceI24.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef FLOWGRAPH_SOURCE_I24_H +#define FLOWGRAPH_SOURCE_I24_H + +#include +#include + +#include "FlowGraphNode.h" + +namespace flowgraph { + +/** + * AudioSource that reads a block of pre-defined 24-bit packed integer data. + */ +class SourceI24 : public FlowGraphSourceBuffered { +public: + explicit SourceI24(int32_t channelCount); + + int32_t onProcess(int32_t numFrames) override; + + const char *getName() override { + return "SourceI24"; + } +}; + +} /* namespace flowgraph */ + +#endif //FLOWGRAPH_SOURCE_I24_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/HyperbolicCosineWindow.h b/src/third_party/oboe/src/flowgraph/resampler/HyperbolicCosineWindow.h new file mode 100644 index 0000000..f6479ae --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/HyperbolicCosineWindow.h @@ -0,0 +1,68 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_HYPERBOLIC_COSINE_WINDOW_H +#define RESAMPLER_HYPERBOLIC_COSINE_WINDOW_H + +#include + +namespace resampler { + +/** + * Calculate a HyperbolicCosineWindow window centered at 0. + * This can be used in place of a Kaiser window. + * + * The code is based on an anonymous contribution by "a concerned citizen": + * https://dsp.stackexchange.com/questions/37714/kaiser-window-approximation + */ +class HyperbolicCosineWindow { +public: + HyperbolicCosineWindow() { + setStopBandAttenuation(60); + } + + /** + * @param attenuation typical values range from 30 to 90 dB + * @return beta + */ + double setStopBandAttenuation(double attenuation) { + double alpha = ((-325.1e-6 * attenuation + 0.1677) * attenuation) - 3.149; + setAlpha(alpha); + return alpha; + } + + void setAlpha(double alpha) { + mAlpha = alpha; + mInverseCoshAlpha = 1.0 / cosh(alpha); + } + + /** + * @param x ranges from -1.0 to +1.0 + */ + double operator()(double x) { + double x2 = x * x; + if (x2 >= 1.0) return 0.0; + double w = mAlpha * sqrt(1.0 - x2); + return cosh(w) * mInverseCoshAlpha; + } + +private: + double mAlpha = 0.0; + double mInverseCoshAlpha = 1.0; +}; + +} // namespace resampler +#endif //RESAMPLER_HYPERBOLIC_COSINE_WINDOW_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.cpp b/src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.cpp new file mode 100644 index 0000000..4bd75b3 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "IntegerRatio.h" + +using namespace resampler; + +// Enough primes to cover the common sample rates. +static const int kPrimes[] = { + 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, + 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97, + 101, 103, 107, 109, 113, 127, 131, 137, 139, 149, + 151, 157, 163, 167, 173, 179, 181, 191, 193, 197, 199}; + +void IntegerRatio::reduce() { + for (int prime : kPrimes) { + if (mNumerator < prime || mDenominator < prime) { + break; + } + + // Find biggest prime factor for numerator. + while (true) { + int top = mNumerator / prime; + int bottom = mDenominator / prime; + if ((top >= 1) + && (bottom >= 1) + && (top * prime == mNumerator) // divided evenly? + && (bottom * prime == mDenominator)) { + mNumerator = top; + mDenominator = bottom; + } else { + break; + } + } + + } +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.h b/src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.h new file mode 100644 index 0000000..fb390f1 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/IntegerRatio.h @@ -0,0 +1,52 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_INTEGER_RATIO_H +#define OBOE_INTEGER_RATIO_H + +#include + +namespace resampler { + +/** + * Represent the ratio of two integers. + */ +class IntegerRatio { +public: + IntegerRatio(int32_t numerator, int32_t denominator) + : mNumerator(numerator), mDenominator(denominator) {} + + /** + * Reduce by removing common prime factors. + */ + void reduce(); + + int32_t getNumerator() { + return mNumerator; + } + + int32_t getDenominator() { + return mDenominator; + } + +private: + int32_t mNumerator; + int32_t mDenominator; +}; + +} + +#endif //OBOE_INTEGER_RATIO_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/KaiserWindow.h b/src/third_party/oboe/src/flowgraph/resampler/KaiserWindow.h new file mode 100644 index 0000000..73dbc41 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/KaiserWindow.h @@ -0,0 +1,87 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef RESAMPLER_KAISER_WINDOW_H +#define RESAMPLER_KAISER_WINDOW_H + +#include + +namespace resampler { + +/** + * Calculate a Kaiser window centered at 0. + */ +class KaiserWindow { +public: + KaiserWindow() { + setStopBandAttenuation(60); + } + + /** + * @param attenuation typical values range from 30 to 90 dB + * @return beta + */ + double setStopBandAttenuation(double attenuation) { + double beta = 0.0; + if (attenuation > 50) { + beta = 0.1102 * (attenuation - 8.7); + } else if (attenuation >= 21) { + double a21 = attenuation - 21; + beta = 0.5842 * pow(a21, 0.4) + (0.07886 * a21); + } + setBeta(beta); + return beta; + } + + void setBeta(double beta) { + mBeta = beta; + mInverseBesselBeta = 1.0 / bessel(beta); + } + + /** + * @param x ranges from -1.0 to +1.0 + */ + double operator()(double x) { + double x2 = x * x; + if (x2 >= 1.0) return 0.0; + double w = mBeta * sqrt(1.0 - x2); + return bessel(w) * mInverseBesselBeta; + } + + // Approximation of a + // modified zero order Bessel function of the first kind. + // Based on a discussion at: + // https://dsp.stackexchange.com/questions/37714/kaiser-window-approximation + static double bessel(double x) { + double y = cosh(0.970941817426052 * x); + y += cosh(0.8854560256532099 * x); + y += cosh(0.7485107481711011 * x); + y += cosh(0.5680647467311558 * x); + y += cosh(0.3546048870425356 * x); + y += cosh(0.120536680255323 * x); + y *= 2; + y += cosh(x); + y /= 13; + return y; + } + +private: + double mBeta = 0.0; + double mInverseBesselBeta = 1.0; +}; + +} // namespace resampler +#endif //RESAMPLER_KAISER_WINDOW_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/LinearResampler.cpp b/src/third_party/oboe/src/flowgraph/resampler/LinearResampler.cpp new file mode 100644 index 0000000..a7748c1 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/LinearResampler.cpp @@ -0,0 +1,42 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "LinearResampler.h" + +using namespace resampler; + +LinearResampler::LinearResampler(const MultiChannelResampler::Builder &builder) + : MultiChannelResampler(builder) { + mPreviousFrame = std::make_unique(getChannelCount()); + mCurrentFrame = std::make_unique(getChannelCount()); +} + +void LinearResampler::writeFrame(const float *frame) { + memcpy(mPreviousFrame.get(), mCurrentFrame.get(), sizeof(float) * getChannelCount()); + memcpy(mCurrentFrame.get(), frame, sizeof(float) * getChannelCount()); +} + +void LinearResampler::readFrame(float *frame) { + float *previous = mPreviousFrame.get(); + float *current = mCurrentFrame.get(); + float phase = (float) getIntegerPhase() / mDenominator; + // iterate across samples in the frame + for (int channel = 0; channel < getChannelCount(); channel++) { + float f0 = *previous++; + float f1 = *current++; + *frame++ = f0 + (phase * (f1 - f0)); + } +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/LinearResampler.h b/src/third_party/oboe/src/flowgraph/resampler/LinearResampler.h new file mode 100644 index 0000000..5dcc881 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/LinearResampler.h @@ -0,0 +1,44 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_LINEAR_RESAMPLER_H +#define OBOE_LINEAR_RESAMPLER_H + +#include +#include +#include +#include "MultiChannelResampler.h" + +namespace resampler { + +/** + * Simple resampler that uses bi-linear interpolation. + */ +class LinearResampler : public MultiChannelResampler { +public: + LinearResampler(const MultiChannelResampler::Builder &builder); + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; + +private: + std::unique_ptr mPreviousFrame; + std::unique_ptr mCurrentFrame; +}; + +} // namespace resampler +#endif //OBOE_LINEAR_RESAMPLER_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.cpp b/src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.cpp new file mode 100644 index 0000000..d630520 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.cpp @@ -0,0 +1,171 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "IntegerRatio.h" +#include "LinearResampler.h" +#include "MultiChannelResampler.h" +#include "PolyphaseResampler.h" +#include "PolyphaseResamplerMono.h" +#include "PolyphaseResamplerStereo.h" +#include "SincResampler.h" +#include "SincResamplerStereo.h" + +using namespace resampler; + +MultiChannelResampler::MultiChannelResampler(const MultiChannelResampler::Builder &builder) + : mNumTaps(builder.getNumTaps()) + , mX(builder.getChannelCount() * builder.getNumTaps() * 2) + , mSingleFrame(builder.getChannelCount()) + , mChannelCount(builder.getChannelCount()) + { + // Reduce sample rates to the smallest ratio. + // For example 44100/48000 would become 147/160. + IntegerRatio ratio(builder.getInputRate(), builder.getOutputRate()); + ratio.reduce(); + mNumerator = ratio.getNumerator(); + mDenominator = ratio.getDenominator(); + mIntegerPhase = mDenominator; +} + +// static factory method +MultiChannelResampler *MultiChannelResampler::make(int32_t channelCount, + int32_t inputRate, + int32_t outputRate, + Quality quality) { + Builder builder; + builder.setInputRate(inputRate); + builder.setOutputRate(outputRate); + builder.setChannelCount(channelCount); + + switch (quality) { + case Quality::Fastest: + builder.setNumTaps(2); + break; + case Quality::Low: + builder.setNumTaps(4); + break; + case Quality::Medium: + default: + builder.setNumTaps(8); + break; + case Quality::High: + builder.setNumTaps(16); + break; + case Quality::Best: + builder.setNumTaps(32); + break; + } + + // Set the cutoff frequency so that we do not get aliasing when down-sampling. + if (inputRate > outputRate) { + builder.setNormalizedCutoff(kDefaultNormalizedCutoff); + } + return builder.build(); +} + +MultiChannelResampler *MultiChannelResampler::Builder::build() { + if (getNumTaps() == 2) { + // Note that this does not do low pass filteringh. + return new LinearResampler(*this); + } + IntegerRatio ratio(getInputRate(), getOutputRate()); + ratio.reduce(); + bool usePolyphase = (getNumTaps() * ratio.getDenominator()) <= kMaxCoefficients; + if (usePolyphase) { + if (getChannelCount() == 1) { + return new PolyphaseResamplerMono(*this); + } else if (getChannelCount() == 2) { + return new PolyphaseResamplerStereo(*this); + } else { + return new PolyphaseResampler(*this); + } + } else { + // Use less optimized resampler that uses a float phaseIncrement. + // TODO mono resampler + if (getChannelCount() == 2) { + return new SincResamplerStereo(*this); + } else { + return new SincResampler(*this); + } + } +} + +void MultiChannelResampler::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[mCursor * getChannelCount()]; + int offset = getNumTaps() * getChannelCount(); + for (int channel = 0; channel < getChannelCount(); channel++) { + // Write twice so we avoid having to wrap when reading. + dest[channel] = dest[channel + offset] = frame[channel]; + } +} + +float MultiChannelResampler::sinc(float radians) { + if (abs(radians) < 1.0e-9) return 1.0f; // avoid divide by zero + return sinf(radians) / radians; // Sinc function +} + +// Generate coefficients in the order they will be used by readFrame(). +// This is more complicated but readFrame() is called repeatedly and should be optimized. +void MultiChannelResampler::generateCoefficients(int32_t inputRate, + int32_t outputRate, + int32_t numRows, + double phaseIncrement, + float normalizedCutoff) { + mCoefficients.resize(getNumTaps() * numRows); + int coefficientIndex = 0; + double phase = 0.0; // ranges from 0.0 to 1.0, fraction between samples + // Stretch the sinc function for low pass filtering. + const float cutoffScaler = normalizedCutoff * + ((outputRate < inputRate) + ? ((float)outputRate / inputRate) + : ((float)inputRate / outputRate)); + const int numTapsHalf = getNumTaps() / 2; // numTaps must be even. + const float numTapsHalfInverse = 1.0f / numTapsHalf; + for (int i = 0; i < numRows; i++) { + float tapPhase = phase - numTapsHalf; + float gain = 0.0; // sum of raw coefficients + int gainCursor = coefficientIndex; + for (int tap = 0; tap < getNumTaps(); tap++) { + float radians = tapPhase * M_PI; + +#if MCR_USE_KAISER + float window = mKaiserWindow(tapPhase * numTapsHalfInverse); +#else + float window = mCoshWindow(tapPhase * numTapsHalfInverse); +#endif + float coefficient = sinc(radians * cutoffScaler) * window; + mCoefficients.at(coefficientIndex++) = coefficient; + gain += coefficient; + tapPhase += 1.0; + } + phase += phaseIncrement; + while (phase >= 1.0) { + phase -= 1.0; + } + + // Correct for gain variations. + float gainCorrection = 1.0 / gain; // normalize the gain + for (int tap = 0; tap < getNumTaps(); tap++) { + mCoefficients.at(gainCursor + tap) *= gainCorrection; + } + } +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.h b/src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.h new file mode 100644 index 0000000..8b23d81 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/MultiChannelResampler.h @@ -0,0 +1,271 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_MULTICHANNEL_RESAMPLER_H +#define OBOE_MULTICHANNEL_RESAMPLER_H + +#include +#include +#include +#include + +#ifndef MCR_USE_KAISER +// It appears from the spectrogram that the HyperbolicCosine window leads to fewer artifacts. +// And it is faster to calculate. +#define MCR_USE_KAISER 0 +#endif + +#if MCR_USE_KAISER +#include "KaiserWindow.h" +#else +#include "HyperbolicCosineWindow.h" +#endif + +namespace resampler { + +class MultiChannelResampler { + +public: + + enum class Quality : int32_t { + Fastest, + Low, + Medium, + High, + Best, + }; + + class Builder { + public: + /** + * Construct an optimal resampler based on the specified parameters. + * @return address of a resampler + */ + MultiChannelResampler *build(); + + /** + * The number of taps in the resampling filter. + * More taps gives better quality but uses more CPU time. + * This typically ranges from 4 to 64. Default is 16. + * + * For polyphase filters, numTaps must be a multiple of four for loop unrolling. + * @param numTaps number of taps for the filter + * @return address of this builder for chaining calls + */ + Builder *setNumTaps(int32_t numTaps) { + mNumTaps = numTaps; + return this; + } + + /** + * Use 1 for mono, 2 for stereo, etc. Default is 1. + * + * @param channelCount number of channels + * @return address of this builder for chaining calls + */ + Builder *setChannelCount(int32_t channelCount) { + mChannelCount = channelCount; + return this; + } + + /** + * Default is 48000. + * + * @param inputRate sample rate of the input stream + * @return address of this builder for chaining calls + */ + Builder *setInputRate(int32_t inputRate) { + mInputRate = inputRate; + return this; + } + + /** + * Default is 48000. + * + * @param outputRate sample rate of the output stream + * @return address of this builder for chaining calls + */ + Builder *setOutputRate(int32_t outputRate) { + mOutputRate = outputRate; + return this; + } + + /** + * Set cutoff frequency relative to the Nyquist rate of the output sample rate. + * Set to 1.0 to match the Nyquist frequency. + * Set lower to reduce aliasing. + * Default is 0.70. + * + * @param normalizedCutoff anti-aliasing filter cutoff + * @return address of this builder for chaining calls + */ + Builder *setNormalizedCutoff(float normalizedCutoff) { + mNormalizedCutoff = normalizedCutoff; + return this; + } + + int32_t getNumTaps() const { + return mNumTaps; + } + + int32_t getChannelCount() const { + return mChannelCount; + } + + int32_t getInputRate() const { + return mInputRate; + } + + int32_t getOutputRate() const { + return mOutputRate; + } + + float getNormalizedCutoff() const { + return mNormalizedCutoff; + } + + protected: + int32_t mChannelCount = 1; + int32_t mNumTaps = 16; + int32_t mInputRate = 48000; + int32_t mOutputRate = 48000; + float mNormalizedCutoff = kDefaultNormalizedCutoff; + }; + + virtual ~MultiChannelResampler() = default; + + /** + * Factory method for making a resampler that is optimal for the given inputs. + * + * @param channelCount number of channels, 2 for stereo + * @param inputRate sample rate of the input stream + * @param outputRate sample rate of the output stream + * @param quality higher quality sounds better but uses more CPU + * @return an optimal resampler + */ + static MultiChannelResampler *make(int32_t channelCount, + int32_t inputRate, + int32_t outputRate, + Quality quality); + + bool isWriteNeeded() const { + return mIntegerPhase >= mDenominator; + } + + /** + * Write a frame containing N samples. + * + * @param frame pointer to the first sample in a frame + */ + void writeNextFrame(const float *frame) { + writeFrame(frame); + advanceWrite(); + } + + /** + * Read a frame containing N samples. + * + * @param frame pointer to the first sample in a frame + */ + void readNextFrame(float *frame) { + readFrame(frame); + advanceRead(); + } + + int getNumTaps() const { + return mNumTaps; + } + + int getChannelCount() const { + return mChannelCount; + } + + static float hammingWindow(float radians, float spread); + + static float sinc(float radians); + +protected: + + explicit MultiChannelResampler(const MultiChannelResampler::Builder &builder); + + /** + * Write a frame containing N samples. + * Call advanceWrite() after calling this. + * @param frame pointer to the first sample in a frame + */ + virtual void writeFrame(const float *frame); + + /** + * Read a frame containing N samples using interpolation. + * Call advanceRead() after calling this. + * @param frame pointer to the first sample in a frame + */ + virtual void readFrame(float *frame) = 0; + + void advanceWrite() { + mIntegerPhase -= mDenominator; + } + + void advanceRead() { + mIntegerPhase += mNumerator; + } + + /** + * Generate the filter coefficients in optimal order. + * @param inputRate sample rate of the input stream + * @param outputRate sample rate of the output stream + * @param numRows number of rows in the array that contain a set of tap coefficients + * @param phaseIncrement how much to increment the phase between rows + * @param normalizedCutoff filter cutoff frequency normalized to Nyquist rate of output + */ + void generateCoefficients(int32_t inputRate, + int32_t outputRate, + int32_t numRows, + double phaseIncrement, + float normalizedCutoff); + + + int32_t getIntegerPhase() { + return mIntegerPhase; + } + + static constexpr int kMaxCoefficients = 8 * 1024; + std::vector mCoefficients; + + const int mNumTaps; + int mCursor = 0; + std::vector mX; // delayed input values for the FIR + std::vector mSingleFrame; // one frame for temporary use + int32_t mIntegerPhase = 0; + int32_t mNumerator = 0; + int32_t mDenominator = 0; + + +private: + +#if MCR_USE_KAISER + KaiserWindow mKaiserWindow; +#else + HyperbolicCosineWindow mCoshWindow; +#endif + + static constexpr float kDefaultNormalizedCutoff = 0.70f; + + const int mChannelCount; +}; + +} +#endif //OBOE_MULTICHANNEL_RESAMPLER_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.cpp b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.cpp new file mode 100644 index 0000000..4862ad8 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.cpp @@ -0,0 +1,61 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "IntegerRatio.h" +#include "PolyphaseResampler.h" + +using namespace resampler; + +PolyphaseResampler::PolyphaseResampler(const MultiChannelResampler::Builder &builder) + : MultiChannelResampler(builder) + { + assert((getNumTaps() % 4) == 0); // Required for loop unrolling. + + int32_t inputRate = builder.getInputRate(); + int32_t outputRate = builder.getOutputRate(); + + int32_t numRows = mDenominator; + double phaseIncrement = (double) inputRate / (double) outputRate; + generateCoefficients(inputRate, outputRate, + numRows, phaseIncrement, + builder.getNormalizedCutoff()); +} + +void PolyphaseResampler::readFrame(float *frame) { + // Clear accumulator for mixing. + std::fill(mSingleFrame.begin(), mSingleFrame.end(), 0.0); + +// printf("PolyphaseResampler: mCoefficientCursor = %4d\n", mCoefficientCursor); + // Multiply input times windowed sinc function. + float *coefficients = &mCoefficients[mCoefficientCursor]; + float *xFrame = &mX[mCursor * getChannelCount()]; + for (int i = 0; i < mNumTaps; i++) { + float coefficient = *coefficients++; +// printf("PolyphaseResampler: coeff = %10.6f, xFrame[0] = %10.6f\n", coefficient, xFrame[0]); + for (int channel = 0; channel < getChannelCount(); channel++) { + mSingleFrame[channel] += *xFrame++ * coefficient; + } + } + + // Advance and wrap through coefficients. + mCoefficientCursor = (mCoefficientCursor + mNumTaps) % mCoefficients.size(); + + // Copy accumulator to output. + for (int channel = 0; channel < getChannelCount(); channel++) { + frame[channel] = mSingleFrame[channel]; + } +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.h b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.h new file mode 100644 index 0000000..c7de118 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResampler.h @@ -0,0 +1,51 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_POLYPHASE_RESAMPLER_H +#define OBOE_POLYPHASE_RESAMPLER_H + +#include +#include +#include +#include +#include "MultiChannelResampler.h" + +namespace resampler { +/** + * Resample that is optimized for a reduced ratio of sample rates. + * All of the coefficients for eacxh possible phase value are precalculated. + */ +class PolyphaseResampler : public MultiChannelResampler { +public: + /** + * + * @param builder containing lots of parameters + */ + explicit PolyphaseResampler(const MultiChannelResampler::Builder &builder); + + virtual ~PolyphaseResampler() = default; + + void readFrame(float *frame) override; + +protected: + + int32_t mCoefficientCursor = 0; + +}; + +} + +#endif //OBOE_POLYPHASE_RESAMPLER_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.cpp b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.cpp new file mode 100644 index 0000000..2dcdc8e --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.cpp @@ -0,0 +1,62 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PolyphaseResamplerMono.h" + +using namespace resampler; + +#define MONO 1 + +PolyphaseResamplerMono::PolyphaseResamplerMono(const MultiChannelResampler::Builder &builder) + : PolyphaseResampler(builder) { + assert(builder.getChannelCount() == MONO); +} + +void PolyphaseResamplerMono::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[mCursor * MONO]; + const int offset = mNumTaps * MONO; + // Write each channel twice so we avoid having to wrap when running the FIR. + const float sample = frame[0]; + // Put ordered writes together. + dest[0] = sample; + dest[offset] = sample; +} + +void PolyphaseResamplerMono::readFrame(float *frame) { + // Clear accumulator. + float sum = 0.0; + + // Multiply input times precomputed windowed sinc function. + const float *coefficients = &mCoefficients[mCoefficientCursor]; + float *xFrame = &mX[mCursor * MONO]; + const int numLoops = mNumTaps >> 2; // n/4 + for (int i = 0; i < numLoops; i++) { + // Manual loop unrolling, might get converted to SIMD. + sum += *xFrame++ * *coefficients++; + sum += *xFrame++ * *coefficients++; + sum += *xFrame++ * *coefficients++; + sum += *xFrame++ * *coefficients++; + } + + mCoefficientCursor = (mCoefficientCursor + mNumTaps) % mCoefficients.size(); + + // Copy accumulator to output. + frame[0] = sum; +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.h b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.h new file mode 100644 index 0000000..d97b513 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerMono.h @@ -0,0 +1,39 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_POLYPHASE_RESAMPLER_MONO_H +#define OBOE_POLYPHASE_RESAMPLER_MONO_H + +#include +#include +#include "PolyphaseResampler.h" + +namespace resampler { + +class PolyphaseResamplerMono : public PolyphaseResampler { +public: + explicit PolyphaseResamplerMono(const MultiChannelResampler::Builder &builder); + + virtual ~PolyphaseResamplerMono() = default; + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; +}; + +} + +#endif //OBOE_POLYPHASE_RESAMPLER_MONO_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.cpp b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.cpp new file mode 100644 index 0000000..5c73a8e --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.cpp @@ -0,0 +1,78 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "PolyphaseResamplerStereo.h" + +using namespace resampler; + +#define STEREO 2 + +PolyphaseResamplerStereo::PolyphaseResamplerStereo(const MultiChannelResampler::Builder &builder) + : PolyphaseResampler(builder) { + assert(builder.getChannelCount() == STEREO); +} + +void PolyphaseResamplerStereo::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[mCursor * STEREO]; + const int offset = mNumTaps * STEREO; + // Write each channel twice so we avoid having to wrap when running the FIR. + const float left = frame[0]; + const float right = frame[1]; + // Put ordered writes together. + dest[0] = left; + dest[1] = right; + dest[offset] = left; + dest[1 + offset] = right; +} + +void PolyphaseResamplerStereo::readFrame(float *frame) { + // Clear accumulators. + float left = 0.0; + float right = 0.0; + + // Multiply input times precomputed windowed sinc function. + const float *coefficients = &mCoefficients[mCoefficientCursor]; + float *xFrame = &mX[mCursor * STEREO]; + const int numLoops = mNumTaps >> 2; // n/4 + for (int i = 0; i < numLoops; i++) { + // Manual loop unrolling, might get converted to SIMD. + float coefficient = *coefficients++; + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + + coefficient = *coefficients++; // next tap + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + + coefficient = *coefficients++; // next tap + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + + coefficient = *coefficients++; // next tap + left += *xFrame++ * coefficient; + right += *xFrame++ * coefficient; + } + + mCoefficientCursor = (mCoefficientCursor + mNumTaps) % mCoefficients.size(); + + // Copy accumulators to output. + frame[0] = left; + frame[1] = right; +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.h b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.h new file mode 100644 index 0000000..3dfd4e2 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/PolyphaseResamplerStereo.h @@ -0,0 +1,39 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_POLYPHASE_RESAMPLER_STEREO_H +#define OBOE_POLYPHASE_RESAMPLER_STEREO_H + +#include +#include +#include "PolyphaseResampler.h" + +namespace resampler { + +class PolyphaseResamplerStereo : public PolyphaseResampler { +public: + explicit PolyphaseResamplerStereo(const MultiChannelResampler::Builder &builder); + + virtual ~PolyphaseResamplerStereo() = default; + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; +}; + +} + +#endif //OBOE_POLYPHASE_RESAMPLER_STEREO_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/README.md b/src/third_party/oboe/src/flowgraph/resampler/README.md new file mode 100644 index 0000000..2026773 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/README.md @@ -0,0 +1,93 @@ +# Sample Rate Converter + +This folder contains a sample rate converter, or "resampler". +It is part of [Oboe](https://github.com/google/oboe) but has no dependencies on Oboe. +So the contents of this folder can be used outside of Oboe. + +The converter is based on a sinc function that has been windowed by a hyperbolic cosine. +We found this had fewer artifacts than the more traditional Kaiser window. + +## Creating a Resampler + +Include the [main header](MultiChannelResampler.h) for the resampler. + + #include "resampler/MultiChannelResampler.h" + +Here is an example of creating a stereo resampler that will convert from 44100 to 48000 Hz. +Only do this once, when you open your stream. Then use the sample resampler to process multiple buffers. + + MultiChannelResampler *resampler = MultiChannelResampler::make( + 2, // channel count + 44100, // input sampleRate + 48000, // output sampleRate + MultiChannelResampler::Medium); // conversion quality + +Possible values for quality include { Fastest, Low, Medium, High, Best }. +Higher quality levels will sound better but consume more CPU because they have more taps in the filter. + +## Fractional Frame Counts + +Note that the number of output frames generated for a given number of input frames can vary. + +For example, suppose you are converting from 44100 Hz to 48000 Hz and using an input buffer with 940 frames. If you calculate the number of output frames you get: + + 940 * 48000 * 44100 = 1023.1292517... + +You cannot generate a fractional number of frames. So the resampler will sometimes generate 1023 frames and sometimes 1024 frames. On average it will generate 1023.1292517 frames. The resampler stores the fraction internally and keeps track of when to consume or generate a frame. + +You can either use a fixed number of input frames or a fixed number of output frames. The other frame count will vary. + +## Calling the Resampler with a fixed number of OUTPUT frames + +In this example, suppose we have a fixed number of output frames and a variable number of input frames. + +Assume you start with these variables and a method that returns the next input frame: + + float *outputBuffer; // multi-channel buffer to be filled + int numOutputFrames; // number of frames of output + +The resampler has a method isWriteNeeded() that tells you whether to write to or read from the resampler. + + int outputFramesLeft = numOutputFrames; + while (outputFramesLeft > 0) { + if(resampler->isWriteNeeded()) { + const float *frame = getNextInputFrame(); // you provide this + resampler->writeNextFrame(frame); + } else { + resampler->readNextFrame(outputBuffer); + outputBuffer += channelCount; + outputFramesLeft--; + } + } + +## Calling the Resampler with a fixed number of INPUT frames + +In this example, suppose we have a fixed number of input frames and a variable number of output frames. + +Assume you start with these variables: + + float *inputBuffer; // multi-channel buffer to be consumed + float *outputBuffer; // multi-channel buffer to be filled + int numInputFrames; // number of frames of input + int numOutputFrames = 0; + int channelCount; // 1 for mono, 2 for stereo + + int inputFramesLeft = numInputFrames; + while (inputFramesLeft > 0) { + if(resampler->isWriteNeeded()) { + resampler->writeNextFrame(inputBuffer); + inputBuffer += channelCount; + inputFramesLeft--; + } else { + resampler->readNextFrame(outputBuffer); + outputBuffer += channelCount; + numOutputFrames++; + } + } + +## Deleting the Resampler + +When you are done, you should delete the Resampler to avoid a memory leak. + + delete resampler; + diff --git a/src/third_party/oboe/src/flowgraph/resampler/SincResampler.cpp b/src/third_party/oboe/src/flowgraph/resampler/SincResampler.cpp new file mode 100644 index 0000000..1084356 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/SincResampler.cpp @@ -0,0 +1,76 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include +#include "SincResampler.h" + +using namespace resampler; + +SincResampler::SincResampler(const MultiChannelResampler::Builder &builder) + : MultiChannelResampler(builder) + , mSingleFrame2(builder.getChannelCount()) { + assert((getNumTaps() % 4) == 0); // Required for loop unrolling. + mNumRows = kMaxCoefficients / getNumTaps(); // no guard row needed +// printf("SincResampler: numRows = %d\n", mNumRows); + mPhaseScaler = (double) mNumRows / mDenominator; + double phaseIncrement = 1.0 / mNumRows; + generateCoefficients(builder.getInputRate(), + builder.getOutputRate(), + mNumRows, + phaseIncrement, + builder.getNormalizedCutoff()); +} + +void SincResampler::readFrame(float *frame) { + // Clear accumulator for mixing. + std::fill(mSingleFrame.begin(), mSingleFrame.end(), 0.0); + std::fill(mSingleFrame2.begin(), mSingleFrame2.end(), 0.0); + + // Determine indices into coefficients table. + double tablePhase = getIntegerPhase() * mPhaseScaler; + int index1 = static_cast(floor(tablePhase)); + if (index1 >= mNumRows) { // no guard row needed because we wrap the indices + tablePhase -= mNumRows; + index1 -= mNumRows; + } + + int index2 = index1 + 1; + if (index2 >= mNumRows) { // no guard row needed because we wrap the indices + index2 -= mNumRows; + } + + float *coefficients1 = &mCoefficients[index1 * getNumTaps()]; + float *coefficients2 = &mCoefficients[index2 * getNumTaps()]; + + float *xFrame = &mX[mCursor * getChannelCount()]; + for (int i = 0; i < mNumTaps; i++) { + float coefficient1 = *coefficients1++; + float coefficient2 = *coefficients2++; + for (int channel = 0; channel < getChannelCount(); channel++) { + float sample = *xFrame++; + mSingleFrame[channel] += sample * coefficient1; + mSingleFrame2[channel] += sample * coefficient2; + } + } + + // Interpolate and copy to output. + float fraction = tablePhase - index1; + for (int channel = 0; channel < getChannelCount(); channel++) { + float low = mSingleFrame[channel]; + float high = mSingleFrame2[channel]; + frame[channel] = low + (fraction * (high - low)); + } +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/SincResampler.h b/src/third_party/oboe/src/flowgraph/resampler/SincResampler.h new file mode 100644 index 0000000..6ab61c9 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/SincResampler.h @@ -0,0 +1,47 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SINC_RESAMPLER_H +#define OBOE_SINC_RESAMPLER_H + +#include +#include +#include +#include "MultiChannelResampler.h" + +namespace resampler { + +/** + * Resampler that can interpolate between coefficients. + * This can be used to support arbitrary ratios. + */ +class SincResampler : public MultiChannelResampler { +public: + explicit SincResampler(const MultiChannelResampler::Builder &builder); + + virtual ~SincResampler() = default; + + void readFrame(float *frame) override; + +protected: + + std::vector mSingleFrame2; // for interpolation + int32_t mNumRows = 0; + double mPhaseScaler = 1.0; +}; + +} +#endif //OBOE_SINC_RESAMPLER_H diff --git a/src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.cpp b/src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.cpp new file mode 100644 index 0000000..bde658a --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.cpp @@ -0,0 +1,80 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "SincResamplerStereo.h" + +using namespace resampler; + +#define STEREO 2 + +SincResamplerStereo::SincResamplerStereo(const MultiChannelResampler::Builder &builder) + : SincResampler(builder) { + assert(builder.getChannelCount() == STEREO); +} + +void SincResamplerStereo::writeFrame(const float *frame) { + // Move cursor before write so that cursor points to last written frame in read. + if (--mCursor < 0) { + mCursor = getNumTaps() - 1; + } + float *dest = &mX[mCursor * STEREO]; + const int offset = mNumTaps * STEREO; + // Write each channel twice so we avoid having to wrap when running the FIR. + const float left = frame[0]; + const float right = frame[1]; + // Put ordered writes together. + dest[0] = left; + dest[1] = right; + dest[offset] = left; + dest[1 + offset] = right; +} + +// Multiply input times windowed sinc function. +void SincResamplerStereo::readFrame(float *frame) { + // Clear accumulator for mixing. + std::fill(mSingleFrame.begin(), mSingleFrame.end(), 0.0); + std::fill(mSingleFrame2.begin(), mSingleFrame2.end(), 0.0); + + // Determine indices into coefficients table. + double tablePhase = getIntegerPhase() * mPhaseScaler; + int index1 = static_cast(floor(tablePhase)); + float *coefficients1 = &mCoefficients[index1 * getNumTaps()]; + int index2 = (index1 + 1); + if (index2 >= mNumRows) { // no guard row needed because we wrap the indices + index2 = 0; + } + float *coefficients2 = &mCoefficients[index2 * getNumTaps()]; + float *xFrame = &mX[mCursor * getChannelCount()]; + for (int i = 0; i < mNumTaps; i++) { + float coefficient1 = *coefficients1++; + float coefficient2 = *coefficients2++; + for (int channel = 0; channel < getChannelCount(); channel++) { + float sample = *xFrame++; + mSingleFrame[channel] += sample * coefficient1; + mSingleFrame2[channel] += sample * coefficient2; + } + } + + // Interpolate and copy to output. + float fraction = tablePhase - index1; + for (int channel = 0; channel < getChannelCount(); channel++) { + float low = mSingleFrame[channel]; + float high = mSingleFrame2[channel]; + frame[channel] = low + (fraction * (high - low)); + } +} diff --git a/src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.h b/src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.h new file mode 100644 index 0000000..7d66c26 --- /dev/null +++ b/src/third_party/oboe/src/flowgraph/resampler/SincResamplerStereo.h @@ -0,0 +1,39 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_SINC_RESAMPLER_STEREO_H +#define OBOE_SINC_RESAMPLER_STEREO_H + +#include +#include +#include "SincResampler.h" + +namespace resampler { + +class SincResamplerStereo : public SincResampler { +public: + explicit SincResamplerStereo(const MultiChannelResampler::Builder &builder); + + virtual ~SincResamplerStereo() = default; + + void writeFrame(const float *frame) override; + + void readFrame(float *frame) override; + +}; + +} +#endif //OBOE_SINC_RESAMPLER_STEREO_H diff --git a/src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.cpp b/src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.cpp new file mode 100644 index 0000000..fcad183 --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.cpp @@ -0,0 +1,352 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include + +#include "oboe/AudioStreamBuilder.h" +#include "AudioInputStreamOpenSLES.h" +#include "AudioStreamOpenSLES.h" +#include "OpenSLESUtilities.h" + +using namespace oboe; + +static SLuint32 OpenSLES_convertInputPreset(InputPreset oboePreset) { + SLuint32 openslPreset = SL_ANDROID_RECORDING_PRESET_NONE; + switch(oboePreset) { + case InputPreset::Generic: + openslPreset = SL_ANDROID_RECORDING_PRESET_GENERIC; + break; + case InputPreset::Camcorder: + openslPreset = SL_ANDROID_RECORDING_PRESET_CAMCORDER; + break; + case InputPreset::VoiceRecognition: + openslPreset = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; + break; + case InputPreset::VoiceCommunication: + openslPreset = SL_ANDROID_RECORDING_PRESET_VOICE_COMMUNICATION; + break; + case InputPreset::Unprocessed: + openslPreset = SL_ANDROID_RECORDING_PRESET_UNPROCESSED; + break; + default: + break; + } + return openslPreset; +} + +AudioInputStreamOpenSLES::AudioInputStreamOpenSLES(const AudioStreamBuilder &builder) + : AudioStreamOpenSLES(builder) { +} + +AudioInputStreamOpenSLES::~AudioInputStreamOpenSLES() { +} + +// Calculate masks specific to INPUT streams. +SLuint32 AudioInputStreamOpenSLES::channelCountToChannelMask(int channelCount) const { + // Derived from internal sles_channel_in_mask_from_count(chanCount); + // in "frameworks/wilhelm/src/android/channels.cpp". + // Yes, it seems strange to use SPEAKER constants to describe inputs. + // But that is how OpenSL ES does it internally. + switch (channelCount) { + case 1: + return SL_SPEAKER_FRONT_LEFT; + case 2: + return SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT; + default: + return channelCountToChannelMaskDefault(channelCount); + } +} + +Result AudioInputStreamOpenSLES::open() { + logUnsupportedAttributes(); + + SLAndroidConfigurationItf configItf = nullptr; + + if (getSdkVersion() < __ANDROID_API_M__ && mFormat == AudioFormat::Float){ + // TODO: Allow floating point format on API <23 using float->int16 converter + return Result::ErrorInvalidFormat; + } + + // If audio format is unspecified then choose a suitable default. + // API 23+: FLOAT + // API <23: INT16 + if (mFormat == AudioFormat::Unspecified){ + mFormat = (getSdkVersion() < __ANDROID_API_M__) ? + AudioFormat::I16 : AudioFormat::Float; + } + + Result oboeResult = AudioStreamOpenSLES::open(); + if (Result::OK != oboeResult) return oboeResult; + + SLuint32 bitsPerSample = static_cast(getBytesPerSample() * kBitsPerByte); + + // configure audio sink + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, // locatorType + static_cast(kBufferQueueLength)}; // numBuffers + + // Define the audio data format. + SLDataFormat_PCM format_pcm = { + SL_DATAFORMAT_PCM, // formatType + static_cast(mChannelCount), // numChannels + static_cast(mSampleRate * kMillisPerSecond), // milliSamplesPerSec + bitsPerSample, // bitsPerSample + bitsPerSample, // containerSize; + channelCountToChannelMask(mChannelCount), // channelMask + getDefaultByteOrder(), + }; + + SLDataSink audioSink = {&loc_bufq, &format_pcm}; + + /** + * API 23 (Marshmallow) introduced support for floating-point data representation and an + * extended data format type: SLAndroidDataFormat_PCM_EX for recording streams (playback streams + * got this in API 21). If running on API 23+ use this newer format type, creating it from our + * original format. + */ + SLAndroidDataFormat_PCM_EX format_pcm_ex; + if (getSdkVersion() >= __ANDROID_API_M__) { + SLuint32 representation = OpenSLES_ConvertFormatToRepresentation(getFormat()); + // Fill in the format structure. + format_pcm_ex = OpenSLES_createExtendedFormat(format_pcm, representation); + // Use in place of the previous format. + audioSink.pFormat = &format_pcm_ex; + } + + + // configure audio source + SLDataLocator_IODevice loc_dev = {SL_DATALOCATOR_IODEVICE, + SL_IODEVICE_AUDIOINPUT, + SL_DEFAULTDEVICEID_AUDIOINPUT, + NULL}; + SLDataSource audioSrc = {&loc_dev, NULL}; + + SLresult result = EngineOpenSLES::getInstance().createAudioRecorder(&mObjectInterface, + &audioSrc, + &audioSink); + + if (SL_RESULT_SUCCESS != result) { + LOGE("createAudioRecorder() result:%s", getSLErrStr(result)); + goto error; + } + + // Configure the stream. + result = (*mObjectInterface)->GetInterface(mObjectInterface, + SL_IID_ANDROIDCONFIGURATION, + &configItf); + + if (SL_RESULT_SUCCESS != result) { + LOGW("%s() GetInterface(SL_IID_ANDROIDCONFIGURATION) failed with %s", + __func__, getSLErrStr(result)); + } else { + SLuint32 presetValue = OpenSLES_convertInputPreset(getInputPreset()); + result = (*configItf)->SetConfiguration(configItf, + SL_ANDROID_KEY_RECORDING_PRESET, + &presetValue, + sizeof(SLuint32)); + if (SL_RESULT_SUCCESS != result + && presetValue != SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION) { + presetValue = SL_ANDROID_RECORDING_PRESET_VOICE_RECOGNITION; + mInputPreset = InputPreset::VoiceRecognition; + (*configItf)->SetConfiguration(configItf, + SL_ANDROID_KEY_RECORDING_PRESET, + &presetValue, + sizeof(SLuint32)); + } + + result = configurePerformanceMode(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + } + + result = (*mObjectInterface)->Realize(mObjectInterface, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("Realize recorder object result:%s", getSLErrStr(result)); + goto error; + } + + result = (*mObjectInterface)->GetInterface(mObjectInterface, SL_IID_RECORD, &mRecordInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("GetInterface RECORD result:%s", getSLErrStr(result)); + goto error; + } + + result = AudioStreamOpenSLES::registerBufferQueueCallback(); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + result = updateStreamParameters(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + oboeResult = configureBufferSizes(mSampleRate); + if (Result::OK != oboeResult) { + goto error; + } + + allocateFifo(); + + setState(StreamState::Open); + return Result::OK; + +error: + return Result::ErrorInternal; // TODO convert error from SLES to OBOE +} + +Result AudioInputStreamOpenSLES::close() { + LOGD("AudioInputStreamOpenSLES::%s()", __func__); + mLock.lock(); + Result result = Result::OK; + if (getState() == StreamState::Closed){ + result = Result::ErrorClosed; + } else { + mLock.unlock(); // avoid recursive lock + requestStop(); + mLock.lock(); + // invalidate any interfaces + mRecordInterface = nullptr; + result = AudioStreamOpenSLES::close(); + } + mLock.unlock(); // avoid recursive lock + return result; +} + +Result AudioInputStreamOpenSLES::setRecordState_l(SLuint32 newState) { + LOGD("AudioInputStreamOpenSLES::%s(%u)", __func__, newState); + Result result = Result::OK; + + if (mRecordInterface == nullptr) { + LOGE("AudioInputStreamOpenSLES::%s() mRecordInterface is null", __func__); + return Result::ErrorInvalidState; + } + SLresult slResult = (*mRecordInterface)->SetRecordState(mRecordInterface, newState); + //LOGD("AudioInputStreamOpenSLES::%s(%u) returned %u", __func__, newState, slResult); + if (SL_RESULT_SUCCESS != slResult) { + LOGE("AudioInputStreamOpenSLES::%s(%u) returned error %s", + __func__, newState, getSLErrStr(slResult)); + result = Result::ErrorInternal; // TODO review + } + return result; +} + +Result AudioInputStreamOpenSLES::requestStart() { + LOGD("AudioInputStreamOpenSLES(): %s() called", __func__); + std::lock_guard lock(mLock); + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Starting: + case StreamState::Started: + return Result::OK; + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + // We use a callback if the user requests one + // OR if we have an internal callback to fill the blocking IO buffer. + setDataCallbackEnabled(true); + + setState(StreamState::Starting); + Result result = setRecordState_l(SL_RECORDSTATE_RECORDING); + if (result == Result::OK) { + setState(StreamState::Started); + // Enqueue the first buffer to start the streaming. + // This does not call the callback function. + enqueueCallbackBuffer(mSimpleBufferQueueInterface); + } else { + setState(initialState); + } + return result; +} + + +Result AudioInputStreamOpenSLES::requestPause() { + LOGW("AudioInputStreamOpenSLES::%s() is intentionally not implemented for input " + "streams", __func__); + return Result::ErrorUnimplemented; // Matches AAudio behavior. +} + +Result AudioInputStreamOpenSLES::requestFlush() { + LOGW("AudioInputStreamOpenSLES::%s() is intentionally not implemented for input " + "streams", __func__); + return Result::ErrorUnimplemented; // Matches AAudio behavior. +} + +Result AudioInputStreamOpenSLES::requestStop() { + LOGD("AudioInputStreamOpenSLES(): %s() called", __func__); + + std::lock_guard lock(mLock); + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Stopping: + case StreamState::Stopped: + return Result::OK; + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + setState(StreamState::Stopping); + + Result result = setRecordState_l(SL_RECORDSTATE_STOPPED); + if (result == Result::OK) { + mPositionMillis.reset32(); // OpenSL ES resets its millisecond position when stopped. + setState(StreamState::Stopped); + } else { + setState(initialState); + } + return result; +} + +void AudioInputStreamOpenSLES::updateFramesWritten() { + if (usingFIFO()) { + AudioStreamBuffered::updateFramesWritten(); + } else { + mFramesWritten = getFramesProcessedByServer(); + } +} + +Result AudioInputStreamOpenSLES::updateServiceFrameCounter() { + Result result = Result::OK; + // Avoid deadlock if another thread is trying to stop or close this stream + // and this is being called from a callback. + if (mLock.try_lock()) { + + if (mRecordInterface == nullptr) { + mLock.unlock(); + return Result::ErrorNull; + } + SLmillisecond msec = 0; + SLresult slResult = (*mRecordInterface)->GetPosition(mRecordInterface, &msec); + if (SL_RESULT_SUCCESS != slResult) { + LOGW("%s(): GetPosition() returned %s", __func__, getSLErrStr(slResult)); + // set result based on SLresult + result = Result::ErrorInternal; + } else { + mPositionMillis.update32(msec); + } + mLock.unlock(); + } + return result; +} diff --git a/src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.h b/src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.h new file mode 100644 index 0000000..7be1c96 --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioInputStreamOpenSLES.h @@ -0,0 +1,65 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AUDIO_INPUT_STREAM_OPENSL_ES_H_ +#define AUDIO_INPUT_STREAM_OPENSL_ES_H_ + + +#include +#include + +#include "oboe/Oboe.h" +#include "AudioStreamOpenSLES.h" + +namespace oboe { + +/** + * INTERNAL USE ONLY + */ + +class AudioInputStreamOpenSLES : public AudioStreamOpenSLES { +public: + AudioInputStreamOpenSLES(); + explicit AudioInputStreamOpenSLES(const AudioStreamBuilder &builder); + + virtual ~AudioInputStreamOpenSLES(); + + Result open() override; + Result close() override; + + Result requestStart() override; + Result requestPause() override; + Result requestFlush() override; + Result requestStop() override; + +protected: + + Result updateServiceFrameCounter() override; + + void updateFramesWritten() override; + +private: + + SLuint32 channelCountToChannelMask(int chanCount) const; + + Result setRecordState_l(SLuint32 newState); + + SLRecordItf mRecordInterface = nullptr; +}; + +} // namespace oboe + +#endif //AUDIO_INPUT_STREAM_OPENSL_ES_H_ diff --git a/src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.cpp b/src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.cpp new file mode 100644 index 0000000..cdb8883 --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.cpp @@ -0,0 +1,448 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include +#include +#include + +#include "oboe/AudioStreamBuilder.h" +#include "AudioOutputStreamOpenSLES.h" +#include "AudioStreamOpenSLES.h" +#include "OpenSLESUtilities.h" +#include "OutputMixerOpenSLES.h" + +using namespace oboe; + +static SLuint32 OpenSLES_convertOutputUsage(Usage oboeUsage) { + SLuint32 openslStream = SL_ANDROID_STREAM_MEDIA; + switch(oboeUsage) { + case Usage::Media: + openslStream = SL_ANDROID_STREAM_MEDIA; + break; + case Usage::VoiceCommunication: + case Usage::VoiceCommunicationSignalling: + openslStream = SL_ANDROID_STREAM_VOICE; + break; + case Usage::Alarm: + openslStream = SL_ANDROID_STREAM_ALARM; + break; + case Usage::Notification: + case Usage::NotificationRingtone: + case Usage::NotificationEvent: + openslStream = SL_ANDROID_STREAM_NOTIFICATION; + break; + case Usage::AssistanceAccessibility: + case Usage::AssistanceNavigationGuidance: + case Usage::AssistanceSonification: + openslStream = SL_ANDROID_STREAM_SYSTEM; + break; + case Usage::Game: + openslStream = SL_ANDROID_STREAM_MEDIA; + break; + case Usage::Assistant: + default: + openslStream = SL_ANDROID_STREAM_SYSTEM; + break; + } + return openslStream; +} + +AudioOutputStreamOpenSLES::AudioOutputStreamOpenSLES(const AudioStreamBuilder &builder) + : AudioStreamOpenSLES(builder) { +} + +// These will wind up in +constexpr int SL_ANDROID_SPEAKER_STEREO = (SL_SPEAKER_FRONT_LEFT | SL_SPEAKER_FRONT_RIGHT); + +constexpr int SL_ANDROID_SPEAKER_QUAD = (SL_ANDROID_SPEAKER_STEREO + | SL_SPEAKER_BACK_LEFT | SL_SPEAKER_BACK_RIGHT); + +constexpr int SL_ANDROID_SPEAKER_5DOT1 = (SL_ANDROID_SPEAKER_QUAD + | SL_SPEAKER_FRONT_CENTER | SL_SPEAKER_LOW_FREQUENCY); + +constexpr int SL_ANDROID_SPEAKER_7DOT1 = (SL_ANDROID_SPEAKER_5DOT1 | SL_SPEAKER_SIDE_LEFT + | SL_SPEAKER_SIDE_RIGHT); + +SLuint32 AudioOutputStreamOpenSLES::channelCountToChannelMask(int channelCount) const { + SLuint32 channelMask = 0; + + switch (channelCount) { + case 1: + channelMask = SL_SPEAKER_FRONT_CENTER; + break; + + case 2: + channelMask = SL_ANDROID_SPEAKER_STEREO; + break; + + case 4: // Quad + channelMask = SL_ANDROID_SPEAKER_QUAD; + break; + + case 6: // 5.1 + channelMask = SL_ANDROID_SPEAKER_5DOT1; + break; + + case 8: // 7.1 + channelMask = SL_ANDROID_SPEAKER_7DOT1; + break; + + default: + channelMask = channelCountToChannelMaskDefault(channelCount); + break; + } + return channelMask; +} + +Result AudioOutputStreamOpenSLES::open() { + logUnsupportedAttributes(); + + SLAndroidConfigurationItf configItf = nullptr; + + + if (getSdkVersion() < __ANDROID_API_L__ && mFormat == AudioFormat::Float){ + // TODO: Allow floating point format on API <21 using float->int16 converter + return Result::ErrorInvalidFormat; + } + + // If audio format is unspecified then choose a suitable default. + // API 21+: FLOAT + // API <21: INT16 + if (mFormat == AudioFormat::Unspecified){ + mFormat = (getSdkVersion() < __ANDROID_API_L__) ? + AudioFormat::I16 : AudioFormat::Float; + } + + Result oboeResult = AudioStreamOpenSLES::open(); + if (Result::OK != oboeResult) return oboeResult; + + SLresult result = OutputMixerOpenSL::getInstance().open(); + if (SL_RESULT_SUCCESS != result) { + AudioStreamOpenSLES::close(); + return Result::ErrorInternal; + } + + SLuint32 bitsPerSample = static_cast(getBytesPerSample() * kBitsPerByte); + + // configure audio source + SLDataLocator_AndroidSimpleBufferQueue loc_bufq = { + SL_DATALOCATOR_ANDROIDSIMPLEBUFFERQUEUE, // locatorType + static_cast(kBufferQueueLength)}; // numBuffers + + // Define the audio data format. + SLDataFormat_PCM format_pcm = { + SL_DATAFORMAT_PCM, // formatType + static_cast(mChannelCount), // numChannels + static_cast(mSampleRate * kMillisPerSecond), // milliSamplesPerSec + bitsPerSample, // bitsPerSample + bitsPerSample, // containerSize; + channelCountToChannelMask(mChannelCount), // channelMask + getDefaultByteOrder(), + }; + + SLDataSource audioSrc = {&loc_bufq, &format_pcm}; + + /** + * API 21 (Lollipop) introduced support for floating-point data representation and an extended + * data format type: SLAndroidDataFormat_PCM_EX. If running on API 21+ use this newer format + * type, creating it from our original format. + */ + SLAndroidDataFormat_PCM_EX format_pcm_ex; + if (getSdkVersion() >= __ANDROID_API_L__) { + SLuint32 representation = OpenSLES_ConvertFormatToRepresentation(getFormat()); + // Fill in the format structure. + format_pcm_ex = OpenSLES_createExtendedFormat(format_pcm, representation); + // Use in place of the previous format. + audioSrc.pFormat = &format_pcm_ex; + } + + result = OutputMixerOpenSL::getInstance().createAudioPlayer(&mObjectInterface, + &audioSrc); + if (SL_RESULT_SUCCESS != result) { + LOGE("createAudioPlayer() result:%s", getSLErrStr(result)); + goto error; + } + + // Configure the stream. + result = (*mObjectInterface)->GetInterface(mObjectInterface, + SL_IID_ANDROIDCONFIGURATION, + (void *)&configItf); + if (SL_RESULT_SUCCESS != result) { + LOGW("%s() GetInterface(SL_IID_ANDROIDCONFIGURATION) failed with %s", + __func__, getSLErrStr(result)); + } else { + result = configurePerformanceMode(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + SLuint32 presetValue = OpenSLES_convertOutputUsage(getUsage()); + result = (*configItf)->SetConfiguration(configItf, + SL_ANDROID_KEY_STREAM_TYPE, + &presetValue, + sizeof(presetValue)); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + } + + result = (*mObjectInterface)->Realize(mObjectInterface, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("Realize player object result:%s", getSLErrStr(result)); + goto error; + } + + result = (*mObjectInterface)->GetInterface(mObjectInterface, SL_IID_PLAY, &mPlayInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("GetInterface PLAY result:%s", getSLErrStr(result)); + goto error; + } + + result = AudioStreamOpenSLES::registerBufferQueueCallback(); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + result = updateStreamParameters(configItf); + if (SL_RESULT_SUCCESS != result) { + goto error; + } + + oboeResult = configureBufferSizes(mSampleRate); + if (Result::OK != oboeResult) { + goto error; + } + + allocateFifo(); + + setState(StreamState::Open); + return Result::OK; + +error: + return Result::ErrorInternal; // TODO convert error from SLES to OBOE +} + +Result AudioOutputStreamOpenSLES::onAfterDestroy() { + OutputMixerOpenSL::getInstance().close(); + return Result::OK; +} + +Result AudioOutputStreamOpenSLES::close() { + mLock.lock(); + Result result = Result::OK; + if (getState() == StreamState::Closed){ + result = Result::ErrorClosed; + } else { + mLock.unlock(); // avoid recursive lock + requestPause(); + mLock.lock(); + // invalidate any interfaces + mPlayInterface = nullptr; + result = AudioStreamOpenSLES::close(); + } + mLock.unlock(); // avoid recursive lock + return result; +} + +Result AudioOutputStreamOpenSLES::setPlayState_l(SLuint32 newState) { + + LOGD("AudioOutputStreamOpenSLES(): %s() called", __func__); + Result result = Result::OK; + + if (mPlayInterface == nullptr){ + LOGE("AudioOutputStreamOpenSLES::%s() mPlayInterface is null", __func__); + return Result::ErrorInvalidState; + } + + SLresult slResult = (*mPlayInterface)->SetPlayState(mPlayInterface, newState); + if (SL_RESULT_SUCCESS != slResult) { + LOGW("AudioOutputStreamOpenSLES(): %s() returned %s", __func__, getSLErrStr(slResult)); + result = Result::ErrorInternal; // TODO convert slResult to Result::Error + } + return result; +} + +Result AudioOutputStreamOpenSLES::requestStart() { + LOGD("AudioOutputStreamOpenSLES(): %s() called", __func__); + + mLock.lock(); + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Starting: + case StreamState::Started: + mLock.unlock(); + return Result::OK; + case StreamState::Closed: + mLock.unlock(); + return Result::ErrorClosed; + default: + break; + } + + // We use a callback if the user requests one + // OR if we have an internal callback to read the blocking IO buffer. + setDataCallbackEnabled(true); + + setState(StreamState::Starting); + Result result = setPlayState_l(SL_PLAYSTATE_PLAYING); + if (result == Result::OK) { + setState(StreamState::Started); + mLock.unlock(); + if (getBufferDepth(mSimpleBufferQueueInterface) == 0) { + // Enqueue the first buffer if needed to start the streaming. + // This might call requestStop() so try to avoid a recursive lock. + processBufferCallback(mSimpleBufferQueueInterface); + } + } else { + setState(initialState); + mLock.unlock(); + } + return result; +} + +Result AudioOutputStreamOpenSLES::requestPause() { + LOGD("AudioOutputStreamOpenSLES(): %s() called", __func__); + + std::lock_guard lock(mLock); + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Pausing: + case StreamState::Paused: + return Result::OK; + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + setState(StreamState::Pausing); + Result result = setPlayState_l(SL_PLAYSTATE_PAUSED); + if (result == Result::OK) { + // Note that OpenSL ES does NOT reset its millisecond position when OUTPUT is paused. + int64_t framesWritten = getFramesWritten(); + if (framesWritten >= 0) { + setFramesRead(framesWritten); + } + setState(StreamState::Paused); + } else { + setState(initialState); + } + return result; +} + +/** + * Flush/clear the queue buffers + */ +Result AudioOutputStreamOpenSLES::requestFlush() { + std::lock_guard lock(mLock); + return requestFlush_l(); +} + +Result AudioOutputStreamOpenSLES::requestFlush_l() { + LOGD("AudioOutputStreamOpenSLES(): %s() called", __func__); + if (getState() == StreamState::Closed) { + return Result::ErrorClosed; + } + + Result result = Result::OK; + if (mPlayInterface == nullptr || mSimpleBufferQueueInterface == nullptr) { + result = Result::ErrorInvalidState; + } else { + SLresult slResult = (*mSimpleBufferQueueInterface)->Clear(mSimpleBufferQueueInterface); + if (slResult != SL_RESULT_SUCCESS){ + LOGW("Failed to clear buffer queue. OpenSLES error: %d", result); + result = Result::ErrorInternal; + } + } + return result; +} + +Result AudioOutputStreamOpenSLES::requestStop() { + LOGD("AudioOutputStreamOpenSLES(): %s() called", __func__); + + std::lock_guard lock(mLock); + StreamState initialState = getState(); + switch (initialState) { + case StreamState::Stopping: + case StreamState::Stopped: + return Result::OK; + case StreamState::Closed: + return Result::ErrorClosed; + default: + break; + } + + setState(StreamState::Stopping); + + Result result = setPlayState_l(SL_PLAYSTATE_STOPPED); + if (result == Result::OK) { + + // Also clear the buffer queue so the old data won't be played if the stream is restarted. + // Call the _l function that expects to already be under a lock. + if (requestFlush_l() != Result::OK) { + LOGW("Failed to flush the stream. Error %s", convertToText(flush())); + } + + mPositionMillis.reset32(); // OpenSL ES resets its millisecond position when stopped. + int64_t framesWritten = getFramesWritten(); + if (framesWritten >= 0) { + setFramesRead(framesWritten); + } + setState(StreamState::Stopped); + } else { + setState(initialState); + } + return result; +} + +void AudioOutputStreamOpenSLES::setFramesRead(int64_t framesRead) { + int64_t millisWritten = framesRead * kMillisPerSecond / getSampleRate(); + mPositionMillis.set(millisWritten); +} + +void AudioOutputStreamOpenSLES::updateFramesRead() { + if (usingFIFO()) { + AudioStreamBuffered::updateFramesRead(); + } else { + mFramesRead = getFramesProcessedByServer(); + } +} + +Result AudioOutputStreamOpenSLES::updateServiceFrameCounter() { + Result result = Result::OK; + // Avoid deadlock if another thread is trying to stop or close this stream + // and this is being called from a callback. + if (mLock.try_lock()) { + + if (mPlayInterface == nullptr) { + mLock.unlock(); + return Result::ErrorNull; + } + SLmillisecond msec = 0; + SLresult slResult = (*mPlayInterface)->GetPosition(mPlayInterface, &msec); + if (SL_RESULT_SUCCESS != slResult) { + LOGW("%s(): GetPosition() returned %s", __func__, getSLErrStr(slResult)); + // set result based on SLresult + result = Result::ErrorInternal; + } else { + mPositionMillis.update32(msec); + } + mLock.unlock(); + } + return result; +} diff --git a/src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.h b/src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.h new file mode 100644 index 0000000..d74faf6 --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioOutputStreamOpenSLES.h @@ -0,0 +1,77 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AUDIO_OUTPUT_STREAM_OPENSL_ES_H_ +#define AUDIO_OUTPUT_STREAM_OPENSL_ES_H_ + + +#include +#include + +#include "oboe/Oboe.h" +#include "AudioStreamOpenSLES.h" + +namespace oboe { + +/** + * INTERNAL USE ONLY + */ +class AudioOutputStreamOpenSLES : public AudioStreamOpenSLES { +public: + AudioOutputStreamOpenSLES(); + explicit AudioOutputStreamOpenSLES(const AudioStreamBuilder &builder); + + virtual ~AudioOutputStreamOpenSLES() = default; + + Result open() override; + Result close() override; + + Result requestStart() override; + Result requestPause() override; + Result requestFlush() override; + Result requestStop() override; + +protected: + + void setFramesRead(int64_t framesRead); + + Result updateServiceFrameCounter() override; + + void updateFramesRead() override; + +private: + + SLuint32 channelCountToChannelMask(int chanCount) const; + + Result onAfterDestroy() override; + + Result requestFlush_l(); + + /** + * Set OpenSL ES PLAYSTATE. + * + * @param newState SL_PLAYSTATE_PAUSED, SL_PLAYSTATE_PLAYING, SL_PLAYSTATE_STOPPED + * @return + */ + Result setPlayState_l(SLuint32 newState); + + SLPlayItf mPlayInterface = nullptr; + +}; + +} // namespace oboe + +#endif //AUDIO_OUTPUT_STREAM_OPENSL_ES_H_ diff --git a/src/third_party/oboe/src/opensles/AudioStreamBuffered.cpp b/src/third_party/oboe/src/opensles/AudioStreamBuffered.cpp new file mode 100644 index 0000000..85b9f8e --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioStreamBuffered.cpp @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include + +#include "oboe/Oboe.h" + +#include "opensles/AudioStreamBuffered.h" +#include "common/AudioClock.h" + +namespace oboe { + +constexpr int kDefaultBurstsPerBuffer = 16; // arbitrary, allows dynamic latency tuning +constexpr int kMinBurstsPerBuffer = 4; // arbitrary, allows dynamic latency tuning +constexpr int kMinFramesPerBuffer = 48 * 32; // arbitrary + +/* + * AudioStream with a FifoBuffer + */ +AudioStreamBuffered::AudioStreamBuffered(const AudioStreamBuilder &builder) + : AudioStream(builder) { +} + +void AudioStreamBuffered::allocateFifo() { + // If the caller does not provide a callback use our own internal + // callback that reads data from the FIFO. + if (usingFIFO()) { + // FIFO is configured with the same format and channels as the stream. + int32_t capacityFrames = getBufferCapacityInFrames(); + if (capacityFrames == oboe::kUnspecified) { + capacityFrames = getFramesPerBurst() * kDefaultBurstsPerBuffer; + } else { + int32_t minFramesPerBufferByBursts = getFramesPerBurst() * kMinBurstsPerBuffer; + if (capacityFrames <= minFramesPerBufferByBursts) { + capacityFrames = minFramesPerBufferByBursts; + } else { + capacityFrames = std::max(kMinFramesPerBuffer, capacityFrames); + // round up to nearest burst + int32_t numBursts = (capacityFrames + getFramesPerBurst() - 1) + / getFramesPerBurst(); + capacityFrames = numBursts * getFramesPerBurst(); + } + } + // TODO consider using std::make_unique if we require c++14 + mFifoBuffer.reset(new FifoBuffer(getBytesPerFrame(), capacityFrames)); + mBufferCapacityInFrames = capacityFrames; + } +} + +void AudioStreamBuffered::updateFramesWritten() { + if (mFifoBuffer) { + mFramesWritten = static_cast(mFifoBuffer->getWriteCounter()); + } // or else it will get updated by processBufferCallback() +} + +void AudioStreamBuffered::updateFramesRead() { + if (mFifoBuffer) { + mFramesRead = static_cast(mFifoBuffer->getReadCounter()); + } // or else it will get updated by processBufferCallback() +} + +// This is called by the OpenSL ES callback to read or write the back end of the FIFO. +DataCallbackResult AudioStreamBuffered::onDefaultCallback(void *audioData, int numFrames) { + int32_t framesTransferred = 0; + + if (getDirection() == oboe::Direction::Output) { + // Read from the FIFO and write to audioData, clear part of buffer if not enough data. + framesTransferred = mFifoBuffer->readNow(audioData, numFrames); + } else { + // Read from audioData and write to the FIFO + framesTransferred = mFifoBuffer->write(audioData, numFrames); // There is no writeNow() + } + + if (framesTransferred < numFrames) { + LOGD("AudioStreamBuffered::%s(): xrun! framesTransferred = %d, numFrames = %d", + __func__, framesTransferred, numFrames); + // TODO If we do not allow FIFO to wrap then our timestamps will drift when there is an XRun! + incrementXRunCount(); + } + markCallbackTime(static_cast(numFrames)); // so foreground knows how long to wait. + return DataCallbackResult::Continue; +} + +void AudioStreamBuffered::markCallbackTime(int32_t numFrames) { + mLastBackgroundSize = numFrames; + mBackgroundRanAtNanoseconds = AudioClock::getNanoseconds(); +} + +int64_t AudioStreamBuffered::predictNextCallbackTime() { + if (mBackgroundRanAtNanoseconds == 0) { + return 0; + } + int64_t nanosPerBuffer = (kNanosPerSecond * mLastBackgroundSize) / getSampleRate(); + const int64_t margin = 200 * kNanosPerMicrosecond; // arbitrary delay so we wake up just after + return mBackgroundRanAtNanoseconds + nanosPerBuffer + margin; +} + +// Common code for read/write. +// @return Result::OK with frames read/written, or Result::Error* +ResultWithValue AudioStreamBuffered::transfer(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + // Validate arguments. + if (buffer == nullptr) { + LOGE("AudioStreamBuffered::%s(): buffer is NULL", __func__); + return ResultWithValue(Result::ErrorNull); + } + if (numFrames < 0) { + LOGE("AudioStreamBuffered::%s(): numFrames is negative", __func__); + return ResultWithValue(Result::ErrorOutOfRange); + } else if (numFrames == 0) { + return ResultWithValue(numFrames); + } + if (timeoutNanoseconds < 0) { + LOGE("AudioStreamBuffered::%s(): timeoutNanoseconds is negative", __func__); + return ResultWithValue(Result::ErrorOutOfRange); + } + + int32_t result = 0; + uint8_t *data = reinterpret_cast(buffer); + int32_t framesLeft = numFrames; + int64_t timeToQuit = 0; + bool repeat = true; + + // Calculate when to timeout. + if (timeoutNanoseconds > 0) { + timeToQuit = AudioClock::getNanoseconds() + timeoutNanoseconds; + } + + // Loop until we get the data, or we have an error, or we timeout. + do { + // read or write + if (getDirection() == Direction::Input) { + result = mFifoBuffer->read(data, framesLeft); + } else { + // between zero and capacity + uint32_t fullFrames = mFifoBuffer->getFullFramesAvailable(); + // Do not write above threshold size. + int32_t emptyFrames = getBufferSizeInFrames() - static_cast(fullFrames); + int32_t framesToWrite = std::max(0, std::min(framesLeft, emptyFrames)); + result = mFifoBuffer->write(data, framesToWrite); + } + if (result > 0) { + data += mFifoBuffer->convertFramesToBytes(result); + framesLeft -= result; + } + + // If we need more data then sleep and try again. + if (framesLeft > 0 && result >= 0 && timeoutNanoseconds > 0) { + int64_t timeNow = AudioClock::getNanoseconds(); + if (timeNow >= timeToQuit) { + LOGE("AudioStreamBuffered::%s(): TIMEOUT", __func__); + repeat = false; // TIMEOUT + } else { + // Figure out how long to sleep. + int64_t sleepForNanos; + int64_t wakeTimeNanos = predictNextCallbackTime(); + if (wakeTimeNanos <= 0) { + // No estimate available. Sleep for one burst. + sleepForNanos = (getFramesPerBurst() * kNanosPerSecond) / getSampleRate(); + } else { + // Don't sleep past timeout. + if (wakeTimeNanos > timeToQuit) { + wakeTimeNanos = timeToQuit; + } + sleepForNanos = wakeTimeNanos - timeNow; + // Avoid rapid loop with no sleep. + const int64_t minSleepTime = kNanosPerMillisecond; // arbitrary + if (sleepForNanos < minSleepTime) { + sleepForNanos = minSleepTime; + } + } + + AudioClock::sleepForNanos(sleepForNanos); + } + + } else { + repeat = false; + } + } while(repeat); + + if (result < 0) { + return ResultWithValue(static_cast(result)); + } else { + int32_t framesWritten = numFrames - framesLeft; + return ResultWithValue(framesWritten); + } +} + +// Write to the FIFO so the callback can read from it. +ResultWithValue AudioStreamBuffered::write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + if (getState() == StreamState::Closed){ + return ResultWithValue(Result::ErrorClosed); + } + + if (getDirection() == Direction::Input) { + return ResultWithValue(Result::ErrorUnavailable); // TODO review, better error code? + } + updateServiceFrameCounter(); + return transfer(const_cast(buffer), numFrames, timeoutNanoseconds); +} + +// Read data from the FIFO that was written by the callback. +ResultWithValue AudioStreamBuffered::read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) { + if (getState() == StreamState::Closed){ + return ResultWithValue(Result::ErrorClosed); + } + + if (getDirection() == Direction::Output) { + return ResultWithValue(Result::ErrorUnavailable); // TODO review, better error code? + } + updateServiceFrameCounter(); + return transfer(buffer, numFrames, timeoutNanoseconds); +} + +// Only supported when we are not using a callback. +ResultWithValue AudioStreamBuffered::setBufferSizeInFrames(int32_t requestedFrames) +{ + if (getState() == StreamState::Closed){ + return ResultWithValue(Result::ErrorClosed); + } + + if (!mFifoBuffer) { + return ResultWithValue(Result::ErrorUnimplemented); + } + + if (requestedFrames > mFifoBuffer->getBufferCapacityInFrames()) { + requestedFrames = mFifoBuffer->getBufferCapacityInFrames(); + } else if (requestedFrames < getFramesPerBurst()) { + requestedFrames = getFramesPerBurst(); + } + mBufferSizeInFrames = requestedFrames; + return ResultWithValue(requestedFrames); +} + +int32_t AudioStreamBuffered::getBufferCapacityInFrames() const { + if (mFifoBuffer) { + return mFifoBuffer->getBufferCapacityInFrames(); + } else { + return AudioStream::getBufferCapacityInFrames(); + } +} + +bool AudioStreamBuffered::isXRunCountSupported() const { + // XRun count is only supported if we're using blocking I/O (not callbacks) + return (getCallback() == nullptr); +} + +} // namespace oboe \ No newline at end of file diff --git a/src/third_party/oboe/src/opensles/AudioStreamBuffered.h b/src/third_party/oboe/src/opensles/AudioStreamBuffered.h new file mode 100644 index 0000000..5923e8d --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioStreamBuffered.h @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_STREAM_BUFFERED_H +#define OBOE_STREAM_BUFFERED_H + +#include +#include +#include "common/OboeDebug.h" +#include "oboe/AudioStream.h" +#include "oboe/AudioStreamCallback.h" +#include "fifo/FifoBuffer.h" + +namespace oboe { + +// A stream that contains a FIFO buffer. +// This is used to implement blocking reads and writes. +class AudioStreamBuffered : public AudioStream { +public: + + AudioStreamBuffered(); + explicit AudioStreamBuffered(const AudioStreamBuilder &builder); + + void allocateFifo(); + + + ResultWithValue write(const void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue read(void *buffer, + int32_t numFrames, + int64_t timeoutNanoseconds) override; + + ResultWithValue setBufferSizeInFrames(int32_t requestedFrames) override; + + int32_t getBufferCapacityInFrames() const override; + + ResultWithValue getXRunCount() const override { + return ResultWithValue(mXRunCount); + } + + bool isXRunCountSupported() const override; + +protected: + + DataCallbackResult onDefaultCallback(void *audioData, int numFrames) override; + + // If there is no callback then we need a FIFO between the App and OpenSL ES. + bool usingFIFO() const { return getCallback() == nullptr; } + + virtual Result updateServiceFrameCounter() = 0; + + void updateFramesRead() override; + void updateFramesWritten() override; + +private: + + int64_t predictNextCallbackTime(); + + void markCallbackTime(int32_t numFrames); + + // Read or write to the FIFO. + ResultWithValue transfer(void *buffer, int32_t numFrames, int64_t timeoutNanoseconds); + + void incrementXRunCount() { + ++mXRunCount; + } + + std::unique_ptr mFifoBuffer{}; + + int64_t mBackgroundRanAtNanoseconds = 0; + int32_t mLastBackgroundSize = 0; + int32_t mXRunCount = 0; +}; + +} // namespace oboe + +#endif //OBOE_STREAM_BUFFERED_H diff --git a/src/third_party/oboe/src/opensles/AudioStreamOpenSLES.cpp b/src/third_party/oboe/src/opensles/AudioStreamOpenSLES.cpp new file mode 100644 index 0000000..3af1a30 --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioStreamOpenSLES.cpp @@ -0,0 +1,396 @@ +/* Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include +#include +#include + + +#include +#include +#include +#include + +#include "common/OboeDebug.h" +#include "oboe/AudioStreamBuilder.h" +#include "AudioStreamOpenSLES.h" +#include "OpenSLESUtilities.h" + +using namespace oboe; + +AudioStreamOpenSLES::AudioStreamOpenSLES(const AudioStreamBuilder &builder) + : AudioStreamBuffered(builder) { + // OpenSL ES does not support device IDs. So overwrite value from builder. + mDeviceId = kUnspecified; + // OpenSL ES does not support session IDs. So overwrite value from builder. + mSessionId = SessionId::None; +} + +static constexpr int32_t kHighLatencyBufferSizeMillis = 20; // typical Android period +static constexpr SLuint32 kAudioChannelCountMax = 30; // TODO Why 30? +static constexpr SLuint32 SL_ANDROID_UNKNOWN_CHANNELMASK = 0; // Matches name used internally. + +SLuint32 AudioStreamOpenSLES::channelCountToChannelMaskDefault(int channelCount) const { + if (channelCount > kAudioChannelCountMax) { + return SL_ANDROID_UNKNOWN_CHANNELMASK; + } + + SLuint32 bitfield = (1 << channelCount) - 1; + + // Check for OS at run-time. + if(getSdkVersion() >= __ANDROID_API_N__) { + return SL_ANDROID_MAKE_INDEXED_CHANNEL_MASK(bitfield); + } + + // Indexed channels masks were added in N. + // For before N, the best we can do is use a positional channel mask. + return bitfield; +} + +static bool s_isLittleEndian() { + static uint32_t value = 1; + return (*reinterpret_cast(&value) == 1); // Does address point to LSB? +} + +SLuint32 AudioStreamOpenSLES::getDefaultByteOrder() { + return s_isLittleEndian() ? SL_BYTEORDER_LITTLEENDIAN : SL_BYTEORDER_BIGENDIAN; +} + +Result AudioStreamOpenSLES::open() { + + LOGI("AudioStreamOpenSLES::open() chans=%d, rate=%d", mChannelCount, mSampleRate); + + SLresult result = EngineOpenSLES::getInstance().open(); + if (SL_RESULT_SUCCESS != result) { + return Result::ErrorInternal; + } + + Result oboeResult = AudioStreamBuffered::open(); + if (oboeResult != Result::OK) { + return oboeResult; + } + // Convert to defaults if UNSPECIFIED + if (mSampleRate == kUnspecified) { + mSampleRate = DefaultStreamValues::SampleRate; + } + if (mChannelCount == kUnspecified) { + mChannelCount = DefaultStreamValues::ChannelCount; + } + + mSharingMode = SharingMode::Shared; + + return Result::OK; +} + +Result AudioStreamOpenSLES::configureBufferSizes(int32_t sampleRate) { + LOGD("AudioStreamOpenSLES:%s(%d) initial mFramesPerBurst = %d, mFramesPerCallback = %d", + __func__, sampleRate, mFramesPerBurst, mFramesPerCallback); + // Decide frames per burst based on hints from caller. + if (mFramesPerCallback != kUnspecified) { + // Requested framesPerCallback must be honored. + mFramesPerBurst = mFramesPerCallback; + } else { + mFramesPerBurst = DefaultStreamValues::FramesPerBurst; + + // Calculate the size of a fixed duration high latency buffer based on sample rate. + int32_t framesPerHighLatencyBuffer = + (kHighLatencyBufferSizeMillis * sampleRate) / kMillisPerSecond; + + // For high latency streams, use a larger buffer size. + // Performance Mode support was added in N_MR1 (7.1) + if (getSdkVersion() >= __ANDROID_API_N_MR1__ + && mPerformanceMode != PerformanceMode::LowLatency + && mFramesPerBurst < framesPerHighLatencyBuffer) { + // Find a multiple of framesPerBurst >= framesPerHighLatencyBuffer. + int32_t numBursts = (framesPerHighLatencyBuffer + mFramesPerBurst - 1) / mFramesPerBurst; + mFramesPerBurst *= numBursts; + LOGD("AudioStreamOpenSLES:%s() NOT low latency, set mFramesPerBurst = %d", + __func__, mFramesPerBurst); + } + mFramesPerCallback = mFramesPerBurst; + } + LOGD("AudioStreamOpenSLES:%s(%d) final mFramesPerBurst = %d, mFramesPerCallback = %d", + __func__, sampleRate, mFramesPerBurst, mFramesPerCallback); + + mBytesPerCallback = mFramesPerCallback * getBytesPerFrame(); + if (mBytesPerCallback <= 0) { + LOGE("AudioStreamOpenSLES::open() bytesPerCallback < 0 = %d, bad format?", + mBytesPerCallback); + return Result::ErrorInvalidFormat; // causing bytesPerFrame == 0 + } + + mCallbackBuffer = std::make_unique(mBytesPerCallback); + + if (!usingFIFO()) { + mBufferCapacityInFrames = mFramesPerBurst * kBufferQueueLength; + mBufferSizeInFrames = mBufferCapacityInFrames; + } + + return Result::OK; +} + +SLuint32 AudioStreamOpenSLES::convertPerformanceMode(PerformanceMode oboeMode) const { + SLuint32 openslMode = SL_ANDROID_PERFORMANCE_NONE; + switch(oboeMode) { + case PerformanceMode::None: + openslMode = SL_ANDROID_PERFORMANCE_NONE; + break; + case PerformanceMode::LowLatency: + openslMode = (getSessionId() == SessionId::None) ? SL_ANDROID_PERFORMANCE_LATENCY : SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS; + break; + case PerformanceMode::PowerSaving: + openslMode = SL_ANDROID_PERFORMANCE_POWER_SAVING; + break; + default: + break; + } + return openslMode; +} + +PerformanceMode AudioStreamOpenSLES::convertPerformanceMode(SLuint32 openslMode) const { + PerformanceMode oboeMode = PerformanceMode::None; + switch(openslMode) { + case SL_ANDROID_PERFORMANCE_NONE: + oboeMode = PerformanceMode::None; + break; + case SL_ANDROID_PERFORMANCE_LATENCY: + case SL_ANDROID_PERFORMANCE_LATENCY_EFFECTS: + oboeMode = PerformanceMode::LowLatency; + break; + case SL_ANDROID_PERFORMANCE_POWER_SAVING: + oboeMode = PerformanceMode::PowerSaving; + break; + default: + break; + } + return oboeMode; +} + +void AudioStreamOpenSLES::logUnsupportedAttributes() { + // Log unsupported attributes + // only report if changed from the default + + // Device ID + if (mDeviceId != kUnspecified) { + LOGW("Device ID [AudioStreamBuilder::setDeviceId()] " + "is not supported on OpenSLES streams."); + } + // Sharing Mode + if (mSharingMode != SharingMode::Shared) { + LOGW("SharingMode [AudioStreamBuilder::setSharingMode()] " + "is not supported on OpenSLES streams."); + } + // Performance Mode + int sdkVersion = getSdkVersion(); + if (mPerformanceMode != PerformanceMode::None && sdkVersion < __ANDROID_API_N_MR1__) { + LOGW("PerformanceMode [AudioStreamBuilder::setPerformanceMode()] " + "is not supported on OpenSLES streams running on pre-Android N-MR1 versions."); + } + // Content Type + if (mContentType != ContentType::Music) { + LOGW("ContentType [AudioStreamBuilder::setContentType()] " + "is not supported on OpenSLES streams."); + } + + // Session Id + if (mSessionId != SessionId::None) { + LOGW("SessionId [AudioStreamBuilder::setSessionId()] " + "is not supported on OpenSLES streams."); + } + + // Input Preset + if (mInputPreset != InputPreset::VoiceRecognition) { + LOGW("InputPreset [AudioStreamBuilder::setInputPreset()] " + "is not supported on OpenSLES streams."); + } +} + +SLresult AudioStreamOpenSLES::configurePerformanceMode(SLAndroidConfigurationItf configItf) { + + if (configItf == nullptr) { + LOGW("%s() called with NULL configuration", __func__); + mPerformanceMode = PerformanceMode::None; + return SL_RESULT_INTERNAL_ERROR; + } + if (getSdkVersion() < __ANDROID_API_N_MR1__) { + LOGW("%s() not supported until N_MR1", __func__); + mPerformanceMode = PerformanceMode::None; + return SL_RESULT_SUCCESS; + } + + SLresult result = SL_RESULT_SUCCESS; + SLuint32 performanceMode = convertPerformanceMode(getPerformanceMode()); + result = (*configItf)->SetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE, + &performanceMode, sizeof(performanceMode)); + if (SL_RESULT_SUCCESS != result) { + LOGW("SetConfiguration(PERFORMANCE_MODE, SL %u) returned %s", + performanceMode, getSLErrStr(result)); + mPerformanceMode = PerformanceMode::None; + } + + return result; +} + +SLresult AudioStreamOpenSLES::updateStreamParameters(SLAndroidConfigurationItf configItf) { + SLresult result = SL_RESULT_SUCCESS; + if(getSdkVersion() >= __ANDROID_API_N_MR1__ && configItf != nullptr) { + SLuint32 performanceMode = 0; + SLuint32 performanceModeSize = sizeof(performanceMode); + result = (*configItf)->GetConfiguration(configItf, SL_ANDROID_KEY_PERFORMANCE_MODE, + &performanceModeSize, &performanceMode); + // A bug in GetConfiguration() before P caused a wrong result code to be returned. + if (getSdkVersion() <= __ANDROID_API_O_MR1__) { + result = SL_RESULT_SUCCESS; // Ignore actual result before P. + } + + if (SL_RESULT_SUCCESS != result) { + LOGW("GetConfiguration(SL_ANDROID_KEY_PERFORMANCE_MODE) returned %d", result); + mPerformanceMode = PerformanceMode::None; // If we can't query it then assume None. + } else { + mPerformanceMode = convertPerformanceMode(performanceMode); // convert SL to Oboe mode + } + } else { + mPerformanceMode = PerformanceMode::None; // If we can't query it then assume None. + } + return result; +} + +Result AudioStreamOpenSLES::close() { + if (mState == StreamState::Closed) { + return Result::ErrorClosed; + } + + AudioStreamBuffered::close(); + + onBeforeDestroy(); + + if (mObjectInterface != nullptr) { + (*mObjectInterface)->Destroy(mObjectInterface); + mObjectInterface = nullptr; + } + + onAfterDestroy(); + + mSimpleBufferQueueInterface = nullptr; + EngineOpenSLES::getInstance().close(); + + setState(StreamState::Closed); + return Result::OK; +} + +SLresult AudioStreamOpenSLES::enqueueCallbackBuffer(SLAndroidSimpleBufferQueueItf bq) { + return (*bq)->Enqueue(bq, mCallbackBuffer.get(), mBytesPerCallback); +} + +int32_t AudioStreamOpenSLES::getBufferDepth(SLAndroidSimpleBufferQueueItf bq) { + SLAndroidSimpleBufferQueueState queueState; + SLresult result = (*bq)->GetState(bq, &queueState); + return (result == SL_RESULT_SUCCESS) ? queueState.count : -1; +} + +void AudioStreamOpenSLES::processBufferCallback(SLAndroidSimpleBufferQueueItf bq) { + bool stopStream = false; + // Ask the app callback to process the buffer. + DataCallbackResult result = fireDataCallback(mCallbackBuffer.get(), mFramesPerCallback); + if (result == DataCallbackResult::Continue) { + // Pass the buffer to OpenSLES. + SLresult enqueueResult = enqueueCallbackBuffer(bq); + if (enqueueResult != SL_RESULT_SUCCESS) { + LOGE("%s() returned %d", __func__, enqueueResult); + stopStream = true; + } + // Update Oboe client position with frames handled by the callback. + if (getDirection() == Direction::Input) { + mFramesRead += mFramesPerCallback; + } else { + mFramesWritten += mFramesPerCallback; + } + } else if (result == DataCallbackResult::Stop) { + LOGD("Oboe callback returned Stop"); + stopStream = true; + } else { + LOGW("Oboe callback returned unexpected value = %d", result); + stopStream = true; + } + if (stopStream) { + requestStop(); + } +} + +// This callback handler is called every time a buffer has been processed by OpenSL ES. +static void bqCallbackGlue(SLAndroidSimpleBufferQueueItf bq, void *context) { + (reinterpret_cast(context))->processBufferCallback(bq); +} + +SLresult AudioStreamOpenSLES::registerBufferQueueCallback() { + // The BufferQueue + SLresult result = (*mObjectInterface)->GetInterface(mObjectInterface, SL_IID_ANDROIDSIMPLEBUFFERQUEUE, + &mSimpleBufferQueueInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("get buffer queue interface:%p result:%s", + mSimpleBufferQueueInterface, + getSLErrStr(result)); + } else { + // Register the BufferQueue callback + result = (*mSimpleBufferQueueInterface)->RegisterCallback(mSimpleBufferQueueInterface, + bqCallbackGlue, this); + if (SL_RESULT_SUCCESS != result) { + LOGE("RegisterCallback result:%s", getSLErrStr(result)); + } + } + return result; +} + +int32_t AudioStreamOpenSLES::getFramesPerBurst() { + return mFramesPerBurst; +} + +int64_t AudioStreamOpenSLES::getFramesProcessedByServer() { + updateServiceFrameCounter(); + int64_t millis64 = mPositionMillis.get(); + int64_t framesProcessed = millis64 * getSampleRate() / kMillisPerSecond; + return framesProcessed; +} + +Result AudioStreamOpenSLES::waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) { + Result oboeResult = Result::ErrorTimeout; + int64_t sleepTimeNanos = 20 * kNanosPerMillisecond; // arbitrary + int64_t timeLeftNanos = timeoutNanoseconds; + + while (true) { + const StreamState state = getState(); // this does not require a lock + if (nextState != nullptr) { + *nextState = state; + } + if (currentState != state) { // state changed? + oboeResult = Result::OK; + break; + } + + // Did we timeout or did user ask for non-blocking? + if (timeoutNanoseconds <= 0) { + break; + } + + if (sleepTimeNanos > timeLeftNanos){ + sleepTimeNanos = timeLeftNanos; + } + AudioClock::sleepForNanos(sleepTimeNanos); + timeLeftNanos -= sleepTimeNanos; + } + + return oboeResult; +} diff --git a/src/third_party/oboe/src/opensles/AudioStreamOpenSLES.h b/src/third_party/oboe/src/opensles/AudioStreamOpenSLES.h new file mode 100644 index 0000000..81fdc63 --- /dev/null +++ b/src/third_party/oboe/src/opensles/AudioStreamOpenSLES.h @@ -0,0 +1,132 @@ +/* + * Copyright 2015 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_AUDIO_STREAM_OPENSL_ES_H_ +#define OBOE_AUDIO_STREAM_OPENSL_ES_H_ + +#include + +#include +#include + +#include "oboe/Oboe.h" +#include "common/MonotonicCounter.h" +#include "opensles/AudioStreamBuffered.h" +#include "opensles/EngineOpenSLES.h" + +namespace oboe { + +constexpr int kBitsPerByte = 8; +constexpr int kBufferQueueLength = 2; // double buffered for callbacks + +/** + * INTERNAL USE ONLY + * + * A stream that wraps OpenSL ES. + * + * Do not instantiate this class directly. + * Use an OboeStreamBuilder to create one. + */ + +class AudioStreamOpenSLES : public AudioStreamBuffered { +public: + + AudioStreamOpenSLES(); + explicit AudioStreamOpenSLES(const AudioStreamBuilder &builder); + + virtual ~AudioStreamOpenSLES() = default; + + virtual Result open() override; + virtual Result close() override; + + /** + * Query the current state, eg. OBOE_STREAM_STATE_PAUSING + * + * @return state or a negative error. + */ + StreamState getState() const override { return mState.load(); } + + int32_t getFramesPerBurst() override; + + + AudioApi getAudioApi() const override { + return AudioApi::OpenSLES; + } + + /** + * Process next OpenSL ES buffer. + * Called by by OpenSL ES framework. + * + * This is public, but don't call it directly. + */ + void processBufferCallback(SLAndroidSimpleBufferQueueItf bq); + + Result waitForStateChange(StreamState currentState, + StreamState *nextState, + int64_t timeoutNanoseconds) override; + +protected: + + SLuint32 channelCountToChannelMaskDefault(int channelCount) const; + + virtual Result onBeforeDestroy() { return Result::OK; } + virtual Result onAfterDestroy() { return Result::OK; } + + static SLuint32 getDefaultByteOrder(); + + SLresult registerBufferQueueCallback(); + + int32_t getBufferDepth(SLAndroidSimpleBufferQueueItf bq); + + SLresult enqueueCallbackBuffer(SLAndroidSimpleBufferQueueItf bq); + + SLresult configurePerformanceMode(SLAndroidConfigurationItf configItf); + + SLresult updateStreamParameters(SLAndroidConfigurationItf configItf); + + PerformanceMode convertPerformanceMode(SLuint32 openslMode) const; + SLuint32 convertPerformanceMode(PerformanceMode oboeMode) const; + + Result configureBufferSizes(int32_t sampleRate); + + void logUnsupportedAttributes(); + + /** + * Internal use only. + * Use this instead of directly setting the internal state variable. + */ + void setState(StreamState state) { + mState.store(state); + } + + int64_t getFramesProcessedByServer(); + + // OpenSLES stuff + SLObjectItf mObjectInterface = nullptr; + SLAndroidSimpleBufferQueueItf mSimpleBufferQueueInterface = nullptr; + + int32_t mBytesPerCallback = oboe::kUnspecified; + MonotonicCounter mPositionMillis; // for tracking OpenSL ES service position + +private: + std::unique_ptr mCallbackBuffer; + std::atomic mState{StreamState::Uninitialized}; + +}; + +} // namespace oboe + +#endif // OBOE_AUDIO_STREAM_OPENSL_ES_H_ diff --git a/src/third_party/oboe/src/opensles/EngineOpenSLES.cpp b/src/third_party/oboe/src/opensles/EngineOpenSLES.cpp new file mode 100644 index 0000000..d82219e --- /dev/null +++ b/src/third_party/oboe/src/opensles/EngineOpenSLES.cpp @@ -0,0 +1,101 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common/OboeDebug.h" +#include "EngineOpenSLES.h" +#include "OpenSLESUtilities.h" + +using namespace oboe; + +EngineOpenSLES &EngineOpenSLES::getInstance() { + static EngineOpenSLES sInstance; + return sInstance; +} + +SLresult EngineOpenSLES::open() { + std::lock_guard lock(mLock); + + SLresult result = SL_RESULT_SUCCESS; + if (mOpenCount++ == 0) { + + // create engine + result = slCreateEngine(&mEngineObject, 0, NULL, 0, NULL, NULL); + if (SL_RESULT_SUCCESS != result) { + LOGE("EngineOpenSLES - slCreateEngine() result:%s", getSLErrStr(result)); + goto error; + } + + // realize the engine + result = (*mEngineObject)->Realize(mEngineObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("EngineOpenSLES - Realize() engine result:%s", getSLErrStr(result)); + goto error; + } + + // get the engine interface, which is needed in order to create other objects + result = (*mEngineObject)->GetInterface(mEngineObject, SL_IID_ENGINE, &mEngineInterface); + if (SL_RESULT_SUCCESS != result) { + LOGE("EngineOpenSLES - GetInterface() engine result:%s", getSLErrStr(result)); + goto error; + } + } + + return result; + +error: + close(); + return result; +} + +void EngineOpenSLES::close() { + std::lock_guard lock(mLock); + if (--mOpenCount == 0) { + if (mEngineObject != nullptr) { + (*mEngineObject)->Destroy(mEngineObject); + mEngineObject = nullptr; + mEngineInterface = nullptr; + } + } +} + +SLresult EngineOpenSLES::createOutputMix(SLObjectItf *objectItf) { + return (*mEngineInterface)->CreateOutputMix(mEngineInterface, objectItf, 0, 0, 0); +} + +SLresult EngineOpenSLES::createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink) { + + const SLInterfaceID ids[] = {SL_IID_BUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION}; + const SLboolean reqs[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + + return (*mEngineInterface)->CreateAudioPlayer(mEngineInterface, objectItf, audioSource, + audioSink, + sizeof(ids) / sizeof(ids[0]), ids, reqs); +} + +SLresult EngineOpenSLES::createAudioRecorder(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink) { + + const SLInterfaceID ids[] = {SL_IID_ANDROIDSIMPLEBUFFERQUEUE, SL_IID_ANDROIDCONFIGURATION }; + const SLboolean reqs[] = {SL_BOOLEAN_TRUE, SL_BOOLEAN_TRUE}; + + return (*mEngineInterface)->CreateAudioRecorder(mEngineInterface, objectItf, audioSource, + audioSink, + sizeof(ids) / sizeof(ids[0]), ids, reqs); +} + diff --git a/src/third_party/oboe/src/opensles/EngineOpenSLES.h b/src/third_party/oboe/src/opensles/EngineOpenSLES.h new file mode 100644 index 0000000..3d238a8 --- /dev/null +++ b/src/third_party/oboe/src/opensles/EngineOpenSLES.h @@ -0,0 +1,65 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_ENGINE_OPENSLES_H +#define OBOE_ENGINE_OPENSLES_H + +#include +#include + +#include +#include + +namespace oboe { + +/** + * INTERNAL USE ONLY + */ +class EngineOpenSLES { +public: + static EngineOpenSLES &getInstance(); + + SLresult open(); + + void close(); + + SLresult createOutputMix(SLObjectItf *objectItf); + + SLresult createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink); + SLresult createAudioRecorder(SLObjectItf *objectItf, + SLDataSource *audioSource, + SLDataSink *audioSink); + +private: + // Make this a safe Singleton + EngineOpenSLES()= default; + ~EngineOpenSLES()= default; + EngineOpenSLES(const EngineOpenSLES&)= delete; + EngineOpenSLES& operator=(const EngineOpenSLES&)= delete; + + std::mutex mLock; + int32_t mOpenCount = 0; + + SLObjectItf mEngineObject = nullptr; + SLEngineItf mEngineInterface = nullptr; +}; + +} // namespace oboe + + +#endif //OBOE_ENGINE_OPENSLES_H diff --git a/src/third_party/oboe/src/opensles/OpenSLESUtilities.cpp b/src/third_party/oboe/src/opensles/OpenSLESUtilities.cpp new file mode 100644 index 0000000..071c0d0 --- /dev/null +++ b/src/third_party/oboe/src/opensles/OpenSLESUtilities.cpp @@ -0,0 +1,93 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "OpenSLESUtilities.h" + +namespace oboe { + +/* + * OSLES Helpers + */ + +const char *getSLErrStr(SLresult code) { + switch (code) { + case 0: + return "SL_RESULT_SUCCESS"; + case 1: + return "SL_RESULT_PRECONDITIONS_VIOLATE"; + case 2: + return "SL_RESULT_PARAMETER_INVALID"; + case 3: + return "SL_RESULT_MEMORY_FAILURE"; + case 4: + return "SL_RESULT_RESOURCE_ERROR"; + case 5: + return "SL_RESULT_RESOURCE_LOST"; + case 6: + return "SL_RESULT_IO_ERROR"; + case 7: + return "SL_RESULT_BUFFER_INSUFFICIENT"; + case 8: + return "SL_RESULT_CONTENT_CORRUPTED"; + case 9: + return "SL_RESULT_CONTENT_UNSUPPORTED"; + case 10: + return "SL_RESULT_CONTENT_NOT_FOUND"; + case 11: + return "SL_RESULT_PERMISSION_DENIED"; + case 12: + return "SL_RESULT_FEATURE_UNSUPPORTED"; + case 13: + return "SL_RESULT_INTERNAL_ERROR"; + case 14: + return "SL_RESULT_UNKNOWN_ERROR"; + case 15: + return "SL_RESULT_OPERATION_ABORTED"; + case 16: + return "SL_RESULT_CONTROL_LOST"; + default: + return "Unknown error"; + } +} + +SLAndroidDataFormat_PCM_EX OpenSLES_createExtendedFormat( + SLDataFormat_PCM format, SLuint32 representation) { + SLAndroidDataFormat_PCM_EX format_pcm_ex; + format_pcm_ex.formatType = SL_ANDROID_DATAFORMAT_PCM_EX; + format_pcm_ex.numChannels = format.numChannels; + format_pcm_ex.sampleRate = format.samplesPerSec; + format_pcm_ex.bitsPerSample = format.bitsPerSample; + format_pcm_ex.containerSize = format.containerSize; + format_pcm_ex.channelMask = format.channelMask; + format_pcm_ex.endianness = format.endianness; + format_pcm_ex.representation = representation; + return format_pcm_ex; +} + +SLuint32 OpenSLES_ConvertFormatToRepresentation(AudioFormat format) { + switch(format) { + case AudioFormat::I16: + return SL_ANDROID_PCM_REPRESENTATION_SIGNED_INT; + case AudioFormat::Float: + return SL_ANDROID_PCM_REPRESENTATION_FLOAT; + case AudioFormat::Invalid: + case AudioFormat::Unspecified: + default: + return 0; + } +} + +} // namespace oboe diff --git a/src/third_party/oboe/src/opensles/OpenSLESUtilities.h b/src/third_party/oboe/src/opensles/OpenSLESUtilities.h new file mode 100644 index 0000000..50c0e2d --- /dev/null +++ b/src/third_party/oboe/src/opensles/OpenSLESUtilities.h @@ -0,0 +1,44 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OPENSLES_OPENSLESUTILITIES_H +#define OBOE_OPENSLES_OPENSLESUTILITIES_H + +#include +#include "oboe/Oboe.h" + +namespace oboe { + +const char *getSLErrStr(SLresult code); + +/** + * Creates an extended PCM format from the supplied format and data representation. This method + * should only be called on Android devices with API level 21+. API 21 introduced the + * SLAndroidDataFormat_PCM_EX object which allows audio samples to be represented using + * single precision floating-point. + * + * @param format + * @param representation + * @return the extended PCM format + */ +SLAndroidDataFormat_PCM_EX OpenSLES_createExtendedFormat(SLDataFormat_PCM format, + SLuint32 representation); + +SLuint32 OpenSLES_ConvertFormatToRepresentation(AudioFormat format); + +} // namespace oboe + +#endif //OBOE_OPENSLES_OPENSLESUTILITIES_H diff --git a/src/third_party/oboe/src/opensles/OutputMixerOpenSLES.cpp b/src/third_party/oboe/src/opensles/OutputMixerOpenSLES.cpp new file mode 100644 index 0000000..e06f306 --- /dev/null +++ b/src/third_party/oboe/src/opensles/OutputMixerOpenSLES.cpp @@ -0,0 +1,74 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +#include "common/OboeDebug.h" +#include "EngineOpenSLES.h" +#include "OpenSLESUtilities.h" +#include "OutputMixerOpenSLES.h" + +using namespace oboe; + +OutputMixerOpenSL &OutputMixerOpenSL::getInstance() { + static OutputMixerOpenSL sInstance; + return sInstance; +} + +SLresult OutputMixerOpenSL::open() { + std::lock_guard lock(mLock); + + SLresult result = SL_RESULT_SUCCESS; + if (mOpenCount++ == 0) { + // get the output mixer + result = EngineOpenSLES::getInstance().createOutputMix(&mOutputMixObject); + if (SL_RESULT_SUCCESS != result) { + LOGE("OutputMixerOpenSL() - createOutputMix() result:%s", getSLErrStr(result)); + goto error; + } + + // realize the output mix + result = (*mOutputMixObject)->Realize(mOutputMixObject, SL_BOOLEAN_FALSE); + if (SL_RESULT_SUCCESS != result) { + LOGE("OutputMixerOpenSL() - Realize() mOutputMixObject result:%s", getSLErrStr(result)); + goto error; + } + } + + return result; + +error: + close(); + return result; +} + +void OutputMixerOpenSL::close() { + std::lock_guard lock(mLock); + + if (--mOpenCount == 0) { + // destroy output mix object, and invalidate all associated interfaces + if (mOutputMixObject != nullptr) { + (*mOutputMixObject)->Destroy(mOutputMixObject); + mOutputMixObject = nullptr; + } + } +} + +SLresult OutputMixerOpenSL::createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource) { + SLDataLocator_OutputMix loc_outmix = {SL_DATALOCATOR_OUTPUTMIX, mOutputMixObject}; + SLDataSink audioSink = {&loc_outmix, NULL}; + return EngineOpenSLES::getInstance().createAudioPlayer(objectItf, audioSource, &audioSink); +} diff --git a/src/third_party/oboe/src/opensles/OutputMixerOpenSLES.h b/src/third_party/oboe/src/opensles/OutputMixerOpenSLES.h new file mode 100644 index 0000000..813fd01 --- /dev/null +++ b/src/third_party/oboe/src/opensles/OutputMixerOpenSLES.h @@ -0,0 +1,58 @@ +/* + * Copyright 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef OBOE_OUTPUT_MIXER_OPENSLES_H +#define OBOE_OUTPUT_MIXER_OPENSLES_H + +#include +#include + +#include +#include + +namespace oboe { + +/** + * INTERNAL USE ONLY + */ + +class OutputMixerOpenSL { +public: + static OutputMixerOpenSL &getInstance(); + + SLresult open(); + + void close(); + + SLresult createAudioPlayer(SLObjectItf *objectItf, + SLDataSource *audioSource); + +private: + // Make this a safe Singleton + OutputMixerOpenSL()= default; + ~OutputMixerOpenSL()= default; + OutputMixerOpenSL(const OutputMixerOpenSL&)= delete; + OutputMixerOpenSL& operator=(const OutputMixerOpenSL&)= delete; + + std::mutex mLock; + int32_t mOpenCount = 0; + + SLObjectItf mOutputMixObject = nullptr; +}; + +} // namespace oboe + +#endif //OBOE_OUTPUT_MIXER_OPENSLES_H diff --git a/src/third_party/r8b/CDSPBlockConvolver.h b/src/third_party/r8b/CDSPBlockConvolver.h new file mode 100644 index 0000000..fe27eaa --- /dev/null +++ b/src/third_party/r8b/CDSPBlockConvolver.h @@ -0,0 +1,646 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPBlockConvolver.h + * + * @brief Single-block overlap-save convolution processor class. + * + * This file includes single-block overlap-save convolution processor class. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPBLOCKCONVOLVER_INCLUDED +#define R8B_CDSPBLOCKCONVOLVER_INCLUDED + +#include "CDSPFIRFilter.h" +#include "CDSPProcessor.h" + +namespace r8b { + +/** + * @brief Single-block overlap-save convolution processing class. + * + * Class that implements single-block overlap-save convolution processing. The + * length of a single FFT block used depends on the length of the filter + * kernel. + * + * The rationale behind "single-block" processing is that increasing the FFT + * block length by 2 is more efficient than performing convolution at the same + * FFT block length but using two blocks. + * + * This class also implements a built-in resampling by any whole-number + * factor, which simplifies the overall resampling objects topology. + */ + +class CDSPBlockConvolver : public CDSPProcessor +{ +public: + /** + * Constructor initializes internal variables and constants of *this + * object. + * + * @param aFilter Pre-calculated filter data. Reference to this object is + * inhertied by *this object, and the object will be released when *this + * object is destroyed. If upsampling is used, filter's gain should be + * equal to the upsampling factor. + * @param aUpFactor The upsampling factor, positive value. E.g. value of 2 + * means 2x upsampling should be performed over the input data. + * @param aDownFactor The downsampling factor, positive value. E.g. value + * of 2 means 2x downsampling should be performed over the output data. + * @param PrevLatency Latency, in samples (any value >=0), which was left + * in the output signal by a previous process. This value is usually + * non-zero if the minimum-phase filters are in use. This value is always + * zero if the linear-phase filters are in use. + * @param aDoConsumeLatency "True" if the output latency should be + * consumed. Does not apply to the fractional part of the latency (if such + * part is available). + */ + + CDSPBlockConvolver( CDSPFIRFilter& aFilter, const int aUpFactor, + const int aDownFactor, const double PrevLatency = 0.0, + const bool aDoConsumeLatency = true ) + : Filter( &aFilter ) + , UpFactor( aUpFactor ) + , DownFactor( aDownFactor ) + , DoConsumeLatency( aDoConsumeLatency ) + , BlockLen2( 2 << Filter -> getBlockLenBits() ) + { + R8BASSERT( UpFactor > 0 ); + R8BASSERT( DownFactor > 0 ); + R8BASSERT( PrevLatency >= 0.0 ); + + int fftinBits; + UpShift = getBitOccupancy( UpFactor ) - 1; + + if(( 1 << UpShift ) == UpFactor ) + { + fftinBits = Filter -> getBlockLenBits() + 1 - UpShift; + PrevInputLen = ( Filter -> getKernelLen() - 1 + UpFactor - 1 ) / + UpFactor; + + InputLen = BlockLen2 - PrevInputLen * UpFactor; + } + else + { + UpShift = -1; + fftinBits = Filter -> getBlockLenBits() + 1; + PrevInputLen = Filter -> getKernelLen() - 1; + InputLen = BlockLen2 - PrevInputLen; + } + + OutOffset = ( Filter -> isZeroPhase() ? Filter -> getLatency() : 0 ); + LatencyFrac = Filter -> getLatencyFrac() + PrevLatency * UpFactor; + Latency = (int) LatencyFrac; + const int InLatency = Latency + Filter -> getLatency() - OutOffset; + LatencyFrac -= Latency; + LatencyFrac /= DownFactor; + + Latency += InputLen + Filter -> getLatency(); + + int fftoutBits; + InputDelay = 0; + UpSkipInit = 0; + DownSkipInit = 0; + DownShift = getBitOccupancy( DownFactor ) - 1; + + if(( 1 << DownShift ) == DownFactor ) + { + fftoutBits = Filter -> getBlockLenBits() + 1 - DownShift; + + if( DownFactor > 1 ) + { + if( UpShift > 0 ) + { + // This case never happens in practice due to mutual + // exclusion of "power of 2" DownFactor and UpFactor + // values. + + R8BASSERT( UpShift == 0 ); + } + else + { + // Make sure InputLen is divisible by DownFactor. + + const int ilc = InputLen & ( DownFactor - 1 ); + PrevInputLen += ilc; + InputLen -= ilc; + Latency -= ilc; + + // Correct InputDelay for input and filter's latency. + + const int lc = InLatency & ( DownFactor - 1 ); + + if( lc > 0 ) + { + InputDelay = DownFactor - lc; + } + + if( !DoConsumeLatency ) + { + Latency /= DownFactor; + } + } + } + } + else + { + fftoutBits = Filter -> getBlockLenBits() + 1; + DownShift = -1; + + if( !DoConsumeLatency && DownFactor > 1 ) + { + DownSkipInit = Latency % DownFactor; + Latency /= DownFactor; + } + } + + fftin = new CDSPRealFFTKeeper( fftinBits ); + + if( fftoutBits == fftinBits ) + { + fftout = fftin; + } + else + { + ffto2 = new CDSPRealFFTKeeper( fftoutBits ); + fftout = ffto2; + } + + WorkBlocks.alloc( BlockLen2 * 2 + PrevInputLen ); + CurInput = &WorkBlocks[ 0 ]; + CurOutput = &WorkBlocks[ BlockLen2 ]; // CurInput and + // CurOutput are memory-aligned. + PrevInput = &WorkBlocks[ BlockLen2 * 2 ]; + + clear(); + + R8BCONSOLE( "CDSPBlockConvolver: flt_len=%i in_len=%i io=%i/%i " + "fft=%i/%i latency=%i\n", Filter -> getKernelLen(), InputLen, + UpFactor, DownFactor, (*fftin) -> getLen(), (*fftout) -> getLen(), + getLatency() ); + } + + virtual ~CDSPBlockConvolver() + { + Filter -> unref(); + } + + virtual int getLatency() const + { + return( DoConsumeLatency ? 0 : Latency ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return(( MaxInLen * UpFactor + DownFactor - 1 ) / DownFactor ); + } + + virtual void clear() + { + memset( &PrevInput[ 0 ], 0, PrevInputLen * sizeof( double )); + + if( DoConsumeLatency ) + { + LatencyLeft = Latency; + } + else + { + LatencyLeft = 0; + + if( DownShift > 0 ) + { + memset( &CurOutput[ 0 ], 0, ( BlockLen2 >> DownShift ) * + sizeof( double )); + } + else + { + memset( &CurOutput[ BlockLen2 - OutOffset ], 0, OutOffset * + sizeof( double )); + + memset( &CurOutput[ 0 ], 0, ( InputLen - OutOffset ) * + sizeof( double )); + } + } + + memset( CurInput, 0, InputDelay * sizeof( double )); + + InDataLeft = InputLen - InputDelay; + UpSkip = UpSkipInit; + DownSkip = DownSkipInit; + } + + virtual int process( double* ip, int l0, double*& op0 ) + { + R8BASSERT( l0 >= 0 ); + R8BASSERT( UpFactor / DownFactor <= 1 || ip != op0 || l0 == 0 ); + + double* op = op0; + int l = l0 * UpFactor; + l0 = 0; + + while( l > 0 ) + { + const int Offs = InputLen - InDataLeft; + + if( l < InDataLeft ) + { + InDataLeft -= l; + + if( UpShift >= 0 ) + { + memcpy( &CurInput[ Offs >> UpShift ], ip, + ( l >> UpShift ) * sizeof( double )); + } + else + { + copyUpsample( ip, &CurInput[ Offs ], l ); + } + + copyToOutput( Offs - OutOffset, op, l, l0 ); + break; + } + + const int b = InDataLeft; + l -= b; + InDataLeft = InputLen; + int ilu; + + if( UpShift >= 0 ) + { + const int bu = b >> UpShift; + memcpy( &CurInput[ Offs >> UpShift ], ip, + bu * sizeof( double )); + + ip += bu; + ilu = InputLen >> UpShift; + } + else + { + copyUpsample( ip, &CurInput[ Offs ], b ); + ilu = InputLen; + } + + const int pil = (int) ( PrevInputLen * sizeof( double )); + memcpy( &CurInput[ ilu ], PrevInput, pil ); + memcpy( PrevInput, &CurInput[ ilu - PrevInputLen ], pil ); + + (*fftin) -> forward( CurInput ); + + if( UpShift > 0 ) + { + #if R8B_FLOATFFT + mirrorInputSpectrum( (float*) CurInput ); + #else // R8B_FLOATFFT + mirrorInputSpectrum( CurInput ); + #endif // R8B_FLOATFFT + } + + if( Filter -> isZeroPhase() ) + { + (*fftout) -> multiplyBlocksZ( Filter -> getKernelBlock(), + CurInput ); + } + else + { + (*fftout) -> multiplyBlocks( Filter -> getKernelBlock(), + CurInput ); + } + + if( DownShift > 0 ) + { + const int z = BlockLen2 >> DownShift; + + #if R8B_FLOATFFT + float* const kb = (float*) Filter -> getKernelBlock(); + float* const p = (float*) CurInput; + #else // R8B_FLOATFFT + const double* const kb = Filter -> getKernelBlock(); + double* const p = CurInput; + #endif // R8B_FLOATFFT + + p[ 1 ] = kb[ z ] * p[ z ] - kb[ z + 1 ] * p[ z + 1 ]; + } + + (*fftout) -> inverse( CurInput ); + + copyToOutput( Offs - OutOffset, op, b, l0 ); + + double* const tmp = CurInput; + CurInput = CurOutput; + CurOutput = tmp; + } + + return( l0 ); + } + +private: + CDSPFIRFilter* Filter; ///< Filter in use. + ///< + CPtrKeeper< CDSPRealFFTKeeper* > fftin; ///< FFT object 1, used to produce + ///< the input spectrum (can embed the "power of 2" upsampling). + ///< + CPtrKeeper< CDSPRealFFTKeeper* > ffto2; ///< FFT object 2 (can be NULL). + ///< + CDSPRealFFTKeeper* fftout; ///< FFT object used to produce the output + ///< signal (can embed the "power of 2" downsampling), may point to + ///< either "fftin" or "ffto2". + ///< + int UpFactor; ///< Upsampling factor. + ///< + int DownFactor; ///< Downsampling factor. + ///< + bool DoConsumeLatency; ///< "True" if the output latency should be + ///< consumed. Does not apply to the fractional part of the latency + ///< (if such part is available). + ///< + int BlockLen2; ///< Equals block length * 2. + ///< + int OutOffset; ///< Output offset, depends on filter's introduced latency. + ///< + int PrevInputLen; ///< The length of previous input data saved, used for + ///< overlap. + ///< + int InputLen; ///< The number of input samples that should be accumulated + ///< before the input block is processed. + ///< + int Latency; ///< Processing latency, in samples. + ///< + double LatencyFrac; ///< Fractional latency, in samples, that is left in + ///< the output signal. + ///< + int UpShift; ///< "Power of 2" upsampling shift. Equals -1 if UpFactor is + ///< not a "power of 2" value. Equals 0 if UpFactor equals 1. + ///< + int DownShift; ///< "Power of 2" downsampling shift. Equals -1 if + ///< DownFactor is not a "power of 2". Equals 0 if DownFactor equals + ///< 1. + ///< + int InputDelay; ///< Additional input delay, in samples. Used to make the + ///< output delay divisible by DownShift. Used only if UpShift <= 0 + ///< and DownShift > 0. + ///< + CFixedBuffer< double > WorkBlocks; ///< Previous input data, input and + ///< output data blocks, overall capacity = BlockLen2 * 2 + + ///< PrevInputLen. Used in the flip-flop manner. + ///< + double* PrevInput; ///< Previous input data buffer, capacity = BlockLen. + ///< + double* CurInput; ///< Input data buffer, capacity = BlockLen2. + ///< + double* CurOutput; ///< Output data buffer, capacity = BlockLen2. + ///< + int InDataLeft; ///< Samples left before processing input and output FFT + ///< blocks. Initialized to InputLen on clear. + ///< + int LatencyLeft; ///< Latency in samples left to skip. + ///< + int UpSkip; ///< The current upsampling sample skip (value in the range + ///< 0 to UpFactor - 1). + ///< + int UpSkipInit; ///< The initial UpSkip value after clear(). + ///< + int DownSkip; ///< The current downsampling sample skip (value in the + ///< range 0 to DownFactor - 1). Not used if DownShift > 0. + ///< + int DownSkipInit; ///< The initial DownSkip value after clear(). + ///< + + /** + * Function copies samples from the input buffer to the output buffer + * while inserting zeros inbetween them to perform the whole-numbered + * upsampling. + * + * @param[in,out] ip0 Input buffer. Will be advanced on function's return. + * @param[out] op Output buffer. + * @param l0 The number of samples to fill in the output buffer, including + * both input samples and interpolation (zero) samples. + */ + + void copyUpsample( double*& ip0, double* op, int l0 ) + { + int b = min( UpSkip, l0 ); + + if( b > 0 ) + { + l0 -= b; + UpSkip -= b; + *op = 0.0; + op++; + b--; + + while( b > 0 ) + { + *op = 0.0; + op++; + b--; + } + } + + double* ip = ip0; + int l = l0 / UpFactor; + int lz = l0 - l * UpFactor; + const int upf = UpFactor; + + if( upf == 3 ) + { + while( l > 0 ) + { + op[ 0 ] = *ip; + op[ 1 ] = 0.0; + op[ 2 ] = 0.0; + ip++; + op += upf; + l--; + } + } + else + if( upf == 5 ) + { + while( l > 0 ) + { + op[ 0 ] = *ip; + op[ 1 ] = 0.0; + op[ 2 ] = 0.0; + op[ 3 ] = 0.0; + op[ 4 ] = 0.0; + ip++; + op += upf; + l--; + } + } + else + { + while( l > 0 ) + { + op[ 0 ] = *ip; + int j; + + for( j = 1; j < upf; j++ ) + { + op[ j ] = 0.0; + } + + ip++; + op += upf; + l--; + } + } + + if( lz > 0 ) + { + *op = *ip; + op++; + ip++; + UpSkip = UpFactor - lz; + + while( lz > 1 ) + { + *op = 0.0; + op++; + lz--; + } + } + + ip0 = ip; + } + + /** + * Function copies sample data from the CurOutput buffer to the specified + * output buffer and advances its position. If necessary, this function + * "consumes" latency and performs downsampling. + * + * @param Offs CurOutput buffer offset, can be negative. + * @param[out] op0 Output buffer pointer, will be advanced. + * @param b The number of output samples available, including those which + * are discarded during whole-number downsampling. + * @param l0 The overall output sample count, will be increased. + */ + + void copyToOutput( int Offs, double*& op0, int b, int& l0 ) + { + if( Offs < 0 ) + { + if( Offs + b <= 0 ) + { + Offs += BlockLen2; + } + else + { + copyToOutput( Offs + BlockLen2, op0, -Offs, l0 ); + b += Offs; + Offs = 0; + } + } + + if( LatencyLeft > 0 ) + { + if( LatencyLeft >= b ) + { + LatencyLeft -= b; + return; + } + + Offs += LatencyLeft; + b -= LatencyLeft; + LatencyLeft = 0; + } + + const int df = DownFactor; + + if( DownShift > 0 ) + { + int Skip = Offs & ( df - 1 ); + + if( Skip > 0 ) + { + Skip = df - Skip; + b -= Skip; + Offs += Skip; + } + + if( b > 0 ) + { + b = ( b + df - 1 ) >> DownShift; + memcpy( op0, &CurOutput[ Offs >> DownShift ], + b * sizeof( double )); + + op0 += b; + l0 += b; + } + } + else + { + if( df > 1 ) + { + const double* ip = &CurOutput[ Offs + DownSkip ]; + int l = ( b + df - 1 - DownSkip ) / df; + DownSkip += l * df - b; + + double* op = op0; + l0 += l; + op0 += l; + + while( l > 0 ) + { + *op = *ip; + op++; + ip += df; + l--; + } + } + else + { + memcpy( op0, &CurOutput[ Offs ], b * sizeof( double )); + op0 += b; + l0 += b; + } + } + } + + /** + * Function performs input spectrum mirroring which is used to perform a + * fast "power of 2" upsampling. Such mirroring is equivalent to insertion + * of zeros into the input signal. + * + * @param p Spectrum data block to mirror. + */ + + template< class T > + void mirrorInputSpectrum( T* const p ) + { + const int bl1 = BlockLen2 >> UpShift; + const int bl2 = bl1 + bl1; + int i; + + for( i = bl1 + 2; i < bl2; i += 2 ) + { + p[ i ] = p[ bl2 - i ]; + p[ i + 1 ] = -p[ bl2 - i + 1 ]; + } + + p[ bl1 ] = p[ 1 ]; + p[ bl1 + 1 ] = 0.0; + p[ 1 ] = p[ 0 ]; + + for( i = 1; i < UpShift; i++ ) + { + const int z = bl1 << i; + memcpy( &p[ z ], p, z * sizeof( T )); + p[ z + 1 ] = 0.0; + } + } +}; + +} // namespace r8b + +#endif // R8B_CDSPBLOCKCONVOLVER_INCLUDED diff --git a/src/third_party/r8b/CDSPFIRFilter.h b/src/third_party/r8b/CDSPFIRFilter.h new file mode 100644 index 0000000..8930fcb --- /dev/null +++ b/src/third_party/r8b/CDSPFIRFilter.h @@ -0,0 +1,721 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPFIRFilter.h + * + * @brief FIR filter generator and filter cache classes. + * + * This file includes low-pass FIR filter generator and filter cache. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPFIRFILTER_INCLUDED +#define R8B_CDSPFIRFILTER_INCLUDED + +#include "CDSPSincFilterGen.h" +#include "CDSPRealFFT.h" + +namespace r8b { + +/** + * Enumeration of filter's phase responses. + */ + +enum EDSPFilterPhaseResponse +{ + fprLinearPhase = 0, ///< Linear-phase response. Features a linear-phase + ///< high-latency response, with the latency expressed as integer + ///< value. + ///< + fprMinPhase ///< Minimum-phase response. Features a minimal latency + ///< response, but the response's phase is non-linear. The latency is + ///< usually expressed as non-integer value, and usually is small, but + ///< is never equal to zero. The minimum-phase filter is transformed + ///< from the linear-phase filter. The transformation has precision + ///< limits which may skew both the -3 dB point and attenuation of the + ///< filter being transformed: as it was measured, the skew happens + ///< purely at random, and in most cases it is within tolerable range. + ///< In a small (1%) random subset of cases the skew is bigger and + ///< cannot be predicted. Minimum-phase transform requires 64-bit + ///< floating point FFT precision, results with 32-bit float FFT are + ///< far from optimal. + ///< +}; + +/** + * @brief Calculation and storage class for FIR filters. + * + * Class that implements calculation and storing of a FIR filter (currently + * contains low-pass filter calculation routine designed for sample rate + * conversion). Objects of this class cannot be created directly, but can be + * obtained via the CDSPFilterCache::getLPFilter() static function. + */ + +class CDSPFIRFilter : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPFIRFilter ); + + friend class CDSPFIRFilterCache; + +public: + ~CDSPFIRFilter() + { + R8BASSERT( RefCount == 0 ); + + delete Next; + } + + /** + * @return The minimal allowed low-pass filter's transition band, in + * percent. + */ + + static double getLPMinTransBand() + { + return( 0.5 ); + } + + /** + * @return The maximal allowed low-pass filter's transition band, in + * percent. + */ + + static double getLPMaxTransBand() + { + return( 45.0 ); + } + + /** + * @return The minimal allowed low-pass filter's stop-band attenuation, in + * decibel. + */ + + static double getLPMinAtten() + { + return( 49.0 ); + } + + /** + * @return The maximal allowed low-pass filter's stop-band attenuation, in + * decibel. + */ + + static double getLPMaxAtten() + { + return( 218.0 ); + } + + /** + * @return "True" if kernel block of *this filter has zero-phase response. + */ + + bool isZeroPhase() const + { + return( IsZeroPhase ); + } + + /** + * @return Filter's latency, in samples (integer part). + */ + + int getLatency() const + { + return( Latency ); + } + + /** + * @return Filter's latency, in samples (fractional part). Always zero for + * linear-phase filters. + */ + + double getLatencyFrac() const + { + return( LatencyFrac ); + } + + /** + * @return Filter kernel length, in samples. Not to be confused with the + * block length. + */ + + int getKernelLen() const + { + return( KernelLen ); + } + + /** + * @return Filter's block length, espressed as Nth power of 2. The actual + * length is twice as large due to zero-padding. + */ + + int getBlockLenBits() const + { + return( BlockLenBits ); + } + + /** + * @return Filter's kernel block, in complex-numbered form obtained via + * the CDSPRealFFT::forward() function call, zero-padded, gain-adjusted + * with the CDSPRealFFT::getInvMulConst() * ReqGain constant, immediately + * suitable for convolution. Kernel block may have "zero-phase" response, + * depending on the isZeroPhase() function's result. + */ + + const double* getKernelBlock() const + { + return( KernelBlock ); + } + + /** + * This function should be called when the filter obtained via the + * filter cache is no longer needed. + */ + + void unref(); + +private: + double ReqNormFreq; ///< Required normalized frequency, 0 to 1 inclusive. + ///< + double ReqTransBand; ///< Required transition band in percent, as passed + ///< by the user. + ///< + double ReqAtten; ///< Required stop-band attenuation in decibel, as passed + ///< by the user (positive value). + ///< + EDSPFilterPhaseResponse ReqPhase; ///< Required filter's phase response. + ///< + double ReqGain; ///< Required overall filter's gain. + ///< + CDSPFIRFilter* Next; ///< Next FIR filter in cache's list. + ///< + int RefCount; ///< The number of references made to *this FIR filter. + ///< + bool IsZeroPhase; ///< "True" if kernel block of *this filter has + ///< zero-phase response. + ///< + int Latency; ///< Filter's latency in samples (integer part). + ///< + double LatencyFrac; ///< Filter's latency in samples (fractional part). + ///< + int KernelLen; ///< Filter kernel length, in samples. + ///< + int BlockLenBits; ///< Block length used to store *this FIR filter, + ///< expressed as Nth power of 2. This value is used directly by the + ///< convolver. + ///< + CFixedBuffer< double > KernelBlock; ///< FIR filter buffer, capacity + ///< equals to 1 << ( BlockLenBits + 1 ). Second part of the buffer + ///< contains zero-padding to allow alias-free convolution. + ///< Memory-aligned. + ///< + + CDSPFIRFilter() + : RefCount( 1 ) + { + } + + /** + * Function builds filter kernel based on the "Req" parameters. + * + * @param ExtAttenCorrs External attentuation correction table, for + * internal use. + */ + + void buildLPFilter( const double* const ExtAttenCorrs ) + { + const double tb = ReqTransBand * 0.01; + double pwr; + double fo1; + double hl; + double atten = -ReqAtten; + + if( tb >= 0.25 ) + { + if( ReqAtten >= 117.0 ) + { + atten -= 1.60; + } + else + if( ReqAtten >= 60.0 ) + { + atten -= 1.91; + } + else + { + atten -= 2.25; + } + } + else + if( tb >= 0.10 ) + { + if( ReqAtten >= 117.0 ) + { + atten -= 0.69; + } + else + if( ReqAtten >= 60.0 ) + { + atten -= 0.73; + } + else + { + atten -= 1.13; + } + } + else + { + if( ReqAtten >= 117.0 ) + { + atten -= 0.21; + } + else + if( ReqAtten >= 60.0 ) + { + atten -= 0.25; + } + else + { + atten -= 0.36; + } + } + + static const int AttenCorrCount = 264; + static const double AttenCorrMin = 49.0; + static const double AttenCorrDiff = 176.25; + int AttenCorr = (int) floor(( -atten - AttenCorrMin ) * + AttenCorrCount / AttenCorrDiff + 0.5 ); + + AttenCorr = min( AttenCorrCount, max( 0, AttenCorr )); + + if( ExtAttenCorrs != NULL ) + { + atten -= ExtAttenCorrs[ AttenCorr ]; + } + else + if( tb >= 0.25 ) + { + static const double AttenCorrScale = 101.0; + static const signed char AttenCorrs[] = { + -127, -127, -125, -125, -122, -119, -115, -110, -104, -97, + -91, -82, -75, -24, -16, -6, 4, 14, 24, 29, 30, 32, 37, 44, + 51, 57, 63, 67, 65, 50, 53, 56, 58, 60, 63, 64, 66, 68, 74, + 77, 78, 78, 78, 79, 79, 60, 60, 60, 61, 59, 52, 47, 41, 36, + 30, 24, 17, 9, 0, -8, -10, -11, -14, -13, -18, -25, -31, -38, + -44, -50, -57, -63, -68, -74, -81, -89, -96, -101, -104, -107, + -109, -110, -86, -84, -85, -82, -80, -77, -73, -67, -62, -55, + -48, -42, -35, -30, -20, -11, -2, 5, 6, 6, 7, 11, 16, 21, 26, + 34, 41, 46, 49, 52, 55, 56, 48, 49, 51, 51, 52, 52, 52, 52, + 52, 51, 51, 50, 47, 47, 50, 48, 46, 42, 38, 35, 31, 27, 24, + 20, 16, 12, 11, 12, 10, 8, 4, -1, -6, -11, -16, -19, -17, -21, + -24, -27, -32, -34, -37, -38, -40, -41, -40, -40, -42, -41, + -44, -45, -43, -41, -34, -31, -28, -24, -21, -18, -14, -10, + -5, -1, 2, 5, 8, 7, 4, 3, 2, 2, 4, 6, 8, 9, 9, 10, 10, 10, 10, + 9, 8, 9, 11, 14, 13, 12, 11, 10, 8, 7, 6, 5, 3, 2, 2, -1, -1, + -3, -3, -4, -4, -5, -4, -6, -7, -9, -5, -1, -1, 0, 1, 0, -2, + -3, -4, -5, -5, -8, -13, -13, -13, -12, -13, -12, -11, -11, + -9, -8, -7, -5, -3, -1, 2, 4, 6, 9, 10, 11, 14, 18, 21, 24, + 27, 30, 34, 37, 37, 39, 40 }; + + atten -= AttenCorrs[ AttenCorr ] / AttenCorrScale; + } + else + if( tb >= 0.10 ) + { + static const double AttenCorrScale = 210.0; + static const signed char AttenCorrs[] = { + -113, -118, -122, -125, -126, -97, -95, -92, -92, -89, -82, + -75, -69, -48, -42, -36, -30, -22, -14, -5, -2, 1, 6, 13, 22, + 28, 35, 41, 48, 55, 56, 56, 61, 65, 71, 77, 81, 83, 85, 85, + 74, 74, 73, 72, 71, 70, 68, 64, 59, 56, 49, 52, 46, 42, 36, + 32, 26, 20, 13, 7, -2, -6, -10, -15, -20, -27, -33, -38, -44, + -43, -48, -53, -57, -63, -69, -73, -75, -79, -81, -74, -76, + -77, -77, -78, -81, -80, -80, -78, -76, -65, -62, -59, -56, + -51, -48, -44, -38, -33, -25, -19, -13, -5, -1, 2, 7, 13, 17, + 21, 25, 30, 35, 40, 45, 50, 53, 56, 57, 55, 58, 59, 62, 64, + 67, 67, 68, 68, 62, 61, 61, 59, 59, 57, 57, 55, 52, 48, 42, + 38, 35, 31, 26, 20, 15, 13, 10, 7, 3, -2, -8, -13, -17, -23, + -28, -34, -37, -40, -41, -45, -48, -50, -53, -57, -59, -62, + -63, -63, -57, -57, -56, -56, -54, -54, -53, -49, -48, -41, + -38, -33, -31, -26, -23, -18, -12, -9, -7, -7, -3, 0, 5, 9, + 14, 16, 20, 22, 21, 23, 25, 27, 28, 29, 34, 33, 35, 33, 31, + 30, 29, 29, 26, 26, 25, 24, 20, 19, 15, 10, 8, 4, 1, -2, -6, + -10, -16, -19, -23, -26, -27, -30, -34, -39, -43, -47, -51, + -52, -54, -56, -58, -59, -62, -63, -66, -65, -65, -64, -59, + -57, -54, -52, -48, -44, -42, -37, -32, -22, -17, -10, -3, 5, + 13, 22, 30, 40, 50, 60, 72 }; + + atten -= AttenCorrs[ AttenCorr ] / AttenCorrScale; + } + else + { + static const double AttenCorrScale = 196.0; + static const signed char AttenCorrs[] = { + -15, -17, -20, -20, -20, -21, -20, -16, -17, -18, -17, -13, + -12, -11, -9, -7, -5, -4, -1, 1, 3, 4, 5, 6, 7, 9, 9, 10, 10, + 10, 11, 11, 11, 12, 12, 12, 10, 11, 10, 10, 8, 10, 11, 10, 11, + 11, 13, 14, 15, 19, 27, 26, 23, 18, 14, 8, 4, -2, -6, -12, + -17, -23, -28, -33, -37, -42, -46, -49, -53, -57, -60, -61, + -64, -65, -67, -66, -66, -66, -65, -64, -61, -59, -56, -52, + -48, -42, -38, -31, -27, -19, -13, -7, -1, 8, 14, 22, 29, 37, + 45, 52, 59, 66, 73, 80, 86, 91, 96, 100, 104, 108, 111, 114, + 115, 117, 118, 120, 120, 118, 117, 114, 113, 111, 107, 103, + 99, 95, 89, 84, 78, 72, 66, 60, 52, 44, 37, 30, 21, 14, 6, -3, + -11, -18, -26, -34, -43, -51, -58, -65, -73, -78, -85, -90, + -97, -102, -107, -113, -115, -118, -121, -125, -125, -126, + -126, -126, -125, -124, -121, -119, -115, -111, -109, -101, + -102, -95, -88, -81, -73, -67, -63, -54, -47, -40, -33, -26, + -18, -11, -5, 2, 8, 14, 19, 25, 31, 36, 37, 43, 47, 49, 51, + 52, 57, 57, 56, 57, 58, 58, 58, 57, 56, 52, 52, 50, 48, 44, + 41, 39, 37, 33, 31, 26, 24, 21, 18, 14, 11, 8, 4, 2, -2, -5, + -7, -9, -11, -13, -15, -16, -18, -19, -20, -23, -24, -24, -25, + -27, -26, -27, -29, -30, -31, -32, -35, -36, -39, -40, -44, + -46, -51, -54, -59, -63, -69, -76, -83, -91, -98 }; + + atten -= AttenCorrs[ AttenCorr ] / AttenCorrScale; + } + + pwr = 7.43932822146293e-8 * sqr( atten ) + 0.000102747434588003 * + cos( 0.00785021930010397 * atten ) * cos( 0.633854318781239 + + 0.103208573657699 * atten ) - 0.00798132247867036 - + 0.000903555213543865 * atten - 0.0969365532127236 * exp( + 0.0779275237937911 * atten ) - 1.37304948662012e-5 * atten * cos( + 0.00785021930010397 * atten ); + + if( pwr <= 0.067665322581 ) + { + if( tb >= 0.25 ) + { + hl = 2.6778150875894 / tb + 300.547590563091 * atan( atan( + 2.68959772209918 * pwr )) / ( 5.5099277187035 * tb - tb * + tanh( cos( asinh( atten )))); + + fo1 = 0.987205355829873 * tb + 1.00011788929851 * atan2( + -0.321432067051302 - 6.19131357321578 * sqrt( pwr ), + hl + -1.14861472207245 / ( hl - 14.1821147585957 ) + pow( + 0.9521145021664, pow( atan2( 1.12018764830637, tb ), + 2.10988901686912 * hl - 20.9691278378345 ))); + } + else + if( tb >= 0.10 ) + { + hl = ( 1.56688617018066 + 142.064321294568 * pwr + + 0.00419441117131136 * cos( 243.633511747297 * pwr ) - + 0.022953443903576 * atten - 0.026629568860284 * cos( + 127.715550622571 * pwr )) / tb; + + fo1 = 0.982299356642411 * tb + 0.999441744774215 * asinh(( + -0.361783054039583 - 5.80540593623676 * sqrt( pwr )) / + hl ); + } + else + { + hl = ( 2.45739657014937 + 269.183679500541 * pwr * cos( + 5.73225668178813 + atan2( cosh( 0.988861169868941 - + 17.2201556280744 * pwr ), 1.08340138240431 * pwr ))) / tb; + + fo1 = 2.291956939 * tb + 0.01942450693 * sqr( tb ) * hl - + 4.67538973161837 * pwr * tb - 1.668433124 * tb * + pow( pwr, pwr ); + } + } + else + { + if( tb >= 0.25 ) + { + hl = ( 1.50258368698213 + 158.556968859477 * asinh( pwr ) * + tanh( 57.9466246871383 * tanh( pwr )) - + 0.0105440479814834 * atten ) / tb; + + fo1 = 0.994024401639321 * tb + ( -0.236282717577215 - + 6.8724924545387 * sqrt( sin( pwr ))) / hl; + } + else + if( tb >= 0.10 ) + { + hl = ( 1.50277377248945 + 158.222625721046 * asinh( pwr ) * + tanh( 1.02875299001715 + 42.072277322604 * pwr ) - + 0.0108380943845632 * atten ) / tb; + + fo1 = 0.992539376734551 * tb + ( -0.251747813037178 - + 6.74159892452584 * sqrt( tanh( tanh( tan( pwr ))))) / hl; + } + else + { + hl = ( 1.15990238966306 * pwr - 5.02124037125213 * sqr( + pwr ) - 0.158676856669827 * atten * cos( 1.1609073390614 * + pwr - 6.33932586197475 * pwr * sqr( pwr ))) / tb; + + fo1 = 0.867344453126885 * tb + 0.052693817907757 * tb * log( + pwr ) + 0.0895511178735932 * tb * atan( 59.7538527741309 * + pwr ) - 0.0745653568081453 * pwr * tb; + } + } + + double WinParams[ 2 ]; + WinParams[ 0 ] = 125.0; + WinParams[ 1 ] = pwr; + + CDSPSincFilterGen sinc; + sinc.Len2 = 0.25 * hl / ReqNormFreq; + sinc.Freq1 = 0.0; + sinc.Freq2 = M_PI * ( 1.0 - fo1 ) * ReqNormFreq; + sinc.initBand( CDSPSincFilterGen :: wftKaiser, WinParams, true ); + + KernelLen = sinc.KernelLen; + BlockLenBits = getBitOccupancy( KernelLen - 1 ) + R8B_EXTFFT; + const int BlockLen = 1 << BlockLenBits; + + KernelBlock.alloc( BlockLen * 2 ); + sinc.generateBand( &KernelBlock[ 0 ], + &CDSPSincFilterGen :: calcWindowKaiser ); + + if( ReqPhase == fprLinearPhase ) + { + IsZeroPhase = true; + Latency = sinc.fl2; + LatencyFrac = 0.0; + } + else + { + IsZeroPhase = false; + double DCGroupDelay; + + calcMinPhaseTransform( &KernelBlock[ 0 ], KernelLen, 16, false, + &DCGroupDelay ); + + Latency = (int) DCGroupDelay; + LatencyFrac = DCGroupDelay - Latency; + } + + CDSPRealFFTKeeper ffto( BlockLenBits + 1 ); + + if( IsZeroPhase ) + { + // Calculate DC gain. + + double s = 0.0; + int i; + + for( i = 0; i < KernelLen; i++ ) + { + s += KernelBlock[ i ]; + } + + s = ffto -> getInvMulConst() * ReqGain / s; + + // Time-shift the filter so that zero-phase response is produced. + // Simultaneously multiply by "s". + + for( i = 0; i <= sinc.fl2; i++ ) + { + KernelBlock[ i ] = KernelBlock[ sinc.fl2 + i ] * s; + } + + for( i = 1; i <= sinc.fl2; i++ ) + { + KernelBlock[ BlockLen * 2 - i ] = KernelBlock[ i ]; + } + + memset( &KernelBlock[ sinc.fl2 + 1 ], 0, + ( BlockLen * 2 - KernelLen ) * sizeof( double )); + } + else + { + normalizeFIRFilter( &KernelBlock[ 0 ], KernelLen, + ffto -> getInvMulConst() * ReqGain ); + + memset( &KernelBlock[ KernelLen ], 0, + ( BlockLen * 2 - KernelLen ) * sizeof( double )); + } + + ffto -> forward( KernelBlock ); + + if( IsZeroPhase ) + { + ffto -> convertToZ( KernelBlock ); + } + + R8BCONSOLE( "CDSPFIRFilter: flt_len=%i latency=%i nfreq=%.4f " + "tb=%.1f att=%.1f gain=%.3f\n", KernelLen, Latency, + ReqNormFreq, ReqTransBand, ReqAtten, ReqGain ); + } +}; + +/** + * @brief FIR filter cache class. + * + * Class that implements cache for calculated FIR filters. The required FIR + * filter should be obtained via the getLPFilter() static function. + */ + +class CDSPFIRFilterCache : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPFIRFilterCache ); + + friend class CDSPFIRFilter; + +public: + /** + * @return The number of filters present in the cache now. This value can + * be monitored for debugging "forgotten" filters. + */ + + static int getObjCount() + { + R8BSYNC( StateSync ); + + return( ObjCount ); + } + + /** + * Function calculates or returns reference to a previously calculated + * (cached) low-pass FIR filter. Note that the real transition band and + * attenuation achieved by the filter varies with the magnitude of the + * required attenuation, and are never 100% exact. + * + * @param ReqNormFreq Required normalized frequency, in the range 0 to 1, + * inclusive. This is the point after which the stop-band spans. + * @param ReqTransBand Required transition band, in percent of the + * 0 to ReqNormFreq spectral bandwidth, in the range + * CDSPFIRFilter::getLPMinTransBand() to + * CDSPFIRFilter::getLPMaxTransBand(), inclusive. The transition band + * specifies the part of the spectrum between the -3 dB and ReqNormFreq + * points. The real resulting -3 dB point varies in the range from -3.00 + * to -3.05 dB, but is generally very close to -3 dB. + * @param ReqAtten Required stop-band attenuation in decibel, in the range + * CDSPFIRFilter::getLPMinAtten() to CDSPFIRFilter::getLPMaxAtten(), + * inclusive. Note that the actual stop-band attenuation of the resulting + * filter may be 0.40-4.46 dB higher. + * @param ReqPhase Required filter's phase response. + * @param ReqGain Required overall filter's gain (1.0 for unity gain). + * @param AttenCorrs Attentuation correction table, to pass to the filter + * generation function. For internal use. + * @return A reference to a new or a previously calculated low-pass FIR + * filter object with the required characteristics. A reference count is + * incremented in the returned filter object which should be released + * after use via the CDSPFIRFilter::unref() function. + */ + + static CDSPFIRFilter& getLPFilter( const double ReqNormFreq, + const double ReqTransBand, const double ReqAtten, + const EDSPFilterPhaseResponse ReqPhase, const double ReqGain, + const double* const AttenCorrs = NULL ) + { + R8BASSERT( ReqNormFreq > 0.0 && ReqNormFreq <= 1.0 ); + R8BASSERT( ReqTransBand >= CDSPFIRFilter :: getLPMinTransBand() ); + R8BASSERT( ReqTransBand <= CDSPFIRFilter :: getLPMaxTransBand() ); + R8BASSERT( ReqAtten >= CDSPFIRFilter :: getLPMinAtten() ); + R8BASSERT( ReqAtten <= CDSPFIRFilter :: getLPMaxAtten() ); + R8BASSERT( ReqGain > 0.0 ); + + R8BSYNC( StateSync ); + + CDSPFIRFilter* PrevObj = NULL; + CDSPFIRFilter* CurObj = Objects; + + while( CurObj != NULL ) + { + if( CurObj -> ReqNormFreq == ReqNormFreq && + CurObj -> ReqTransBand == ReqTransBand && + CurObj -> ReqAtten == ReqAtten && + CurObj -> ReqPhase == ReqPhase && + CurObj -> ReqGain == ReqGain ) + { + break; + } + + if( CurObj -> Next == NULL && ObjCount >= R8B_FILTER_CACHE_MAX ) + { + if( CurObj -> RefCount == 0 ) + { + // Delete the last filter which is not used. + + PrevObj -> Next = NULL; + delete CurObj; + ObjCount--; + } + else + { + // Move the last filter to the top of the list since it + // seems to be in use for a long time. + + PrevObj -> Next = NULL; + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + } + + CurObj = NULL; + break; + } + + PrevObj = CurObj; + CurObj = CurObj -> Next; + } + + if( CurObj != NULL ) + { + CurObj -> RefCount++; + + if( PrevObj == NULL ) + { + return( *CurObj ); + } + + // Remove the filter from the list temporarily. + + PrevObj -> Next = CurObj -> Next; + } + else + { + // Create a new filter object (with RefCount == 1) and build the + // filter kernel. + + CurObj = new CDSPFIRFilter(); + CurObj -> ReqNormFreq = ReqNormFreq; + CurObj -> ReqTransBand = ReqTransBand; + CurObj -> ReqAtten = ReqAtten; + CurObj -> ReqPhase = ReqPhase; + CurObj -> ReqGain = ReqGain; + ObjCount++; + + CurObj -> buildLPFilter( AttenCorrs ); + } + + // Insert the filter at the start of the list. + + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + + return( *CurObj ); + } + +private: + static CSyncObject StateSync; ///< Cache state synchronizer. + ///< + static CPtrKeeper< CDSPFIRFilter* > Objects; ///< The chain of cached + ///< objects. + ///< + static int ObjCount; ///< The number of objects currently preset in the + ///< cache. + ///< +}; + +// --------------------------------------------------------------------------- +// CDSPFIRFilter PUBLIC +// --------------------------------------------------------------------------- + +inline void CDSPFIRFilter :: unref() +{ + R8BSYNC( CDSPFIRFilterCache :: StateSync ); + + RefCount--; +} + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPFIRFILTER_INCLUDED diff --git a/src/third_party/r8b/CDSPFracInterpolator.h b/src/third_party/r8b/CDSPFracInterpolator.h new file mode 100644 index 0000000..097ccd3 --- /dev/null +++ b/src/third_party/r8b/CDSPFracInterpolator.h @@ -0,0 +1,1019 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPFracInterpolator.h + * + * @brief Fractional delay interpolator and filter bank classes. + * + * This file includes fractional delay interpolator class. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPFRACINTERPOLATOR_INCLUDED +#define R8B_CDSPFRACINTERPOLATOR_INCLUDED + +#include "CDSPSincFilterGen.h" +#include "CDSPProcessor.h" + +namespace r8b { + +#if R8B_FLTTEST + extern int InterpFilterFracs; ///< Force this number of fractional filter + ///< positions. -1 - use default. + ///< + extern int InterpFilterFracsThird; ///< Force this number of fractional + ///< filter positions for one-third filters. -1 - use default. + ///< +#endif // R8B_FLTTEST + +/** + * @brief Sinc function-based fractional delay filter bank class. + * + * Class implements storage and initialization of a bank of sinc-based + * fractional delay filters, expressed as 0th, 1st, 2nd or 3rd order + * polynomial interpolation coefficients. The filters are windowed by the + * "Kaiser" power-raised window function. + */ + +class CDSPFracDelayFilterBank : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPFracDelayFilterBank ); + + friend class CDSPFracDelayFilterBankCache; + +public: + /** + * Constructor. + * + * @param aFilterFracs The number of fractional delay positions to sample, + * -1 - use default. + * @param aElementSize The size of each filter's tap, in "double" values. + * This parameter corresponds to the complexity of interpolation. 4 should + * be set for 3rd order, 3 for 2nd order, 2 for linear interpolation, 1 + * for whole-numbered stepping. + * @param aInterpPoints The number of points the interpolation is based + * on. This value should not be confused with the ElementSize. Set to 2 + * for linear or no interpolation. + * @param aReqAtten Required filter attentuation. + * @param aIsThird "True" if one-third filter is required. + */ + + CDSPFracDelayFilterBank( const int aFilterFracs, const int aElementSize, + const int aInterpPoints, const double aReqAtten, const bool aIsThird ) + : InitFilterFracs( aFilterFracs ) + , ElementSize( aElementSize ) + , InterpPoints( aInterpPoints ) + , ReqAtten( aReqAtten ) + , IsThird( aIsThird ) + , Next( NULL ) + , RefCount( 1 ) + { + R8BASSERT( ElementSize >= 1 && ElementSize <= 4 ); + + // Kaiser window function Params, for half and third-band. + + const double* const Params = getWinParams( ReqAtten, IsThird, + FilterLen ); + + FilterSize = FilterLen * ElementSize; + + if( InitFilterFracs == -1 ) + { + FilterFracs = (int) ceil( 1.792462178761753 * + exp( 0.033300466782047 * ReqAtten )); + + #if R8B_FLTTEST + + if( IsThird ) + { + if( InterpFilterFracsThird != -1 ) + { + FilterFracs = InterpFilterFracsThird; + } + } + else + { + if( InterpFilterFracs != -1 ) + { + FilterFracs = InterpFilterFracs; + } + } + + #endif // R8B_FLTTEST + } + else + { + FilterFracs = InitFilterFracs; + } + + Table.alloc( FilterSize * ( FilterFracs + InterpPoints )); + + CDSPSincFilterGen sinc; + sinc.Len2 = FilterLen / 2; + + double* p = Table; + const int pc2 = InterpPoints / 2; + int i; + + for( i = -pc2 + 1; i <= FilterFracs + pc2; i++ ) + { + sinc.FracDelay = (double) ( FilterFracs - i ) / FilterFracs; + sinc.initFrac( CDSPSincFilterGen :: wftKaiser, Params, true ); + sinc.generateFrac( p, &CDSPSincFilterGen :: calcWindowKaiser, + ElementSize ); + + normalizeFIRFilter( p, FilterLen, 1.0, ElementSize ); + p += FilterSize; + } + + const int TablePos2 = FilterSize; + const int TablePos3 = FilterSize * 2; + const int TablePos4 = FilterSize * 3; + const int TablePos5 = FilterSize * 4; + const int TablePos6 = FilterSize * 5; + const int TablePos7 = FilterSize * 6; + const int TablePos8 = FilterSize * 7; + double* const TableEnd = Table + ( FilterFracs + 1 ) * FilterSize; + p = Table; + + if( InterpPoints == 8 ) + { + if( ElementSize == 3 ) + { + // Calculate 2nd order spline (polynomial) interpolation + // coefficients using 8 points. + + while( p < TableEnd ) + { + calcSpline2p8Coeffs( p, p[ 0 ], p[ TablePos2 ], + p[ TablePos3 ], p[ TablePos4 ], p[ TablePos5 ], + p[ TablePos6 ], p[ TablePos7 ], p[ TablePos8 ]); + + p += ElementSize; + } + } + else + if( ElementSize == 4 ) + { + // Calculate 3rd order spline (polynomial) interpolation + // coefficients using 8 points. + + while( p < TableEnd ) + { + calcSpline3p8Coeffs( p, p[ 0 ], p[ TablePos2 ], + p[ TablePos3 ], p[ TablePos4 ], p[ TablePos5 ], + p[ TablePos6 ], p[ TablePos7 ], p[ TablePos8 ]); + + p += ElementSize; + } + } + } + else + { + if( ElementSize == 2 ) + { + // Calculate linear interpolation coefficients. + + while( p < TableEnd ) + { + p[ 1 ] = p[ TablePos2 ] - p[ 0 ]; + p += ElementSize; + } + } + } + + R8BCONSOLE( "CDSPFracDelayFilterBank: fracs=%i order=%i taps=%i " + "att=%.1f third=%i\n", FilterFracs, ElementSize - 1, FilterLen, + ReqAtten, (int) IsThird ); + } + + ~CDSPFracDelayFilterBank() + { + delete Next; + } + + /** + * Function "rounds" the specified attenuation to the nearest effective + * value. + * + * @param[in,out] att Required filter attentuation. Will be rounded to the + * nearest value. + * @param aIsThird "True" if one-third filter is required. + */ + + static void roundReqAtten( double& att, const bool aIsThird ) + { + int tmp; + getWinParams( att, aIsThird, tmp ); + } + + /** + * Function returns the length of the filter. + */ + + int getFilterLen() const + { + return( FilterLen ); + } + + /** + * Function returns the number of fractional positions sampled by the + * bank. + */ + + int getFilterFracs() const + { + return( FilterFracs ); + } + + /** + * @param i Filter index, in the range 0 to FilterFracs, inclusive. + * @return Reference to the filter. + */ + + const double& operator []( const int i ) const + { + R8BASSERT( i >= 0 && i <= FilterFracs ); + + return( Table[ i * FilterSize ]); + } + + /** + * This function should be called when the filter obtained via the + * filter bank cache is no longer needed. + */ + + void unref(); + +private: + int FilterLen; ///< Filter length. + ///< + int FilterFracs; ///< Fractional position count. + ///< + int InitFilterFracs; ///< Fractional position count as supplied to the + ///< constructor, may equal -1. + ///< + int ElementSize; ///< Filter element size. + ///< + int InterpPoints; ///< Interpolation points to use. + ///< + double ReqAtten; ///< Filter's attentuation. + ///< + bool IsThird; ///< "True" if one-third filter is in use. + ///< + int FilterSize; ///< This constant specifies the "size" of a single filter + ///< in "double" elements. + ///< + CFixedBuffer< double > Table; ///< The table of fractional delay filters + ///< for all discrete fractional x = 0..1 sample positions, and + ///< interpolation coefficients. + ///< + CDSPFracDelayFilterBank* Next; ///< Next filter bank in cache's list. + ///< + int RefCount; ///< The number of references made to *this filter bank. + ///< Not considered for "static" filter bank objects. + ///< + + /** + * Function returns windowing function parameters for the specified + * attenuation and filter type. + * + * @param[in,out] att Required filter attentuation. Will be rounded to the + * nearest value. + * @param aIsThird "True" if one-third filter is required. + * @param[out] fltlen Resulting filter length. + */ + + static const double* getWinParams( double& att, const bool aIsThird, + int& fltlen ) + { + const int CoeffCount = 13; + static const double Coeffs[ CoeffCount ][ 3 ] = { + { 2.6504246356892924, 1.9035845248358245, 51.7280 }, // 0.0516 + { 4.0759654812373016, 1.5747323142948524, 67.1095 }, // 0.0048 + { 4.9036508646352033, 1.6207644759455790, 81.8379 }, // 0.0009 + { 5.6131421124830716, 1.6947677220415129, 96.4021 }, // 0.0002 + { 5.9433751253133691, 1.8730186383321272, 111.1300 }, // 0.0000 + { 6.8308658253825660, 1.8549555120377224, 125.4649 }, // 0.0000 + { 7.6648458853758372, 1.8565765953924642, 139.7378 }, // 0.0000 + { 8.2038730802326842, 1.9269521308895179, 154.0532 }, // 0.0000 + { 8.7865151489187561, 1.9775307528231671, 168.2101 }, // 0.0000 + { 9.5945013206755156, 1.9718457932433306, 182.1076 }, // 0.0000 + { 10.5163048616210250, 1.9504085061576968, 195.5668 }, // 0.0000 + { 10.2382664677006100, 2.1608878780497056, 209.0609 }, // 0.0000 + { 10.9976663155261660, 2.1536415815428249, 222.5009 }, // 0.0000 + }; + + const int CoeffCountThird = 10; + static const double CoeffsThird[ CoeffCountThird ][ 3 ] = { + { 4.0738201365282452, 1.5774150265957998, 67.2431 }, // 0.0050 + { 4.9502289040040495, 1.7149006172407628, 86.4870 }, // 0.0008 + { 5.5995071332976192, 1.8930163359641823, 106.1171 }, // 0.0001 + { 6.3627287856776054, 1.9945748303811506, 125.2304 }, // 0.0000 + { 7.4299554386534528, 1.9893399585993299, 144.3469 }, // 0.0000 + { 8.0667710807396436, 2.0928202837610885, 163.4098 }, // 0.0000 + { 8.7469991933128526, 2.1640274270903488, 181.0694 }, // 0.0000 + { 10.0823164330540570, 2.0896732996403280, 199.2880 }, // 0.0000 + { 19.1718281840114810, 1.2030083075440616, 215.2990 }, // 0.0000 + { 21.0914128488567630, 1.1919045429676862, 233.9152 }, // 0.0000 + }; + + const double* Params; + int i = 0; + + if( aIsThird ) + { + while( i != CoeffCountThird - 1 && CoeffsThird[ i ][ 2 ] < att ) + { + i++; + } + + Params = &CoeffsThird[ i ][ 0 ]; + att = CoeffsThird[ i ][ 2 ]; + } + else + { + while( i != CoeffCount - 1 && Coeffs[ i ][ 2 ] < att ) + { + i++; + } + + Params = &Coeffs[ i ][ 0 ]; + att = Coeffs[ i ][ 2 ]; + } + + fltlen = ( i + 3 ) * 2; + + return( Params ); + } +}; + +/** + * @brief Fractional delay filter cache class. + * + * Class implements cache storage of fractional delay filter banks. + */ + +class CDSPFracDelayFilterBankCache : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPFracDelayFilterBankCache ); + + friend class CDSPFracDelayFilterBank; + +public: + /** + * @return The number of filters present in the cache now. This value can + * be monitored for debugging "forgotten" filters. + */ + + static int getObjCount() + { + R8BSYNC( StateSync ); + + return( ObjCount ); + } + + /** + * Function calculates or returns reference to a previously calculated + * (cached) fractional delay filter bank. + * + * @param aFilterFracs The number of fractional delay positions to sample, + * -1 - use default. + * @param aElementSize The size of each filter's tap, in "double" values. + * @param aInterpPoints The number of points the interpolation is based + * on. + * @param ReqAtten Required filter attentuation. + * @param IsThird "True" if one-third filter is required. + * @param IsStatic "True" if a permanent static filter should be returned + * that is never removed from the cache until application terminates. + */ + + static CDSPFracDelayFilterBank& getFilterBank( const int aFilterFracs, + const int aElementSize, const int aInterpPoints, + double ReqAtten, const bool IsThird, const bool IsStatic ) + { + CDSPFracDelayFilterBank :: roundReqAtten( ReqAtten, IsThird ); + + R8BSYNC( StateSync ); + + if( IsStatic ) + { + CDSPFracDelayFilterBank* CurObj = StaticObjects; + + while( CurObj != NULL ) + { + if( CurObj -> InitFilterFracs == aFilterFracs && + CurObj -> ElementSize == aElementSize && + CurObj -> InterpPoints == aInterpPoints && + CurObj -> ReqAtten == ReqAtten && + CurObj -> IsThird == IsThird ) + { + return( *CurObj ); + } + + CurObj = CurObj -> Next; + } + + // Create a new filter bank and build it. + + CurObj = new CDSPFracDelayFilterBank( aFilterFracs, aElementSize, + aInterpPoints, ReqAtten, IsThird ); + + // Insert the bank at the start of the list. + + CurObj -> Next = StaticObjects.unkeep(); + StaticObjects = CurObj; + + return( *CurObj ); + } + + CDSPFracDelayFilterBank* PrevObj = NULL; + CDSPFracDelayFilterBank* CurObj = Objects; + + while( CurObj != NULL ) + { + if( CurObj -> InitFilterFracs == aFilterFracs && + CurObj -> ElementSize == aElementSize && + CurObj -> InterpPoints == aInterpPoints && + CurObj -> ReqAtten == ReqAtten && + CurObj -> IsThird == IsThird ) + { + break; + } + + if( CurObj -> Next == NULL && ObjCount >= R8B_FRACBANK_CACHE_MAX ) + { + if( CurObj -> RefCount == 0 ) + { + // Delete the last bank which is not used. + + PrevObj -> Next = NULL; + delete CurObj; + ObjCount--; + } + else + { + // Move the last bank to the top of the list since it + // seems to be in use for a long time. + + PrevObj -> Next = NULL; + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + } + + CurObj = NULL; + break; + } + + PrevObj = CurObj; + CurObj = CurObj -> Next; + } + + if( CurObj != NULL ) + { + CurObj -> RefCount++; + + if( PrevObj == NULL ) + { + return( *CurObj ); + } + + // Remove the bank from the list temporarily. + + PrevObj -> Next = CurObj -> Next; + } + else + { + // Create a new filter bank (with RefCount == 1) and build it. + + CurObj = new CDSPFracDelayFilterBank( aFilterFracs, aElementSize, + aInterpPoints, ReqAtten, IsThird ); + + ObjCount++; + } + + // Insert the bank at the start of the list. + + CurObj -> Next = Objects.unkeep(); + Objects = CurObj; + + return( *CurObj ); + } + +private: + static CSyncObject StateSync; ///< Cache state synchronizer. + ///< + static CPtrKeeper< CDSPFracDelayFilterBank* > Objects; ///< The chain of + ///< cached objects. + ///< + static CPtrKeeper< CDSPFracDelayFilterBank* > StaticObjects; ///< The + ///< chain of static objects. + ///< + static int ObjCount; ///< The number of objects currently preset in the + ///< Objects cache. + ///< +}; + +// --------------------------------------------------------------------------- +// CDSPFracDelayFilterBank PUBLIC +// --------------------------------------------------------------------------- + +inline void CDSPFracDelayFilterBank :: unref() +{ + R8BSYNC( CDSPFracDelayFilterBankCache :: StateSync ); + + RefCount--; +} + +/** + * @param l Number 1. + * @param s Number 2. + * @param[out] GCD Resulting GCD. + * @return "True" if the greatest common denominator of 2 numbers was + * found. + */ + +inline bool findGCD( double l, double s, double& GCD ) +{ + int it = 0; + + while( it < 50 ) + { + if( s <= 0.0 ) + { + GCD = l; + return( true ); + } + + const double r = l - s; + l = s; + s = ( r < 0.0 ? -r : r ); + it++; + } + + return( false ); +} + +/** + * Function evaluates source and destination sample rate ratio and returns + * the required input and output stepping. Function returns "false" if + * *this class cannot be used to perform interpolation using these sample + * rates. + * + * @param SSampleRate Source sample rate. + * @param DSampleRate Destination sample rate. + * @param[out] ResInStep Resulting input step. + * @param[out] ResOutStep Resulting output step. + * @return "True" if stepping was acquired. + */ + +inline bool getWholeStepping( const double SSampleRate, + const double DSampleRate, int& ResInStep, int& ResOutStep ) +{ + double GCD; + + if( !findGCD( SSampleRate, DSampleRate, GCD ) || GCD < 1.0 ) + { + return( false ); + } + + const double InStep0 = SSampleRate / GCD; + ResInStep = (int) InStep0; + const double OutStep0 = DSampleRate / GCD; + ResOutStep = (int) OutStep0; + + if( InStep0 != ResInStep || OutStep0 != ResOutStep ) + { + return( false ); + } + + if( ResOutStep > 1500 ) + { + // Do not allow large output stepping due to low cache + // performance of large filter banks. + + return( false ); + } + + return( true ); +} + +/** + * @brief Fractional delay filter bank-based interpolator class. + * + * Class implements the fractional delay interpolator. This implementation at + * first puts the input signal into a ring buffer and then performs + * interpolation. The interpolation is performed using sinc-based fractional + * delay filters. These filters are contained in a bank, and for higher + * precision they are interpolated between adjacent filters. + * + * To increase sample timing precision, this class uses "resettable counter" + * approach. This gives zero overall sample timing error. With the + * R8B_FASTTIMING configuration option enabled, the sample timing experiences + * a very minor drift. + */ + +class CDSPFracInterpolator : public CDSPProcessor +{ +public: + /** + * Constructor initalizes the interpolator. It is important to call the + * getMaxOutLen() function afterwards to obtain the optimal output buffer + * length. + * + * @param aSrcSampleRate Source sample rate. + * @param aDstSampleRate Destination sample rate. + * @param ReqAtten Required filter attentuation. + * @param IsThird "True" if one-third filter is required. + * @param PrevLatency Latency, in samples (any value >=0), which was left + * in the output signal by a previous process. This latency will be + * consumed completely. + */ + + CDSPFracInterpolator( const double aSrcSampleRate, + const double aDstSampleRate, const double ReqAtten, + const bool IsThird, const double PrevLatency ) + : SrcSampleRate( aSrcSampleRate ) + , DstSampleRate( aDstSampleRate ) + #if R8B_FASTTIMING + , FracStep( aSrcSampleRate / aDstSampleRate ) + #endif // R8B_FASTTIMING + { + R8BASSERT( SrcSampleRate > 0.0 ); + R8BASSERT( DstSampleRate > 0.0 ); + R8BASSERT( PrevLatency >= 0.0 ); + R8BASSERT( BufLenBits >= 5 ); + R8BASSERT(( 1 << BufLenBits ) >= FilterLen * 3 ); + + InitFracPos = PrevLatency; + Latency = (int) InitFracPos; + InitFracPos -= Latency; + + #if R8B_FLTTEST + + IsWhole = false; + LatencyFrac = 0.0; + FilterBank = new CDSPFracDelayFilterBank( -1, 3, 8, ReqAtten, + IsThird ); + + #else // R8B_FLTTEST + + IsWhole = getWholeStepping( SrcSampleRate, DstSampleRate, InStep, + OutStep ); + + if( IsWhole ) + { + InitFracPosW = (int) ( InitFracPos * OutStep ); + LatencyFrac = InitFracPos - (double) InitFracPosW / OutStep; + FilterBank = &CDSPFracDelayFilterBankCache :: getFilterBank( + OutStep, 1, 2, ReqAtten, IsThird, false ); + } + else + { + LatencyFrac = 0.0; + FilterBank = &CDSPFracDelayFilterBankCache :: getFilterBank( + -1, 3, 8, ReqAtten, IsThird, true ); + } + + #endif // R8B_FLTTEST + + FilterLen = FilterBank -> getFilterLen(); + fl2 = FilterLen >> 1; + fll = fl2 - 1; + flo = fll + fl2; + + static const CConvolveFn FltConvFn0[ 13 ] = { + &CDSPFracInterpolator :: convolve0< 6 >, + &CDSPFracInterpolator :: convolve0< 8 >, + &CDSPFracInterpolator :: convolve0< 10 >, + &CDSPFracInterpolator :: convolve0< 12 >, + &CDSPFracInterpolator :: convolve0< 14 >, + &CDSPFracInterpolator :: convolve0< 16 >, + &CDSPFracInterpolator :: convolve0< 18 >, + &CDSPFracInterpolator :: convolve0< 20 >, + &CDSPFracInterpolator :: convolve0< 22 >, + &CDSPFracInterpolator :: convolve0< 24 >, + &CDSPFracInterpolator :: convolve0< 26 >, + &CDSPFracInterpolator :: convolve0< 28 >, + &CDSPFracInterpolator :: convolve0< 30 > + }; + + convfn = ( IsWhole ? FltConvFn0[ fl2 - 3 ] : + &CDSPFracInterpolator :: convolve2 ); + + R8BCONSOLE( "CDSPFracInterpolator: src=%.2f dst=%.2f taps=%i " + "fracs=%i third=%i step=%.6f\n", SrcSampleRate, DstSampleRate, + FilterLen, ( IsWhole ? OutStep : FilterBank -> getFilterFracs() ), + (int) IsThird, aSrcSampleRate / aDstSampleRate ); + + clear(); + } + + virtual ~CDSPFracInterpolator() + { + #if R8B_FLTTEST + + delete FilterBank; + + #else // R8B_FLTTEST + + FilterBank -> unref(); + + #endif // R8B_FLTTEST + } + + virtual int getLatency() const + { + return( 0 ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return( (int) ceil( MaxInLen * DstSampleRate / SrcSampleRate ) + 1 ); + } + + virtual void clear() + { + LatencyLeft = Latency; + BufLeft = 0; + WritePos = 0; + ReadPos = BufLen - fll; // Set "read" position to account for filter's + // latency at zero fractional delay. + + memset( &Buf[ ReadPos ], 0, fll * sizeof( double )); + + if( IsWhole ) + { + InPosFracW = InitFracPosW; + } + else + { + InPosFrac = InitFracPos; + + #if !R8B_FASTTIMING + InCounter = 0; + InPosInt = 0; + InPosShift = InitFracPos * DstSampleRate / SrcSampleRate; + #endif // !R8B_FASTTIMING + } + } + + virtual int process( double* ip, int l, double*& op0 ) + { + R8BASSERT( l >= 0 ); + R8BASSERT( ip != op0 || l == 0 || SrcSampleRate > DstSampleRate ); + + if( LatencyLeft > 0 ) + { + if( LatencyLeft >= l ) + { + LatencyLeft -= l; + return( 0 ); + } + + l -= LatencyLeft; + ip += LatencyLeft; + LatencyLeft = 0; + } + + double* op = op0; + + while( l > 0 ) + { + // Add new input samples to both halves of the ring buffer. + + const int b = min( min( l, BufLen - WritePos ), + BufLen - fll - BufLeft ); + + double* const wp1 = Buf + WritePos; + memcpy( wp1, ip, b * sizeof( double )); + + if( WritePos < flo ) + { + const int c = min( b, flo - WritePos ); + memcpy( wp1 + BufLen, wp1, c * sizeof( double )); + } + + ip += b; + WritePos = ( WritePos + b ) & BufLenMask; + l -= b; + BufLeft += b; + + // Produce as many output samples as possible. + + op = ( *this.*convfn )( op ); + } + + #if !R8B_FASTTIMING + + if( !IsWhole && InCounter > 1000 ) + { + // Reset the interpolation position counter to achieve a + // higher sample timing precision. + + InCounter = 0; + InPosInt = 0; + InPosShift = InPosFrac * DstSampleRate / SrcSampleRate; + } + + #endif // !R8B_FASTTIMING + + return( (int) ( op - op0 )); + } + +private: + static const int BufLenBits = 8; ///< The length of the ring buffer, + ///< expressed as Nth power of 2. This value can be reduced if it is + ///< known that only short input buffers will be passed to the + ///< interpolator. The minimum value of this parameter is 5, and + ///< 1 << BufLenBits should be at least 3 times larger than the + ///< FilterLen. However, this condition can be easily met if the input + ///< signal is suitably downsampled first before the interpolation is + ///< performed. + ///< + static const int BufLen = 1 << BufLenBits; ///< The length of the ring + ///< buffer. The actual length is twice as long to allow "beyond max + ///< position" positioning. + ///< + static const int BufLenMask = BufLen - 1; ///< Mask used for quick buffer + ///< position wrapping. + ///< + int FilterLen; ///< Filter length, in taps. Even value. + ///< + int fl2; ///< Right-side (half) filter length. + ///< + int fll; ///< Input latency. + ///< + int flo; ///< Overrun length. + ///< + double Buf[ BufLen + 29 ]; ///< The ring buffer, including overrun + ///< protection for maximal filter length. + ///< + double SrcSampleRate; ///< Source sample rate. + ///< + double DstSampleRate; ///< Destination sample rate. + ///< + bool IsWhole; ///< "True" if whole-number stepping is in use. + ///< + int InStep; ///< Input whole-number stepping. + ///< + int OutStep; ///< Output whole-number stepping (corresponds to filter bank + ///< size). + ///< + double InitFracPos; ///< Initial fractional position, in samples, in the + ///< range [0; 1). + ///< + int InitFracPosW; ///< Initial fractional position for whole-number + ///< stepping. + ///< + int Latency; ///< Initial latency that should be removed from the input. + ///< + double LatencyFrac; ///< Left-over fractional latency. + ///< + int BufLeft; ///< The number of samples left in the buffer to process. + ///< + int WritePos; ///< The current buffer write position. Incremented together + ///< with the BufLeft variable. + ///< + int ReadPos; ///< The current buffer read position. + ///< + int LatencyLeft; ///< Input latency left to remove. + ///< + double InPosFrac; ///< Interpolation position (fractional part). + ///< + int InPosFracW; ///< Interpolation position (fractional part) for + ///< whole-number stepping. Corresponds to the index into the filter + ///< bank. + ///< + CDSPFracDelayFilterBank* FilterBank; ///< Filter bank in use, may be + ///< whole-number stepping filter bank or static bank. + ///< +#if R8B_FASTTIMING + double FracStep; ///< Fractional sample timing step. +#else // R8B_FASTTIMING + int InCounter; ///< Interpolation step counter. + ///< + int InPosInt; ///< Interpolation position (integer part). + ///< + double InPosShift; ///< Interpolation position fractional shift. + ///< +#endif // R8B_FASTTIMING + + typedef double*( CDSPFracInterpolator :: *CConvolveFn )( double* op ); ///< + ///< Convolution funtion type. + ///< + CConvolveFn convfn; ///< Convolution function in use. + ///< + + /** + * Convolution function for 0th order resampling. + * + * @param[out] op Output buffer. + * @return Advanced "op" value. + * @tparam fltlen Filter length. + */ + + template< int fltlen > + double* convolve0( double* op ) + { + while( BufLeft > fl2 ) + { + const double* const ftp = &(*FilterBank)[ InPosFracW ]; + const double* const rp = Buf + ReadPos; + double s = 0.0; + int i; + + for( i = 0; i < fltlen; i++ ) + { + s += ftp[ i ] * rp[ i ]; + } + + *op = s; + op++; + + InPosFracW += InStep; + const int PosIncr = InPosFracW / OutStep; + InPosFracW -= PosIncr * OutStep; + + ReadPos = ( ReadPos + PosIncr ) & BufLenMask; + BufLeft -= PosIncr; + } + + return( op ); + } + + /** + * Convolution function for 2nd order resampling. + * + * @param[out] op Output buffer. + * @return Advanced "op" value. + */ + + double* convolve2( double* op ) + { + while( BufLeft > fl2 ) + { + double x = InPosFrac * FilterBank -> getFilterFracs(); + const int fti = (int) x; // Function table index. + x -= fti; // Coefficient for interpolation between + // adjacent fractional delay filters. + const double x2 = x * x; + const double* const ftp = &(*FilterBank)[ fti ]; + const double* const rp = Buf + ReadPos; + double s = 0.0; + int ii = 0; + int i; + + for( i = 0; i < FilterLen; i++ ) + { + s += ( ftp[ ii ] + ftp[ ii + 1 ] * x + + ftp[ ii + 2 ] * x2 ) * rp[ i ]; + + ii += 3; + } + + *op = s; + op++; + + #if R8B_FASTTIMING + + InPosFrac += FracStep; + const int PosIncr = (int) InPosFrac; + InPosFrac -= PosIncr; + + #else // R8B_FASTTIMING + + InCounter++; + const double NextInPos = ( InCounter + InPosShift ) * + SrcSampleRate / DstSampleRate; + + const int NextInPosInt = (int) NextInPos; + const int PosIncr = NextInPosInt - InPosInt; + InPosInt = NextInPosInt; + InPosFrac = NextInPos - NextInPosInt; + + #endif // R8B_FASTTIMING + + ReadPos = ( ReadPos + PosIncr ) & BufLenMask; + BufLeft -= PosIncr; + } + + return( op ); + } +}; + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPFRACINTERPOLATOR_INCLUDED diff --git a/src/third_party/r8b/CDSPHBDownsampler.h b/src/third_party/r8b/CDSPHBDownsampler.h new file mode 100644 index 0000000..75a9b50 --- /dev/null +++ b/src/third_party/r8b/CDSPHBDownsampler.h @@ -0,0 +1,393 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPHBDownsampler.h + * + * @brief Half-band downsampling convolver class. + * + * This file includes half-band downsampling convolver class. + * + * r8brain-free-src Copyright (c) 2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPHBDOWNSAMPLER_INCLUDED +#define R8B_CDSPHBDOWNSAMPLER_INCLUDED + +#include "CDSPHBUpsampler.h" + +namespace r8b { + +/** + * @brief Half-band downsampler class. + * + * Class implements brute-force half-band 2X downsampling that uses small + * sparse symmetric FIR filters. The output has 2.0 gain. + */ + +class CDSPHBDownsampler : public CDSPProcessor +{ +public: + /** + * Constructor initalizes the half-band downsampler. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * downsampling ratio, e.g. at 4x downsampling 0 is used, at 8x + * downsampling 1 is used, etc. + * @param IsThird "True" if 1/3 resampling is performed. + * @param PrevLatency Latency, in samples (any value >=0), which was left + * in the output signal by a previous process. Whole-number latency will + * be consumed by *this object while remaining fractional latency can be + * obtained via the getLatencyFrac() function. + */ + + CDSPHBDownsampler( const double ReqAtten, const int SteepIndex, + const bool IsThird, const double PrevLatency ) + { + static const CConvolveFn FltConvFn[ 14 ] = { + &CDSPHBDownsampler :: convolve1, &CDSPHBDownsampler :: convolve2, + &CDSPHBDownsampler :: convolve3, &CDSPHBDownsampler :: convolve4, + &CDSPHBDownsampler :: convolve5, &CDSPHBDownsampler :: convolve6, + &CDSPHBDownsampler :: convolve7, &CDSPHBDownsampler :: convolve8, + &CDSPHBDownsampler :: convolve9, &CDSPHBDownsampler :: convolve10, + &CDSPHBDownsampler :: convolve11, &CDSPHBDownsampler :: convolve12, + &CDSPHBDownsampler :: convolve13, + &CDSPHBDownsampler :: convolve14 }; + + int fltt; + double att; + + if( IsThird ) + { + CDSPHBUpsampler :: getHBFilterThird( ReqAtten, SteepIndex, fltp, + fltt, att ); + } + else + { + CDSPHBUpsampler :: getHBFilter( ReqAtten, SteepIndex, fltp, fltt, + att ); + } + + convfn = FltConvFn[ fltt - 1 ]; + fll = fltt * 2 - 1; + fl2 = fll; + flo = fll + fl2; + + LatencyFrac = PrevLatency * 0.5; + Latency = (int) LatencyFrac; + LatencyFrac -= Latency; + + R8BCONSOLE( "CDSPHBDownsampler: taps=%i third=%i att=%.1f io=1/2\n", + fltt, (int) IsThird, att ); + + clear(); + } + + virtual int getLatency() const + { + return( 0 ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return(( MaxInLen + 1 ) / 2 ); + } + + virtual void clear() + { + LatencyLeft = Latency; + BufLeft = 0; + WritePos = 0; + ReadPos = BufLen - fll; // Set "read" position to + // account for filter's latency. + + memset( &Buf[ ReadPos ], 0, fll * sizeof( double )); + } + + virtual int process( double* ip, int l, double*& op0 ) + { + R8BASSERT( l >= 0 ); + + double* op = op0; + + while( l > 0 ) + { + // Add new input samples to both halves of the ring buffer. + + const int b = min( min( l, BufLen - WritePos ), + BufLen - fll - BufLeft ); + + double* const wp1 = Buf + WritePos; + memcpy( wp1, ip, b * sizeof( double )); + + if( WritePos < flo ) + { + const int c = min( b, flo - WritePos ); + memcpy( wp1 + BufLen, wp1, c * sizeof( double )); + } + + ip += b; + WritePos = ( WritePos + b ) & BufLenMask; + l -= b; + BufLeft += b; + + if( BufLeft > fl2 ) + { + const int c = ( BufLeft - fl2 + 1 ) >> 1; + + double* const opend = op + c; + ( *convfn )( op, opend, fltp, Buf + fll, ReadPos ); + + op = opend; + BufLeft -= c + c; + } + } + + int ol = (int) ( op - op0 ); + + if( LatencyLeft > 0 ) + { + if( LatencyLeft >= ol ) + { + LatencyLeft -= ol; + return( 0 ); + } + + ol -= LatencyLeft; + op0 += LatencyLeft; + LatencyLeft = 0; + } + + return( ol ); + } + +private: + static const int BufLenBits = 8; ///< The length of the ring buffer, + ///< expressed as Nth power of 2. This value can be reduced if it is + ///< known that only short input buffers will be passed to the + ///< interpolator. The minimum value of this parameter is 5, and + ///< 1 << BufLenBits should be at least 3 times larger than the + ///< FilterLen. + ///< + static const int BufLen = 1 << BufLenBits; ///< The length of the ring + ///< buffer. The actual length is twice as long to allow "beyond max + ///< position" positioning. + ///< + static const int BufLenMask = BufLen - 1; ///< Mask used for quick buffer + ///< position wrapping. + ///< + double Buf[ BufLen + 54 ]; ///< The ring buffer, including overrun + ///< protection for the largest filter. + ///< + const double* fltp; ///< Half-band filter taps. + ///< + int fll; ///< Input latency. + ///< + int fl2; ///< Right-side filter length. + ///< + int flo; ///< Overrun length. + ///< + int Latency; ///< Initial latency that should be removed from the output. + ///< + double LatencyFrac; ///< Fractional latency left on the output. + ///< + int BufLeft; ///< The number of samples left in the buffer to process. + ///< When this value is below FilterLenD2Plus1, the interpolation + ///< cycle ends. + ///< + int WritePos; ///< The current buffer write position. Incremented together + ///< with the BufLeft variable. + ///< + int ReadPos; ///< The current buffer read position. + ///< + int LatencyLeft; ///< Latency left to remove. + ///< + typedef void( *CConvolveFn )( double* op, double* const opend, + const double* const flt, const double* const rp0, int& ReadPos0 ); ///< + ///< Convolution funtion type. + ///< + CConvolveFn convfn; ///< Convolution function in use. + ///< + +#define R8BHBC1( fn ) \ + static void fn( double* op, double* const opend, const double* const flt, \ + const double* const rp0, int& ReadPos0 ) \ + { \ + int rpos = ReadPos0; \ + while( op < opend ) \ + { \ + const double* const rp = rp0 + rpos; \ + *op = rp[ 0 ] + + +#define R8BHBC2 \ + rpos = ( rpos + 2 ) & BufLenMask; \ + op++; \ + } \ + ReadPos0 = rpos; \ + } + + R8BHBC1( convolve1 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]); + R8BHBC2 + + R8BHBC1( convolve2 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]); + R8BHBC2 + + R8BHBC1( convolve3 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]); + R8BHBC2 + + R8BHBC1( convolve4 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]); + R8BHBC2 + + R8BHBC1( convolve5 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]); + R8BHBC2 + + R8BHBC1( convolve6 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]); + R8BHBC2 + + R8BHBC1( convolve7 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]); + R8BHBC2 + + R8BHBC1( convolve8 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]) + + flt[ 7 ] * ( rp[ 15 ] + rp[ -15 ]); + R8BHBC2 + + R8BHBC1( convolve9 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]) + + flt[ 7 ] * ( rp[ 15 ] + rp[ -15 ]) + + flt[ 8 ] * ( rp[ 17 ] + rp[ -17 ]); + R8BHBC2 + + R8BHBC1( convolve10 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]) + + flt[ 7 ] * ( rp[ 15 ] + rp[ -15 ]) + + flt[ 8 ] * ( rp[ 17 ] + rp[ -17 ]) + + flt[ 9 ] * ( rp[ 19 ] + rp[ -19 ]); + R8BHBC2 + + R8BHBC1( convolve11 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]) + + flt[ 7 ] * ( rp[ 15 ] + rp[ -15 ]) + + flt[ 8 ] * ( rp[ 17 ] + rp[ -17 ]) + + flt[ 9 ] * ( rp[ 19 ] + rp[ -19 ]) + + flt[ 10 ] * ( rp[ 21 ] + rp[ -21 ]); + R8BHBC2 + + R8BHBC1( convolve12 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]) + + flt[ 7 ] * ( rp[ 15 ] + rp[ -15 ]) + + flt[ 8 ] * ( rp[ 17 ] + rp[ -17 ]) + + flt[ 9 ] * ( rp[ 19 ] + rp[ -19 ]) + + flt[ 10 ] * ( rp[ 21 ] + rp[ -21 ]) + + flt[ 11 ] * ( rp[ 23 ] + rp[ -23 ]); + R8BHBC2 + + R8BHBC1( convolve13 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]) + + flt[ 7 ] * ( rp[ 15 ] + rp[ -15 ]) + + flt[ 8 ] * ( rp[ 17 ] + rp[ -17 ]) + + flt[ 9 ] * ( rp[ 19 ] + rp[ -19 ]) + + flt[ 10 ] * ( rp[ 21 ] + rp[ -21 ]) + + flt[ 11 ] * ( rp[ 23 ] + rp[ -23 ]) + + flt[ 12 ] * ( rp[ 25 ] + rp[ -25 ]); + R8BHBC2 + + R8BHBC1( convolve14 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ -1 ]) + + flt[ 1 ] * ( rp[ 3 ] + rp[ -3 ]) + + flt[ 2 ] * ( rp[ 5 ] + rp[ -5 ]) + + flt[ 3 ] * ( rp[ 7 ] + rp[ -7 ]) + + flt[ 4 ] * ( rp[ 9 ] + rp[ -9 ]) + + flt[ 5 ] * ( rp[ 11 ] + rp[ -11 ]) + + flt[ 6 ] * ( rp[ 13 ] + rp[ -13 ]) + + flt[ 7 ] * ( rp[ 15 ] + rp[ -15 ]) + + flt[ 8 ] * ( rp[ 17 ] + rp[ -17 ]) + + flt[ 9 ] * ( rp[ 19 ] + rp[ -19 ]) + + flt[ 10 ] * ( rp[ 21 ] + rp[ -21 ]) + + flt[ 11 ] * ( rp[ 23 ] + rp[ -23 ]) + + flt[ 12 ] * ( rp[ 25 ] + rp[ -25 ]) + + flt[ 13 ] * ( rp[ 27 ] + rp[ -27 ]); + R8BHBC2 + +#undef R8BHBC1 +#undef R8BHBC2 +}; + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPHBDOWNSAMPLER_INCLUDED diff --git a/src/third_party/r8b/CDSPHBUpsampler.h b/src/third_party/r8b/CDSPHBUpsampler.h new file mode 100644 index 0000000..5e07bf0 --- /dev/null +++ b/src/third_party/r8b/CDSPHBUpsampler.h @@ -0,0 +1,857 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPHBUpsampler.h + * + * @brief Half-band upsampling class. + * + * This file includes half-band upsampling class. + * + * r8brain-free-src Copyright (c) 2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPHBUPSAMPLER_INCLUDED +#define R8B_CDSPHBUPSAMPLER_INCLUDED + +#include "CDSPProcessor.h" + +namespace r8b { + +/** + * @brief Half-band upsampling class. + * + * Class implements brute-force half-band 2X upsampling that uses small + * sparse symmetric FIR filters. It is very efficient and should be used at + * latter upsampling steps after initial steep 2X upsampling. + */ + +class CDSPHBUpsampler : public CDSPProcessor +{ +public: + /** + * Function that provides filter data for various steepness indices and + * attenuations. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * upsampling/downsampling ratio, e.g. at 4x 0 is used, at 8x 1 is used, + * etc. + */ + + static void getHBFilter( const double ReqAtten, const int SteepIndex, + const double*& flt, int& fltt, double& att ) + { + static const int FltCount = 11; + static const double HBKernel_4[ 4 ] = { // att -64.9241 dB, frac 4.0 + 6.1073830069265711e-001, -1.4463982571471876e-001, + 4.1136036923118187e-002, -7.4740105856914872e-003 }; + static const double HBKernel_5[ 5 ] = { // att -87.4775 dB, frac 4.0 + 6.1553054142504338e-001, -1.5591352339398118e-001, + 5.2404802661298266e-002, -1.4230574348726146e-002, + 2.2457593805377831e-003 }; + static const double HBKernel_6[ 6 ] = { // att -104.5154 dB, frac 4.0 + 6.1883766561934184e-001, -1.6396282655874558e-001, + 6.1104571279325129e-002, -2.0317756445217543e-002, + 5.0264527466018826e-003, -6.9392938429507279e-004 }; + static const double HBKernel_7[ 7 ] = { // att -120.6199 dB, frac 4.0 + 6.2125313688727779e-001, -1.6999763849273491e-001, + 6.8014108060738196e-002, -2.5679821316697125e-002, + 7.9798828249699784e-003, -1.7871060154498470e-003, + 2.1836606459564009e-004 }; + static const double HBKernel_8[ 8 ] = { // att -136.5151 dB, frac 4.0 + 6.2309299085367287e-001, -1.7468969193368433e-001, + 7.3628746444973150e-002, -3.0378268550055314e-002, + 1.0908085227657214e-002, -3.1287343330312556e-003, + 6.3632014609722092e-004, -6.9597139145649502e-005 }; + static const double HBKernel_9[ 9 ] = { // att -152.3240 dB, frac 4.0 + 6.2454069594794803e-001, -1.7844303649890664e-001, + 7.8279410808762842e-002, -3.4501119561829857e-002, + 1.3717889826645487e-002, -4.6090109007760798e-003, + 1.2192752061406873e-003, -2.2647618541786664e-004, + 2.2395554542567748e-005 }; + static const double HBKernel_10[ 10 ] = { // att -168.0859 dB, frac 4.0 + 6.2570883988448611e-001, -1.8151274643053061e-001, + 8.2191863294185458e-002, -3.8131779329357615e-002, + 1.6367492549512565e-002, -6.1530178832078578e-003, + 1.9277693942420303e-003, -4.7165916432255402e-004, + 8.0491894752808465e-005, -7.2581515842465856e-006 }; + static const double HBKernel_11[ 11 ] = { // att -183.7962 dB, frac 4.0 + 6.2667167706646965e-001, -1.8407153341833782e-001, + 8.5529995600327216e-002, -4.1346831452173063e-002, + 1.8844831683400131e-002, -7.7125170314919214e-003, + 2.7268674834296570e-003, -7.9745028391855826e-004, + 1.8116344571770699e-004, -2.8569149678673122e-005, + 2.3667021922861879e-006 }; + static const double HBKernel_12[ 12 ] = { // att -199.4768 dB, frac 4.0 + 6.2747849729182659e-001, -1.8623616781335248e-001, + 8.8409755856508648e-002, -4.4207468780136254e-002, + 2.1149175912217915e-002, -9.2551508154301194e-003, + 3.5871562052326249e-003, -1.1923167600753576e-003, + 3.2627812001613326e-004, -6.9106902008709490e-005, + 1.0122897772888322e-005, -7.7531878091292963e-007 }; + static const double HBKernel_13[ 13 ] = { // att -215.1364 dB, frac 4.0 + 6.2816416238782957e-001, -1.8809076918442266e-001, + 9.0918539368474965e-002, -4.6765502172995604e-002, + 2.3287520069933797e-002, -1.0760626940880943e-002, + 4.4853921118213676e-003, -1.6438774496992904e-003, + 5.1441308429384374e-004, -1.3211724349740752e-004, + 2.6191316362108199e-005, -3.5802424384280469e-006, + 2.5491272423372411e-007 }; + static const double HBKernel_14[ 14 ] = { // att -230.7526 dB, frac 4.0 + 6.2875473147254901e-001, -1.8969942008858576e-001, + 9.3126095475258408e-002, -4.9067252227455962e-002, + 2.5273009767563311e-002, -1.2218646838258702e-002, + 5.4048946497798353e-003, -2.1409921992386689e-003, + 7.4250304371305991e-004, -2.1924546773651068e-004, + 5.3015823597863675e-005, -9.8743070771832892e-006, + 1.2650397198764347e-006, -8.4146728313072455e-008 }; + static const double FltAttens[ FltCount ] = { + 64.9241, 87.4775, 104.5154, 120.6199, 136.5151, 152.3240, + 168.0859, 183.7962, 199.4768, 215.1364, 230.7526 }; + static const double* const FltPtrs[ FltCount ] = { HBKernel_4, + HBKernel_5, HBKernel_6, HBKernel_7, HBKernel_8, HBKernel_9, + HBKernel_10, HBKernel_11, HBKernel_12, HBKernel_13, + HBKernel_14 }; + + static const int FltCountB = 7; // 0.125 + static const double HBKernel_2b[ 2 ] = { // StopAtten = -46.2556 dB + 5.6965643437574798e-001, -7.0243561190601822e-002 }; + static const double HBKernel_3b[ 3 ] = { // StopAtten = -93.6536 dB + 5.9040785316467748e-001, -1.0462338733801557e-001, + 1.4234900265846395e-002 }; + static const double HBKernel_4b[ 4 ] = { // StopAtten = -123.4514 dB + 6.0140277542879073e-001, -1.2564483854573050e-001, + 2.7446500598030887e-002, -3.2051079559036744e-003 }; + static const double HBKernel_5b[ 5 ] = { // StopAtten = -152.4403 dB + 6.0818642429044178e-001, -1.3981140187082763e-001, + 3.8489164053787661e-002, -7.6218861795043225e-003, + 7.5772358126258155e-004 }; + static const double HBKernel_6b[ 6 ] = { // StopAtten = -181.2501 dB + 6.1278392271870352e-001, -1.5000053763409249e-001, + 4.7575323519283508e-002, -1.2320702806281281e-002, + 2.1462442604041065e-003, -1.8425092396978648e-004 }; + static const double HBKernel_7b[ 7 ] = { // StopAtten = -209.9472 dB + 6.1610372237019151e-001, -1.5767891821295410e-001, + 5.5089690570484962e-002, -1.6895755290596615e-002, + 3.9416641999499014e-003, -6.0603620400878633e-004, + 4.5632598748568398e-005 }; + static const double HBKernel_8b[ 8 ] = { // StopAtten = -238.5612 dB + 6.1861282849648180e-001, -1.6367179296640288e-001, + 6.1369859727781417e-002, -2.1184465440918565e-002, + 5.9623352367661475e-003, -1.2483096884685629e-003, + 1.7099294398059683e-004, -1.1448310399897466e-005 }; + static const double FltAttensB[ FltCountB ] = { + 46.2556, 93.6536, 123.4514, 152.4403, 181.2501, 209.9472, + 238.5612 }; + static const double* const FltPtrsB[ FltCountB ] = { HBKernel_2b, + HBKernel_3b, HBKernel_4b, HBKernel_5b, HBKernel_6b, HBKernel_7b, + HBKernel_8b }; + + static const int FltCountC = 5; // 0.0625 + static const double HBKernel_2c[ 2 ] = { // StopAtten = -87.9438 dB + 5.6430278013478086e-001, -6.4338068855764208e-002 }; + static const double HBKernel_3c[ 3 ] = { // StopAtten = -130.8862 dB + 5.8706402915553113e-001, -9.9362380958695873e-002, + 1.2298637065878193e-002 }; + static const double HBKernel_4c[ 4 ] = { // StopAtten = -172.3191 dB + 5.9896586135108265e-001, -1.2111680603660968e-001, + 2.4763118077755664e-002, -2.6121758134936002e-003 }; + static const double HBKernel_5c[ 5 ] = { // StopAtten = -213.4984 dB + 6.0626808278478261e-001, -1.3588224019070938e-001, + 3.5544305138258458e-002, -6.5127022013993230e-003, + 5.8255449020627736e-004 }; + static const double HBKernel_6c[ 6 ] = { // StopAtten = -254.5179 dB + 6.1120171157732273e-001, -1.4654486624691154e-001, + 4.4582957343679119e-002, -1.0840542911916273e-002, + 1.7343703931622656e-003, -1.3363015552414481e-004 }; + static const double FltAttensC[ FltCountC ] = { + 87.9438, 130.8862, 172.3191, 213.4984, 254.5179 }; + static const double* const FltPtrsC[ FltCountC ] = { HBKernel_2c, + HBKernel_3c, HBKernel_4c, HBKernel_5c, HBKernel_6c }; + + static const int FltCountD = 3; // 0.03125 + static const double HBKernel_2d[ 2 ] = { // StopAtten = -113.1456 dB + 5.6295152180538044e-001, -6.2953706070191726e-002 }; + static const double HBKernel_3d[ 3 ] = { // StopAtten = -167.1446 dB + 5.8621968728761675e-001, -9.8080551656624410e-002, + 1.1860868762030571e-002 }; + static const double HBKernel_4d[ 4 ] = { // StopAtten = -220.6519 dB + 5.9835028661892165e-001, -1.1999986095168852e-001, + 2.4132530901858028e-002, -2.4829565783680927e-003 }; + static const double FltAttensD[ FltCountD ] = { + 113.1456, 167.1446, 220.6519 }; + static const double* const FltPtrsD[ FltCountD ] = { HBKernel_2d, + HBKernel_3d, HBKernel_4d }; + + static const int FltCountE = 4; // 0.015625 + static const double HBKernel_1e[ 1 ] = { // StopAtten = -60.9962 dB + 5.0030136284718241e-001 }; + static const double HBKernel_2e[ 2 ] = { // StopAtten = -137.3130 dB + 5.6261293163934145e-001, -6.2613067826625832e-002 }; + static const double HBKernel_3e[ 3 ] = { // StopAtten = -203.2997 dB + 5.8600808139033378e-001, -9.7762185874608526e-002, + 1.1754104552667852e-002 }; + static const double HBKernel_4e[ 4 ] = { // StopAtten = -268.8561 dB + 5.9819599535791312e-001, -1.1972157884617740e-001, + 2.3977307400990484e-002, -2.4517239127622593e-003 }; + static const double FltAttensE[ FltCountE ] = { + 60.9962, 137.3130, 203.2997, 268.8561 }; + static const double* const FltPtrsE[ FltCountE ] = { HBKernel_1e, + HBKernel_2e, HBKernel_3e, HBKernel_4e }; + + static const int FltCountG = 3; + static const double HBKernel_1g[ 1 ] = { // att -93.9165 dB, frac 256.0 + 5.0001882524896712e-001 }; + static const double HBKernel_2g[ 2 ] = { // att -185.4886 dB, frac 256.0 + 5.6250705922473820e-001, -6.2507059756319761e-002 }; + static const double HBKernel_3g[ 3 ] = { // att -275.5531 dB, frac 256.0 + 5.8594191093025305e-001, -9.7662866644414148e-002, + 1.1720955714177778e-002 }; + static const double FltAttensG[ FltCountG ] = { + 93.9165, 185.4886, 275.5531 }; + static const double* const FltPtrsG[ FltCountG ] = { HBKernel_1g, + HBKernel_2g, HBKernel_3g }; + + int k = 0; + + if( SteepIndex <= 0 ) + { + while( k != FltCount - 1 && FltAttens[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrs[ k ]; + fltt = 4 + k; + att = FltAttens[ k ]; + } + else + if( SteepIndex == 1 ) + { + while( k != FltCountB - 1 && FltAttensB[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsB[ k ]; + fltt = 2 + k; + att = FltAttensB[ k ]; + } + else + if( SteepIndex == 2 ) + { + while( k != FltCountC - 1 && FltAttensC[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsC[ k ]; + fltt = 2 + k; + att = FltAttensC[ k ]; + } + else + if( SteepIndex == 3 ) + { + while( k != FltCountD - 1 && FltAttensD[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsD[ k ]; + fltt = 2 + k; + att = FltAttensD[ k ]; + } + else + if( SteepIndex == 4 || SteepIndex == 5 ) + { + while( k != FltCountE - 1 && FltAttensE[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsE[ k ]; + fltt = 1 + k; + att = FltAttensE[ k ]; + } + else + { + while( k != FltCountG - 1 && FltAttensG[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsG[ k ]; + fltt = 1 + k; + att = FltAttensG[ k ]; + } + } + + /** + * Function that provides filter data for various steepness indices and + * attenuations. For 1/3 resamplings. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * upsampling/downsampling ratio, e.g. at 4x 0 is used, at 8x 1 is used, + * etc. + */ + + static void getHBFilterThird( const double ReqAtten, const int SteepIndex, + const double*& flt, int& fltt, double& att ) + { + static const int FltCount = 7; + static const double HBKernel_3[ 3 ] = { // att -75.0994 dB, frac 6.0 + 5.9381789425210385e-001, -1.1030344037819353e-001, + 1.6601396044066741e-002 }; + static const double HBKernel_4[ 4 ] = { // att -102.5310 dB, frac 6.0 + 6.0388679447131843e-001, -1.3043900369548017e-001, + 3.0518777984447295e-002, -3.9738477033171900e-003 }; + static const double HBKernel_5[ 5 ] = { // att -126.5360 dB, frac 6.0 + 6.1014115058940344e-001, -1.4393081816630204e-001, + 4.1760642892854860e-002, -8.9692183234068596e-003, + 9.9871340618369218e-004 }; + static const double HBKernel_6[ 6 ] = { // att -150.1830 dB, frac 6.0 + 6.1439563420561982e-001, -1.5360187826939378e-001, + 5.0840891346007507e-002, -1.4053648740740480e-002, + 2.6771286587896391e-003, -2.5815816045721899e-004 }; + static const double HBKernel_7[ 7 ] = { // att -173.7067 dB, frac 6.0 + 6.1747493476475102e-001, -1.6087373733655960e-001, + 5.8263075644905349e-002, -1.8872408175697929e-002, + 4.7421376553202421e-003, -8.0196529637661137e-004, + 6.7964807425180754e-005 }; + static const double HBKernel_8[ 8 ] = { // att -197.1454 dB, frac 6.0 + 6.1980610946074488e-001, -1.6654070574184196e-001, + 6.4416567396953492e-002, -2.3307744316524541e-002, + 6.9909157209589430e-003, -1.5871946236745982e-003, + 2.4017727258609085e-004, -1.8125308111373566e-005 }; + static const double HBKernel_9[ 9 ] = { // att -220.5199 dB, frac 6.0 + 6.2163188987470752e-001, -1.7108115412330563e-001, + 6.9588371105224839e-002, -2.7339625869282957e-002, + 9.2954473703765472e-003, -2.5537181861669997e-003, + 5.2572296540671394e-004, -7.1813366796731157e-005, + 4.8802392556669750e-006 }; + static const double FltAttens[ FltCount ] = { + 75.0994, 102.5310, 126.5360, 150.1830, 173.7067, 197.1454, + 220.5199 }; + static const double* const FltPtrs[ FltCount ] = { HBKernel_3, + HBKernel_4, HBKernel_5, HBKernel_6, HBKernel_7, HBKernel_8, + HBKernel_9 }; + + static const int FltCountB = 5; + static const double HBKernel_2b[ 2 ] = { // att -75.4413 dB, frac 12.0 + 5.6569875353984056e-001, -6.5811416441328888e-002 }; + static const double HBKernel_3b[ 3 ] = { // att -115.7198 dB, frac 12.0 + 5.8793612182667099e-001, -1.0070583248877137e-001, + 1.2771337947163270e-002 }; + static const double HBKernel_4b[ 4 ] = { // att -152.1528 dB, frac 12.0 + 5.9960155600859322e-001, -1.2228154335192955e-001, + 2.5433718917658079e-002, -2.7537562530760588e-003 }; + static const double HBKernel_5b[ 5 ] = { // att -188.2914 dB, frac 12.0 + 6.0676859170270769e-001, -1.3689667009297382e-001, + 3.6288512627614941e-002, -6.7838855288962756e-003, + 6.2345167652090897e-004 }; + static const double HBKernel_6b[ 6 ] = { // att -224.2705 dB, frac 12.0 + 6.1161456377889145e-001, -1.4743902036519768e-001, + 4.5344160828746795e-002, -1.1207372108402218e-002, + 1.8328498006058664e-003, -1.4518194076022933e-004 }; + static const double FltAttensB[ FltCountB ] = { + 75.4413, 115.7198, 152.1528, 188.2914, 224.2705 }; + static const double* const FltPtrsB[ FltCountB ] = { HBKernel_2b, + HBKernel_3b, HBKernel_4b, HBKernel_5b, HBKernel_6b }; + + static const int FltCountC = 4; + static const double HBKernel_2c[ 2 ] = { // att -102.9806 dB, frac 24.0 + 5.6330232648142842e-001, -6.3309247177420730e-002 }; + static const double HBKernel_3c[ 3 ] = { // att -152.1187 dB, frac 24.0 + 5.8643891113575064e-001, -9.8411593011501639e-002, + 1.1972706651455891e-002 }; + static const double HBKernel_4c[ 4 ] = { // att -200.6182 dB, frac 24.0 + 5.9851012364429712e-001, -1.2028885240905723e-001, + 2.4294521088349529e-002, -2.5157924167197453e-003 }; + static const double HBKernel_5c[ 5 ] = { // att -248.8728 dB, frac 24.0 + 6.0590922849004858e-001, -1.3515953371903033e-001, + 3.5020856634677522e-002, -6.3256195330255094e-003, + 5.5506812768978109e-004 }; + static const double FltAttensC[ FltCountC ] = { + 102.9806, 152.1187, 200.6182, 248.8728 }; + static const double* const FltPtrsC[ FltCountC ] = { HBKernel_2c, + HBKernel_3c, HBKernel_4c, HBKernel_5c }; + + static const int FltCountD = 4; + static const double HBKernel_1d[ 1 ] = { // att -48.6615 dB, frac 48.0 + 5.0053598654836240e-001 }; + static const double HBKernel_2d[ 2 ] = { // att -127.3033 dB, frac 48.0 + 5.6270074379958679e-001, -6.2701174487726163e-002 }; + static const double HBKernel_3d[ 3 ] = { // att -188.2989 dB, frac 48.0 + 5.8606296210257025e-001, -9.7844644764129421e-002, + 1.1781683046197223e-002 }; + static const double HBKernel_4d[ 4 ] = { // att -248.8578 dB, frac 48.0 + 5.9823601283411165e-001, -1.1979369067338455e-001, + 2.4017459011435899e-002, -2.4597811725236445e-003 }; + static const double FltAttensD[ FltCountD ] = { + 48.6615, 127.3033, 188.2989, 248.8578 }; + static const double* const FltPtrsD[ FltCountD ] = { HBKernel_1d, + HBKernel_2d, HBKernel_3d, HBKernel_4d }; + + static const int FltCountE = 3; + static const double HBKernel_1e[ 1 ] = { // att -73.2782 dB, frac 96.0 + 5.0013388897382527e-001 }; + static const double HBKernel_2e[ 2 ] = { // att -151.4076 dB, frac 96.0 + 5.6255019604318290e-001, -6.2550222932385172e-002 }; + static const double HBKernel_3e[ 3 ] = { // att -224.4366 dB, frac 96.0 + 5.8596887233874539e-001, -9.7703321108182931e-002, + 1.1734448775437802e-002 }; + static const double FltAttensE[ FltCountE ] = { + 73.2782, 151.4076, 224.4366 }; + static const double* const FltPtrsE[ FltCountE ] = { HBKernel_1e, + HBKernel_2e, HBKernel_3e }; + + static const int FltCountG = 3; + static const double HBKernel_1g[ 1 ] = { // att -101.2873 dB, frac 384.0 + 5.0000836666064941e-001 }; + static const double HBKernel_2g[ 2 ] = { // att -199.5761 dB, frac 384.0 + 5.6250313744967606e-001, -6.2503137554676916e-002 }; + static const double HBKernel_3g[ 3 ] = { // att -296.4833 dB, frac 384.0 + 5.8593945769687561e-001, -9.7659186594368730e-002, + 1.1719728897494584e-002 }; + static const double FltAttensG[ FltCountG ] = { + 101.2873, 199.5761, 296.4833 }; + static const double* const FltPtrsG[ FltCountG ] = { HBKernel_1g, + HBKernel_2g, HBKernel_3g }; + + int k = 0; + + if( SteepIndex <= 0 ) + { + while( k != FltCount - 1 && FltAttens[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrs[ k ]; + fltt = 3 + k; + att = FltAttens[ k ]; + } + else + if( SteepIndex == 1 ) + { + while( k != FltCountB - 1 && FltAttensB[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsB[ k ]; + fltt = 2 + k; + att = FltAttensB[ k ]; + } + else + if( SteepIndex == 2 ) + { + while( k != FltCountC - 1 && FltAttensC[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsC[ k ]; + fltt = 2 + k; + att = FltAttensC[ k ]; + } + else + if( SteepIndex == 3 ) + { + while( k != FltCountD - 1 && FltAttensD[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsD[ k ]; + fltt = 1 + k; + att = FltAttensD[ k ]; + } + else + if( SteepIndex == 4 || SteepIndex == 5 ) + { + while( k != FltCountE - 1 && FltAttensE[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsE[ k ]; + fltt = 1 + k; + att = FltAttensE[ k ]; + } + else + { + while( k != FltCountG - 1 && FltAttensG[ k ] < ReqAtten ) + { + k++; + } + + flt = FltPtrsG[ k ]; + fltt = 1 + k; + att = FltAttensG[ k ]; + } + } + + /** + * Constructor initalizes the half-band upsampler. + * + * @param ReqAtten Required half-band filter attentuation. + * @param SteepIndex Steepness index - 0=steepest. Corresponds to general + * upsampling ratio, e.g. at 4x upsampling 0 is used, at 8x upsampling 1 + * is used, etc. + * @param IsThird "True" if 1/3 resampling is performed. + * @param PrevLatency Latency, in samples (any value >=0), which was left + * in the output signal by a previous process. Whole-number latency will + * be consumed by *this object while remaining fractional latency can be + * obtained via the getLatencyFrac() function. + */ + + CDSPHBUpsampler( const double ReqAtten, const int SteepIndex, + const bool IsThird, const double PrevLatency ) + { + static const CConvolveFn FltConvFn[ 14 ] = { + &CDSPHBUpsampler :: convolve1, &CDSPHBUpsampler :: convolve2, + &CDSPHBUpsampler :: convolve3, &CDSPHBUpsampler :: convolve4, + &CDSPHBUpsampler :: convolve5, &CDSPHBUpsampler :: convolve6, + &CDSPHBUpsampler :: convolve7, &CDSPHBUpsampler :: convolve8, + &CDSPHBUpsampler :: convolve9, &CDSPHBUpsampler :: convolve10, + &CDSPHBUpsampler :: convolve11, &CDSPHBUpsampler :: convolve12, + &CDSPHBUpsampler :: convolve13, &CDSPHBUpsampler :: convolve14 }; + + int fltt; + double att; + + if( IsThird ) + { + getHBFilterThird( ReqAtten, SteepIndex, fltp, fltt, att ); + } + else + { + getHBFilter( ReqAtten, SteepIndex, fltp, fltt, att ); + } + + convfn = FltConvFn[ fltt - 1 ]; + fll = fltt - 1; + fl2 = fltt; + flo = fll + fl2; + + LatencyFrac = PrevLatency * 2.0; + Latency = (int) LatencyFrac; + LatencyFrac -= Latency; + + R8BCONSOLE( "CDSPHBUpsampler: sti=%i third=%i taps=%i att=%.1f " + "io=2/1\n", SteepIndex, (int) IsThird, fltt, att ); + + clear(); + } + + virtual int getLatency() const + { + return( 0 ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + virtual int getMaxOutLen( const int MaxInLen ) const + { + R8BASSERT( MaxInLen >= 0 ); + + return( MaxInLen * 2 ); + } + + virtual void clear() + { + LatencyLeft = Latency; + BufLeft = 0; + WritePos = 0; + ReadPos = BufLen - fll; // Set "read" position to + // account for filter's latency. + + memset( &Buf[ ReadPos ], 0, fll * sizeof( double )); + } + + virtual int process( double* ip, int l, double*& op0 ) + { + R8BASSERT( l >= 0 ); + + double* op = op0; + + while( l > 0 ) + { + // Add new input samples to both halves of the ring buffer. + + const int b = min( min( l, BufLen - WritePos ), + BufLen - fll - BufLeft ); + + double* const wp1 = Buf + WritePos; + memcpy( wp1, ip, b * sizeof( double )); + + if( WritePos < flo ) + { + const int c = min( b, flo - WritePos ); + memcpy( wp1 + BufLen, wp1, c * sizeof( double )); + } + + ip += b; + WritePos = ( WritePos + b ) & BufLenMask; + l -= b; + BufLeft += b; + + if( BufLeft > fl2 ) + { + const int c = BufLeft - fl2; + + double* const opend = op + c + c; + ( *convfn )( op, opend, fltp, Buf + fll, ReadPos ); + + op = opend; + BufLeft -= c; + } + } + + int ol = (int) ( op - op0 ); + + if( LatencyLeft > 0 ) + { + if( LatencyLeft >= ol ) + { + LatencyLeft -= ol; + return( 0 ); + } + + ol -= LatencyLeft; + op0 += LatencyLeft; + LatencyLeft = 0; + } + + return( ol ); + } + +private: + static const int BufLenBits = 8; ///< The length of the ring buffer, + ///< expressed as Nth power of 2. This value can be reduced if it is + ///< known that only short input buffers will be passed to the + ///< interpolator. The minimum value of this parameter is 5, and + ///< 1 << BufLenBits should be at least 3 times larger than the + ///< FilterLen. + ///< + static const int BufLen = 1 << BufLenBits; ///< The length of the ring + ///< buffer. The actual length is twice as long to allow "beyond max + ///< position" positioning. + ///< + static const int BufLenMask = BufLen - 1; ///< Mask used for quick buffer + ///< position wrapping. + ///< + double Buf[ BufLen + 27 ]; ///< The ring buffer, including overrun + ///< protection for the largest filter. + ///< + const double* fltp; ///< Half-band filter taps. + ///< + int fll; ///< Input latency. + ///< + int fl2; ///< Right-side filter length. + ///< + int flo; ///< Overrrun length. + ///< + int Latency; ///< Initial latency that should be removed from the output. + ///< + double LatencyFrac; ///< Fractional latency left on the output. + ///< + int BufLeft; ///< The number of samples left in the buffer to process. + ///< When this value is below FilterLenD2Plus1, the interpolation + ///< cycle ends. + ///< + int WritePos; ///< The current buffer write position. Incremented together + ///< with the BufLeft variable. + ///< + int ReadPos; ///< The current buffer read position. + ///< + int LatencyLeft; ///< Latency left to remove. + ///< + typedef void( *CConvolveFn )( double* op, double* const opend, + const double* const flt, const double* const rp0, int& ReadPos0 ); ///< + ///< Convolution funtion type. + ///< + CConvolveFn convfn; ///< Convolution function in use. + ///< + +#define R8BHBC1( fn ) \ + static void fn( double* op, double* const opend, const double* const flt, \ + const double* const rp0, int& ReadPos0 ) \ + { \ + int rpos = ReadPos0; \ + while( op < opend ) \ + { \ + const double* const rp = rp0 + rpos; \ + op[ 0 ] = rp[ 0 ]; \ + op[ 1 ] = + +#define R8BHBC2 \ + rpos = ( rpos + 1 ) & BufLenMask; \ + op += 2; \ + } \ + ReadPos0 = rpos; \ + } + + R8BHBC1( convolve1 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]); + R8BHBC2 + + R8BHBC1( convolve2 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]); + R8BHBC2 + + R8BHBC1( convolve3 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]); + R8BHBC2 + + R8BHBC1( convolve4 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]); + R8BHBC2 + + R8BHBC1( convolve5 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]); + R8BHBC2 + + R8BHBC1( convolve6 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]); + R8BHBC2 + + R8BHBC1( convolve7 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]); + R8BHBC2 + + R8BHBC1( convolve8 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]); + R8BHBC2 + + R8BHBC1( convolve9 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]); + R8BHBC2 + + R8BHBC1( convolve10 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]); + R8BHBC2 + + R8BHBC1( convolve11 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]); + R8BHBC2 + + R8BHBC1( convolve12 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]); + R8BHBC2 + + R8BHBC1( convolve13 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]) + + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]); + R8BHBC2 + + R8BHBC1( convolve14 ) + flt[ 0 ] * ( rp[ 1 ] + rp[ 0 ]) + + flt[ 1 ] * ( rp[ 2 ] + rp[ -1 ]) + + flt[ 2 ] * ( rp[ 3 ] + rp[ -2 ]) + + flt[ 3 ] * ( rp[ 4 ] + rp[ -3 ]) + + flt[ 4 ] * ( rp[ 5 ] + rp[ -4 ]) + + flt[ 5 ] * ( rp[ 6 ] + rp[ -5 ]) + + flt[ 6 ] * ( rp[ 7 ] + rp[ -6 ]) + + flt[ 7 ] * ( rp[ 8 ] + rp[ -7 ]) + + flt[ 8 ] * ( rp[ 9 ] + rp[ -8 ]) + + flt[ 9 ] * ( rp[ 10 ] + rp[ -9 ]) + + flt[ 10 ] * ( rp[ 11 ] + rp[ -10 ]) + + flt[ 11 ] * ( rp[ 12 ] + rp[ -11 ]) + + flt[ 12 ] * ( rp[ 13 ] + rp[ -12 ]) + + flt[ 13 ] * ( rp[ 14 ] + rp[ -13 ]); + R8BHBC2 + +#undef R8BHBC1 +#undef R8BHBC2 +}; + +// --------------------------------------------------------------------------- + +} // namespace r8b + +#endif // R8B_CDSPHBUPSAMPLER_INCLUDED diff --git a/src/third_party/r8b/CDSPProcessor.h b/src/third_party/r8b/CDSPProcessor.h new file mode 100644 index 0000000..1bdcabb --- /dev/null +++ b/src/third_party/r8b/CDSPProcessor.h @@ -0,0 +1,103 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPProcessor.h + * + * @brief The base virtual class for DSP processing algorithms. + * + * This file includes the base virtual class for DSP processing algorithm + * classes like FIR filtering and interpolation. + * + * r8brain-free-src Copyright (c) 2013-2018 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPPROCESSOR_INCLUDED +#define R8B_CDSPPROCESSOR_INCLUDED + +#include "r8bbase.h" + +namespace r8b { + +/** + * @brief The base virtual class for DSP processing algorithms. + * + * This class can be used as a base class for various DSP processing + * algorithms (processors). DSP processors that are derived from this class + * can be seamlessly integrated into various DSP processing graphs. + */ + +class CDSPProcessor : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPProcessor ); + +public: + CDSPProcessor() + { + } + + virtual ~CDSPProcessor() + { + } + + /** + * @return The latency, in samples, which is present in the output signal. + * This value is usually zero if the DSP processor "consumes" the latency + * automatically. + */ + + virtual int getLatency() const = 0; + + /** + * @return Fractional latency, in samples, which is present in the output + * signal. This value is usually zero if a linear-phase filtering is used. + * With minimum-phase filters in use, this value can be non-zero even if + * the getLatency() function returns zero. + */ + + virtual double getLatencyFrac() const = 0; + + /** + * @param MaxInLen The number of samples planned to process at once, at + * most. + * @return The maximal length of the output buffer required when + * processing the "MaxInLen" number of input samples. + */ + + virtual int getMaxOutLen( const int MaxInLen ) const = 0; + + /** + * Function clears (resets) the state of *this object and returns it to + * the state after construction. All input data accumulated in the + * internal buffer so far will be discarded. + */ + + virtual void clear() = 0; + + /** + * Function performs DSP processing. + * + * @param ip Input data pointer. + * @param l0 How many samples to process. + * @param[out] op0 Output data pointer. The capacity of this buffer should + * be equal to the value returned by the getMaxOutLen() function for the + * given "l0". This buffer can be equal to "ip" only if the + * getMaxOutLen( l0 ) function returned a value lesser than "l0". This + * pointer can be incremented on function's return if latency compensation + * was performed by the processor. Note that on function's return, this + * pointer may point to some internal buffers, including the "ip" buffer, + * ignoring the originally passed value. + * @return The number of output samples written to the "op0" buffer and + * available after processing. This value can be smaller or larger in + * comparison to the original "l0" value due to processing and filter's + * latency compensation that took place, and due to resampling if it was + * performed. + */ + + virtual int process( double* ip, int l0, double*& op0 ) = 0; +}; + +} // namespace r8b + +#endif // R8B_CDSPPROCESSOR_INCLUDED diff --git a/src/third_party/r8b/CDSPRealFFT.h b/src/third_party/r8b/CDSPRealFFT.h new file mode 100644 index 0000000..ae134df --- /dev/null +++ b/src/third_party/r8b/CDSPRealFFT.h @@ -0,0 +1,718 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPRealFFT.h + * + * @brief Real-valued FFT transform class. + * + * This file includes FFT object implementation. All created FFT objects are + * kept in a global list after use for future reusal. Such approach minimizes + * time necessary to initialize the FFT object of the required length. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPREALFFT_INCLUDED +#define R8B_CDSPREALFFT_INCLUDED + +#include "r8bbase.h" + +#if !R8B_IPP && !R8B_PFFFT + #include "fft4g.h" +#endif // !R8B_IPP && !R8B_PFFFT + +namespace r8b { + +/** + * @brief Real-valued FFT transform class. + * + * Class implements a wrapper for real-valued discrete fast Fourier transform + * functions. The object of this class can only be obtained via the + * CDSPRealFFTKeeper class. + * + * Uses functions from the FFT package by: Copyright(C) 1996-2001 Takuya OOURA + * http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html + * + * Also uses Intel IPP library functions if available (the R8B_IPP=1 macro was + * defined). Note that IPP library's FFT functions are 2-3 times more + * efficient on the modern Intel Core i7-3770K processor than Ooura's + * functions. It may be worthwhile investing in IPP. Note, that FFT functions + * take less than 20% of the overall sample rate conversion time. However, + * when the "power of 2" resampling is used the performance of FFT functions + * becomes "everything". + */ + +class CDSPRealFFT : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPRealFFT ); + + friend class CDSPRealFFTKeeper; + +public: + /** + * @return A multiplication constant that should be used after inverse + * transform to obtain a correct value scale. + */ + + double getInvMulConst() const + { + return( InvMulConst ); + } + + /** + * @return The length (the number of real values in a transform) of *this + * FFT object, expressed as Nth power of 2. + */ + + int getLenBits() const + { + return( LenBits ); + } + + /** + * @return The length (the number of real values in a transform) of *this + * FFT object. + */ + + int getLen() const + { + return( Len ); + } + + /** + * Function performs in-place forward FFT. + * + * @param[in,out] p Pointer to data block to transform, length should be + * equal to *this object's getLen(). + */ + + void forward( double* const p ) const + { + #if R8B_FLOATFFT + + float* const op = (float*) p; + int i; + + for( i = 0; i < Len; i++ ) + { + op[ i ] = (float) p[ i ]; + } + + #endif // R8B_FLOATFFT + + #if R8B_IPP + + ippsFFTFwd_RToPerm_64f( p, p, SPtr, WorkBuffer ); + + #elif R8B_PFFFT + + pffft_transform_ordered( setup, op, op, work, PFFFT_FORWARD ); + + #else // R8B_PFFFT + + ooura_fft :: rdft( Len, 1, p, wi.getPtr(), wd.getPtr() ); + + #endif // R8B_IPP + } + + /** + * Function performs in-place inverse FFT. + * + * @param[in,out] p Pointer to data block to transform, length should be + * equal to *this object's getLen(). + */ + + void inverse( double* const p ) const + { + #if R8B_IPP + + ippsFFTInv_PermToR_64f( p, p, SPtr, WorkBuffer ); + + #elif R8B_PFFFT + + pffft_transform_ordered( setup, (float*) p, (float*) p, work, + PFFFT_BACKWARD ); + + #else // R8B_PFFFT + + ooura_fft :: rdft( Len, -1, p, wi.getPtr(), wd.getPtr() ); + + #endif // R8B_IPP + + #if R8B_FLOATFFT + + const float* const ip = (const float*) p; + int i; + + for( i = Len - 1; i >= 0; i-- ) + { + p[ i ] = ip[ i ]; + } + + #endif // R8B_FLOATFFT + } + + /** + * Function multiplies two complex-valued data blocks and places result in + * a new data block. Length of all data blocks should be equal to *this + * object's block length. Input blocks should have been produced with the + * forward() function of *this object. + * + * @param aip1 Input data block 1. + * @param aip2 Input data block 2. + * @param[out] aop Output data block, should not be equal to aip1 nor + * aip2. + */ + + void multiplyBlocks( const double* const aip1, const double* const aip2, + double* const aop ) const + { + #if R8B_FLOATFFT + + const float* const ip1 = (const float*) aip1; + const float* const ip2 = (const float*) aip2; + float* const op = (float*) aop; + + #else // R8B_FLOATFFT + + const double* const ip1 = aip1; + const double* const ip2 = aip2; + double* const op = aop; + + #endif // R8B_FLOATFFT + + #if R8B_IPP + + ippsMulPerm_64f( (Ipp64f*) ip1, (Ipp64f*) ip2, (Ipp64f*) op, Len ); + + #else // R8B_IPP + + op[ 0 ] = ip1[ 0 ] * ip2[ 0 ]; + op[ 1 ] = ip1[ 1 ] * ip2[ 1 ]; + + int i = 2; + + while( i < Len ) + { + op[ i ] = ip1[ i ] * ip2[ i ] - ip1[ i + 1 ] * ip2[ i + 1 ]; + op[ i + 1 ] = ip1[ i ] * ip2[ i + 1 ] + ip1[ i + 1 ] * ip2[ i ]; + i += 2; + } + + #endif // R8B_IPP + } + + /** + * Function multiplies two complex-valued data blocks in-place. Length of + * both data blocks should be equal to *this object's block length. Blocks + * should have been produced with the forward() function of *this object. + * + * @param aip Input data block 1. + * @param[in,out] aop Output/input data block 2. + */ + + void multiplyBlocks( const double* const aip, double* const aop ) const + { + #if R8B_FLOATFFT + + const float* const ip = (const float*) aip; + float* const op = (float*) aop; + float t; + + #else // R8B_FLOATFFT + + const double* const ip = aip; + double* const op = aop; + double t; + + #endif // R8B_FLOATFFT + + #if R8B_IPP + + ippsMulPerm_64f( (Ipp64f*) op, (Ipp64f*) ip, (Ipp64f*) op, Len ); + + #else // R8B_IPP + + op[ 0 ] *= ip[ 0 ]; + op[ 1 ] *= ip[ 1 ]; + + int i = 2; + + while( i < Len ) + { + t = op[ i ] * ip[ i ] - op[ i + 1 ] * ip[ i + 1 ]; + op[ i + 1 ] = op[ i ] * ip[ i + 1 ] + op[ i + 1 ] * ip[ i ]; + op[ i ] = t; + i += 2; + } + + #endif // R8B_IPP + } + + /** + * Function multiplies two complex-valued data blocks in-place, + * considering that the "ip" block contains "zero-phase" response. Length + * of both data blocks should be equal to *this object's block length. + * Blocks should have been produced with the forward() function of *this + * object. + * + * @param aip Input data block 1, "zero-phase" response. This block should + * be first transformed via the convertToZ() function. + * @param[in,out] aop Output/input data block 2. + */ + + void multiplyBlocksZ( const double* const aip, double* const aop ) const + { + #if R8B_FLOATFFT + + const float* const ip = (const float*) aip; + float* const op = (float*) aop; + + #else // R8B_FLOATFFT + + const double* const ip = aip; + double* const op = aop; + + #endif // R8B_FLOATFFT + + #if R8B_IPP + + ippsMul_64f_I( (const Ipp64f*) ip, (Ipp64f*) op, Len ); + + #else // R8B_IPP + + int i; + + for( i = 0; i < Len; i++ ) + { + op[ i ] *= ip[ i ]; + } + + #endif // R8B_IPP + } + + /** + * Function converts the specified forward-transformed block into + * "zero-phase" form suitable for use with the multiplyBlocksZ() function. + * + * @param[in,out] ap Block to transform. + */ + + void convertToZ( double* const ap ) const + { + #if R8B_FLOATFFT + + float* const p = (float*) ap; + + #else // R8B_FLOATFFT + + double* const p = ap; + + #endif // R8B_FLOATFFT + + int i = 2; + + while( i < Len ) + { + p[ i + 1 ] = p[ i ]; + i += 2; + } + } + +private: + int LenBits; ///< Length of FFT block (expressed as Nth power of 2). + ///< + int Len; ///< Length of FFT block (number of real values). + ///< + double InvMulConst; ///< Inverse FFT multiply constant. + ///< + CDSPRealFFT* Next; ///< Next object in a singly-linked list. + ///< + + #if R8B_IPP + IppsFFTSpec_R_64f* SPtr; ///< Pointer to initialized data buffer + ///< to be passed to IPP's FFT functions. + ///< + CFixedBuffer< unsigned char > SpecBuffer; ///< Working buffer. + ///< + CFixedBuffer< unsigned char > WorkBuffer; ///< Working buffer. + ///< + #elif R8B_PFFFT + PFFFT_Setup* setup; ///< PFFFT setup object. + ///< + CFixedBuffer< float > work; ///< Working buffer. + ///< + #else // R8B_PFFFT + CFixedBuffer< int > wi; ///< Working buffer (ints). + ///< + CFixedBuffer< double > wd; ///< Working buffer (doubles). + ///< + #endif // R8B_IPP + + /** + * A simple class that keeps the pointer to the object and deletes it + * automatically. + */ + + class CObjKeeper + { + R8BNOCTOR( CObjKeeper ); + + public: + CObjKeeper() + : Object( NULL ) + { + } + + ~CObjKeeper() + { + delete Object; + } + + CObjKeeper& operator = ( CDSPRealFFT* const aObject ) + { + Object = aObject; + return( *this ); + } + + operator CDSPRealFFT* () const + { + return( Object ); + } + + private: + CDSPRealFFT* Object; ///< FFT object being kept. + ///< + }; + + CDSPRealFFT() + { + } + + /** + * Constructor initializes FFT object. + * + * @param aLenBits The length of FFT block (Nth power of 2), specifies the + * number of real values in a block. Values from 1 to 30 inclusive are + * supported. + */ + + CDSPRealFFT( const int aLenBits ) + : LenBits( aLenBits ) + , Len( 1 << aLenBits ) + #if R8B_IPP + , InvMulConst( 1.0 / Len ) + #elif R8B_PFFFT + , InvMulConst( 1.0 / Len ) + #else // R8B_PFFFT + , InvMulConst( 2.0 / Len ) + #endif // R8B_IPP + { + #if R8B_IPP + + int SpecSize; + int SpecBufferSize; + int BufferSize; + + ippsFFTGetSize_R_64f( LenBits, IPP_FFT_NODIV_BY_ANY, + ippAlgHintFast, &SpecSize, &SpecBufferSize, &BufferSize ); + + CFixedBuffer< unsigned char > InitBuffer( SpecBufferSize ); + SpecBuffer.alloc( SpecSize ); + WorkBuffer.alloc( BufferSize ); + + ippsFFTInit_R_64f( &SPtr, LenBits, IPP_FFT_NODIV_BY_ANY, + ippAlgHintFast, SpecBuffer, InitBuffer ); + + #elif R8B_PFFFT + + setup = pffft_new_setup( Len, PFFFT_REAL ); + work.alloc( Len ); + + #else // R8B_PFFFT + + wi.alloc( (int) ceil( 2.0 + sqrt( (double) ( Len >> 1 )))); + wi[ 0 ] = 0; + wd.alloc( Len >> 1 ); + + #endif // R8B_IPP + } + + ~CDSPRealFFT() + { + #if R8B_PFFFT + pffft_destroy_setup( setup ); + #endif // R8B_PFFFT + + delete Next; + } +}; + +/** + * @brief A "keeper" class for real-valued FFT transform objects. + * + * Class implements "keeper" functionality for handling CDSPRealFFT objects. + * The allocated FFT objects are placed on the global static list of objects + * for future reuse instead of deallocation. + */ + +class CDSPRealFFTKeeper : public R8B_BASECLASS +{ + R8BNOCTOR( CDSPRealFFTKeeper ); + +public: + CDSPRealFFTKeeper() + : Object( NULL ) + { + } + + /** + * Function acquires FFT object with the specified block length. + * + * @param LenBits The length of FFT block (Nth power of 2), in the range + * [1; 30] inclusive, specifies the number of real values in a FFT block. + */ + + CDSPRealFFTKeeper( const int LenBits ) + { + Object = acquire( LenBits ); + } + + ~CDSPRealFFTKeeper() + { + if( Object != NULL ) + { + release( Object ); + } + } + + /** + * @return Pointer to the acquired FFT object. + */ + + const CDSPRealFFT* operator -> () const + { + R8BASSERT( Object != NULL ); + + return( Object ); + } + + /** + * Function acquires FFT object with the specified block length. This + * function can be called any number of times. + * + * @param LenBits The length of FFT block (Nth power of 2), in the range + * [1; 30] inclusive, specifies the number of real values in a FFT block. + */ + + void init( const int LenBits ) + { + if( Object != NULL ) + { + if( Object -> LenBits == LenBits ) + { + return; + } + + release( Object ); + } + + Object = acquire( LenBits ); + } + + /** + * Function releases a previously acquired FFT object. + */ + + void reset() + { + if( Object != NULL ) + { + release( Object ); + Object = NULL; + } + } + +private: + CDSPRealFFT* Object; ///< FFT object. + ///< + + static CSyncObject StateSync; ///< FFTObjects synchronizer. + ///< + static CDSPRealFFT :: CObjKeeper FFTObjects[]; ///< Pool of FFT objects of + ///< various lengths. + ///< + + /** + * Function acquires FFT object from the global pool. + * + * @param LenBits FFT block length (expressed as Nth power of 2). + */ + + CDSPRealFFT* acquire( const int LenBits ) + { + R8BASSERT( LenBits > 0 && LenBits <= 30 ); + + R8BSYNC( StateSync ); + + if( FFTObjects[ LenBits ] == NULL ) + { + return( new CDSPRealFFT( LenBits )); + } + + CDSPRealFFT* ffto = FFTObjects[ LenBits ]; + FFTObjects[ LenBits ] = ffto -> Next; + + return( ffto ); + } + + /** + * Function releases a previously acquired FFT object. + * + * @param ffto FFT object to release. + */ + + void release( CDSPRealFFT* const ffto ) + { + R8BSYNC( StateSync ); + + ffto -> Next = FFTObjects[ ffto -> LenBits ]; + FFTObjects[ ffto -> LenBits ] = ffto; + } +}; + +/** + * Function calculates the minimum-phase transform of the filter kernel, using + * a discrete Hilbert transform in cepstrum domain. + * + * For more details, see part III.B of + * http://www.hpl.hp.com/personal/Niranjan_Damera-Venkata/files/ComplexMinPhase.pdf + * + * @param[in,out] Kernel Filter kernel buffer. + * @param KernelLen Filter kernel's length, in samples. + * @param LenMult Kernel length multiplier. Used as a coefficient of the + * "oversampling" in the frequency domain. Such oversampling is needed to + * improve the precision of the minimum-phase transform. If the filter's + * attenuation is high, this multiplier should be increased or otherwise the + * required attenuation will not be reached due to "smoothing" effect of this + * transform. + * @param DoFinalMul "True" if the final multiplication after transform should + * be performed or not. Such multiplication returns the gain of the signal to + * its original value. This parameter can be set to "false" if normalization + * of the resulting filter kernel is planned to be used. + * @param[out] DCGroupDelay If not NULL, this variable receives group delay + * at DC offset, in samples (can be a non-integer value). + */ + +inline void calcMinPhaseTransform( double* const Kernel, const int KernelLen, + const int LenMult = 2, const bool DoFinalMul = true, + double* const DCGroupDelay = NULL ) +{ + R8BASSERT( KernelLen > 0 ); + R8BASSERT( LenMult >= 2 ); + + const int LenBits = getBitOccupancy(( KernelLen * LenMult ) - 1 ); + const int Len = 1 << LenBits; + const int Len2 = Len >> 1; + int i; + + CFixedBuffer< double > ip( Len ); + CFixedBuffer< double > ip2( Len2 + 1 ); + + memcpy( &ip[ 0 ], Kernel, KernelLen * sizeof( double )); + memset( &ip[ KernelLen ], 0, ( Len - KernelLen ) * sizeof( double )); + + CDSPRealFFTKeeper ffto( LenBits ); + ffto -> forward( ip ); + + // Create the "log |c|" spectrum while saving the original power spectrum + // in the "ip2" buffer. + + #if R8B_FLOATFFT + float* const aip = (float*) &ip[ 0 ]; + float* const aip2 = (float*) &ip2[ 0 ]; + const float nzbias = 1e-35; + #else // R8B_FLOATFFT + double* const aip = &ip[ 0 ]; + double* const aip2 = &ip2[ 0 ]; + const double nzbias = 1e-300; + #endif // R8B_FLOATFFT + + aip2[ 0 ] = aip[ 0 ]; + aip[ 0 ] = log( fabs( aip[ 0 ]) + nzbias ); + aip2[ Len2 ] = aip[ 1 ]; + aip[ 1 ] = log( fabs( aip[ 1 ]) + nzbias ); + + for( i = 1; i < Len2; i++ ) + { + aip2[ i ] = sqrt( aip[ i * 2 ] * aip[ i * 2 ] + + aip[ i * 2 + 1 ] * aip[ i * 2 + 1 ]); + + aip[ i * 2 ] = log( aip2[ i ] + nzbias ); + aip[ i * 2 + 1 ] = 0.0; + } + + // Convert to cepstrum and apply discrete Hilbert transform. + + ffto -> inverse( ip ); + + const double m1 = ffto -> getInvMulConst(); + const double m2 = -m1; + + ip[ 0 ] = 0.0; + + for( i = 1; i < Len2; i++ ) + { + ip[ i ] *= m1; + } + + ip[ Len2 ] = 0.0; + + for( i = Len2 + 1; i < Len; i++ ) + { + ip[ i ] *= m2; + } + + // Convert Hilbert-transformed cepstrum back to the "log |c|" spectrum and + // perform its exponentiation, multiplied by the power spectrum previously + // saved in the "ip2" buffer. + + ffto -> forward( ip ); + + aip[ 0 ] = aip2[ 0 ]; + aip[ 1 ] = aip2[ Len2 ]; + + for( i = 1; i < Len2; i++ ) + { + aip[ i * 2 + 0 ] = cos( aip[ i * 2 + 1 ]) * aip2[ i ]; + aip[ i * 2 + 1 ] = sin( aip[ i * 2 + 1 ]) * aip2[ i ]; + } + + ffto -> inverse( ip ); + + if( DoFinalMul ) + { + for( i = 0; i < KernelLen; i++ ) + { + Kernel[ i ] = ip[ i ] * m1; + } + } + else + { + memcpy( &Kernel[ 0 ], &ip[ 0 ], KernelLen * sizeof( double )); + } + + if( DCGroupDelay != NULL ) + { + double tmp; + + calcFIRFilterResponseAndGroupDelay( Kernel, KernelLen, 0.0, + tmp, tmp, *DCGroupDelay ); + } +} + +} // namespace r8b + +#endif // VOX_CDSPREALFFT_INCLUDED diff --git a/src/third_party/r8b/CDSPResampler.h b/src/third_party/r8b/CDSPResampler.h new file mode 100644 index 0000000..76f7bf0 --- /dev/null +++ b/src/third_party/r8b/CDSPResampler.h @@ -0,0 +1,753 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPResampler.h + * + * @brief The master sample rate converter (resampler) class. + * + * This file includes the master sample rate converter (resampler) class that + * combines all elements of this library into a single front-end class. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPRESAMPLER_INCLUDED +#define R8B_CDSPRESAMPLER_INCLUDED + +#include "CDSPHBDownsampler.h" +#include "CDSPHBUpsampler.h" +#include "CDSPBlockConvolver.h" +#include "CDSPFracInterpolator.h" + +namespace r8b { + +/** + * @brief The master sample rate converter (resampler) class. + * + * This class can be considered the "master" sample rate converter (resampler) + * class since it combines all functionality of this library into a single + * front-end class to perform sample rate conversion to/from any sample rate, + * including non-integer sample rates. + * + * Note that objects of this class can be constructed on the stack as it has a + * small member data size. The default template parameters of this class are + * suited for 27-bit fixed point resampling. + * + * Use the CDSPResampler16 class for 16-bit resampling. + * + * Use the CDSPResampler16IR class for 16-bit impulse response resampling. + * + * Use the CDSPResampler24 class for 24-bit resampling (including 32-bit + * floating point resampling). + */ + +class CDSPResampler : public CDSPProcessor +{ +public: + /** + * Constructor initalizes the resampler object. + * + * Note that increasing the transition band and decreasing attenuation + * reduces the filter length, this in turn reduces the "input before + * output" delay. However, the filter length has only a minor influence on + * the overall resampling speed. + * + * It should be noted that the ReqAtten specifies the minimal difference + * between the loudest input signal component and the produced aliasing + * artifacts during resampling. For example, if ReqAtten=100 was specified + * when performing 2x upsampling, the analysis of the resulting signal may + * display high-frequency components which are quieter than the loudest + * part of the input signal by only 100 decibel meaning the high-frequency + * part did not become "magically" completely silent after resampling. You + * have to specify a higher ReqAtten value if you need a totally clean + * high-frequency content. On the other hand, it may not be reasonable to + * have a high-frequency content cleaner than the input signal itself: if + * the input signal is 16-bit, setting ReqAtten to 150 will make its + * high-frequency content 24-bit, but the original part of the signal will + * remain 16-bit. + * + * @param SrcSampleRate Source signal sample rate. Both sample rates can + * be specified as a ratio, e.g. SrcSampleRate = 1.0, DstSampleRate = 2.0. + * @param DstSampleRate Destination signal sample rate. The "power of 2" + * ratios between the source and destination sample rates force resampler + * to use several fast "power of 2" resampling steps, without using + * fractional interpolation at all. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. The resampler relies on + * this value as it allocates intermediate buffers. Input buffers longer + * than this value should never be supplied to the resampler. Note that + * upsampling produces more samples than was provided on input, so at + * higher upsampling ratios it is advisable to use smaller MaxInLen + * values to reduce memory footprint. When downsampling, a larger MaxInLen + * is suggested in order to increase downsampling performance. + * @param ReqTransBand Required transition band, in percent of the + * spectral space of the input signal (or the output signal if + * downsampling is performed) between filter's -3 dB point and the Nyquist + * frequency. The range is from CDSPFIRFilter::getLPMinTransBand() to + * CDSPFIRFilter::getLPMaxTransBand(), inclusive. When upsampling 88200 or + * 96000 audio to a higher sample rates the ReqTransBand can be + * considerably increased, up to 30. The selection of ReqTransBand depends + * on the level of desire to preserve the high-frequency content. While + * values 0.5 to 2 are extremely "greedy" settings, not necessary in most + * cases, values 2 to 3 can be used in most cases. Values 3 to 4 are + * relaxed settings, but they still offer a flat frequency response up to + * 21kHz with 44.1k source or destination sample rate. + * @param ReqAtten Required stop-band attenuation in decibel, in the + * range CDSPFIRFilter::getLPMinAtten() to CDSPFIRFilter::getLPMaxAtten(), + * inclusive. The actual attenuation may be 0.40-4.46 dB higher. The + * general formula for selecting the ReqAtten is 6.02 * Bits + 40, where + * "Bits" is the bit resolution (e.g. 16, 24), "40" is an added resolution + * for stationary signals, this value can be decreased to 20 to 10 if the + * signal being resampled is mostly non-stationary (e.g. impulse + * response). + * @param ReqPhase Required filter's phase response. Note that this + * setting does not affect interpolator's phase response which is always + * linear-phase. Also note that if the "power of 2" resampling was engaged + * by the resampler together with the minimum-phase response, the audio + * stream may become fractionally delayed, depending on the minimum-phase + * filter's actual fractional delay. Linear-phase filters do not have + * fractional delay. + * @see CDSPFIRFilterCache::getLPFilter() + */ + + CDSPResampler( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0, + const double ReqAtten = 206.91, + const EDSPFilterPhaseResponse ReqPhase = fprLinearPhase ) + : StepCapacity( 0 ) + , StepCount( 0 ) + , MaxInLen( aMaxInLen ) + , CurMaxOutLen( aMaxInLen ) + , LatencyFrac( 0.0 ) + { + R8BASSERT( SrcSampleRate > 0.0 ); + R8BASSERT( DstSampleRate > 0.0 ); + R8BASSERT( MaxInLen > 0 ); + + R8BCONSOLE( "* CDSPResampler: src=%.1f dst=%.1f len=%i tb=%.1f " + "att=%.2f ph=%i\n", SrcSampleRate, DstSampleRate, aMaxInLen, + ReqTransBand, ReqAtten, (int) ReqPhase ); + + if( SrcSampleRate == DstSampleRate ) + { + return; + } + + TmpBufCapacities[ 0 ] = 0; + TmpBufCapacities[ 1 ] = 0; + CurTmpBuf = 0; + + // Try some common efficient ratios requiring only a single step. + + const int CommonRatioCount = 5; + const int CommonRatios[ CommonRatioCount ][ 2 ] = { + { 1, 2 }, + { 1, 3 }, + { 2, 3 }, + { 3, 2 }, + { 3, 4 } + }; + + int i; + + for( i = 0; i < CommonRatioCount; i++ ) + { + const int num = CommonRatios[ i ][ 0 ]; + const int den = CommonRatios[ i ][ 1 ]; + + if( SrcSampleRate * num == DstSampleRate * den ) + { + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( + 1.0 / ( num > den ? num : den ), ReqTransBand, + ReqAtten, ReqPhase, num ), num, den, LatencyFrac )); + + createTmpBuffers(); + return; + } + } + + // Try whole-number power-of-2 or 3*power-of-2 upsampling. + + for( i = 2; i <= 3; i++ ) + { + bool WasFound = false; + int c = 0; + + while( true ) + { + const double NewSR = SrcSampleRate * ( i << c ); + + if( NewSR == DstSampleRate ) + { + WasFound = true; + break; + } + + if( NewSR > DstSampleRate ) + { + break; + } + + c++; + } + + if( WasFound ) + { + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( 1.0 / i, ReqTransBand, + ReqAtten, ReqPhase, i ), i, 1, LatencyFrac )); + + const bool IsThird = ( i == 3 ); + + for( i = 0; i < c; i++ ) + { + addProcessor( new CDSPHBUpsampler( ReqAtten, i, IsThird, + LatencyFrac )); + } + + createTmpBuffers(); + return; + } + } + + if( DstSampleRate * 2 > SrcSampleRate ) + { + // Upsampling or fractional downsampling down to 2X. + + const double NormFreq = ( DstSampleRate > SrcSampleRate ? 0.5 : + 0.5 * DstSampleRate / SrcSampleRate ); + + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( NormFreq, ReqTransBand, + ReqAtten, ReqPhase, 2.0 ), 2, 1, LatencyFrac )); + + // Try intermediate interpolated'd resampling with subsequent 2X + // or 3X upsampling. + + const double ThreshSampleRate = SrcSampleRate * 1.01; + int c = 0; + int div = 1; + + while( true ) + { + const int ndiv = div * 2; + + if( DstSampleRate < ThreshSampleRate * ndiv ) + { + break; + } + + div = ndiv; + c++; + } + + int c2 = 0; + int div2 = 1; + + while( true ) + { + const int ndiv = div * ( c2 == 0 ? 3 : 2 ); + + if( DstSampleRate < ThreshSampleRate * ndiv ) + { + break; + } + + div2 = ndiv; + c2++; + } + + const double SrcSampleRate2 = SrcSampleRate * 2.0; + int tmp1; + int tmp2; + + if( c == 1 && getWholeStepping( SrcSampleRate2, DstSampleRate, + tmp1, tmp2 )) + { + // Do not use intermediate interpolation if whole stepping is + // available as it performs very fast. + + c = 0; + } + + if( c > 0 ) + { + // Add steps using intermediate interpolation. + + int num; + + if( c2 > 0 && div2 > div ) + { + div = div2; + c = c2; + num = 3; + } + else + { + num = 2; + } + + addProcessor( new CDSPFracInterpolator( SrcSampleRate2 * div, + DstSampleRate, ReqAtten, false, LatencyFrac )); + + const double tb = 100.0 * ( 1.0 - SrcSampleRate * div / + DstSampleRate ) / 1.75; // Divide TransBand by a constant + // that assures a linear response in the pass-band. + + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( 1.0 / num, tb, + ReqAtten, ReqPhase, num ), num, 1, LatencyFrac )); + + for( i = 1; i < c; i++ ) + { + addProcessor( new CDSPHBUpsampler( ReqAtten, i - 1, + ( num == 3 ), LatencyFrac )); + } + } + else + { + addProcessor( new CDSPFracInterpolator( SrcSampleRate2, + DstSampleRate, ReqAtten, false, LatencyFrac )); + } + + createTmpBuffers(); + return; + } + + // Use downsampling steps, including power-of-2 downsampling. + + double CheckSR = DstSampleRate * 4.0; + int c = 0; + double FinGain = 1.0; + + while( CheckSR <= SrcSampleRate ) + { + c++; + CheckSR *= 2.0; + FinGain *= 0.5; + } + + const int SrcSRDiv = ( 1 << c ); + int downf; + double NormFreq = 0.5; + bool UseInterp = true; + bool IsThird = false; + + for( downf = 2; downf <= 3; downf++ ) + { + if( DstSampleRate * SrcSRDiv * downf == SrcSampleRate ) + { + NormFreq = 1.0 / downf; + UseInterp = false; + IsThird = ( downf == 3 ); + break; + } + } + + if( UseInterp ) + { + downf = 1; + NormFreq = DstSampleRate * SrcSRDiv / SrcSampleRate; + IsThird = ( NormFreq * 3.0 <= 1.0 ); + } + + for( i = 0; i < c; i++ ) + { + // Use a fixed very relaxed 2X downsampling filters, that at + // the final stage only guarantees stop-band between 0.75 and + // pi. 0.5-0.75 range will be aliased to 0.25-0.5 range which + // will then be filtered out by the final filter. + + addProcessor( new CDSPHBDownsampler( ReqAtten, c - 1 - i, IsThird, + LatencyFrac )); + } + + addProcessor( new CDSPBlockConvolver( + CDSPFIRFilterCache :: getLPFilter( NormFreq, ReqTransBand, + ReqAtten, ReqPhase, FinGain ), 1, downf, LatencyFrac )); + + if( UseInterp ) + { + addProcessor( new CDSPFracInterpolator( SrcSampleRate, + DstSampleRate * SrcSRDiv, ReqAtten, IsThird, LatencyFrac )); + } + + createTmpBuffers(); + } + + virtual ~CDSPResampler() + { + int i; + + for( i = 0; i < StepCount; i++ ) + { + delete Steps[ i ]; + } + } + + virtual int getLatency() const + { + return( 0 ); + } + + virtual double getLatencyFrac() const + { + return( LatencyFrac ); + } + + /** + * This function ignores the supplied parameter and returns the maximal + * output buffer length that depends on the MaxInLen supplied to the + * constructor. + */ + + virtual int getMaxOutLen( const int/* MaxInLen */ ) const + { + return( CurMaxOutLen ); + } + + /** + * Function clears (resets) the state of *this object and returns it to + * the state after construction. All input data accumulated in the + * internal buffer so far will be discarded. + * + * This function makes it possible to use *this object for converting + * separate streams from the same source sample rate to the same + * destination sample rate without reconstructing the object. It is more + * efficient to clear the state of the resampler object than to destroy it + * and create a new object. + */ + + virtual void clear() + { + int i; + + for( i = 0; i < StepCount; i++ ) + { + Steps[ i ] -> clear(); + } + } + + /** + * Function performs sample rate conversion. + * + * If the source and destination sample rates are equal, the resampler + * will do nothing and will simply return the input buffer unchanged. + * + * You do not need to allocate an intermediate output buffer for use with + * this function. If required, the resampler will allocate a suitable + * intermediate output buffer itself. + * + * @param ip0 Input buffer. This buffer is never used as output buffer by + * this function. This pointer may be returned in "op0" if no resampling + * is happening (source sample rate equals destination sample rate). + * @param l The number of samples available in the input buffer. Should + * not exceed the MaxInLen supplied in the constructor. + * @param[out] op0 This variable receives the pointer to the resampled + * data. On function's return, this pointer points to *this object's + * internal buffer. In real-time applications it is suggested to pass this + * pointer to the next output audio block and consume any data left from + * the previous output audio block first before calling the process() + * function again. The buffer pointed to by the "op0" on return is owned + * by the resampler, so it should not be freed by the caller. + * @return The number of samples available in the "op0" output buffer. If + * the data from the output buffer "op0" is going to be written to a + * bigger output buffer, it is suggested to check the returned number of + * samples so that no overflow of the bigger output buffer happens. + */ + + virtual int process( double* ip0, int l, double*& op0 ) + { + R8BASSERT( l >= 0 ); + + double* ip = ip0; + int i; + + for( i = 0; i < StepCount; i++ ) + { + double* op = TmpBufs[ i & 1 ]; + l = Steps[ i ] -> process( ip, l, op ); + ip = op; + } + + op0 = ip; + return( l ); + } + + /** + * Function performs resampling of an input sample buffer of the specified + * length in the "one-shot" mode. This function can be useful when impulse + * response resampling is required. + * + * @param ip Input buffer pointer. + * @param iplen Length of the input buffer in samples. + * @param[out] op Output buffer pointer. + * @param oplen Length of the output buffer in samples. + * @tparam Tin Input buffer type. + * @tparam Tout Output buffer type. + */ + + template< class Tin, class Tout > + void oneshot( const Tin* ip, int iplen, Tout* op, int oplen ) + { + CFixedBuffer< double > Buf( MaxInLen ); + bool IsZero = false; + + while( oplen > 0 ) + { + int rc; + double* p; + int i; + + if( iplen == 0 ) + { + rc = MaxInLen; + p = &Buf[ 0 ]; + + if( !IsZero ) + { + IsZero = true; + memset( p, 0, MaxInLen * sizeof( double )); + } + } + else + { + rc = min( iplen, MaxInLen ); + + if( sizeof( Tin ) == sizeof( double )) + { + p = (double*) ip; + } + else + { + p = &Buf[ 0 ]; + + for( i = 0; i < rc; i++ ) + { + p[ i ] = ip[ i ]; + } + } + + ip += rc; + iplen -= rc; + } + + double* op0; + int wc = process( p, rc, op0 ); + wc = min( oplen, wc ); + + for( i = 0; i < wc; i++ ) + { + op[ i ] = (Tout) op0[ i ]; + } + + op += wc; + oplen -= wc; + } + + clear(); + } + + /** + * Function obtains overall input sample count required to produce first + * output sample. Function works by iteratively passing 1 sample at a time + * until output begins. This is a relatively CPU-consuming operation. This + * function should be called after the clear() function call or after + * object's construction. The function itself calls the clear() function + * before return. + */ + + int getInLenBeforeOutStart() + { + int inc = 0; + + while( true ) + { + double ins = 0.0; + double* op; + + if( process( &ins, 1, op ) > 0 ) + { + clear(); + return( inc ); + } + + inc++; + } + } + +private: + CFixedBuffer< CDSPProcessor* > Steps; ///< Array of processing steps. + ///< + int StepCapacity; ///< The capacity of the Steps array. + ///< + int StepCount; ///< The number of created processing steps. + ///< + int MaxInLen; ///< Maximal input length. + ///< + CFixedBuffer< double > TmpBufAll; ///< Buffer containing both temporary + ///< buffers. + ///< + double* TmpBufs[ 2 ]; ///< Temporary output buffers. + ///< + int TmpBufCapacities[ 2 ]; ///< Capacities of temporary buffers, updated + ///< during processing steps building. + ///< + int CurTmpBuf; ///< Current temporary buffer. + ///< + int CurMaxOutLen; ///< Current maximal output length. + ///< + double LatencyFrac; ///< Current fractional latency. After object's + ///< construction, equals to the remaining fractional latency in the + ///< output. + ///< + + /** + * Function adds processor, updates MaxOutLen variable and adjusts length + * of temporary internal buffers. + * + * @param Proc Processor to add. This pointer is inherited and will be + * destroyed on *this object's destruction. + */ + + void addProcessor( CDSPProcessor* const Proc ) + { + if( StepCount == StepCapacity ) + { + // Reallocate and increase Steps array's capacity. + + const int NewCapacity = StepCapacity + 8; + Steps.realloc( StepCapacity, NewCapacity ); + StepCapacity = NewCapacity; + } + + LatencyFrac = Proc -> getLatencyFrac(); + CurMaxOutLen = Proc -> getMaxOutLen( CurMaxOutLen ); + + if( CurMaxOutLen > TmpBufCapacities[ CurTmpBuf ]) + { + TmpBufCapacities[ CurTmpBuf ] = CurMaxOutLen; + } + + CurTmpBuf ^= 1; + + Steps[ StepCount ] = Proc; + StepCount++; + } + + /** + * Function creates temporary buffers. + */ + + void createTmpBuffers() + { + const int ol = TmpBufCapacities[ 0 ] + TmpBufCapacities[ 1 ]; + + if( ol > 0 ) + { + TmpBufAll.alloc( ol ); + TmpBufs[ 0 ] = &TmpBufAll[ 0 ]; + TmpBufs[ 1 ] = &TmpBufAll[ TmpBufCapacities[ 0 ]]; + } + + R8BCONSOLE( "* CDSPResampler: init done\n" ); + } +}; + +/** + * @brief The resampler class for 16-bit resampling. + * + * This class defines resampling parameters suitable for 16-bit resampling, + * using linear-phase low-pass filter. See the r8b::CDSPResampler class for + * details. + */ + +class CDSPResampler16 : public CDSPResampler +{ +public: + /** + * Constructor initializes the 16-bit resampler. See the + * r8b::CDSPResampler class for details. + * + * @param SrcSampleRate Source signal sample rate. + * @param DstSampleRate Destination signal sample rate. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. + * @param ReqTransBand Required transition band, in percent. + */ + + CDSPResampler16( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0 ) + : CDSPResampler( SrcSampleRate, DstSampleRate, aMaxInLen, ReqTransBand, + 136.45, fprLinearPhase ) + { + } +}; + +/** + * @brief The resampler class for 16-bit impulse response resampling. + * + * This class defines resampling parameters suitable for 16-bit impulse + * response resampling, using linear-phase low-pass filter. Impulse responses + * usually do not feature stationary signal components and thus need resampler + * with a less SNR. See the r8b::CDSPResampler class for details. + */ + +class CDSPResampler16IR : public CDSPResampler +{ +public: + /** + * Constructor initializes the 16-bit impulse response resampler. See the + * r8b::CDSPResampler class for details. + * + * @param SrcSampleRate Source signal sample rate. + * @param DstSampleRate Destination signal sample rate. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. + * @param ReqTransBand Required transition band, in percent. + */ + + CDSPResampler16IR( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0 ) + : CDSPResampler( SrcSampleRate, DstSampleRate, aMaxInLen, ReqTransBand, + 109.56, fprLinearPhase ) + { + } +}; + +/** + * @brief The resampler class for 24-bit resampling. + * + * This class defines resampling parameters suitable for 24-bit resampling + * (including 32-bit floating point resampling), using linear-phase low-pass + * filter. See the r8b::CDSPResampler class for details. + */ + +class CDSPResampler24 : public CDSPResampler +{ +public: + /** + * Constructor initializes the 24-bit resampler (including 32-bit floating + * point). See the r8b::CDSPResampler class for details. + * + * @param SrcSampleRate Source signal sample rate. + * @param DstSampleRate Destination signal sample rate. + * @param aMaxInLen The maximal planned length of the input buffer (in + * samples) that will be passed to the resampler. + * @param ReqTransBand Required transition band, in percent. + */ + + CDSPResampler24( const double SrcSampleRate, const double DstSampleRate, + const int aMaxInLen, const double ReqTransBand = 2.0 ) + : CDSPResampler( SrcSampleRate, DstSampleRate, aMaxInLen, ReqTransBand, + 180.15, fprLinearPhase ) + { + } +}; + +} // namespace r8b + +#endif // R8B_CDSPRESAMPLER_INCLUDED diff --git a/src/third_party/r8b/CDSPSincFilterGen.h b/src/third_party/r8b/CDSPSincFilterGen.h new file mode 100644 index 0000000..2df82fc --- /dev/null +++ b/src/third_party/r8b/CDSPSincFilterGen.h @@ -0,0 +1,687 @@ +//$ nobt +//$ nocpp + +/** + * @file CDSPSincFilterGen.h + * + * @brief Sinc function-based FIR filter generator class. + * + * This file includes the CDSPSincFilterGen class implementation that + * generates FIR filters. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8B_CDSPSINCFILTERGEN_INCLUDED +#define R8B_CDSPSINCFILTERGEN_INCLUDED + +#include "r8bbase.h" + +namespace r8b { + +/** + * @brief Sinc function-based FIR filter generator class. + * + * Structure that holds state used to perform generation of sinc functions of + * various types, windowed by the Blackman window by default (but the window + * function can be changed if necessary). + */ + +class CDSPSincFilterGen +{ +public: + double Len2; ///< Required half filter kernel's length in samples (can be + ///< a fractional value). Final physical kernel length will be + ///< provided in the KernelLen variable. Len2 should be >= 2. + ///< + int KernelLen; ///< Resulting length of the filter kernel, this variable + ///< is set after the call to one of the "init" functions. + ///< + int fl2; ///< Internal "half kernel length" value. This value can be used + ///< as filter's latency in samples (taps), this variable is set after + ///< the call to one of the "init" functions. + ///< + + union + { + struct + { + double Freq1; ///< Required corner circular frequency 1 [0; pi]. + ///< Used only in the generateBand() function. + ///< + double Freq2; ///< Required corner circular frequency 2 [0; pi]. + ///< Used only in the generateBand() function. The range + ///< [Freq1; Freq2] defines a pass band for the generateBand() + ///< function. + ///< + }; + + struct + { + double FracDelay; ///< Fractional delay in the range [0; 1], used + ///< only in the generateFrac() function. Note that the + ///< FracDelay parameter is actually inversed. At 0.0 value it + ///< produces 1 sample delay (with the latency equal to fl2), + ///< at 1.0 value it produces 0 sample delay (with the latency + ///< equal to fl2 - 1). + ///< + }; + }; + + /** + * Window function type. + */ + + enum EWindowFunctionType + { + wftCosine, ///< Generalized cosine window function. No parameters + ///< required. The "Power" parameter is optional. + ///< + wftKaiser, ///< Kaiser window function. Requires the "Beta" parameter. + ///< The "Power" parameter is optional. + ///< + wftGaussian ///< Gaussian window function. Requires the "Sigma" + ///< parameter. The "Power" parameter is optional. + ///< + }; + + typedef double( CDSPSincFilterGen :: *CWindowFunc )(); ///< Window + ///< calculation function pointer type. + ///< + + /** + * Function initializes *this structure for generation of a window + * function, odd-sized. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If NULL, the table values + * may be used. + * @param UsePower "True" if the power factor should be used to raise the + * window function. If "true", the power factor should be specified as the + * last value in the Params array. If Params is NULL, the table or default + * value of -1.0 (off) will be used. + */ + + void initWindow( const EWindowFunctionType WinType = wftCosine, + const double* const Params = NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) floor( Len2 ); + KernelLen = fl2 + fl2 + 1; + + setWindow( WinType, Params, UsePower, true ); + } + + /** + * Function initializes *this structure for generation of band-limited + * sinc filter kernel. The generateBand() or generateBandPow() functions + * should be used to calculate the filter. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If NULL, the table values + * may be used. + * @param UsePower "True" if the power factor should be used to raise the + * window function. If "true", the power factor should be specified as the + * last value in the Params array. If Params is NULL, the table or default + * value of -1.0 (off) will be used. + */ + + void initBand( const EWindowFunctionType WinType = wftCosine, + const double* const Params = NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) floor( Len2 ); + KernelLen = fl2 + fl2 + 1; + f1.init( Freq1, 0.0 ); + f2.init( Freq2, 0.0 ); + + setWindow( WinType, Params, UsePower, true ); + } + + /** + * Function initializes *this structure for Hilbert transformation filter + * calculation. Freq1 and Freq2 variables are not used. + * The generateHilbert() function should be used to calculate the filter. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If NULL, the table values + * may be used. + * @param UsePower "True" if the power factor should be used to raise the + * window function. If "true", the power factor should be specified as the + * last value in the Params array. If Params is NULL, the table or default + * value of -1.0 (off) will be used. + */ + + void initHilbert( const EWindowFunctionType WinType = wftCosine, + const double* const Params = NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) floor( Len2 ); + KernelLen = fl2 + fl2 + 1; + + setWindow( WinType, Params, UsePower, true ); + } + + /** + * Function initializes *this structure for generation of full-bandwidth + * fractional delay sinc filter kernel. Freq1 and Freq2 variables are not + * used. The generateFrac() function should be used to calculate the + * filter. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If NULL, the table values + * may be used. + * @param UsePower "True" if the power factor should be used to raise the + * window function. If "true", the power factor should be specified as the + * last value in the Params array. If Params is NULL, the table or default + * value of -1.0 (off) will be used. + */ + + void initFrac( const EWindowFunctionType WinType = wftCosine, + const double* const Params = NULL, const bool UsePower = false ) + { + R8BASSERT( Len2 >= 2.0 ); + + fl2 = (int) ceil( Len2 ); + KernelLen = fl2 + fl2; + + setWindow( WinType, Params, UsePower, false, FracDelay ); + } + + /** + * @return The next "Hann" window function coefficient. + */ + + double calcWindowHann() + { + return( 0.5 + 0.5 * w1.generate() ); + } + + /** + * @return The next "Hamming" window function coefficient. + */ + + double calcWindowHamming() + { + return( 0.54 + 0.46 * w1.generate() ); + } + + /** + * @return The next "Blackman" window function coefficient. + */ + + double calcWindowBlackman() + { + return( 0.42 + 0.5 * w1.generate() + 0.08 * w2.generate() ); + } + + /** + * @return The next "Nuttall" window function coefficient. + */ + + double calcWindowNuttall() + { + return( 0.355768 + 0.487396 * w1.generate() + + 0.144232 * w2.generate() + 0.012604 * w3.generate() ); + } + + /** + * @return The next "Blackman-Nuttall" window function coefficient. + */ + + double calcWindowBlackmanNuttall() + { + return( 0.3635819 + 0.4891775 * w1.generate() + + 0.1365995 * w2.generate() + 0.0106411 * w3.generate() ); + } + + /** + * @return The next "Kaiser" window function coefficient. + */ + + double calcWindowKaiser() + { + const double n = 1.0 - sqr( wn / Len2 + KaiserLen2Frac ); + wn++; + + if( n < 0.0 ) + { + return( 0.0 ); + } + + return( besselI0( KaiserBeta * sqrt( n )) / KaiserDiv ); + } + + /** + * @return The next "Gaussian" window function coefficient. + */ + + double calcWindowGaussian() + { + const double f = exp( -0.5 * sqr( wn / GaussianSigma + + GaussianSigmaFrac )); + + wn++; + + return( f ); + } + + /** + * Function calculates window function only. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + */ + + template< class T > + void generateWindow( T* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman ) + { + op += fl2; + T* op2 = op; + + int l = fl2; + + if( Power < 0.0 ) + { + *op = ( *this.*wfunc )(); + + while( l > 0 ) + { + const double v = ( *this.*wfunc )(); + + op++; + op2--; + *op = v; + *op2 = v; + l--; + } + } + else + { + *op = pows(( *this.*wfunc )(), Power ); + + while( l > 0 ) + { + const double v = pows(( *this.*wfunc )(), Power ); + + op++; + op2--; + *op = v; + *op2 = v; + l--; + } + } + } + + /** + * Function calculates band-limited windowed sinc function-based filter + * kernel. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + */ + + template< class T > + void generateBand( T* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman ) + { + op += fl2; + T* op2 = op; + f1.generate(); + f2.generate(); + int t = 1; + + if( Power < 0.0 ) + { + *op = ( Freq2 - Freq1 ) * ( *this.*wfunc )() / M_PI; + + while( t <= fl2 ) + { + const double v = ( f2.generate() - f1.generate() ) * + ( *this.*wfunc )() / t / M_PI; + + op++; + op2--; + *op = v; + *op2 = v; + t++; + } + } + else + { + *op = ( Freq2 - Freq1 ) * pows(( *this.*wfunc )(), Power ) / M_PI; + + while( t <= fl2 ) + { + const double v = ( f2.generate() - f1.generate() ) * + pows(( *this.*wfunc )(), Power ) / t / M_PI; + + op++; + op2--; + *op = v; + *op2 = v; + t++; + } + } + } + + /** + * Function calculates windowed Hilbert transformer filter kernel. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + */ + + template< class T > + void generateHilbert( T* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman ) + { + static const double fvalues[ 2 ] = { 0.0, 2.0 }; + op += fl2; + T* op2 = op; + + ( *this.*wfunc )(); + *op = 0.0; + + int t = 1; + + if( Power < 0.0 ) + { + while( t <= fl2 ) + { + const double v = fvalues[ t & 1 ] * + ( *this.*wfunc )() / t / M_PI; + + op++; + op2--; + *op = v; + *op2 = -v; + t++; + } + } + else + { + while( t <= fl2 ) + { + const double v = fvalues[ t & 1 ] * + pows( ( *this.*wfunc )(), Power ) / t / M_PI; + + op++; + op2--; + *op = v; + *op2 = -v; + t++; + } + } + } + + /** + * Function calculates windowed fractional delay filter kernel. + * + * @param[out] op Output buffer, length = KernelLen. + * @param wfunc Window calculation function to use. + * @param opinc Output buffer increment, in "op" elements. + */ + + template< class T > + void generateFrac( T* op, + CWindowFunc wfunc = &CDSPSincFilterGen :: calcWindowBlackman, + const int opinc = 1 ) + { + R8BASSERT( opinc != 0 ); + + double f[ 2 ]; + f[ 0 ] = sin( FracDelay * M_PI ); + f[ 1 ] = -f[ 0 ]; + + int t = -fl2; + + if( t + FracDelay < -Len2 ) + { + ( *this.*wfunc )(); + *op = 0.0; + op += opinc; + t++; + } + + int mt = ( FracDelay >= 1.0 - 1e-13 && FracDelay <= 1.0 + 1e-13 ? + -1 : 0 ); + + if( Power < 0.0 ) + { + while( t < mt ) + { + *op = f[ t & 1 ] * ( *this.*wfunc )() / ( t + FracDelay ) / + M_PI; + + op += opinc; + t++; + } + + double ut = t + FracDelay; + *op = ( fabs( ut ) <= 1e-13 ? ( *this.*wfunc )() : + f[ t & 1 ] * ( *this.*wfunc )() / ut / M_PI ); + + mt = fl2 - 2; + + while( t < mt ) + { + op += opinc; + t++; + *op = f[ t & 1 ] * ( *this.*wfunc )() / ( t + FracDelay ) / + M_PI; + } + + op += opinc; + t++; + ut = t + FracDelay; + *op = ( ut > Len2 ? 0.0 : + f[ t & 1 ] * ( *this.*wfunc )() / ut / M_PI ); + } + else + { + while( t < mt ) + { + *op = f[ t & 1 ] * pows( ( *this.*wfunc )(), Power ) / + ( t + FracDelay ) / M_PI; + + op += opinc; + t++; + } + + double ut = t + FracDelay; + *op = ( fabs( ut ) <= 1e-13 ? pows( ( *this.*wfunc )(), Power ) : + f[ t & 1 ] * pows( ( *this.*wfunc )(), Power ) / ut / M_PI ); + + mt = fl2 - 2; + + while( t < mt ) + { + op += opinc; + t++; + *op = f[ t & 1 ] * pows( ( *this.*wfunc )(), Power ) / + ( t + FracDelay ) / M_PI; + } + + op += opinc; + t++; + ut = t + FracDelay; + *op = ( ut > Len2 ? 0.0 : + f[ t & 1 ] * pows( ( *this.*wfunc )(), Power ) / ut / M_PI ); + } + } + +private: + double Power; ///< The power factor used to raise the window function. + ///< Equals a negative value if the power factor should not be used. + ///< + CSineGen f1; ///< Sine function 1. Used in the generateBand() function. + ///< + CSineGen f2; ///< Sine function 2. Used in the generateBand() function. + ///< + int wn; ///< Window function integer position. 0 - center of the window + ///< function. This variable may not be used by some window functions. + ///< + CSineGen w1; ///< Cosine wave 1 for window function. + ///< + CSineGen w2; ///< Cosine wave 2 for window function. + ///< + CSineGen w3; ///< Cosine wave 3 for window function. + ///< + + union + { + struct + { + double KaiserBeta; ///< Kaiser window function's "Beta" + ///< coefficient. + ///< + double KaiserDiv; ///< Kaiser window function's divisor. + ///< + double KaiserLen2Frac; ///< Equals FracDelay / Len2. + ///< + }; + + struct + { + double GaussianSigma; ///< Gaussian window function's "Sigma" + ///< coefficient. + ///< + double GaussianSigmaFrac; ///< Equals FracDelay / GaussianSigma. + ///< + }; + }; + + /** + * Function initializes Kaiser window function calculation. The FracDelay + * variable should be initialized when using this window function. + * + * @param Params Function parameters. If NULL, the default values will be + * used. If not NULL, the first parameter should specify the "Beta" value. + * @param UsePower "True" if the power factor should be used to raise the + * window function. + * @param IsCentered "True" if centered window should be used. This + * parameter usually equals to "false" for fractional delay filters only. + */ + + void setWindowKaiser( const double* Params, const bool UsePower, + const bool IsCentered ) + { + wn = ( IsCentered ? 0 : -fl2 ); + + if( Params == NULL ) + { + KaiserBeta = 9.5945013206755156; + Power = ( UsePower ? 1.9718457932433306 : -1.0 ); + } + else + { + KaiserBeta = clampr( Params[ 0 ], 1.0, 350.0 ); + Power = ( UsePower ? fabs( Params[ 1 ]) : -1.0 ); + } + + KaiserDiv = besselI0( KaiserBeta ); + KaiserLen2Frac = FracDelay / Len2; + } + + /** + * Function initializes Gaussian window function calculation. The FracDelay + * variable should be initialized when using this window function. + * + * @param Params Function parameters. If NULL, the table values will be + * used. If not NULL, the first parameter should specify the "Sigma" + * value. + * @param UsePower "True" if the power factor should be used to raise the + * window function. + * @param IsCentered "True" if centered window should be used. This + * parameter usually equals to "false" for fractional delay filters only. + */ + + void setWindowGaussian( const double* Params, const bool UsePower, + const bool IsCentered ) + { + wn = ( IsCentered ? 0 : -fl2 ); + + if( Params == NULL ) + { + GaussianSigma = 1.0; + Power = -1.0; + } + else + { + GaussianSigma = clampr( fabs( Params[ 0 ]), 1e-1, 100.0 ); + Power = ( UsePower ? fabs( Params[ 1 ]) : -1.0 ); + } + + GaussianSigma *= Len2; + GaussianSigmaFrac = FracDelay / GaussianSigma; + } + + /** + * Function initializes calculation of window function of the specified + * type. + * + * @param WinType Window function type. + * @param Params Window function's parameters. If NULL, the table values + * may be used. + * @param UsePower "True" if the power factor should be used to raise the + * window function. If "true", the power factor should be specified as the + * last value in the Params array. If Params is NULL, the table or default + * value of -1.0 (off) will be used. + * @param IsCentered "True" if centered window should be used. This + * parameter usually equals to "false" for fractional delay filters only. + * @param UseFracDelay Fractional delay to use. + */ + + void setWindow( const EWindowFunctionType WinType, + const double* const Params, const bool UsePower, + const bool IsCentered, const double UseFracDelay = 0.0 ) + { + FracDelay = UseFracDelay; + + if( WinType == wftCosine ) + { + if( IsCentered ) + { + w1.init( M_PI / Len2, M_PI * 0.5 ); + w2.init( M_2PI / Len2, M_PI * 0.5 ); + w3.init( M_3PI / Len2, M_PI * 0.5 ); + } + else + { + const double step1 = M_PI / Len2; + w1.init( step1, M_PI * 0.5 - step1 * fl2 + + step1 * FracDelay ); + + const double step2 = M_2PI / Len2; + w2.init( step2, M_PI * 0.5 - step2 * fl2 + + step2 * FracDelay ); + + const double step3 = M_3PI / Len2; + w3.init( step3, M_PI * 0.5 - step3 * fl2 + + step3 * FracDelay ); + } + + Power = ( UsePower && Params != NULL ? Params[ 0 ] : -1.0 ); + } + else + if( WinType == wftKaiser ) + { + setWindowKaiser( Params, UsePower, IsCentered ); + } + else + if( WinType == wftGaussian ) + { + setWindowGaussian( Params, UsePower, IsCentered ); + } + } +}; + +} // namespace r8b + +#endif // R8B_CDSPSINCFILTERGEN_INCLUDED diff --git a/src/third_party/r8b/pffft.cpp b/src/third_party/r8b/pffft.cpp new file mode 100644 index 0000000..5f43e43 --- /dev/null +++ b/src/third_party/r8b/pffft.cpp @@ -0,0 +1,1891 @@ +//$ nobt + +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Based on original fortran 77 code from FFTPACKv4 from NETLIB + (http://www.netlib.org/fftpack), authored by Dr Paul Swarztrauber + of NCAR, in 1985. + + As confirmed by the NCAR fftpack software curators, the following + FFTPACKv5 license applies to FFTPACKv4 sources. My changes are + released under the same terms. + + FFTPACK license: + + http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + + Copyright (c) 2004 the University Corporation for Atmospheric + Research ("UCAR"). All rights reserved. Developed by NCAR's + Computational and Information Systems Laboratory, UCAR, + www.cisl.ucar.edu. + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. + + + PFFFT : a Pretty Fast FFT. + + This file is largerly based on the original FFTPACK implementation, modified in + order to take advantage of SIMD instructions of modern CPUs. +*/ + +/* + ChangeLog: + - 2011/10/02, version 1: This is the very first release of this file. +*/ + +/* detect compiler flavour */ +#if defined(_MSC_VER) +# define COMPILER_MSVC +#elif defined(__GNUC__) +# define COMPILER_GCC +#endif + +#ifdef COMPILER_MSVC +# define _USE_MATH_DEFINES +#endif + +#include "pffft.h" +#include +#include +#include +#include + +#if defined(COMPILER_GCC) +# define ALWAYS_INLINE(return_type) inline return_type __attribute__ ((always_inline)) +# define NEVER_INLINE(return_type) return_type __attribute__ ((noinline)) +# define RESTRICT __restrict +# define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ varname__[size__]; +#elif defined(COMPILER_MSVC) +# define ALWAYS_INLINE(return_type) __forceinline return_type +# define NEVER_INLINE(return_type) __declspec(noinline) return_type +# define RESTRICT __restrict +# define VLA_ARRAY_ON_STACK(type__, varname__, size__) type__ *varname__ = (type__*)_alloca(size__ * sizeof(type__)) +#endif + + +#ifdef COMPILER_MSVC +#pragma warning( disable : 4244 4305 4204 4456 ) +#endif + +/* + vector support macros: the rest of the code is independant of + SSE/Altivec/NEON -- adding support for other platforms with 4-element + vectors should be limited to these macros +*/ + + +// define PFFFT_SIMD_DISABLE if you want to use scalar code instead of simd code +//#define PFFFT_SIMD_DISABLE + +/* + Altivec support macros +*/ +#if !defined(PFFFT_SIMD_DISABLE) && (defined(__ppc__) || defined(__ppc64__)) +typedef vector float v4sf; +# define SIMD_SZ 4 +# define VZERO() ((vector float) vec_splat_u8(0)) +# define VMUL(a,b) vec_madd(a,b, VZERO()) +# define VADD(a,b) vec_add(a,b) +# define VMADD(a,b,c) vec_madd(a,b,c) +# define VSUB(a,b) vec_sub(a,b) +inline v4sf ld_ps1(const float *p) { v4sf v=vec_lde(0,p); return vec_splat(vec_perm(v, v, vec_lvsl(0, p)), 0); } +# define LD_PS1(p) ld_ps1(&p) +# define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = vec_mergeh(in1, in2); out2 = vec_mergel(in1, in2); out1 = tmp__; } +# define UNINTERLEAVE2(in1, in2, out1, out2) { \ + vector unsigned char vperm1 = (vector unsigned char)(0,1,2,3,8,9,10,11,16,17,18,19,24,25,26,27); \ + vector unsigned char vperm2 = (vector unsigned char)(4,5,6,7,12,13,14,15,20,21,22,23,28,29,30,31); \ + v4sf tmp__ = vec_perm(in1, in2, vperm1); out2 = vec_perm(in1, in2, vperm2); out1 = tmp__; \ + } +# define VTRANSPOSE4(x0,x1,x2,x3) { \ + v4sf y0 = vec_mergeh(x0, x2); \ + v4sf y1 = vec_mergel(x0, x2); \ + v4sf y2 = vec_mergeh(x1, x3); \ + v4sf y3 = vec_mergel(x1, x3); \ + x0 = vec_mergeh(y0, y2); \ + x1 = vec_mergel(y0, y2); \ + x2 = vec_mergeh(y1, y3); \ + x3 = vec_mergel(y1, y3); \ + } +# define VSWAPHL(a,b) vec_perm(a,b, (vector unsigned char)(16,17,18,19,20,21,22,23,8,9,10,11,12,13,14,15)) +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0xF) == 0) + +/* + SSE1 support macros +*/ +#elif !defined(PFFFT_SIMD_DISABLE) && (defined(__x86_64__) || defined(__i386__) || defined(_M_X64) || defined(i386) || defined(_M_IX86)) + +#include +typedef __m128 v4sf; +# define SIMD_SZ 4 // 4 floats by simd vector -- this is pretty much hardcoded in the preprocess/finalize functions anyway so you will have to work if you want to enable AVX with its 256-bit vectors. +# define VZERO() _mm_setzero_ps() +# define VMUL(a,b) _mm_mul_ps(a,b) +# define VADD(a,b) _mm_add_ps(a,b) +# define VMADD(a,b,c) _mm_add_ps(_mm_mul_ps(a,b), c) +# define VSUB(a,b) _mm_sub_ps(a,b) +# define LD_PS1(p) _mm_set1_ps(p) +# define INTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_unpacklo_ps(in1, in2); out2 = _mm_unpackhi_ps(in1, in2); out1 = tmp__; } +# define UNINTERLEAVE2(in1, in2, out1, out2) { v4sf tmp__ = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(2,0,2,0)); out2 = _mm_shuffle_ps(in1, in2, _MM_SHUFFLE(3,1,3,1)); out1 = tmp__; } +# define VTRANSPOSE4(x0,x1,x2,x3) _MM_TRANSPOSE4_PS(x0,x1,x2,x3) +# define VSWAPHL(a,b) _mm_shuffle_ps(b, a, _MM_SHUFFLE(3,2,1,0)) +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0xF) == 0) + +/* + ARM NEON support macros +*/ +#elif !defined(PFFFT_SIMD_DISABLE) && (defined(__arm__) || defined(__aarch64__)) +# include +typedef float32x4_t v4sf; +# define SIMD_SZ 4 +# define VZERO() vdupq_n_f32(0) +# define VMUL(a,b) vmulq_f32(a,b) +# define VADD(a,b) vaddq_f32(a,b) +# define VMADD(a,b,c) vmlaq_f32(c,a,b) +# define VSUB(a,b) vsubq_f32(a,b) +# define LD_PS1(p) vld1q_dup_f32(&(p)) +# define INTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vzipq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; } +# define UNINTERLEAVE2(in1, in2, out1, out2) { float32x4x2_t tmp__ = vuzpq_f32(in1,in2); out1=tmp__.val[0]; out2=tmp__.val[1]; } +# define VTRANSPOSE4(x0,x1,x2,x3) { \ + float32x4x2_t t0_ = vzipq_f32(x0, x2); \ + float32x4x2_t t1_ = vzipq_f32(x1, x3); \ + float32x4x2_t u0_ = vzipq_f32(t0_.val[0], t1_.val[0]); \ + float32x4x2_t u1_ = vzipq_f32(t0_.val[1], t1_.val[1]); \ + x0 = u0_.val[0]; x1 = u0_.val[1]; x2 = u1_.val[0]; x3 = u1_.val[1]; \ + } +// marginally faster version +//# define VTRANSPOSE4(x0,x1,x2,x3) { asm("vtrn.32 %q0, %q1;\n vtrn.32 %q2,%q3\n vswp %f0,%e2\n vswp %f1,%e3" : "+w"(x0), "+w"(x1), "+w"(x2), "+w"(x3)::); } +# define VSWAPHL(a,b) vcombine_f32(vget_low_f32(b), vget_high_f32(a)) +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x3) == 0) +#else +# if !defined(PFFFT_SIMD_DISABLE) +# warning "building with simd disabled !\n"; +# define PFFFT_SIMD_DISABLE // fallback to scalar code +# endif +#endif + +// fallback mode for situations where SSE/Altivec are not available, use scalar mode instead +#ifdef PFFFT_SIMD_DISABLE +typedef float v4sf; +# define SIMD_SZ 1 +# define VZERO() 0.f +# define VMUL(a,b) ((a)*(b)) +# define VADD(a,b) ((a)+(b)) +# define VMADD(a,b,c) ((a)*(b)+(c)) +# define VSUB(a,b) ((a)-(b)) +# define LD_PS1(p) (p) +# define VALIGNED(ptr) ((((uintptr_t)(ptr)) & 0x3) == 0) +#endif + +// shortcuts for complex multiplcations +#define VCPLXMUL(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VSUB(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VADD(ai,tmp); } +#define VCPLXMULCONJ(ar,ai,br,bi) { v4sf tmp; tmp=VMUL(ar,bi); ar=VMUL(ar,br); ar=VADD(ar,VMUL(ai,bi)); ai=VMUL(ai,br); ai=VSUB(ai,tmp); } +#ifndef SVMUL +// multiply a scalar with a vector +#define SVMUL(f,v) VMUL(LD_PS1(f),v) +#endif + +#if !defined(PFFFT_SIMD_DISABLE) +typedef union v4sf_union { + v4sf v; + float f[4]; +} v4sf_union; + +#include + +#define assertv4(v,f0,f1,f2,f3) assert(v.f[0] == (f0) && v.f[1] == (f1) && v.f[2] == (f2) && v.f[3] == (f3)) + +/* detect bugs with the vector support macros */ +void validate_pffft_simd() { + float f[16] = { 0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15 }; + v4sf_union a0, a1, a2, a3, t, u; + memcpy(a0.f, f, 4*sizeof(float)); + memcpy(a1.f, f+4, 4*sizeof(float)); + memcpy(a2.f, f+8, 4*sizeof(float)); + memcpy(a3.f, f+12, 4*sizeof(float)); + + t = a0; u = a1; t.v = VZERO(); + printf("VZERO=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 0, 0, 0, 0); + t.v = VADD(a1.v, a2.v); + printf("VADD(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 12, 14, 16, 18); + t.v = VMUL(a1.v, a2.v); + printf("VMUL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 32, 45, 60, 77); + t.v = VMADD(a1.v, a2.v,a0.v); + printf("VMADD(4:7,8:11,0:3)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); assertv4(t, 32, 46, 62, 80); + + INTERLEAVE2(a1.v,a2.v,t.v,u.v); + printf("INTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]); + assertv4(t, 4, 8, 5, 9); assertv4(u, 6, 10, 7, 11); + UNINTERLEAVE2(a1.v,a2.v,t.v,u.v); + printf("UNINTERLEAVE2(4:7,8:11)=[%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3], u.f[0], u.f[1], u.f[2], u.f[3]); + assertv4(t, 4, 6, 8, 10); assertv4(u, 5, 7, 9, 11); + + t.v=LD_PS1(f[15]); + printf("LD_PS1(15)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); + assertv4(t, 15, 15, 15, 15); + t.v = VSWAPHL(a1.v, a2.v); + printf("VSWAPHL(4:7,8:11)=[%2g %2g %2g %2g]\n", t.f[0], t.f[1], t.f[2], t.f[3]); + assertv4(t, 8, 9, 6, 7); + VTRANSPOSE4(a0.v, a1.v, a2.v, a3.v); + printf("VTRANSPOSE4(0:3,4:7,8:11,12:15)=[%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g] [%2g %2g %2g %2g]\n", + a0.f[0], a0.f[1], a0.f[2], a0.f[3], a1.f[0], a1.f[1], a1.f[2], a1.f[3], + a2.f[0], a2.f[1], a2.f[2], a2.f[3], a3.f[0], a3.f[1], a3.f[2], a3.f[3]); + assertv4(a0, 0, 4, 8, 12); assertv4(a1, 1, 5, 9, 13); assertv4(a2, 2, 6, 10, 14); assertv4(a3, 3, 7, 11, 15); +} +#endif //!PFFFT_SIMD_DISABLE + +/* SSE and co like 16-bytes aligned pointers */ +#define MALLOC_V4SF_ALIGNMENT 64 // with a 64-byte alignment, we are even aligned on L2 cache lines... +void *pffft_aligned_malloc(size_t nb_bytes) { + void *p, *p0 = malloc(nb_bytes + MALLOC_V4SF_ALIGNMENT); + if (!p0) return (void *) 0; + p = (void *) (((size_t) p0 + MALLOC_V4SF_ALIGNMENT) & (~((size_t) (MALLOC_V4SF_ALIGNMENT-1)))); + *((void **) p - 1) = p0; + return p; +} + +void pffft_aligned_free(void *p) { + if (p) free(*((void **) p - 1)); +} + +int pffft_simd_size() { return SIMD_SZ; } + +/* + passf2 and passb2 has been merged here, fsign = -1 for passf2, +1 for passb2 +*/ +static NEVER_INLINE(void) passf2_ps(int ido, int l1, const v4sf *cc, v4sf *ch, const float *wa1, float fsign) { + int k, i; + int l1ido = l1*ido; + if (ido <= 2) { + for (k=0; k < l1ido; k += ido, ch += ido, cc+= 2*ido) { + ch[0] = VADD(cc[0], cc[ido+0]); + ch[l1ido] = VSUB(cc[0], cc[ido+0]); + ch[1] = VADD(cc[1], cc[ido+1]); + ch[l1ido + 1] = VSUB(cc[1], cc[ido+1]); + } + } else { + for (k=0; k < l1ido; k += ido, ch += ido, cc += 2*ido) { + for (i=0; i 2); + for (k=0; k< l1ido; k += ido, cc+= 3*ido, ch +=ido) { + for (i=0; i 2); + for (k = 0; k < l1; ++k, cc += 5*ido, ch += ido) { + for (i = 0; i < ido-1; i += 2) { + ti5 = VSUB(cc_ref(i , 2), cc_ref(i , 5)); + ti2 = VADD(cc_ref(i , 2), cc_ref(i , 5)); + ti4 = VSUB(cc_ref(i , 3), cc_ref(i , 4)); + ti3 = VADD(cc_ref(i , 3), cc_ref(i , 4)); + tr5 = VSUB(cc_ref(i-1, 2), cc_ref(i-1, 5)); + tr2 = VADD(cc_ref(i-1, 2), cc_ref(i-1, 5)); + tr4 = VSUB(cc_ref(i-1, 3), cc_ref(i-1, 4)); + tr3 = VADD(cc_ref(i-1, 3), cc_ref(i-1, 4)); + ch_ref(i-1, 1) = VADD(cc_ref(i-1, 1), VADD(tr2, tr3)); + ch_ref(i , 1) = VADD(cc_ref(i , 1), VADD(ti2, ti3)); + cr2 = VADD(cc_ref(i-1, 1), VADD(SVMUL(tr11, tr2),SVMUL(tr12, tr3))); + ci2 = VADD(cc_ref(i , 1), VADD(SVMUL(tr11, ti2),SVMUL(tr12, ti3))); + cr3 = VADD(cc_ref(i-1, 1), VADD(SVMUL(tr12, tr2),SVMUL(tr11, tr3))); + ci3 = VADD(cc_ref(i , 1), VADD(SVMUL(tr12, ti2),SVMUL(tr11, ti3))); + cr5 = VADD(SVMUL(ti11, tr5), SVMUL(ti12, tr4)); + ci5 = VADD(SVMUL(ti11, ti5), SVMUL(ti12, ti4)); + cr4 = VSUB(SVMUL(ti12, tr5), SVMUL(ti11, tr4)); + ci4 = VSUB(SVMUL(ti12, ti5), SVMUL(ti11, ti4)); + dr3 = VSUB(cr3, ci4); + dr4 = VADD(cr3, ci4); + di3 = VADD(ci3, cr4); + di4 = VSUB(ci3, cr4); + dr5 = VADD(cr2, ci5); + dr2 = VSUB(cr2, ci5); + di5 = VSUB(ci2, cr5); + di2 = VADD(ci2, cr5); + wr1=wa1[i], wi1=fsign*wa1[i+1], wr2=wa2[i], wi2=fsign*wa2[i+1]; + wr3=wa3[i], wi3=fsign*wa3[i+1], wr4=wa4[i], wi4=fsign*wa4[i+1]; + VCPLXMUL(dr2, di2, LD_PS1(wr1), LD_PS1(wi1)); + ch_ref(i - 1, 2) = dr2; + ch_ref(i, 2) = di2; + VCPLXMUL(dr3, di3, LD_PS1(wr2), LD_PS1(wi2)); + ch_ref(i - 1, 3) = dr3; + ch_ref(i, 3) = di3; + VCPLXMUL(dr4, di4, LD_PS1(wr3), LD_PS1(wi3)); + ch_ref(i - 1, 4) = dr4; + ch_ref(i, 4) = di4; + VCPLXMUL(dr5, di5, LD_PS1(wr4), LD_PS1(wi4)); + ch_ref(i - 1, 5) = dr5; + ch_ref(i, 5) = di5; + } + } +#undef ch_ref +#undef cc_ref +} + +static NEVER_INLINE(void) radf2_ps(int ido, int l1, const v4sf * RESTRICT cc, v4sf * RESTRICT ch, const float *wa1) { + static const float minus_one = -1.f; + int i, k, l1ido = l1*ido; + for (k=0; k < l1ido; k += ido) { + v4sf a = cc[k], b = cc[k + l1ido]; + ch[2*k] = VADD(a, b); + ch[2*(k+ido)-1] = VSUB(a, b); + } + if (ido < 2) return; + if (ido != 2) { + for (k=0; k < l1ido; k += ido) { + for (i=2; i 5) { + wa[i1-1] = wa[i-1]; + wa[i1] = wa[i]; + } + } + l1 = l2; + } +} /* cffti1 */ + + +v4sf *cfftf1_ps(int n, const v4sf *input_readonly, v4sf *work1, v4sf *work2, const float *wa, const int *ifac, int isign) { + v4sf *in = (v4sf*)input_readonly; + v4sf *out = (in == work2 ? work1 : work2); + int nf = ifac[1], k1; + int l1 = 1; + int iw = 0; + assert(in != out && work1 != work2); + for (k1=2; k1<=nf+1; k1++) { + int ip = ifac[k1]; + int l2 = ip*l1; + int ido = n / l2; + int idot = ido + ido; + switch (ip) { + case 5: { + int ix2 = iw + idot; + int ix3 = ix2 + idot; + int ix4 = ix3 + idot; + passf5_ps(idot, l1, in, out, &wa[iw], &wa[ix2], &wa[ix3], &wa[ix4], isign); + } break; + case 4: { + int ix2 = iw + idot; + int ix3 = ix2 + idot; + passf4_ps(idot, l1, in, out, &wa[iw], &wa[ix2], &wa[ix3], isign); + } break; + case 2: { + passf2_ps(idot, l1, in, out, &wa[iw], isign); + } break; + case 3: { + int ix2 = iw + idot; + passf3_ps(idot, l1, in, out, &wa[iw], &wa[ix2], isign); + } break; + default: + assert(0); + } + l1 = l2; + iw += (ip - 1)*idot; + if (out == work2) { + out = work1; in = work2; + } else { + out = work2; in = work1; + } + } + + return in; /* this is in fact the output .. */ +} + + +struct PFFFT_Setup { + int N; + int Ncvec; // nb of complex simd vectors (N/4 if PFFFT_COMPLEX, N/8 if PFFFT_REAL) + int ifac[15]; + pffft_transform_t transform; + v4sf *data; // allocated room for twiddle coefs + float *e; // points into 'data' , N/4*3 elements + float *twiddle; // points into 'data', N/4 elements +}; + +PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform) { + PFFFT_Setup *s = (PFFFT_Setup*)malloc(sizeof(PFFFT_Setup)); + int k, m; + /* unfortunately, the fft size must be a multiple of 16 for complex FFTs + and 32 for real FFTs -- a lot of stuff would need to be rewritten to + handle other cases (or maybe just switch to a scalar fft, I don't know..) */ + if (transform == PFFFT_REAL) { assert((N%(2*SIMD_SZ*SIMD_SZ))==0 && N>0); } + if (transform == PFFFT_COMPLEX) { assert((N%(SIMD_SZ*SIMD_SZ))==0 && N>0); } + //assert((N % 32) == 0); + s->N = N; + s->transform = transform; + /* nb of complex simd vectors */ + s->Ncvec = (transform == PFFFT_REAL ? N/2 : N)/SIMD_SZ; + s->data = (v4sf*)pffft_aligned_malloc(2*s->Ncvec * sizeof(v4sf)); + s->e = (float*)s->data; + s->twiddle = (float*)(s->data + (2*s->Ncvec*(SIMD_SZ-1))/SIMD_SZ); + + if (transform == PFFFT_REAL) { + for (k=0; k < s->Ncvec; ++k) { + int i = k/SIMD_SZ; + int j = k%SIMD_SZ; + for (m=0; m < SIMD_SZ-1; ++m) { + float A = -2*(float)M_PI*(m+1)*k / N; + s->e[(2*(i*3 + m) + 0) * SIMD_SZ + j] = cosf(A); + s->e[(2*(i*3 + m) + 1) * SIMD_SZ + j] = sinf(A); + } + } + rffti1_ps(N/SIMD_SZ, s->twiddle, s->ifac); + } else { + for (k=0; k < s->Ncvec; ++k) { + int i = k/SIMD_SZ; + int j = k%SIMD_SZ; + for (m=0; m < SIMD_SZ-1; ++m) { + float A = -2*(float)M_PI*(m+1)*k / N; + s->e[(2*(i*3 + m) + 0)*SIMD_SZ + j] = cosf(A); + s->e[(2*(i*3 + m) + 1)*SIMD_SZ + j] = sinf(A); + } + } + cffti1_ps(N/SIMD_SZ, s->twiddle, s->ifac); + } + + /* check that N is decomposable with allowed prime factors */ + for (k=0, m=1; k < s->ifac[1]; ++k) { m *= s->ifac[2+k]; } + if (m != N/SIMD_SZ) { + pffft_destroy_setup(s); s = 0; + } + + return s; +} + + +void pffft_destroy_setup(PFFFT_Setup *s) { + pffft_aligned_free(s->data); + free(s); +} + +#if !defined(PFFFT_SIMD_DISABLE) + +/* [0 0 1 2 3 4 5 6 7 8] -> [0 8 7 6 5 4 3 2 1] */ +static void reversed_copy(int N, const v4sf *in, int in_stride, v4sf *out) { + v4sf g0, g1; + int k; + INTERLEAVE2(in[0], in[1], g0, g1); in += in_stride; + + *--out = VSWAPHL(g0, g1); // [g0l, g0h], [g1l g1h] -> [g1l, g0h] + for (k=1; k < N; ++k) { + v4sf h0, h1; + INTERLEAVE2(in[0], in[1], h0, h1); in += in_stride; + *--out = VSWAPHL(g1, h0); + *--out = VSWAPHL(h0, h1); + g1 = h1; + } + *--out = VSWAPHL(g1, g0); +} + +static void unreversed_copy(int N, const v4sf *in, v4sf *out, int out_stride) { + v4sf g0, g1, h0, h1; + int k; + g0 = g1 = in[0]; ++in; + for (k=1; k < N; ++k) { + h0 = *in++; h1 = *in++; + g1 = VSWAPHL(g1, h0); + h0 = VSWAPHL(h0, h1); + UNINTERLEAVE2(h0, g1, out[0], out[1]); out += out_stride; + g1 = h1; + } + h0 = *in++; h1 = g0; + g1 = VSWAPHL(g1, h0); + h0 = VSWAPHL(h0, h1); + UNINTERLEAVE2(h0, g1, out[0], out[1]); +} + +void pffft_zreorder(PFFFT_Setup *setup, const float *in, float *out, pffft_direction_t direction) { + int k, N = setup->N, Ncvec = setup->Ncvec; + const v4sf *vin = (const v4sf*)in; + v4sf *vout = (v4sf*)out; + assert(in != out); + if (setup->transform == PFFFT_REAL) { + int k, dk = N/32; + if (direction == PFFFT_FORWARD) { + for (k=0; k < dk; ++k) { + INTERLEAVE2(vin[k*8 + 0], vin[k*8 + 1], vout[2*(0*dk + k) + 0], vout[2*(0*dk + k) + 1]); + INTERLEAVE2(vin[k*8 + 4], vin[k*8 + 5], vout[2*(2*dk + k) + 0], vout[2*(2*dk + k) + 1]); + } + reversed_copy(dk, vin+2, 8, (v4sf*)(out + N/2)); + reversed_copy(dk, vin+6, 8, (v4sf*)(out + N)); + } else { + for (k=0; k < dk; ++k) { + UNINTERLEAVE2(vin[2*(0*dk + k) + 0], vin[2*(0*dk + k) + 1], vout[k*8 + 0], vout[k*8 + 1]); + UNINTERLEAVE2(vin[2*(2*dk + k) + 0], vin[2*(2*dk + k) + 1], vout[k*8 + 4], vout[k*8 + 5]); + } + unreversed_copy(dk, (v4sf*)(in + N/4), (v4sf*)(out + N - 6*SIMD_SZ), -8); + unreversed_copy(dk, (v4sf*)(in + 3*N/4), (v4sf*)(out + N - 2*SIMD_SZ), -8); + } + } else { + if (direction == PFFFT_FORWARD) { + for (k=0; k < Ncvec; ++k) { + int kk = (k/4) + (k%4)*(Ncvec/4); + INTERLEAVE2(vin[k*2], vin[k*2+1], vout[kk*2], vout[kk*2+1]); + } + } else { + for (k=0; k < Ncvec; ++k) { + int kk = (k/4) + (k%4)*(Ncvec/4); + UNINTERLEAVE2(vin[kk*2], vin[kk*2+1], vout[k*2], vout[k*2+1]); + } + } + } +} + +void pffft_cplx_finalize(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + assert(in != out); + for (k=0; k < dk; ++k) { + r0 = in[8*k+0]; i0 = in[8*k+1]; + r1 = in[8*k+2]; i1 = in[8*k+3]; + r2 = in[8*k+4]; i2 = in[8*k+5]; + r3 = in[8*k+6]; i3 = in[8*k+7]; + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + VCPLXMUL(r1,i1,e[k*6+0],e[k*6+1]); + VCPLXMUL(r2,i2,e[k*6+2],e[k*6+3]); + VCPLXMUL(r3,i3,e[k*6+4],e[k*6+5]); + + sr0 = VADD(r0,r2); dr0 = VSUB(r0, r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r1, r3); + si0 = VADD(i0,i2); di0 = VSUB(i0, i2); + si1 = VADD(i1,i3); di1 = VSUB(i1, i3); + + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 -1 0 0 -1 0 1] [r1] + [1 -1 1 -1 0 0 0 0] [r2] + [1 0 -1 0 0 1 0 -1] [r3] + [0 0 0 0 1 1 1 1] * [i0] + [0 1 0 -1 1 0 -1 0] [i1] + [0 0 0 0 1 -1 1 -1] [i2] + [0 -1 0 1 1 0 -1 0] [i3] + */ + + r0 = VADD(sr0, sr1); i0 = VADD(si0, si1); + r1 = VADD(dr0, di1); i1 = VSUB(di0, dr1); + r2 = VSUB(sr0, sr1); i2 = VSUB(si0, si1); + r3 = VSUB(dr0, di1); i3 = VADD(di0, dr1); + + *out++ = r0; *out++ = i0; *out++ = r1; *out++ = i1; + *out++ = r2; *out++ = i2; *out++ = r3; *out++ = i3; + } +} + +void pffft_cplx_preprocess(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + assert(in != out); + for (k=0; k < dk; ++k) { + r0 = in[8*k+0]; i0 = in[8*k+1]; + r1 = in[8*k+2]; i1 = in[8*k+3]; + r2 = in[8*k+4]; i2 = in[8*k+5]; + r3 = in[8*k+6]; i3 = in[8*k+7]; + + sr0 = VADD(r0,r2); dr0 = VSUB(r0, r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r1, r3); + si0 = VADD(i0,i2); di0 = VSUB(i0, i2); + si1 = VADD(i1,i3); di1 = VSUB(i1, i3); + + r0 = VADD(sr0, sr1); i0 = VADD(si0, si1); + r1 = VSUB(dr0, di1); i1 = VADD(di0, dr1); + r2 = VSUB(sr0, sr1); i2 = VSUB(si0, si1); + r3 = VADD(dr0, di1); i3 = VSUB(di0, dr1); + + VCPLXMULCONJ(r1,i1,e[k*6+0],e[k*6+1]); + VCPLXMULCONJ(r2,i2,e[k*6+2],e[k*6+3]); + VCPLXMULCONJ(r3,i3,e[k*6+4],e[k*6+5]); + + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + *out++ = r0; *out++ = i0; *out++ = r1; *out++ = i1; + *out++ = r2; *out++ = i2; *out++ = r3; *out++ = i3; + } +} + + +static ALWAYS_INLINE(void) pffft_real_finalize_4x4(const v4sf *in0, const v4sf *in1, const v4sf *in, + const v4sf *e, v4sf *out) { + v4sf r0, i0, r1, i1, r2, i2, r3, i3; + v4sf sr0, dr0, sr1, dr1, si0, di0, si1, di1; + r0 = *in0; i0 = *in1; + r1 = *in++; i1 = *in++; r2 = *in++; i2 = *in++; r3 = *in++; i3 = *in++; + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 -1 0 0 -1 0 1] [r1] + [1 0 -1 0 0 1 0 -1] [r2] + [1 -1 1 -1 0 0 0 0] [r3] + [0 0 0 0 1 1 1 1] * [i0] + [0 -1 0 1 -1 0 1 0] [i1] + [0 -1 0 1 1 0 -1 0] [i2] + [0 0 0 0 -1 1 -1 1] [i3] + */ + + //cerr << "matrix initial, before e , REAL:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n"; + //cerr << "matrix initial, before e, IMAG :\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n"; + + VCPLXMUL(r1,i1,e[0],e[1]); + VCPLXMUL(r2,i2,e[2],e[3]); + VCPLXMUL(r3,i3,e[4],e[5]); + + //cerr << "matrix initial, real part:\n 1: " << r0 << "\n 1: " << r1 << "\n 1: " << r2 << "\n 1: " << r3 << "\n"; + //cerr << "matrix initial, imag part:\n 1: " << i0 << "\n 1: " << i1 << "\n 1: " << i2 << "\n 1: " << i3 << "\n"; + + sr0 = VADD(r0,r2); dr0 = VSUB(r0,r2); + sr1 = VADD(r1,r3); dr1 = VSUB(r3,r1); + si0 = VADD(i0,i2); di0 = VSUB(i0,i2); + si1 = VADD(i1,i3); di1 = VSUB(i3,i1); + + r0 = VADD(sr0, sr1); + r3 = VSUB(sr0, sr1); + i0 = VADD(si0, si1); + i3 = VSUB(si1, si0); + r1 = VADD(dr0, di1); + r2 = VSUB(dr0, di1); + i1 = VSUB(dr1, di0); + i2 = VADD(dr1, di0); + + *out++ = r0; + *out++ = i0; + *out++ = r1; + *out++ = i1; + *out++ = r2; + *out++ = i2; + *out++ = r3; + *out++ = i3; + +} + +static NEVER_INLINE(void) pffft_real_finalize(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */ + + v4sf_union cr, ci, *uout = (v4sf_union*)out; + v4sf save = in[7], zero=VZERO(); + float xr0, xi0, xr1, xi1, xr2, xi2, xr3, xi3; + static const float s = (float)M_SQRT2/2; + + cr.v = in[0]; ci.v = in[Ncvec*2-1]; + assert(in != out); + pffft_real_finalize_4x4(&zero, &zero, in+1, e, out); + + /* + [cr0 cr1 cr2 cr3 ci0 ci1 ci2 ci3] + + [Xr(1)] ] [1 1 1 1 0 0 0 0] + [Xr(N/4) ] [0 0 0 0 1 s 0 -s] + [Xr(N/2) ] [1 0 -1 0 0 0 0 0] + [Xr(3N/4)] [0 0 0 0 1 -s 0 s] + [Xi(1) ] [1 -1 1 -1 0 0 0 0] + [Xi(N/4) ] [0 0 0 0 0 -s -1 -s] + [Xi(N/2) ] [0 -1 0 1 0 0 0 0] + [Xi(3N/4)] [0 0 0 0 0 -s 1 -s] + */ + + xr0=(cr.f[0]+cr.f[2]) + (cr.f[1]+cr.f[3]); uout[0].f[0] = xr0; + xi0=(cr.f[0]+cr.f[2]) - (cr.f[1]+cr.f[3]); uout[1].f[0] = xi0; + xr2=(cr.f[0]-cr.f[2]); uout[4].f[0] = xr2; + xi2=(cr.f[3]-cr.f[1]); uout[5].f[0] = xi2; + xr1= ci.f[0] + s*(ci.f[1]-ci.f[3]); uout[2].f[0] = xr1; + xi1=-ci.f[2] - s*(ci.f[1]+ci.f[3]); uout[3].f[0] = xi1; + xr3= ci.f[0] - s*(ci.f[1]-ci.f[3]); uout[6].f[0] = xr3; + xi3= ci.f[2] - s*(ci.f[1]+ci.f[3]); uout[7].f[0] = xi3; + + for (k=1; k < dk; ++k) { + v4sf save_next = in[8*k+7]; + pffft_real_finalize_4x4(&save, &in[8*k+0], in + 8*k+1, + e + k*6, out + k*8); + save = save_next; + } + +} + +static ALWAYS_INLINE(void) pffft_real_preprocess_4x4(const v4sf *in, + const v4sf *e, v4sf *out, int first) { + v4sf r0=in[0], i0=in[1], r1=in[2], i1=in[3], r2=in[4], i2=in[5], r3=in[6], i3=in[7]; + /* + transformation for each column is: + + [1 1 1 1 0 0 0 0] [r0] + [1 0 0 -1 0 -1 -1 0] [r1] + [1 -1 -1 1 0 0 0 0] [r2] + [1 0 0 -1 0 1 1 0] [r3] + [0 0 0 0 1 -1 1 -1] * [i0] + [0 -1 1 0 1 0 0 1] [i1] + [0 0 0 0 1 1 -1 -1] [i2] + [0 1 -1 0 1 0 0 1] [i3] + */ + + v4sf sr0 = VADD(r0,r3), dr0 = VSUB(r0,r3); + v4sf sr1 = VADD(r1,r2), dr1 = VSUB(r1,r2); + v4sf si0 = VADD(i0,i3), di0 = VSUB(i0,i3); + v4sf si1 = VADD(i1,i2), di1 = VSUB(i1,i2); + + r0 = VADD(sr0, sr1); + r2 = VSUB(sr0, sr1); + r1 = VSUB(dr0, si1); + r3 = VADD(dr0, si1); + i0 = VSUB(di0, di1); + i2 = VADD(di0, di1); + i1 = VSUB(si0, dr1); + i3 = VADD(si0, dr1); + + VCPLXMULCONJ(r1,i1,e[0],e[1]); + VCPLXMULCONJ(r2,i2,e[2],e[3]); + VCPLXMULCONJ(r3,i3,e[4],e[5]); + + VTRANSPOSE4(r0,r1,r2,r3); + VTRANSPOSE4(i0,i1,i2,i3); + + if (!first) { + *out++ = r0; + *out++ = i0; + } + *out++ = r1; + *out++ = i1; + *out++ = r2; + *out++ = i2; + *out++ = r3; + *out++ = i3; +} + +static NEVER_INLINE(void) pffft_real_preprocess(int Ncvec, const v4sf *in, v4sf *out, const v4sf *e) { + int k, dk = Ncvec/SIMD_SZ; // number of 4x4 matrix blocks + /* fftpack order is f0r f1r f1i f2r f2i ... f(n-1)r f(n-1)i f(n)r */ + + v4sf_union Xr, Xi, *uout = (v4sf_union*)out; + float cr0, ci0, cr1, ci1, cr2, ci2, cr3, ci3; + static const float s = (float)M_SQRT2; + assert(in != out); + for (k=0; k < 4; ++k) { + Xr.f[k] = ((float*)in)[8*k]; + Xi.f[k] = ((float*)in)[8*k+4]; + } + + pffft_real_preprocess_4x4(in, e, out+1, 1); // will write only 6 values + + /* + [Xr0 Xr1 Xr2 Xr3 Xi0 Xi1 Xi2 Xi3] + + [cr0] [1 0 2 0 1 0 0 0] + [cr1] [1 0 0 0 -1 0 -2 0] + [cr2] [1 0 -2 0 1 0 0 0] + [cr3] [1 0 0 0 -1 0 2 0] + [ci0] [0 2 0 2 0 0 0 0] + [ci1] [0 s 0 -s 0 -s 0 -s] + [ci2] [0 0 0 0 0 -2 0 2] + [ci3] [0 -s 0 s 0 -s 0 -s] + */ + for (k=1; k < dk; ++k) { + pffft_real_preprocess_4x4(in+8*k, e + k*6, out-1+k*8, 0); + } + + cr0=(Xr.f[0]+Xi.f[0]) + 2*Xr.f[2]; uout[0].f[0] = cr0; + cr1=(Xr.f[0]-Xi.f[0]) - 2*Xi.f[2]; uout[0].f[1] = cr1; + cr2=(Xr.f[0]+Xi.f[0]) - 2*Xr.f[2]; uout[0].f[2] = cr2; + cr3=(Xr.f[0]-Xi.f[0]) + 2*Xi.f[2]; uout[0].f[3] = cr3; + ci0= 2*(Xr.f[1]+Xr.f[3]); uout[2*Ncvec-1].f[0] = ci0; + ci1= s*(Xr.f[1]-Xr.f[3]) - s*(Xi.f[1]+Xi.f[3]); uout[2*Ncvec-1].f[1] = ci1; + ci2= 2*(Xi.f[3]-Xi.f[1]); uout[2*Ncvec-1].f[2] = ci2; + ci3=-s*(Xr.f[1]-Xr.f[3]) - s*(Xi.f[1]+Xi.f[3]); uout[2*Ncvec-1].f[3] = ci3; +} + + +void pffft_transform_internal(PFFFT_Setup *setup, const float *finput, float *foutput, v4sf *scratch, + pffft_direction_t direction, int ordered) { + int k, Ncvec = setup->Ncvec; + int nf_odd = (setup->ifac[1] & 1); + + // temporary buffer is allocated on the stack if the scratch pointer is NULL + int stack_allocate = (scratch == 0 ? Ncvec*2 : 1); + VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate); + + const v4sf *vinput = (const v4sf*)finput; + v4sf *voutput = (v4sf*)foutput; + v4sf *buff[2] = { voutput, scratch ? scratch : scratch_on_stack }; + int ib = (nf_odd ^ ordered ? 1 : 0); + + assert(VALIGNED(finput) && VALIGNED(foutput)); + + //assert(finput != foutput); + if (direction == PFFFT_FORWARD) { + ib = !ib; + if (setup->transform == PFFFT_REAL) { + ib = (rfftf1_ps(Ncvec*2, vinput, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + pffft_real_finalize(Ncvec, buff[ib], buff[!ib], (v4sf*)setup->e); + } else { + v4sf *tmp = buff[ib]; + for (k=0; k < Ncvec; ++k) { + UNINTERLEAVE2(vinput[k*2], vinput[k*2+1], tmp[k*2], tmp[k*2+1]); + } + ib = (cfftf1_ps(Ncvec, buff[ib], buff[!ib], buff[ib], + setup->twiddle, &setup->ifac[0], -1) == buff[0] ? 0 : 1); + pffft_cplx_finalize(Ncvec, buff[ib], buff[!ib], (v4sf*)setup->e); + } + if (ordered) { + pffft_zreorder(setup, (float*)buff[!ib], (float*)buff[ib], PFFFT_FORWARD); + } else ib = !ib; + } else { + if (vinput == buff[ib]) { + ib = !ib; // may happen when finput == foutput + } + if (ordered) { + pffft_zreorder(setup, (float*)vinput, (float*)buff[ib], PFFFT_BACKWARD); + vinput = buff[ib]; ib = !ib; + } + if (setup->transform == PFFFT_REAL) { + pffft_real_preprocess(Ncvec, vinput, buff[ib], (v4sf*)setup->e); + ib = (rfftb1_ps(Ncvec*2, buff[ib], buff[0], buff[1], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + pffft_cplx_preprocess(Ncvec, vinput, buff[ib], (v4sf*)setup->e); + ib = (cfftf1_ps(Ncvec, buff[ib], buff[0], buff[1], + setup->twiddle, &setup->ifac[0], +1) == buff[0] ? 0 : 1); + for (k=0; k < Ncvec; ++k) { + INTERLEAVE2(buff[ib][k*2], buff[ib][k*2+1], buff[ib][k*2], buff[ib][k*2+1]); + } + } + } + + if (buff[ib] != voutput) { + /* extra copy required -- this situation should only happen when finput == foutput */ + assert(finput==foutput); + for (k=0; k < Ncvec; ++k) { + v4sf a = buff[ib][2*k], b = buff[ib][2*k+1]; + voutput[2*k] = a; voutput[2*k+1] = b; + } + ib = !ib; + } + assert(buff[ib] == voutput); +} + +void pffft_zconvolve_accumulate(PFFFT_Setup *s, const float *a, const float *b, float *ab, float scaling) { + int Ncvec = s->Ncvec; + const v4sf * RESTRICT va = (const v4sf*)a; + const v4sf * RESTRICT vb = (const v4sf*)b; + v4sf * RESTRICT vab = (v4sf*)ab; + +#if defined(__arm__) || defined(__aarch64__) + __builtin_prefetch(va); + __builtin_prefetch(vb); + __builtin_prefetch(vab); + __builtin_prefetch(va+2); + __builtin_prefetch(vb+2); + __builtin_prefetch(vab+2); + __builtin_prefetch(va+4); + __builtin_prefetch(vb+4); + __builtin_prefetch(vab+4); + __builtin_prefetch(va+6); + __builtin_prefetch(vb+6); + __builtin_prefetch(vab+6); +# ifndef __clang__ +# define ZCONVOLVE_USING_INLINE_NEON_ASM +# endif +#endif + + float ar, ai, br, bi, abr, abi; +#ifndef ZCONVOLVE_USING_INLINE_ASM + v4sf vscal = LD_PS1(scaling); + int i; +#endif + + assert(VALIGNED(a) && VALIGNED(b) && VALIGNED(ab)); + ar = ((v4sf_union*)va)[0].f[0]; + ai = ((v4sf_union*)va)[1].f[0]; + br = ((v4sf_union*)vb)[0].f[0]; + bi = ((v4sf_union*)vb)[1].f[0]; + abr = ((v4sf_union*)vab)[0].f[0]; + abi = ((v4sf_union*)vab)[1].f[0]; + +#ifdef ZCONVOLVE_USING_INLINE_ASM // inline asm version, unfortunately miscompiled by clang 3.2, at least on ubuntu.. so this will be restricted to gcc + const float *a_ = a, *b_ = b; float *ab_ = ab; + int N = Ncvec; + asm volatile("mov r8, %2 \n" + "vdup.f32 q15, %4 \n" + "1: \n" + "pld [%0,#64] \n" + "pld [%1,#64] \n" + "pld [%2,#64] \n" + "pld [%0,#96] \n" + "pld [%1,#96] \n" + "pld [%2,#96] \n" + "vld1.f32 {q0,q1}, [%0,:128]! \n" + "vld1.f32 {q4,q5}, [%1,:128]! \n" + "vld1.f32 {q2,q3}, [%0,:128]! \n" + "vld1.f32 {q6,q7}, [%1,:128]! \n" + "vld1.f32 {q8,q9}, [r8,:128]! \n" + + "vmul.f32 q10, q0, q4 \n" + "vmul.f32 q11, q0, q5 \n" + "vmul.f32 q12, q2, q6 \n" + "vmul.f32 q13, q2, q7 \n" + "vmls.f32 q10, q1, q5 \n" + "vmla.f32 q11, q1, q4 \n" + "vld1.f32 {q0,q1}, [r8,:128]! \n" + "vmls.f32 q12, q3, q7 \n" + "vmla.f32 q13, q3, q6 \n" + "vmla.f32 q8, q10, q15 \n" + "vmla.f32 q9, q11, q15 \n" + "vmla.f32 q0, q12, q15 \n" + "vmla.f32 q1, q13, q15 \n" + "vst1.f32 {q8,q9},[%2,:128]! \n" + "vst1.f32 {q0,q1},[%2,:128]! \n" + "subs %3, #2 \n" + "bne 1b \n" + : "+r"(a_), "+r"(b_), "+r"(ab_), "+r"(N) : "r"(scaling) : "r8", "q0","q1","q2","q3","q4","q5","q6","q7","q8","q9", "q10","q11","q12","q13","q15","memory"); +#else // default routine, works fine for non-arm cpus with current compilers + for (i=0; i < Ncvec; i += 2) { + v4sf ar, ai, br, bi; + ar = va[2*i+0]; ai = va[2*i+1]; + br = vb[2*i+0]; bi = vb[2*i+1]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+0] = VMADD(ar, vscal, vab[2*i+0]); + vab[2*i+1] = VMADD(ai, vscal, vab[2*i+1]); + ar = va[2*i+2]; ai = va[2*i+3]; + br = vb[2*i+2]; bi = vb[2*i+3]; + VCPLXMUL(ar, ai, br, bi); + vab[2*i+2] = VMADD(ar, vscal, vab[2*i+2]); + vab[2*i+3] = VMADD(ai, vscal, vab[2*i+3]); + } +#endif + if (s->transform == PFFFT_REAL) { + ((v4sf_union*)vab)[0].f[0] = abr + ar*br*scaling; + ((v4sf_union*)vab)[1].f[0] = abi + ai*bi*scaling; + } +} + + +#else // defined(PFFFT_SIMD_DISABLE) + +// standard routine using scalar floats, without SIMD stuff. + +#define pffft_zreorder_nosimd pffft_zreorder +void pffft_zreorder_nosimd(PFFFT_Setup *setup, const float *in, float *out, pffft_direction_t direction) { + int k, N = setup->N; + if (setup->transform == PFFFT_COMPLEX) { + for (k=0; k < 2*N; ++k) out[k] = in[k]; + return; + } + else if (direction == PFFFT_FORWARD) { + float x_N = in[N-1]; + for (k=N-1; k > 1; --k) out[k] = in[k-1]; + out[0] = in[0]; + out[1] = x_N; + } else { + float x_N = in[1]; + for (k=1; k < N-1; ++k) out[k] = in[k+1]; + out[0] = in[0]; + out[N-1] = x_N; + } +} + +#define pffft_transform_internal_nosimd pffft_transform_internal +void pffft_transform_internal_nosimd(PFFFT_Setup *setup, const float *input, float *output, float *scratch, + pffft_direction_t direction, int ordered) { + int Ncvec = setup->Ncvec; + int nf_odd = (setup->ifac[1] & 1); + + // temporary buffer is allocated on the stack if the scratch pointer is NULL + int stack_allocate = (scratch == 0 ? Ncvec*2 : 1); + VLA_ARRAY_ON_STACK(v4sf, scratch_on_stack, stack_allocate); + float *buff[2]; + int ib; + if (scratch == 0) scratch = scratch_on_stack; + buff[0] = output; buff[1] = scratch; + + if (setup->transform == PFFFT_COMPLEX) ordered = 0; // it is always ordered. + ib = (nf_odd ^ ordered ? 1 : 0); + + if (direction == PFFFT_FORWARD) { + if (setup->transform == PFFFT_REAL) { + ib = (rfftf1_ps(Ncvec*2, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0], -1) == buff[0] ? 0 : 1); + } + if (ordered) { + pffft_zreorder(setup, buff[ib], buff[!ib], PFFFT_FORWARD); ib = !ib; + } + } else { + if (input == buff[ib]) { + ib = !ib; // may happen when finput == foutput + } + if (ordered) { + pffft_zreorder(setup, input, buff[!ib], PFFFT_BACKWARD); + input = buff[!ib]; + } + if (setup->transform == PFFFT_REAL) { + ib = (rfftb1_ps(Ncvec*2, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0]) == buff[0] ? 0 : 1); + } else { + ib = (cfftf1_ps(Ncvec, input, buff[ib], buff[!ib], + setup->twiddle, &setup->ifac[0], +1) == buff[0] ? 0 : 1); + } + } + if (buff[ib] != output) { + int k; + // extra copy required -- this situation should happens only when finput == foutput + assert(input==output); + for (k=0; k < Ncvec; ++k) { + float a = buff[ib][2*k], b = buff[ib][2*k+1]; + output[2*k] = a; output[2*k+1] = b; + } + ib = !ib; + } + assert(buff[ib] == output); +} + +#define pffft_zconvolve_accumulate_nosimd pffft_zconvolve_accumulate +void pffft_zconvolve_accumulate_nosimd(PFFFT_Setup *s, const float *a, const float *b, + float *ab, float scaling) { + int i, Ncvec = s->Ncvec; + + if (s->transform == PFFFT_REAL) { + // take care of the fftpack ordering + ab[0] += a[0]*b[0]*scaling; + ab[2*Ncvec-1] += a[2*Ncvec-1]*b[2*Ncvec-1]*scaling; + ++ab; ++a; ++b; --Ncvec; + } + for (i=0; i < Ncvec; ++i) { + float ar, ai, br, bi; + ar = a[2*i+0]; ai = a[2*i+1]; + br = b[2*i+0]; bi = b[2*i+1]; + VCPLXMUL(ar, ai, br, bi); + ab[2*i+0] += ar*scaling; + ab[2*i+1] += ai*scaling; + } +} + +#endif // defined(PFFFT_SIMD_DISABLE) + +void pffft_transform(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction) { + pffft_transform_internal(setup, input, output, (v4sf*)work, direction, 0); +} + +void pffft_transform_ordered(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction) { + pffft_transform_internal(setup, input, output, (v4sf*)work, direction, 1); +} diff --git a/src/third_party/r8b/pffft.h b/src/third_party/r8b/pffft.h new file mode 100644 index 0000000..d9fbbaf --- /dev/null +++ b/src/third_party/r8b/pffft.h @@ -0,0 +1,178 @@ +/* Copyright (c) 2013 Julien Pommier ( pommier@modartt.com ) + + Based on original fortran 77 code from FFTPACKv4 from NETLIB, + authored by Dr Paul Swarztrauber of NCAR, in 1985. + + As confirmed by the NCAR fftpack software curators, the following + FFTPACKv5 license applies to FFTPACKv4 sources. My changes are + released under the same terms. + + FFTPACK license: + + http://www.cisl.ucar.edu/css/software/fftpack5/ftpk.html + + Copyright (c) 2004 the University Corporation for Atmospheric + Research ("UCAR"). All rights reserved. Developed by NCAR's + Computational and Information Systems Laboratory, UCAR, + www.cisl.ucar.edu. + + Redistribution and use of the Software in source and binary forms, + with or without modification, is permitted provided that the + following conditions are met: + + - Neither the names of NCAR's Computational and Information Systems + Laboratory, the University Corporation for Atmospheric Research, + nor the names of its sponsors or contributors may be used to + endorse or promote products derived from this Software without + specific prior written permission. + + - Redistributions of source code must retain the above copyright + notices, this list of conditions, and the disclaimer below. + + - Redistributions in binary form must reproduce the above copyright + notice, this list of conditions, and the disclaimer below in the + documentation and/or other materials provided with the + distribution. + + THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + EXPRESS OR IMPLIED, INCLUDING, BUT NOT LIMITED TO THE WARRANTIES OF + MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + NONINFRINGEMENT. IN NO EVENT SHALL THE CONTRIBUTORS OR COPYRIGHT + HOLDERS BE LIABLE FOR ANY CLAIM, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES OR OTHER LIABILITY, WHETHER IN AN + ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS WITH THE + SOFTWARE. +*/ + +/* + PFFFT : a Pretty Fast FFT. + + This is basically an adaptation of the single precision fftpack + (v4) as found on netlib taking advantage of SIMD instruction found + on cpus such as intel x86 (SSE1), powerpc (Altivec), and arm (NEON). + + For architectures where no SIMD instruction is available, the code + falls back to a scalar version. + + Restrictions: + + - 1D transforms only, with 32-bit single precision. + + - supports only transforms for inputs of length N of the form + N=(2^a)*(3^b)*(5^c), a >= 5, b >=0, c >= 0 (32, 48, 64, 96, 128, + 144, 160, etc are all acceptable lengths). Performance is best for + 128<=N<=8192. + + - all (float*) pointers in the functions below are expected to + have an "simd-compatible" alignment, that is 16 bytes on x86 and + powerpc CPUs. + + You can allocate such buffers with the functions + pffft_aligned_malloc / pffft_aligned_free (or with stuff like + posix_memalign..) + +*/ + +#ifndef PFFFT_H +#define PFFFT_H + +#include // for size_t +#include // Addition by AV. + +#ifdef __cplusplus +extern "C" { +#endif + + /* opaque struct holding internal stuff (precomputed twiddle factors) + this struct can be shared by many threads as it contains only + read-only data. + */ + typedef struct PFFFT_Setup PFFFT_Setup; + + /* direction of the transform */ + typedef enum { PFFFT_FORWARD, PFFFT_BACKWARD } pffft_direction_t; + + /* type of transform */ + typedef enum { PFFFT_REAL, PFFFT_COMPLEX } pffft_transform_t; + + /* + prepare for performing transforms of size N -- the returned + PFFFT_Setup structure is read-only so it can safely be shared by + multiple concurrent threads. + */ + PFFFT_Setup *pffft_new_setup(int N, pffft_transform_t transform); + void pffft_destroy_setup(PFFFT_Setup *); + /* + Perform a Fourier transform , The z-domain data is stored in the + most efficient order for transforming it back, or using it for + convolution. If you need to have its content sorted in the + "usual" way, that is as an array of interleaved complex numbers, + either use pffft_transform_ordered , or call pffft_zreorder after + the forward fft, and before the backward fft. + + Transforms are not scaled: PFFFT_BACKWARD(PFFFT_FORWARD(x)) = N*x. + Typically you will want to scale the backward transform by 1/N. + + The 'work' pointer should point to an area of N (2*N for complex + fft) floats, properly aligned. If 'work' is NULL, then stack will + be used instead (this is probably the best strategy for small + FFTs, say for N < 16384). + + input and output may alias. + */ + void pffft_transform(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction); + + /* + Similar to pffft_transform, but makes sure that the output is + ordered as expected (interleaved complex numbers). This is + similar to calling pffft_transform and then pffft_zreorder. + + input and output may alias. + */ + void pffft_transform_ordered(PFFFT_Setup *setup, const float *input, float *output, float *work, pffft_direction_t direction); + + /* + call pffft_zreorder(.., PFFFT_FORWARD) after pffft_transform(..., + PFFFT_FORWARD) if you want to have the frequency components in + the correct "canonical" order, as interleaved complex numbers. + + (for real transforms, both 0-frequency and half frequency + components, which are real, are assembled in the first entry as + F(0)+i*F(n/2+1). Note that the original fftpack did place + F(n/2+1) at the end of the arrays). + + input and output should not alias. + */ + void pffft_zreorder(PFFFT_Setup *setup, const float *input, float *output, pffft_direction_t direction); + + /* + Perform a multiplication of the frequency components of dft_a and + dft_b and accumulate them into dft_ab. The arrays should have + been obtained with pffft_transform(.., PFFFT_FORWARD) and should + *not* have been reordered with pffft_zreorder (otherwise just + perform the operation yourself as the dft coefs are stored as + interleaved complex numbers). + + the operation performed is: dft_ab += (dft_a * fdt_b)*scaling + + The dft_a, dft_b and dft_ab pointers may alias. + */ + void pffft_zconvolve_accumulate(PFFFT_Setup *setup, const float *dft_a, const float *dft_b, float *dft_ab, float scaling); + + /* + the float buffers must have the correct alignment (16-byte boundary + on intel and powerpc). This function may be used to obtain such + correctly aligned buffers. + */ + void *pffft_aligned_malloc(size_t nb_bytes); + void pffft_aligned_free(void *); + + /* return 4 or 1 wether support SSE/Altivec instructions was enable when building pffft.c */ + int pffft_simd_size(); + +#ifdef __cplusplus +} +#endif + +#endif // PFFFT_H diff --git a/src/third_party/r8b/r8bbase.cpp b/src/third_party/r8b/r8bbase.cpp new file mode 100644 index 0000000..7c27d1c --- /dev/null +++ b/src/third_party/r8b/r8bbase.cpp @@ -0,0 +1,39 @@ +/** + * @file r8bbase.cpp + * + * @brief C++ file that should be compiled and included into your application. + * + * This is a single library file that should be compiled and included into the + * project that uses the "r8brain-free-src" sample rate converter. This file + * defines several global static objects used by the library. + * + * You may also need to include to your project: the "Kernel32" library + * (on Windows) and the "pthread" library on Mac OS X and Linux. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#include "CDSPFIRFilter.h" +#include "CDSPFracInterpolator.h" + +namespace r8b { + +#if R8B_FLTTEST + int InterpFilterFracs = -1; + int InterpFilterFracsThird = -1; +#endif // R8B_FLTTEST + +CSyncObject CDSPRealFFTKeeper :: StateSync; +CDSPRealFFT :: CObjKeeper CDSPRealFFTKeeper :: FFTObjects[ 31 ]; + +CSyncObject CDSPFIRFilterCache :: StateSync; +CPtrKeeper< CDSPFIRFilter* > CDSPFIRFilterCache :: Objects; +int CDSPFIRFilterCache :: ObjCount = 0; + +CSyncObject CDSPFracDelayFilterBankCache :: StateSync; +CPtrKeeper< CDSPFracDelayFilterBank* > CDSPFracDelayFilterBankCache :: Objects; +CPtrKeeper< CDSPFracDelayFilterBank* > CDSPFracDelayFilterBankCache :: StaticObjects; +int CDSPFracDelayFilterBankCache :: ObjCount = 0; + +} // namespace r8b diff --git a/src/third_party/r8b/r8bbase.h b/src/third_party/r8b/r8bbase.h new file mode 100644 index 0000000..f1c0b7e --- /dev/null +++ b/src/third_party/r8b/r8bbase.h @@ -0,0 +1,1240 @@ +//$ nobt + +/** + * @file r8bbase.h + * + * @brief The "base" inclusion file with basic classes and functions. + * + * This is the "base" inclusion file for the "r8brain-free-src" sample rate + * converter. This inclusion file contains implementations of several small + * utility classes and functions used by the library. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + * + * @mainpage + * + * @section intro_sec Introduction + * + * Open source (under the MIT license) high-quality professional audio sample + * rate converter (SRC) (resampling) library. Features routines for SRC, both + * up- and downsampling, to/from any sample rate, including non-integer sample + * rates: it can be also used for conversion to/from SACD sample rate and even + * go beyond that. SRC routines were implemented in multi-platform C++ code, + * and have a high level of optimality. + * + * For more information, please visit + * https://github.com/avaneev/r8brain-free-src + * + * @section license License + * + * The MIT License (MIT) + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * + * Permission is hereby granted, free of charge, to any person obtaining a + * copy of this software and associated documentation files (the "Software"), + * to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, + * and/or sell copies of the Software, and to permit persons to whom the + * Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + * DEALINGS IN THE SOFTWARE. + * + * Please credit the creator of this library in your documentation in the + * following way: "Sample rate converter designed by Aleksey Vaneev of + * Voxengo" + * + * @version 4.6 + */ + +#ifndef R8BBASE_INCLUDED +#define R8BBASE_INCLUDED + +#include +#include +#include +#include +#include "r8bconf.h" + +#if defined( R8B_WIN ) + #include +#else // R8B_WIN + #include +#endif // R8B_WIN + +/** + * @brief The "r8brain-free-src" library namespace. + * + * The "r8brain-free-src" sample rate converter library namespace. + */ + +namespace r8b { + +/** + * Macro defines r8brain-free-src version string. + */ + +#define R8B_VERSION "4.6" + +#if !defined( M_PI ) + /** + * The macro equals to "pi" constant, fits 53-bit floating point mantissa. + */ + + #define M_PI 3.14159265358979324 +#endif // M_PI + +#if !defined( M_2PI ) + /** + * The M_2PI macro equals to "2 * pi" constant, fits 53-bit floating point + * mantissa. + */ + + #define M_2PI 6.28318530717958648 +#endif // M_2PI + +#if !defined( M_3PI ) + /** + * The M_3PI macro equals to "3 * pi" constant, fits 53-bit floating point + * mantissa. + */ + + #define M_3PI 9.42477796076937972 +#endif // M_3PI + +#if !defined( M_4PI ) + /** + * The M_4PI macro equals to "4 * pi" constant, fits 53-bit floating point + * mantissa. + */ + + #define M_4PI 12.56637061435917295 +#endif // M_4PI + +#if !defined( M_PId2 ) + /** + * The macro equals to "pi divided by 2" constant, fits 53-bit floating + * point mantissa. + */ + + #define M_PId2 1.57079632679489662 +#endif // M_PId2 + +/** + * A special macro that defines empty copy-constructor and copy operator with + * the "private:" prefix. This macro should be used in classes that cannot be + * copied in a standard C++ way. + * + * This macro does not need to be defined in classes derived from a class + * where such macro was already used. + * + * @param ClassName The name of the class which uses this macro. + */ + +#define R8BNOCTOR( ClassName ) \ + private: \ + ClassName( const ClassName& ) { } \ + ClassName& operator = ( const ClassName& ) { return( *this ); } + +/** + * @brief The default base class for objects created on heap. + * + * Class that implements "new" and "delete" operators that use standard + * malloc() and free() functions. + */ + +class CStdClassAllocator +{ +public: + /** + * @param n The size of the object, in bytes. + * @param p Pointer to object's pre-allocated memory block. + * @return Pointer to object. + */ + + void* operator new( size_t, void* p ) + { + return( p ); + } + + /** + * @param n The size of the object, in bytes. + * @return Pointer to the allocated memory block for the object. + */ + + void* operator new( size_t n ) + { + return( :: malloc( n )); + } + + /** + * @param n The size of the object, in bytes. + * @return Pointer to the allocated memory block for the object. + */ + + void* operator new[]( size_t n ) + { + return( :: malloc( n )); + } + + /** + * Operator frees a previously allocated memory block for the object. + * + * @param p Pointer to the allocated memory block for the object. + */ + + void operator delete( void* p ) + { + :: free( p ); + } + + /** + * Operator frees a previously allocated memory block for the object. + * + * @param p Pointer to the allocated memory block for the object. + */ + + void operator delete[]( void* p ) + { + :: free( p ); + } +}; + +/** + * @brief The default base class for objects that allocate blocks of memory. + * + * Memory buffer allocator that uses "stdlib" standard memory functions. + */ + +class CStdMemAllocator : public CStdClassAllocator +{ +public: + /** + * Function allocates memory block. + * + * @param Size The size of the block, in bytes. + * @result The pointer to the allocated block. + */ + + static void* allocmem( const size_t Size ) + { + return( :: malloc( Size )); + } + + /** + * Function reallocates a previously allocated memory block. + * + * @param p Pointer to the allocated block, can be NULL. + * @param Size The new size of the block, in bytes. + * @result The pointer to the (re)allocated block. + */ + + static void* reallocmem( void* p, const size_t Size ) + { + return( :: realloc( p, Size )); + } + + /** + * Function frees a previously allocated memory block. + * + * @param p Pointer to the allocated block, can be NULL. + */ + + static void freemem( void* p ) + { + :: free( p ); + } +}; + +/** + * @brief Templated memory buffer class for element buffers of fixed capacity. + * + * Fixed memory buffer object. Supports allocation of a fixed amount of + * memory. Does not store buffer's capacity - the user should know the actual + * capacity of the buffer. Does not feature "internal" storage, memory is + * always allocated via the R8B_MEMALLOCCLASS class's functions. Thus the + * object of this class can be moved in memory. + * + * This class manages memory space only - it does not perform element class + * construction nor destruction operations. + * + * This class applies 256-bit memory address alignment to the allocated data + * block. + * + * @param T The class of the stored elements (e.g. "double"). + */ + +template< class T > +class CFixedBuffer : public R8B_MEMALLOCCLASS +{ + R8BNOCTOR( CFixedBuffer ); + +public: + CFixedBuffer() + : Data0( NULL ) + , Data( NULL ) + { + } + + /** + * Constructor allocates memory so that the specified number of elements + * of type T can be stored in *this buffer object. + * + * @param Capacity Storage for this number of elements to allocate. + */ + + CFixedBuffer( const int Capacity ) + { + R8BASSERT( Capacity > 0 || Capacity == 0 ); + + Data0 = allocmem( Capacity * sizeof( T ) + Alignment ); + Data = (T*) alignptr( Data0, Alignment ); + + R8BASSERT( Data0 != NULL || Capacity == 0 ); + } + + ~CFixedBuffer() + { + freemem( Data0 ); + } + + /** + * Function allocates memory so that the specified number of elements of + * type T can be stored in *this buffer object. + * + * @param Capacity Storage for this number of elements to allocate. + */ + + void alloc( const int Capacity ) + { + R8BASSERT( Capacity > 0 || Capacity == 0 ); + + freemem( Data0 ); + Data0 = allocmem( Capacity * sizeof( T ) + Alignment ); + Data = (T*) alignptr( Data0, Alignment ); + + R8BASSERT( Data0 != NULL || Capacity == 0 ); + } + + /** + * Function reallocates memory so that the specified number of elements of + * type T can be stored in *this buffer object. Previously allocated data + * is copied to the new memory buffer. + * + * @param PrevCapacity Previous capacity of *this buffer. + * @param NewCapacity Storage for this number of elements to allocate. + */ + + void realloc( const int PrevCapacity, const int NewCapacity ) + { + R8BASSERT( PrevCapacity >= 0 ); + R8BASSERT( NewCapacity >= 0 ); + + void* const NewData0 = allocmem( NewCapacity * sizeof( T ) + + Alignment ); + + T* const NewData = (T*) alignptr( NewData0, Alignment ); + const size_t CopySize = ( PrevCapacity > NewCapacity ? + NewCapacity : PrevCapacity ) * sizeof( T ); + + if( CopySize > 0 ) + { + memcpy( NewData, Data, CopySize ); + } + + freemem( Data0 ); + Data0 = NewData0; + Data = NewData; + + R8BASSERT( Data0 != NULL || NewCapacity == 0 ); + } + + /** + * Function deallocates a previously allocated buffer. + */ + + void free() + { + freemem( Data0 ); + Data0 = NULL; + Data = NULL; + } + + /** + * @return Pointer to the first element of the allocated buffer, NULL if + * not allocated. + */ + + T* getPtr() const + { + return( Data ); + } + + /** + * @return Pointer to the first element of the allocated buffer, NULL if + * not allocated. + */ + + operator T* () const + { + return( Data ); + } + +private: + static const size_t Alignment = 32; ///< Data buffer alignment, in bytes. + ///< + void* Data0; ///< Buffer pointer, original unaligned. + ///< + T* Data; ///< Element buffer pointer, aligned. + ///< + + /** + * This macro forces provided pointer ptr to be aligned to align bytes. + * Works with power-of-2 alignments only. If no alignment is necessary, + * "align" bytes will be added to the pointer value. + * + * @tparam Tp Pointer type. + */ + + template< class Tp > + inline Tp alignptr( const Tp ptr, const uintptr_t align ) + { + return( (Tp) ( (uintptr_t) ptr + align - + ( (uintptr_t) ptr & ( align - 1 ))) ); + } +}; + +/** + * @brief Pointer-to-object "keeper" class with automatic deletion. + * + * An auxiliary class that can be used for keeping a pointer to object that + * should be deleted together with the "keeper" by calling object's "delete" + * operator. + * + * @param T Pointer type to operate with, must include the asterisk (e.g. + * "CDSPFIRFilter*"). + */ + +template< class T > +class CPtrKeeper +{ + R8BNOCTOR( CPtrKeeper ); + +public: + CPtrKeeper() + : Object( NULL ) + { + } + + /** + * Constructor assigns a pointer to object to *this keeper. + * + * @param aObject Pointer to object to keep, can be NULL. + */ + + template< class T2 > + CPtrKeeper( T2 const aObject ) + : Object( aObject ) + { + } + + ~CPtrKeeper() + { + delete Object; + } + + /** + * Function assigns a pointer to object to *this keeper. A previously + * keeped pointer will be reset and object deleted. + * + * @param aObject Pointer to object to keep, can be NULL. + */ + + template< class T2 > + void operator = ( T2 const aObject ) + { + reset(); + Object = aObject; + } + + /** + * @return Pointer to keeped object, NULL if no object is being kept. + */ + + T operator -> () const + { + return( Object ); + } + + /** + * @return Pointer to keeped object, NULL if no object is being kept. + */ + + operator T () const + { + return( Object ); + } + + /** + * Function resets the keeped pointer and deletes the keeped object. + */ + + void reset() + { + T DelObj = Object; + Object = NULL; + delete DelObj; + } + + /** + * @return Function returns the keeped pointer and resets it in *this + * keeper without object deletion. + */ + + T unkeep() + { + T ResObject = Object; + Object = NULL; + return( ResObject ); + } + +private: + T Object; ///< Pointer to keeped object. + ///< +}; + +/** + * @brief Multi-threaded synchronization object class. + * + * This class uses standard OS thread-locking (mutex) mechanism which is + * fairly efficient in most cases. + * + * The acquire() function can be called recursively, in the same thread, for + * this kind of thread-locking mechanism. This will not produce a dead-lock. + */ + +class CSyncObject +{ + R8BNOCTOR( CSyncObject ); + +public: + CSyncObject() + { + #if defined( R8B_WIN ) + InitializeCriticalSectionAndSpinCount( &CritSec, 4000 ); + #else // R8B_WIN + pthread_mutexattr_t MutexAttrs; + pthread_mutexattr_init( &MutexAttrs ); + pthread_mutexattr_settype( &MutexAttrs, PTHREAD_MUTEX_RECURSIVE ); + pthread_mutex_init( &Mutex, &MutexAttrs ); + pthread_mutexattr_destroy( &MutexAttrs ); + #endif // R8B_WIN + } + + ~CSyncObject() + { + #if defined( R8B_WIN ) + DeleteCriticalSection( &CritSec ); + #else // R8B_WIN + pthread_mutex_destroy( &Mutex ); + #endif // R8B_WIN + } + + /** + * Function "acquires" *this thread synchronizer object immediately or + * waits until another thread releases it. + */ + + void acquire() + { + #if defined( R8B_WIN ) + EnterCriticalSection( &CritSec ); + #else // R8B_WIN + pthread_mutex_lock( &Mutex ); + #endif // R8B_WIN + } + + /** + * Function "releases" *this previously acquired thread synchronizer + * object. + */ + + void release() + { + #if defined( R8B_WIN ) + LeaveCriticalSection( &CritSec ); + #else // R8B_WIN + pthread_mutex_unlock( &Mutex ); + #endif // R8B_WIN + } + +private: + #if defined( R8B_WIN ) + CRITICAL_SECTION CritSec; ///< Standard Windows critical section + ///< structure. + ///< + #else // R8B_WIN + pthread_mutex_t Mutex; ///< pthread.h mutex object. + ///< + #endif // R8B_WIN +}; + +/** + * @brief A "keeper" class for CSyncObject-based synchronization. + * + * Sync keeper class. The object of this class can be used as auto-init and + * auto-deinit object for calling the acquire() and release() functions of an + * object of the CSyncObject class. This "keeper" object is best used in + * functions as an "automatic" object allocated on the stack, possibly via the + * R8BSYNC() macro. + */ + +class CSyncKeeper +{ + R8BNOCTOR( CSyncKeeper ); + +public: + CSyncKeeper() + : SyncObj( NULL ) + { + } + + /** + * @param aSyncObj Pointer to the sync object which should be used for + * sync'ing, can be NULL. + */ + + CSyncKeeper( CSyncObject* const aSyncObj ) + : SyncObj( aSyncObj ) + { + if( SyncObj != NULL ) + { + SyncObj -> acquire(); + } + } + + /** + * @param aSyncObj Reference to the sync object which should be used for + * sync'ing. + */ + + CSyncKeeper( CSyncObject& aSyncObj ) + : SyncObj( &aSyncObj ) + { + SyncObj -> acquire(); + } + + ~CSyncKeeper() + { + if( SyncObj != NULL ) + { + SyncObj -> release(); + } + } + +protected: + CSyncObject* SyncObj; ///< Sync object in use (can be NULL). + ///< +}; + +/** + * The synchronization macro. The R8BSYNC( obj ) macro, which creates and + * object of the r8b::CSyncKeeper class on stack, should be put before + * sections of the code that may potentially change data asynchronously with + * other threads at the same time. The R8BSYNC( obj ) macro "acquires" the + * synchronization object thus blocking execution of other threads that also + * use the same R8BSYNC( obj ) macro. The blocked section begins with the + * R8BSYNC( obj ) macro and finishes at the end of the current C++ code block. + * Multiple R8BSYNC() macros may be defined from within the same code block. + * + * @param SyncObject An object of the CSyncObject type that is used for + * synchronization. + */ + +#define R8BSYNC( SyncObject ) R8BSYNC_( SyncObject, __LINE__ ) +#define R8BSYNC_( SyncObject, id ) R8BSYNC__( SyncObject, id ) +#define R8BSYNC__( SyncObject, id ) CSyncKeeper SyncKeeper##id( SyncObject ) + +/** + * @brief Sine signal generator class. + * + * Class implements sine signal generator without biasing. + */ + +class CSineGen +{ +public: + CSineGen() + { + } + + /** + * Constructor initializes *this sine signal generator. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add 0.5 * M_PI for cosine + * function. + */ + + CSineGen( const double si, const double ph ) + : svalue1( sin( ph )) + , svalue2( sin( ph - si )) + , sincr( 2.0 * cos( si )) + { + } + + /** + * Constructor initializes *this sine signal generator. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add 0.5 * M_PI for cosine + * function. + * @param g The overall gain factor, 1.0 for unity gain (-1.0 to 1.0 + * amplitude). + */ + + CSineGen( const double si, const double ph, const double g ) + : svalue1( sin( ph ) * g ) + , svalue2( sin( ph - si ) * g ) + , sincr( 2.0 * cos( si )) + { + } + + /** + * Function initializes *this sine signal generator. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add 0.5 * M_PI for cosine + * function. + */ + + void init( const double si, const double ph ) + { + svalue1 = sin( ph ); + svalue2 = sin( ph - si ); + sincr = 2.0 * cos( si ); + } + + /** + * Function initializes *this sine signal generator. + * + * @param si Sine function increment, in radians. + * @param ph Starting phase, in radians. Add 0.5 * M_PI for cosine + * function. + * @param g The overall gain factor, 1.0 for unity gain (-1.0 to 1.0 + * amplitude). + */ + + void init( const double si, const double ph, const double g ) + { + svalue1 = sin( ph ) * g; + svalue2 = sin( ph - si ) * g; + sincr = 2.0 * cos( si ); + } + + /** + * @return Next value of the sine function, without biasing. + */ + + double generate() + { + const double res = svalue1; + + svalue1 = sincr * res - svalue2; + svalue2 = res; + + return( res ); + } + +private: + double svalue1; ///< Current sine value. + ///< + double svalue2; ///< Previous sine value. + ///< + double sincr; ///< Sine value increment. + ///< +}; + +/** + * @param v Input value. + * @return Calculated bit occupancy of the specified input value. Bit + * occupancy means how many significant lower bits are necessary to store a + * specified value. Function treats the input value as unsigned. + */ + +inline int getBitOccupancy( const int v ) +{ + static const char OccupancyTable[] = + { + 1, 1, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, + 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8 + }; + + const int tt = v >> 16; + + if( tt != 0 ) + { + const int t = v >> 24; + return( t != 0 ? 24 + OccupancyTable[ t & 0xFF ] : + 16 + OccupancyTable[ tt ]); + } + else + { + const int t = v >> 8; + return( t != 0 ? 8 + OccupancyTable[ t ] : OccupancyTable[ v ]); + } +} + +/** + * Function calculates frequency response of the specified FIR filter at the + * specified circular frequency. Phase can be calculated as atan2( im, re ). + * + * @param flt FIR filter's coefficients. + * @param fltlen Number of coefficients (taps) in the filter. + * @param th Circular frequency [0; pi]. + * @param[out] re0 Resulting real part of the complex frequency response. + * @param[out] im0 Resulting imaginary part of the complex frequency response. + * @param fltlat Filter's latency in samples. + */ + +inline void calcFIRFilterResponse( const double* flt, int fltlen, + const double th, double& re0, double& im0, const int fltlat = 0 ) +{ + const double sincr = 2.0 * cos( th ); + double cvalue1; + double svalue1; + + if( fltlat == 0 ) + { + cvalue1 = 1.0; + svalue1 = 0.0; + } + else + { + cvalue1 = cos( -fltlat * th ); + svalue1 = sin( -fltlat * th ); + } + + double cvalue2 = cos( -( fltlat + 1 ) * th ); + double svalue2 = sin( -( fltlat + 1 ) * th ); + + double re = 0.0; + double im = 0.0; + + while( fltlen > 0 ) + { + re += cvalue1 * flt[ 0 ]; + im += svalue1 * flt[ 0 ]; + flt++; + fltlen--; + + double tmp = cvalue1; + cvalue1 = sincr * cvalue1 - cvalue2; + cvalue2 = tmp; + + tmp = svalue1; + svalue1 = sincr * svalue1 - svalue2; + svalue2 = tmp; + } + + re0 = re; + im0 = im; +} + +/** + * Function calculates frequency response and group delay of the specified FIR + * filter at the specified circular frequency. The group delay is calculated + * by evaluating the filter's response at close side-band frequencies of "th". + * + * @param flt FIR filter's coefficients. + * @param fltlen Number of coefficients (taps) in the filter. + * @param th Circular frequency [0; pi]. + * @param[out] re Resulting real part of the complex frequency response. + * @param[out] im Resulting imaginary part of the complex frequency response. + * @param[out] gd Resulting group delay at the specified frequency, in + * samples. + */ + +inline void calcFIRFilterResponseAndGroupDelay( const double* const flt, + const int fltlen, const double th, double& re, double& im, double& gd ) +{ + // Calculate response at "th". + + calcFIRFilterResponse( flt, fltlen, th, re, im ); + + // Calculate response at close sideband frequencies. + + const int Count = 2; + const double thd2 = 1e-9; + double ths[ Count ] = { th - thd2, th + thd2 }; + + if( ths[ 0 ] < 0.0 ) + { + ths[ 0 ] = 0.0; + } + + if( ths[ 1 ] > M_PI ) + { + ths[ 1 ] = M_PI; + } + + double ph1[ Count ]; + int i; + + for( i = 0; i < Count; i++ ) + { + double re1; + double im1; + + calcFIRFilterResponse( flt, fltlen, ths[ i ], re1, im1 ); + ph1[ i ] = atan2( im1, re1 ); + } + + if( fabs( ph1[ 1 ] - ph1[ 0 ]) > M_PI ) + { + if( ph1[ 1 ] > ph1[ 0 ]) + { + ph1[ 1 ] -= M_2PI; + } + else + { + ph1[ 1 ] += M_2PI; + } + } + + const double thd = ths[ 1 ] - ths[ 0 ]; + gd = ( ph1[ 1 ] - ph1[ 0 ]) / thd; +} + +/** + * Function normalizes FIR filter so that its frequency response at DC is + * equal to DCGain. + * + * @param[in,out] p Filter coefficients. + * @param l Filter length. + * @param DCGain Filter's gain at DC (linear, non-decibel value). + * @param pstep "p" array step. + */ + +inline void normalizeFIRFilter( double* const p, const int l, + const double DCGain, const int pstep = 1 ) +{ + R8BASSERT( l > 0 ); + R8BASSERT( pstep != 0 ); + + double s = 0.0; + double* pp = p; + int i = l; + + while( i > 0 ) + { + s += *pp; + pp += pstep; + i--; + } + + s = DCGain / s; + pp = p; + i = l; + + while( i > 0 ) + { + *pp *= s; + pp += pstep; + i--; + } +} + +/** + * Function calculates coefficients used to calculate 3rd order spline + * (polynomial) on the equidistant lattice, using 8 points. + * + * @param[out] c Output coefficients buffer, length = 4. + * @param xm3 Point at x-3 position. + * @param xm2 Point at x-2 position. + * @param xm1 Point at x-1 position. + * @param x0 Point at x position. + * @param x1 Point at x+1 position. + * @param x2 Point at x+2 position. + * @param x3 Point at x+3 position. + * @param x4 Point at x+4 position. + */ + +inline void calcSpline3p8Coeffs( double* c, const double xm3, + const double xm2, const double xm1, const double x0, const double x1, + const double x2, const double x3, const double x4 ) +{ + c[ 0 ] = x0; + c[ 1 ] = ( 61.0 * ( x1 - xm1 ) + 16.0 * ( xm2 - x2 ) + + 3.0 * ( x3 - xm3 )) / 76.0; + + c[ 2 ] = ( 106.0 * ( xm1 + x1 ) + 10.0 * x3 + 6.0 * xm3 - 3.0 * x4 - + 29.0 * ( xm2 + x2 ) - 167.0 * x0 ) / 76.0; + + c[ 3 ] = ( 91.0 * ( x0 - x1 ) + 45.0 * ( x2 - xm1 ) + + 13.0 * ( xm2 - x3 ) + 3.0 * ( x4 - xm3 )) / 76.0; +} + +/** + * Function calculates coefficients used to calculate 2rd order spline + * (polynomial) on the equidistant lattice, using 8 points. This function is + * based on the calcSpline3Coeffs8() function, but without the 3rd order + * coefficient. + * + * @param[out] c Output coefficients buffer, length = 3. + * @param xm3 Point at x-3 position. + * @param xm2 Point at x-2 position. + * @param xm1 Point at x-1 position. + * @param x0 Point at x position. + * @param x1 Point at x+1 position. + * @param x2 Point at x+2 position. + * @param x3 Point at x+3 position. + * @param x4 Point at x+4 position. + */ + +inline void calcSpline2p8Coeffs( double* c, const double xm3, + const double xm2, const double xm1, const double x0, const double x1, + const double x2, const double x3, const double x4 ) +{ + c[ 0 ] = x0; + c[ 1 ] = ( 61.0 * ( x1 - xm1 ) + 16.0 * ( xm2 - x2 ) + + 3.0 * ( x3 - xm3 )) / 76.0; + + c[ 2 ] = ( 106.0 * ( xm1 + x1 ) + 10.0 * x3 + 6.0 * xm3 - 3.0 * x4 - + 29.0 * ( xm2 + x2 ) - 167.0 * x0 ) / 76.0; +} + +/** + * Function calculates coefficients used to calculate 3rd order segment + * interpolation polynomial on the equidistant lattice, using 4 points. + * + * @param[out] c Output coefficients buffer, length = 4. + * @param[in] y Equidistant point values. Value at offset 1 corresponds to + * x=0 point. + */ + +inline void calcInterpCoeffs3p4( double* const c, const double* const y ) +{ + c[ 0 ] = y[ 1 ]; + c[ 1 ] = 0.5 * ( y[ 2 ] - y[ 0 ]); + c[ 2 ] = y[ 0 ] - 2.5 * y[ 1 ] + y[ 2 ] + y[ 2 ] - 0.5 * y[ 3 ]; + c[ 3 ] = 0.5 * ( y[ 3 ] - y[ 0 ] ) + 1.5 * ( y[ 1 ] - y[ 2 ]); +} + +/** + * Function calculates coefficients used to calculate 3rd order segment + * interpolation polynomial on the equidistant lattice, using 6 points. + * + * @param[out] c Output coefficients buffer, length = 4. + * @param[in] y Equidistant point values. Value at offset 2 corresponds to + * x=0 point. + */ + +inline void calcInterpCoeffs3p6( double* const c, const double* const y ) +{ + c[ 0 ] = y[ 2 ]; + c[ 1 ] = ( 11.0 * ( y[ 3 ] - y[ 1 ]) + 2.0 * ( y[ 0 ] - y[ 4 ])) / 14.0; + c[ 2 ] = ( 20.0 * ( y[ 1 ] + y[ 3 ]) + 2.0 * y[ 5 ] - 4.0 * y[ 0 ] - + 7.0 * y[ 4 ] - 31.0 * y[ 2 ]) / 14.0; + + c[ 3 ] = ( 17.0 * ( y[ 2 ] - y[ 3 ]) + 9.0 * ( y[ 4 ] - y[ 1 ]) + + 2.0 * ( y[ 0 ] - y[ 5 ])) / 14.0; +} + +/** + * Function calculates coefficients used to calculate 3rd order segment + * interpolation polynomial on the equidistant lattice, using 8 points. + * + * @param[out] c Output coefficients buffer, length = 4. + * @param[in] y Equidistant point values. Value at offset 3 corresponds to + * x=0 point. + */ + +inline void calcInterpCoeffs3p8( double* const c, const double* const y ) +{ + c[ 0 ] = y[ 3 ]; + c[ 1 ] = ( 61.0 * ( y[ 4 ] - y[ 2 ]) + 16.0 * ( y[ 1 ] - y[ 5 ]) + + 3.0 * ( y[ 6 ] - y[ 0 ])) / 76.0; + + c[ 2 ] = ( 106.0 * ( y[ 2 ] + y[ 4 ]) + 10.0 * y[ 6 ] + 6.0 * y[ 0 ] - + 3.0 * y[ 7 ] - 29.0 * ( y[ 1 ] + y[ 5 ]) - 167.0 * y[ 3 ]) / 76.0; + + c[ 3 ] = ( 91.0 * ( y[ 3 ] - y[ 4 ]) + 45.0 * ( y[ 5 ] - y[ 2 ]) + + 13.0 * ( y[ 1 ] - y[ 6 ]) + 3.0 * ( y[ 7 ] - y[ 0 ])) / 76.0; +} + +/** + * Function calculates coefficients used to calculate 3rd order segment + * interpolation polynomial on the equidistant lattice, using 8 points. + * + * @param[out] c Output coefficients buffer, length = 3. + * @param[in] y Equidistant point values. Value at offset 3 corresponds to + * x=0 point. + */ + +inline void calcInterpCoeffs2p8( double* const c, const double* const y ) +{ + c[ 0 ] = y[ 3 ]; + c[ 1 ] = ( 61.0 * ( y[ 4 ] - y[ 2 ]) + 16.0 * ( y[ 1 ] - y[ 5 ]) + + 3.0 * ( y[ 6 ] - y[ 0 ])) / 76.0; + + c[ 2 ] = ( 106.0 * ( y[ 2 ] + y[ 4 ]) + 10.0 * y[ 6 ] + 6.0 * y[ 0 ] - + 3.0 * y[ 7 ] - 29.0 * ( y[ 1 ] + y[ 5 ]) - 167.0 * y[ 3 ]) / 76.0; +} + +#if !defined( min ) + +/** + * @param v1 Value 1. + * @param v2 Value 2. + * @return The minimum of 2 values. + */ + +template< class T > +inline T min( const T& v1, const T& v2 ) +{ + return( v1 < v2 ? v1 : v2 ); +} + +#endif // min + +#if !defined( max ) + +/** + * @param v1 Value 1. + * @param v2 Value 2. + * @return The maximum of 2 values. + */ + +template< class T > +inline T max( const T& v1, const T& v2 ) +{ + return( v1 > v2 ? v1 : v2 ); +} + +#endif // max + +/** + * Function "clamps" (clips) the specified value so that it is not lesser than + * "minv", and not greater than "maxv". + * + * @param Value Value to clamp. + * @param minv Minimal allowed value. + * @param maxv Maximal allowed value. + * @return "Clamped" value. + */ + +inline double clampr( const double Value, const double minv, + const double maxv ) +{ + if( Value < minv ) + { + return( minv ); + } + else + if( Value > maxv ) + { + return( maxv ); + } + else + { + return( Value ); + } +} + +/** + * @param x Value to square. + * @return Squared value of the argument. + */ + +inline double sqr( const double x ) +{ + return( x * x ); +} + +/** + * @param v Input value. + * @param p Power factor. + * @return Returns pow() function's value with input value's sign check. + */ + +inline double pows( const double v, const double p ) +{ + return( v < 0.0 ? -pow( -v, p ) : pow( v, p )); +} + +/** + * @param v Input value. + * @return Calculated single-argument Gaussian function of the input value. + */ + +inline double gauss( const double v ) +{ + return( exp( -( v * v ))); +} + +/** + * @param v Input value. + * @return Calculated inverse hyperbolic sine of the input value. + */ + +inline double asinh( const double v ) +{ + return( log( v + sqrt( v * v + 1.0 ))); +} + +/** + * @param x Input value. + * @return Calculated zero-th order modified Bessel function of the first kind + * of the input value. Approximate value. + */ + +inline double besselI0( const double x ) +{ + const double ax = fabs( x ); + double y; + + if( ax < 3.75 ) + { + y = x / 3.75; + y *= y; + + return( 1.0 + y * ( 3.5156229 + y * ( 3.0899424 + y * ( 1.2067492 + + y * ( 0.2659732 + y * ( 0.360768e-1 + y * 0.45813e-2 )))))); + } + + y = 3.75 / ax; + + return( exp( ax ) / sqrt( ax ) * ( 0.39894228 + y * ( 0.1328592e-1 + + y * ( 0.225319e-2 + y * ( -0.157565e-2 + y * ( 0.916281e-2 + + y * ( -0.2057706e-1 + y * ( 0.2635537e-1 + y * ( -0.1647633e-1 + + y * 0.392377e-2 ))))))))); +} + +} // namespace r8b + +#endif // R8BBASE_INCLUDED diff --git a/src/third_party/r8b/r8bconf.h b/src/third_party/r8b/r8bconf.h new file mode 100644 index 0000000..1018805 --- /dev/null +++ b/src/third_party/r8b/r8bconf.h @@ -0,0 +1,198 @@ +//$ nobt +//$ nocpp + +/** + * @file r8bconf.h + * + * @brief The "configuration" inclusion file you can modify. + * + * This is the "configuration" inclusion file for the "r8brain-free-src" + * sample rate converter. You may redefine the macros here as you see fit. + * + * r8brain-free-src Copyright (c) 2013-2019 Aleksey Vaneev + * See the "License.txt" file for license. + */ + +#ifndef R8BCONF_INCLUDED +#define R8BCONF_INCLUDED + +#define R8B_PFFFT 1 +#define R8B_FASTTIMING 1 +#define R8B_EXTFFT 1 + +#if defined( _WIN32 ) || defined( _WIN64 ) + #define R8B_WIN 1 +#elif defined( __APPLE__ ) + #define R8B_MAC 1 +#else // defined( __APPLE__ ) + #define R8B_LNX 1 // Assume Linux (Unix) platform by default. +#endif // defined( __APPLE__ ) + +#if !defined( R8B_FLTLEN ) + /** + * This macro defines the default fractional delay filter length. Macro is + * used by the r8b::CDSPResampler class. + */ + + #define R8B_FLTLEN 28 +#endif // !defined( R8B_FLTLEN ) + +#if !defined( R8B_FLTFRACS ) + /** + * This macro defines the default number of fractional delay filters that + * are sampled by the filter bank. Macro is used by the r8b::CDSPResampler + * class. In order to get consistent results when resampling to/from + * different sample rates, it is suggested to set this macro to a suitable + * prime number. + */ + + #define R8B_FLTFRACS 1733 +#endif // !defined( R8B_FLTFRACS ) + +#if !defined( R8B_IPP ) + /** + * Set the R8B_IPP macro definition to 1 to enable the use of Intel IPP's + * fast Fourier transform functions. Also uncomment and correct the IPP + * header inclusion macros. + * + * Do not forget to call the ippInit() function at the start of the + * application, before using this library's functions. + */ + + #define R8B_IPP 0 + +// #include +// #include +#endif // !defined( R8B_IPP ) + +#if !defined( R8BASSERT ) + /** + * Assertion macro used to check for certain run-time conditions. By + * default no action is taken if assertion fails. + * + * @param e Expression to check. + */ + + #define R8BASSERT( e ) +#endif // !defined( R8BASSERT ) + +#if !defined( R8BCONSOLE ) + /** + * Console output macro, used to output various resampler status strings, + * including filter design parameters, convolver parameters. + * + * @param e Expression to send to the console, usually consists of a + * standard "printf" format string followed by several parameters + * (__VA_ARGS__). + */ + + #define R8BCONSOLE( ... ) +#endif // !defined( R8BCONSOLE ) + +#if !defined( R8B_BASECLASS ) + /** + * Macro defines the name of the class from which all classes that are + * designed to be created on heap are derived. The default + * r8b::CStdClassAllocator class uses "stdlib" memory allocation + * functions. + * + * The classes that are best placed on stack or as class members are not + * derived from any class. + */ + + #define R8B_BASECLASS :: r8b :: CStdClassAllocator +#endif // !defined( R8B_BASECLASS ) + +#if !defined( R8B_MEMALLOCCLASS ) + /** + * Macro defines the name of the class that implements raw memory + * allocation functions, see the r8b::CStdMemAllocator class for details. + */ + + #define R8B_MEMALLOCCLASS :: r8b :: CStdMemAllocator +#endif // !defined( R8B_MEMALLOCCLASS ) + +#if !defined( R8B_FILTER_CACHE_MAX ) + /** + * This macro specifies the number of filters kept in the cache at most. + * The actual number can be higher if many different filters are in use at + * the same time. + */ + + #define R8B_FILTER_CACHE_MAX 96 +#endif // !defined( R8B_FILTER_CACHE_MAX ) + +#if !defined( R8B_FRACBANK_CACHE_MAX ) + /** + * This macro specifies the number of whole-number stepping fractional + * delay filter banks kept in the cache at most. The actual number can be + * higher if many different filter banks are in use at the same time. As + * filter banks are usually big objects, it is advisable to keep this + * cache size small. + */ + + #define R8B_FRACBANK_CACHE_MAX 12 +#endif // !defined( R8B_FRACBANK_CACHE_MAX ) + +#if !defined( R8B_FLTTEST ) + /** + * This macro, when equal to 1, enables fractional delay filter bank + * testing: in this mode the filter bank becomes dynamic member of the + * CDSPFracInterpolator object instead of being a global static object. + */ + + #define R8B_FLTTEST 0 +#endif // !defined( R8B_FLTTEST ) + +#if !defined( R8B_FASTTIMING ) + /** + * This macro, when equal to 1, enables fast approach to interpolation + * sample timing. This approach improves interpolation performance + * (by around 15%) at the expense of a minor sample timing drift which is + * on the order of 1e-6 samples per 10 billion output samples. This + * setting does not apply to whole-number stepping if it is in use as this + * stepping provides zero timing error without performance impact. Also + * does not apply to the cases when whole-numbered resampling is in actual + * use. + */ + + #define R8B_FASTTIMING 0 +#endif // !defined( R8B_FASTTIMING ) + +#if !defined( R8B_EXTFFT ) + /** + * This macro, when equal to 1, extends length of low-pass filters' FFT + * block by a factor of 2 by zero-padding them. This usually improves the + * overall time performance of the resampler at the expense of higher + * overall latency (initial processing delay). If such delay is not an + * issue, setting this macro to 1 is preferrable. This macro can only have + * a value of 0 or 1. + */ + + #define R8B_EXTFFT 0 +#endif // !defined( R8B_EXTFFT ) + +#if !defined( R8B_PFFFT ) + /** + * When defined as 1, enables PFFFT routines which are fast, but limited + * to 24-bit precision. + */ + + #define R8B_PFFFT 0 +#endif // !defined( R8B_PFFFT ) + +#if R8B_PFFFT + #include "pffft.h" + #define R8B_FLOATFFT 1 +#endif // R8B_PFFFT + +#if !defined( R8B_FLOATFFT ) + /** + * The R8B_FLOATFFT definition enables double-to-float buffer conversion + * for FFT operations for algorithms that work with "float" values. + */ + + #define R8B_FLOATFFT 0 +#endif // !defined( R8B_FLOATFFT ) + +#endif // R8BCONF_INCLUDED diff --git a/src/third_party/stb/stb_image.h b/src/third_party/stb/stb_image.h new file mode 100644 index 0000000..2bfbd0b --- /dev/null +++ b/src/third_party/stb/stb_image.h @@ -0,0 +1,4673 @@ +/* stbi-1.33 - public domain JPEG/PNG reader - http://nothings.org/stb_image.c + when you control the images you're loading + no warranty implied; use at your own risk + + QUICK NOTES: + Primarily of interest to game developers and other people who can + avoid problematic images and only need the trivial interface + + JPEG baseline (no JPEG progressive) + PNG 8-bit-per-channel only + + TGA (not sure what subset, if a subset) + BMP non-1bpp, non-RLE + PSD (composited view only, no extra channels) + + GIF (*comp always reports as 4-channel) + HDR (radiance rgbE format) + PIC (Softimage PIC) + + - decode from memory or through FILE (define STBI_NO_STDIO to remove code) + - decode from arbitrary I/O callbacks + - overridable dequantizing-IDCT, YCbCr-to-RGB conversion (define STBI_SIMD) + + Latest revisions: + 1.33 (2011-07-14) minor fixes suggested by Dave Moore + 1.32 (2011-07-13) info support for all filetypes (SpartanJ) + 1.31 (2011-06-19) a few more leak fixes, bug in PNG handling (SpartanJ) + 1.30 (2011-06-11) added ability to load files via io callbacks (Ben Wenger) + 1.29 (2010-08-16) various warning fixes from Aurelien Pocheville + 1.28 (2010-08-01) fix bug in GIF palette transparency (SpartanJ) + 1.27 (2010-08-01) cast-to-uint8 to fix warnings (Laurent Gomila) + allow trailing 0s at end of image data (Laurent Gomila) + 1.26 (2010-07-24) fix bug in file buffering for PNG reported by SpartanJ + + See end of file for full revision history. + + TODO: + stbi_info support for BMP,PSD,HDR,PIC + + + ============================ Contributors ========================= + + Image formats Optimizations & bugfixes + Sean Barrett (jpeg, png, bmp) Fabian "ryg" Giesen + Nicolas Schulz (hdr, psd) + Jonathan Dummer (tga) Bug fixes & warning fixes + Jean-Marc Lienher (gif) Marc LeBlanc + Tom Seddon (pic) Christpher Lloyd + Thatcher Ulrich (psd) Dave Moore + Won Chun + the Horde3D community + Extensions, features Janez Zemva + Jetro Lauha (stbi_info) Jonathan Blow + James "moose2000" Brown (iPhone PNG) Laurent Gomila + Ben "Disch" Wenger (io callbacks) Aruelien Pocheville + Martin "SpartanJ" Golini Ryamond Barbiero + David Woo + + + If your name should be here but isn't, let Sean know. + +*/ + +#ifndef STBI_INCLUDE_STB_IMAGE_H +#define STBI_INCLUDE_STB_IMAGE_H + +// To get a header file for this, either cut and paste the header, +// or create stb_image.h, #define STBI_HEADER_FILE_ONLY, and +// then include stb_image.c from it. + +//// begin header file //////////////////////////////////////////////////// +// +// Limitations: +// - no jpeg progressive support +// - non-HDR formats support 8-bit samples only (jpeg, png) +// - no delayed line count (jpeg) -- IJG doesn't support either +// - no 1-bit BMP +// - GIF always returns *comp=4 +// +// Basic usage (see HDR discussion below): +// int x,y,n; +// unsigned char *data = stbi_load(filename, &x, &y, &n, 0); +// // ... process data if not NULL ... +// // ... x = width, y = height, n = # 8-bit components per pixel ... +// // ... replace '0' with '1'..'4' to force that many components per pixel +// // ... but 'n' will always be the number that it would have been if you said 0 +// stbi_image_free(data) +// +// Standard parameters: +// int *x -- outputs image width in pixels +// int *y -- outputs image height in pixels +// int *comp -- outputs # of image components in image file +// int req_comp -- if non-zero, # of image components requested in result +// +// The return value from an image loader is an 'unsigned char *' which points +// to the pixel data. The pixel data consists of *y scanlines of *x pixels, +// with each pixel consisting of N interleaved 8-bit components; the first +// pixel pointed to is top-left-most in the image. There is no padding between +// image scanlines or between pixels, regardless of format. The number of +// components N is 'req_comp' if req_comp is non-zero, or *comp otherwise. +// If req_comp is non-zero, *comp has the number of components that _would_ +// have been output otherwise. E.g. if you set req_comp to 4, you will always +// get RGBA output, but you can check *comp to easily see if it's opaque. +// +// An output image with N components has the following components interleaved +// in this order in each pixel: +// +// N=#comp components +// 1 grey +// 2 grey, alpha +// 3 red, green, blue +// 4 red, green, blue, alpha +// +// If image loading fails for any reason, the return value will be NULL, +// and *x, *y, *comp will be unchanged. The function stbi_failure_reason() +// can be queried for an extremely brief, end-user unfriendly explanation +// of why the load failed. Define STBI_NO_FAILURE_STRINGS to avoid +// compiling these strings at all, and STBI_FAILURE_USERMSG to get slightly +// more user-friendly ones. +// +// Paletted PNG, BMP, GIF, and PIC images are automatically depalettized. +// +// =========================================================================== +// +// iPhone PNG support: +// +// By default we convert iphone-formatted PNGs back to RGB; nominally they +// would silently load as BGR, except the existing code should have just +// failed on such iPhone PNGs. But you can disable this conversion by +// by calling stbi_convert_iphone_png_to_rgb(0), in which case +// you will always just get the native iphone "format" through. +// +// Call stbi_set_unpremultiply_on_load(1) as well to force a divide per +// pixel to remove any premultiplied alpha *only* if the image file explicitly +// says there's premultiplied data (currently only happens in iPhone images, +// and only if iPhone convert-to-rgb processing is on). +// +// =========================================================================== +// +// HDR image support (disable by defining STBI_NO_HDR) +// +// stb_image now supports loading HDR images in general, and currently +// the Radiance .HDR file format, although the support is provided +// generically. You can still load any file through the existing interface; +// if you attempt to load an HDR file, it will be automatically remapped to +// LDR, assuming gamma 2.2 and an arbitrary scale factor defaulting to 1; +// both of these constants can be reconfigured through this interface: +// +// stbi_hdr_to_ldr_gamma(2.2f); +// stbi_hdr_to_ldr_scale(1.0f); +// +// (note, do not use _inverse_ constants; stbi_image will invert them +// appropriately). +// +// Additionally, there is a new, parallel interface for loading files as +// (linear) floats to preserve the full dynamic range: +// +// float *data = stbi_loadf(filename, &x, &y, &n, 0); +// +// If you load LDR images through this interface, those images will +// be promoted to floating point values, run through the inverse of +// constants corresponding to the above: +// +// stbi_ldr_to_hdr_scale(1.0f); +// stbi_ldr_to_hdr_gamma(2.2f); +// +// Finally, given a filename (or an open file or memory block--see header +// file for details) containing image data, you can query for the "most +// appropriate" interface to use (that is, whether the image is HDR or +// not), using: +// +// stbi_is_hdr(char *filename); +// +// =========================================================================== +// +// I/O callbacks +// +// I/O callbacks allow you to read from arbitrary sources, like packaged +// files or some other source. Data read from callbacks are processed +// through a small internal buffer (currently 128 bytes) to try to reduce +// overhead. +// +// The three functions you must define are "read" (reads some bytes of data), +// "skip" (skips some bytes of data), "eof" (reports if the stream is at the end). + + +#ifndef STBI_NO_STDIO + +#if defined(_MSC_VER) && _MSC_VER >= 0x1400 +#define _CRT_SECURE_NO_WARNINGS // suppress bogus warnings about fopen() +#endif + +#include +#endif + +#define STBI_VERSION 1 + +enum +{ + STBI_default = 0, // only used for req_comp + + STBI_grey = 1, + STBI_grey_alpha = 2, + STBI_rgb = 3, + STBI_rgb_alpha = 4 +}; + +typedef unsigned char stbi_uc; + +#ifdef __cplusplus +extern "C" { +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// PRIMARY API - works on images of any type +// + +// +// load image by filename, open file, or memory buffer +// + +extern stbi_uc *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp); + +#ifndef STBI_NO_STDIO +extern stbi_uc *stbi_load (char const *filename, int *x, int *y, int *comp, int req_comp); +extern stbi_uc *stbi_load_from_file (FILE *f, int *x, int *y, int *comp, int req_comp); +// for stbi_load_from_file, file pointer is left pointing immediately after image +#endif + +typedef struct +{ + int (*read) (void *user,char *data,int size); // fill 'data' with 'size' bytes. return number of bytes actually read + void (*skip) (void *user,unsigned n); // skip the next 'n' bytes + int (*eof) (void *user); // returns nonzero if we are at end of file/data +} stbi_io_callbacks; + +extern stbi_uc *stbi_load_from_callbacks (stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp); + +#ifndef STBI_NO_HDR + extern float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp); + + #ifndef STBI_NO_STDIO + extern float *stbi_loadf (char const *filename, int *x, int *y, int *comp, int req_comp); + extern float *stbi_loadf_from_file (FILE *f, int *x, int *y, int *comp, int req_comp); + #endif + + extern float *stbi_loadf_from_callbacks (stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp); + + extern void stbi_hdr_to_ldr_gamma(float gamma); + extern void stbi_hdr_to_ldr_scale(float scale); + + extern void stbi_ldr_to_hdr_gamma(float gamma); + extern void stbi_ldr_to_hdr_scale(float scale); +#endif // STBI_NO_HDR + +// stbi_is_hdr is always defined +extern int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user); +extern int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len); +#ifndef STBI_NO_STDIO +extern int stbi_is_hdr (char const *filename); +extern int stbi_is_hdr_from_file(FILE *f); +#endif // STBI_NO_STDIO + + +// get a VERY brief reason for failure +// NOT THREADSAFE +extern const char *stbi_failure_reason (void); + +// free the loaded image -- this is just free() +extern void stbi_image_free (void *retval_from_stbi_load); + +// get image dimensions & components without fully decoding +extern int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp); +extern int stbi_info_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp); + +#ifndef STBI_NO_STDIO +extern int stbi_info (char const *filename, int *x, int *y, int *comp); +extern int stbi_info_from_file (FILE *f, int *x, int *y, int *comp); + +#endif + + + +// for image formats that explicitly notate that they have premultiplied alpha, +// we just return the colors as stored in the file. set this flag to force +// unpremultiplication. results are undefined if the unpremultiply overflow. +extern void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply); + +// indicate whether we should process iphone images back to canonical format, +// or just pass them through "as-is" +extern void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert); + + +// ZLIB client - used by PNG, available for other purposes + +extern char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen); +extern char *stbi_zlib_decode_malloc(const char *buffer, int len, int *outlen); +extern int stbi_zlib_decode_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + +extern char *stbi_zlib_decode_noheader_malloc(const char *buffer, int len, int *outlen); +extern int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen); + + +// define faster low-level operations (typically SIMD support) +#ifdef STBI_SIMD +typedef void (*stbi_idct_8x8)(stbi_uc *out, int out_stride, short data[64], unsigned short *dequantize); +// compute an integer IDCT on "input" +// input[x] = data[x] * dequantize[x] +// write results to 'out': 64 samples, each run of 8 spaced by 'out_stride' +// CLAMP results to 0..255 +typedef void (*stbi_YCbCr_to_RGB_run)(stbi_uc *output, stbi_uc const *y, stbi_uc const *cb, stbi_uc const *cr, int count, int step); +// compute a conversion from YCbCr to RGB +// 'count' pixels +// write pixels to 'output'; each pixel is 'step' bytes (either 3 or 4; if 4, write '255' as 4th), order R,G,B +// y: Y input channel +// cb: Cb input channel; scale/biased to be 0..255 +// cr: Cr input channel; scale/biased to be 0..255 + +extern void stbi_install_idct(stbi_idct_8x8 func); +extern void stbi_install_YCbCr_to_RGB(stbi_YCbCr_to_RGB_run func); +#endif // STBI_SIMD + + +#ifdef __cplusplus +} +#endif + +// +// +//// end header file ///////////////////////////////////////////////////// +#endif // STBI_INCLUDE_STB_IMAGE_H + +#ifndef STBI_HEADER_FILE_ONLY + +#ifndef STBI_NO_HDR +#include // ldexp +#include // strcmp, strtok +#endif + +#ifndef STBI_NO_STDIO +#include +#endif +#include +#include +#include +#include + +#ifndef _MSC_VER + #ifdef __cplusplus + #define stbi_inline inline + #else + #define stbi_inline + #endif +#else + #define stbi_inline __forceinline +#endif + + +// implementation: +typedef unsigned char uint8; +typedef unsigned short uint16; +typedef signed short int16; +typedef unsigned int uint32; +typedef signed int int32; +typedef unsigned int uint; + +// should produce compiler error if size is wrong +typedef unsigned char validate_uint32[sizeof(uint32)==4 ? 1 : -1]; + +#if defined(STBI_NO_STDIO) && !defined(STBI_NO_WRITE) +#define STBI_NO_WRITE +#endif + +#define STBI_NOTUSED(v) (void)sizeof(v) + +#ifdef _MSC_VER +#define STBI_HAS_LROTL +#endif + +#ifdef STBI_HAS_LROTL + #define stbi_lrot(x,y) _lrotl(x,y) +#else + #define stbi_lrot(x,y) (((x) << (y)) | ((x) >> (32 - (y)))) +#endif + +/////////////////////////////////////////////// +// +// stbi struct and start_xxx functions + +// stbi structure is our basic context used by all images, so it +// contains all the IO context, plus some basic image information +typedef struct +{ + uint32 img_x, img_y; + int img_n, img_out_n; + + stbi_io_callbacks io; + void *io_user_data; + + int read_from_callbacks; + int buflen; + uint8 buffer_start[128]; + + uint8 *img_buffer, *img_buffer_end; + uint8 *img_buffer_original; +} stbi; + + +static void refill_buffer(stbi *s); + +// initialize a memory-decode context +static void start_mem(stbi *s, uint8 const *buffer, int len) +{ + s->io.read = NULL; + s->read_from_callbacks = 0; + s->img_buffer = s->img_buffer_original = (uint8 *) buffer; + s->img_buffer_end = (uint8 *) buffer+len; +} + +// initialize a callback-based context +static void start_callbacks(stbi *s, stbi_io_callbacks *c, void *user) +{ + s->io = *c; + s->io_user_data = user; + s->buflen = sizeof(s->buffer_start); + s->read_from_callbacks = 1; + s->img_buffer_original = s->buffer_start; + refill_buffer(s); +} + +#ifndef STBI_NO_STDIO + +static int stdio_read(void *user, char *data, int size) +{ + return (int) fread(data,1,size,(FILE*) user); +} + +static void stdio_skip(void *user, unsigned n) +{ + fseek((FILE*) user, n, SEEK_CUR); +} + +static int stdio_eof(void *user) +{ + return feof((FILE*) user); +} + +static stbi_io_callbacks stbi_stdio_callbacks = +{ + stdio_read, + stdio_skip, + stdio_eof, +}; + +static void start_file(stbi *s, FILE *f) +{ + start_callbacks(s, &stbi_stdio_callbacks, (void *) f); +} + +//static void stop_file(stbi *s) { } + +#endif // !STBI_NO_STDIO + +static void stbi_rewind(stbi *s) +{ + // conceptually rewind SHOULD rewind to the beginning of the stream, + // but we just rewind to the beginning of the initial buffer, because + // we only use it after doing 'test', which only ever looks at at most 92 bytes + s->img_buffer = s->img_buffer_original; +} + +static int stbi_jpeg_test(stbi *s); +static stbi_uc *stbi_jpeg_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_jpeg_info(stbi *s, int *x, int *y, int *comp); +static int stbi_png_test(stbi *s); +static stbi_uc *stbi_png_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_png_info(stbi *s, int *x, int *y, int *comp); +static int stbi_bmp_test(stbi *s); +static stbi_uc *stbi_bmp_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_tga_test(stbi *s); +static stbi_uc *stbi_tga_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_tga_info(stbi *s, int *x, int *y, int *comp); +static int stbi_psd_test(stbi *s); +static stbi_uc *stbi_psd_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_hdr_test(stbi *s); +static float *stbi_hdr_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_pic_test(stbi *s); +static stbi_uc *stbi_pic_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_gif_test(stbi *s); +static stbi_uc *stbi_gif_load(stbi *s, int *x, int *y, int *comp, int req_comp); +static int stbi_gif_info(stbi *s, int *x, int *y, int *comp); + + +// this is not threadsafe +static const char *failure_reason; + +const char *stbi_failure_reason(void) +{ + return failure_reason; +} + +static int e(const char *str) +{ + failure_reason = str; + return 0; +} + +// e - error +// epf - error returning pointer to float +// epuc - error returning pointer to unsigned char + +#ifdef STBI_NO_FAILURE_STRINGS + #define e(x,y) 0 +#elif defined(STBI_FAILURE_USERMSG) + #define e(x,y) e(y) +#else + #define e(x,y) e(x) +#endif + +#define epf(x,y) ((float *) (e(x,y)?NULL:NULL)) +#define epuc(x,y) ((unsigned char *) (e(x,y)?NULL:NULL)) + +void stbi_image_free(void *retval_from_stbi_load) +{ + free(retval_from_stbi_load); +} + +#ifndef STBI_NO_HDR +static float *ldr_to_hdr(stbi_uc *data, int x, int y, int comp); +static stbi_uc *hdr_to_ldr(float *data, int x, int y, int comp); +#endif + +static unsigned char *stbi_load_main(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + if (stbi_jpeg_test(s)) return stbi_jpeg_load(s,x,y,comp,req_comp); + if (stbi_png_test(s)) return stbi_png_load(s,x,y,comp,req_comp); + if (stbi_bmp_test(s)) return stbi_bmp_load(s,x,y,comp,req_comp); + if (stbi_gif_test(s)) return stbi_gif_load(s,x,y,comp,req_comp); + if (stbi_psd_test(s)) return stbi_psd_load(s,x,y,comp,req_comp); + if (stbi_pic_test(s)) return stbi_pic_load(s,x,y,comp,req_comp); + + #ifndef STBI_NO_HDR + if (stbi_hdr_test(s)) { + float *hdr = stbi_hdr_load(s, x,y,comp,req_comp); + return hdr_to_ldr(hdr, *x, *y, req_comp ? req_comp : *comp); + } + #endif + + // test tga last because it's a crappy test! + if (stbi_tga_test(s)) + return stbi_tga_load(s,x,y,comp,req_comp); + return epuc("unknown image type", "Image not of any known type, or corrupt"); +} + +#ifndef STBI_NO_STDIO +unsigned char *stbi_load(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = fopen(filename, "rb"); + unsigned char *result; + if (!f) return epuc("can't fopen", "Unable to open file"); + result = stbi_load_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +unsigned char *stbi_load_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi s; + start_file(&s,f); + return stbi_load_main(&s,x,y,comp,req_comp); +} +#endif //!STBI_NO_STDIO + +unsigned char *stbi_load_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi s; + start_mem(&s,buffer,len); + return stbi_load_main(&s,x,y,comp,req_comp); +} + +unsigned char *stbi_load_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi s; + start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi_load_main(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_HDR + +float *stbi_loadf_main(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + unsigned char *data; + #ifndef STBI_NO_HDR + if (stbi_hdr_test(s)) + return stbi_hdr_load(s,x,y,comp,req_comp); + #endif + data = stbi_load_main(s, x, y, comp, req_comp); + if (data) + return ldr_to_hdr(data, *x, *y, req_comp ? req_comp : *comp); + return epf("unknown image type", "Image not of any known type, or corrupt"); +} + +float *stbi_loadf_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp, int req_comp) +{ + stbi s; + start_mem(&s,buffer,len); + return stbi_loadf_main(&s,x,y,comp,req_comp); +} + +float *stbi_loadf_from_callbacks(stbi_io_callbacks const *clbk, void *user, int *x, int *y, int *comp, int req_comp) +{ + stbi s; + start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi_loadf_main(&s,x,y,comp,req_comp); +} + +#ifndef STBI_NO_STDIO +float *stbi_loadf(char const *filename, int *x, int *y, int *comp, int req_comp) +{ + FILE *f = fopen(filename, "rb"); + float *result; + if (!f) return epf("can't fopen", "Unable to open file"); + result = stbi_loadf_from_file(f,x,y,comp,req_comp); + fclose(f); + return result; +} + +float *stbi_loadf_from_file(FILE *f, int *x, int *y, int *comp, int req_comp) +{ + stbi s; + start_file(&s,f); + return stbi_loadf_main(&s,x,y,comp,req_comp); +} +#endif // !STBI_NO_STDIO + +#endif // !STBI_NO_HDR + +// these is-hdr-or-not is defined independent of whether STBI_NO_HDR is +// defined, for API simplicity; if STBI_NO_HDR is defined, it always +// reports false! + +int stbi_is_hdr_from_memory(stbi_uc const *buffer, int len) +{ + #ifndef STBI_NO_HDR + stbi s; + start_mem(&s,buffer,len); + return stbi_hdr_test(&s); + #else + STBI_NOTUSED(buffer); + STBI_NOTUSED(len); + return 0; + #endif +} + +#ifndef STBI_NO_STDIO +extern int stbi_is_hdr (char const *filename) +{ + FILE *f = fopen(filename, "rb"); + int result=0; + if (f) { + result = stbi_is_hdr_from_file(f); + fclose(f); + } + return result; +} + +extern int stbi_is_hdr_from_file(FILE *f) +{ + #ifndef STBI_NO_HDR + stbi s; + start_file(&s,f); + return stbi_hdr_test(&s); + #else + return 0; + #endif +} +#endif // !STBI_NO_STDIO + +extern int stbi_is_hdr_from_callbacks(stbi_io_callbacks const *clbk, void *user) +{ + #ifndef STBI_NO_HDR + stbi s; + start_callbacks(&s, (stbi_io_callbacks *) clbk, user); + return stbi_hdr_test(&s); + #else + return 0; + #endif +} + +#ifndef STBI_NO_HDR +static float h2l_gamma_i=1.0f/2.2f, h2l_scale_i=1.0f; +static float l2h_gamma=2.2f, l2h_scale=1.0f; + +void stbi_hdr_to_ldr_gamma(float gamma) { h2l_gamma_i = 1/gamma; } +void stbi_hdr_to_ldr_scale(float scale) { h2l_scale_i = 1/scale; } + +void stbi_ldr_to_hdr_gamma(float gamma) { l2h_gamma = gamma; } +void stbi_ldr_to_hdr_scale(float scale) { l2h_scale = scale; } +#endif + + +////////////////////////////////////////////////////////////////////////////// +// +// Common code used by all image loaders +// + +enum +{ + SCAN_load=0, + SCAN_type, + SCAN_header +}; + +static void refill_buffer(stbi *s) +{ + int n = (s->io.read)(s->io_user_data,(char*)s->buffer_start,s->buflen); + if (n == 0) { + // at end of file, treat same as if from memory + s->read_from_callbacks = 0; + s->img_buffer = s->img_buffer_end-1; + *s->img_buffer = 0; + } else { + s->img_buffer = s->buffer_start; + s->img_buffer_end = s->buffer_start + n; + } +} + +stbi_inline static int get8(stbi *s) +{ + if (s->img_buffer < s->img_buffer_end) + return *s->img_buffer++; + if (s->read_from_callbacks) { + refill_buffer(s); + return *s->img_buffer++; + } + return 0; +} + +stbi_inline static int at_eof(stbi *s) +{ + if (s->io.read) { + if (!(s->io.eof)(s->io_user_data)) return 0; + // if feof() is true, check if buffer = end + // special case: we've only got the special 0 character at the end + if (s->read_from_callbacks == 0) return 1; + } + + return s->img_buffer >= s->img_buffer_end; +} + +stbi_inline static uint8 get8u(stbi *s) +{ + return (uint8) get8(s); +} + +static void skip(stbi *s, int n) +{ + if (s->io.read) { + int blen = s->img_buffer_end - s->img_buffer; + if (blen < n) { + s->img_buffer = s->img_buffer_end; + (s->io.skip)(s->io_user_data, n - blen); + return; + } + } + s->img_buffer += n; +} + +static int getn(stbi *s, stbi_uc *buffer, int n) +{ + if (s->io.read) { + int blen = s->img_buffer_end - s->img_buffer; + if (blen < n) { + int res, count; + + memcpy(buffer, s->img_buffer, blen); + + count = (s->io.read)(s->io_user_data, (char*) buffer + blen, n - blen); + res = (count == (n-blen)); + s->img_buffer = s->img_buffer_end; + return res; + } + } + + if (s->img_buffer+n <= s->img_buffer_end) { + memcpy(buffer, s->img_buffer, n); + s->img_buffer += n; + return 1; + } else + return 0; +} + +static int get16(stbi *s) +{ + int z = get8(s); + return (z << 8) + get8(s); +} + +static uint32 get32(stbi *s) +{ + uint32 z = get16(s); + return (z << 16) + get16(s); +} + +static int get16le(stbi *s) +{ + int z = get8(s); + return z + (get8(s) << 8); +} + +static uint32 get32le(stbi *s) +{ + uint32 z = get16le(s); + return z + (get16le(s) << 16); +} + +////////////////////////////////////////////////////////////////////////////// +// +// generic converter from built-in img_n to req_comp +// individual types do this automatically as much as possible (e.g. jpeg +// does all cases internally since it needs to colorspace convert anyway, +// and it never has alpha, so very few cases ). png can automatically +// interleave an alpha=255 channel, but falls back to this for other cases +// +// assume data buffer is malloced, so malloc a new one and free that one +// only failure mode is malloc failing + +static uint8 compute_y(int r, int g, int b) +{ + return (uint8) (((r*77) + (g*150) + (29*b)) >> 8); +} + +static unsigned char *convert_format(unsigned char *data, int img_n, int req_comp, uint x, uint y) +{ + int i,j; + unsigned char *good; + + if (req_comp == img_n) return data; + assert(req_comp >= 1 && req_comp <= 4); + + good = (unsigned char *) malloc(req_comp * x * y); + if (good == NULL) { + free(data); + return epuc("outofmem", "Out of memory"); + } + + for (j=0; j < (int) y; ++j) { + unsigned char *src = data + j * x * img_n ; + unsigned char *dest = good + j * x * req_comp; + + #define COMBO(a,b) ((a)*8+(b)) + #define CASE(a,b) case COMBO(a,b): for(i=x-1; i >= 0; --i, src += a, dest += b) + // convert source image with img_n components to one with req_comp components; + // avoid switch per pixel, so use switch per scanline and massive macros + switch (COMBO(img_n, req_comp)) { + CASE(1,2) dest[0]=src[0], dest[1]=255; break; + CASE(1,3) dest[0]=dest[1]=dest[2]=src[0]; break; + CASE(1,4) dest[0]=dest[1]=dest[2]=src[0], dest[3]=255; break; + CASE(2,1) dest[0]=src[0]; break; + CASE(2,3) dest[0]=dest[1]=dest[2]=src[0]; break; + CASE(2,4) dest[0]=dest[1]=dest[2]=src[0], dest[3]=src[1]; break; + CASE(3,4) dest[0]=src[0],dest[1]=src[1],dest[2]=src[2],dest[3]=255; break; + CASE(3,1) dest[0]=compute_y(src[0],src[1],src[2]); break; + CASE(3,2) dest[0]=compute_y(src[0],src[1],src[2]), dest[1] = 255; break; + CASE(4,1) dest[0]=compute_y(src[0],src[1],src[2]); break; + CASE(4,2) dest[0]=compute_y(src[0],src[1],src[2]), dest[1] = src[3]; break; + CASE(4,3) dest[0]=src[0],dest[1]=src[1],dest[2]=src[2]; break; + default: assert(0); + } + #undef CASE + } + + free(data); + return good; +} + +#ifndef STBI_NO_HDR +static float *ldr_to_hdr(stbi_uc *data, int x, int y, int comp) +{ + int i,k,n; + float *output = (float *) malloc(x * y * comp * sizeof(float)); + if (output == NULL) { free(data); return epf("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + output[i*comp + k] = (float) pow(data[i*comp+k]/255.0f, l2h_gamma) * l2h_scale; + } + if (k < comp) output[i*comp + k] = data[i*comp+k]/255.0f; + } + free(data); + return output; +} + +#define float2int(x) ((int) (x)) +static stbi_uc *hdr_to_ldr(float *data, int x, int y, int comp) +{ + int i,k,n; + stbi_uc *output = (stbi_uc *) malloc(x * y * comp); + if (output == NULL) { free(data); return epuc("outofmem", "Out of memory"); } + // compute number of non-alpha components + if (comp & 1) n = comp; else n = comp-1; + for (i=0; i < x*y; ++i) { + for (k=0; k < n; ++k) { + float z = (float) pow(data[i*comp+k]*h2l_scale_i, h2l_gamma_i) * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (uint8) float2int(z); + } + if (k < comp) { + float z = data[i*comp+k] * 255 + 0.5f; + if (z < 0) z = 0; + if (z > 255) z = 255; + output[i*comp + k] = (uint8) float2int(z); + } + } + free(data); + return output; +} +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// "baseline" JPEG/JFIF decoder (not actually fully baseline implementation) +// +// simple implementation +// - channel subsampling of at most 2 in each dimension +// - doesn't support delayed output of y-dimension +// - simple interface (only one output format: 8-bit interleaved RGB) +// - doesn't try to recover corrupt jpegs +// - doesn't allow partial loading, loading multiple at once +// - still fast on x86 (copying globals into locals doesn't help x86) +// - allocates lots of intermediate memory (full size of all components) +// - non-interleaved case requires this anyway +// - allows good upsampling (see next) +// high-quality +// - upsampled channels are bilinearly interpolated, even across blocks +// - quality integer IDCT derived from IJG's 'slow' +// performance +// - fast huffman; reasonable integer IDCT +// - uses a lot of intermediate memory, could cache poorly +// - load http://nothings.org/remote/anemones.jpg 3 times on 2.8Ghz P4 +// stb_jpeg: 1.34 seconds (MSVC6, default release build) +// stb_jpeg: 1.06 seconds (MSVC6, processor = Pentium Pro) +// IJL11.dll: 1.08 seconds (compiled by intel) +// IJG 1998: 0.98 seconds (MSVC6, makefile provided by IJG) +// IJG 1998: 0.95 seconds (MSVC6, makefile + proc=PPro) + +// huffman decoding acceleration +#define FAST_BITS 9 // larger handles more cases; smaller stomps less cache + +typedef struct +{ + uint8 fast[1 << FAST_BITS]; + // weirdly, repacking this into AoS is a 10% speed loss, instead of a win + uint16 code[256]; + uint8 values[256]; + uint8 size[257]; + unsigned int maxcode[18]; + int delta[17]; // old 'firstsymbol' - old 'firstcode' +} huffman; + +typedef struct +{ + #ifdef STBI_SIMD + unsigned short dequant2[4][64]; + #endif + stbi *s; + huffman huff_dc[4]; + huffman huff_ac[4]; + uint8 dequant[4][64]; + +// sizes for components, interleaved MCUs + int img_h_max, img_v_max; + int img_mcu_x, img_mcu_y; + int img_mcu_w, img_mcu_h; + +// definition of jpeg image component + struct + { + int id; + int h,v; + int tq; + int hd,ha; + int dc_pred; + + int x,y,w2,h2; + uint8 *data; + void *raw_data; + uint8 *linebuf; + } img_comp[4]; + + uint32 code_buffer; // jpeg entropy-coded buffer + int code_bits; // number of valid bits + unsigned char marker; // marker seen while filling entropy buffer + int nomore; // flag if we saw a marker so must stop + + int scan_n, order[4]; + int restart_interval, todo; +} jpeg; + +static int build_huffman(huffman *h, int *count) +{ + int i,j,k=0,code; + // build size list for each symbol (from JPEG spec) + for (i=0; i < 16; ++i) + for (j=0; j < count[i]; ++j) + h->size[k++] = (uint8) (i+1); + h->size[k] = 0; + + // compute actual symbols (from jpeg spec) + code = 0; + k = 0; + for(j=1; j <= 16; ++j) { + // compute delta to add to code to compute symbol id + h->delta[j] = k - code; + if (h->size[k] == j) { + while (h->size[k] == j) + h->code[k++] = (uint16) (code++); + if (code-1 >= (1 << j)) return e("bad code lengths","Corrupt JPEG"); + } + // compute largest code + 1 for this size, preshifted as needed later + h->maxcode[j] = code << (16-j); + code <<= 1; + } + h->maxcode[j] = 0xffffffff; + + // build non-spec acceleration table; 255 is flag for not-accelerated + memset(h->fast, 255, 1 << FAST_BITS); + for (i=0; i < k; ++i) { + int s = h->size[i]; + if (s <= FAST_BITS) { + int c = h->code[i] << (FAST_BITS-s); + int m = 1 << (FAST_BITS-s); + for (j=0; j < m; ++j) { + h->fast[c+j] = (uint8) i; + } + } + } + return 1; +} + +static void grow_buffer_unsafe(jpeg *j) +{ + do { + int b = j->nomore ? 0 : get8(j->s); + if (b == 0xff) { + int c = get8(j->s); + if (c != 0) { + j->marker = (unsigned char) c; + j->nomore = 1; + return; + } + } + j->code_buffer |= b << (24 - j->code_bits); + j->code_bits += 8; + } while (j->code_bits <= 24); +} + +// (1 << n) - 1 +static uint32 bmask[17]={0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535}; + +// decode a jpeg huffman value from the bitstream +stbi_inline static int decode(jpeg *j, huffman *h) +{ + unsigned int temp; + int c,k; + + if (j->code_bits < 16) grow_buffer_unsafe(j); + + // look at the top FAST_BITS and determine what symbol ID it is, + // if the code is <= FAST_BITS + c = (j->code_buffer >> (32 - FAST_BITS)) & ((1 << FAST_BITS)-1); + k = h->fast[c]; + if (k < 255) { + int s = h->size[k]; + if (s > j->code_bits) + return -1; + j->code_buffer <<= s; + j->code_bits -= s; + return h->values[k]; + } + + // naive test is to shift the code_buffer down so k bits are + // valid, then test against maxcode. To speed this up, we've + // preshifted maxcode left so that it has (16-k) 0s at the + // end; in other words, regardless of the number of bits, it + // wants to be compared against something shifted to have 16; + // that way we don't need to shift inside the loop. + temp = j->code_buffer >> 16; + for (k=FAST_BITS+1 ; ; ++k) + if (temp < h->maxcode[k]) + break; + if (k == 17) { + // error! code not found + j->code_bits -= 16; + return -1; + } + + if (k > j->code_bits) + return -1; + + // convert the huffman code to the symbol id + c = ((j->code_buffer >> (32 - k)) & bmask[k]) + h->delta[k]; + assert((((j->code_buffer) >> (32 - h->size[c])) & bmask[h->size[c]]) == h->code[c]); + + // convert the id to a symbol + j->code_bits -= k; + j->code_buffer <<= k; + return h->values[c]; +} + +// combined JPEG 'receive' and JPEG 'extend', since baseline +// always extends everything it receives. +stbi_inline static int extend_receive(jpeg *j, int n) +{ + unsigned int m = 1 << (n-1); + unsigned int k; + if (j->code_bits < n) grow_buffer_unsafe(j); + + #if 1 + k = stbi_lrot(j->code_buffer, n); + j->code_buffer = k & ~bmask[n]; + k &= bmask[n]; + j->code_bits -= n; + #else + k = (j->code_buffer >> (32 - n)) & bmask[n]; + j->code_bits -= n; + j->code_buffer <<= n; + #endif + // the following test is probably a random branch that won't + // predict well. I tried to table accelerate it but failed. + // maybe it's compiling as a conditional move? + if (k < m) + return (-1 << n) + k + 1; + else + return k; +} + +// given a value that's at position X in the zigzag stream, +// where does it appear in the 8x8 matrix coded as row-major? +static uint8 dezigzag[64+15] = +{ + 0, 1, 8, 16, 9, 2, 3, 10, + 17, 24, 32, 25, 18, 11, 4, 5, + 12, 19, 26, 33, 40, 48, 41, 34, + 27, 20, 13, 6, 7, 14, 21, 28, + 35, 42, 49, 56, 57, 50, 43, 36, + 29, 22, 15, 23, 30, 37, 44, 51, + 58, 59, 52, 45, 38, 31, 39, 46, + 53, 60, 61, 54, 47, 55, 62, 63, + // let corrupt input sample past end + 63, 63, 63, 63, 63, 63, 63, 63, + 63, 63, 63, 63, 63, 63, 63 +}; + +// decode one 64-entry block-- +static int decode_block(jpeg *j, short data[64], huffman *hdc, huffman *hac, int b) +{ + int diff,dc,k; + int t = decode(j, hdc); + if (t < 0) return e("bad huffman code","Corrupt JPEG"); + + // 0 all the ac values now so we can do it 32-bits at a time + memset(data,0,64*sizeof(data[0])); + + diff = t ? extend_receive(j, t) : 0; + dc = j->img_comp[b].dc_pred + diff; + j->img_comp[b].dc_pred = dc; + data[0] = (short) dc; + + // decode AC components, see JPEG spec + k = 1; + do { + int r,s; + int rs = decode(j, hac); + if (rs < 0) return e("bad huffman code","Corrupt JPEG"); + s = rs & 15; + r = rs >> 4; + if (s == 0) { + if (rs != 0xf0) break; // end block + k += 16; + } else { + k += r; + // decode into unzigzag'd location + data[dezigzag[k++]] = (short) extend_receive(j,s); + } + } while (k < 64); + return 1; +} + +// take a -128..127 value and clamp it and convert to 0..255 +stbi_inline static uint8 clamp(int x) +{ + // trick to use a single test to catch both cases + if ((unsigned int) x > 255) { + if (x < 0) return 0; + if (x > 255) return 255; + } + return (uint8) x; +} + +#define f2f(x) (int) (((x) * 4096 + 0.5)) +#define fsh(x) ((x) << 12) + +// derived from jidctint -- DCT_ISLOW +#define IDCT_1D(s0,s1,s2,s3,s4,s5,s6,s7) \ + int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; \ + p2 = s2; \ + p3 = s6; \ + p1 = (p2+p3) * f2f(0.5411961f); \ + t2 = p1 + p3*f2f(-1.847759065f); \ + t3 = p1 + p2*f2f( 0.765366865f); \ + p2 = s0; \ + p3 = s4; \ + t0 = fsh(p2+p3); \ + t1 = fsh(p2-p3); \ + x0 = t0+t3; \ + x3 = t0-t3; \ + x1 = t1+t2; \ + x2 = t1-t2; \ + t0 = s7; \ + t1 = s5; \ + t2 = s3; \ + t3 = s1; \ + p3 = t0+t2; \ + p4 = t1+t3; \ + p1 = t0+t3; \ + p2 = t1+t2; \ + p5 = (p3+p4)*f2f( 1.175875602f); \ + t0 = t0*f2f( 0.298631336f); \ + t1 = t1*f2f( 2.053119869f); \ + t2 = t2*f2f( 3.072711026f); \ + t3 = t3*f2f( 1.501321110f); \ + p1 = p5 + p1*f2f(-0.899976223f); \ + p2 = p5 + p2*f2f(-2.562915447f); \ + p3 = p3*f2f(-1.961570560f); \ + p4 = p4*f2f(-0.390180644f); \ + t3 += p1+p4; \ + t2 += p2+p3; \ + t1 += p2+p4; \ + t0 += p1+p3; + +#ifdef STBI_SIMD +typedef unsigned short stbi_dequantize_t; +#else +typedef uint8 stbi_dequantize_t; +#endif + +// .344 seconds on 3*anemones.jpg +static void idct_block(uint8 *out, int out_stride, short data[64], stbi_dequantize_t *dequantize) +{ + int i,val[64],*v=val; + stbi_dequantize_t *dq = dequantize; + uint8 *o; + short *d = data; + + // columns + for (i=0; i < 8; ++i,++d,++dq, ++v) { + // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing + if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 + && d[40]==0 && d[48]==0 && d[56]==0) { + // no shortcut 0 seconds + // (1|2|3|4|5|6|7)==0 0 seconds + // all separate -0.047 seconds + // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds + int dcterm = d[0] * dq[0] << 2; + v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; + } else { + IDCT_1D(d[ 0]*dq[ 0],d[ 8]*dq[ 8],d[16]*dq[16],d[24]*dq[24], + d[32]*dq[32],d[40]*dq[40],d[48]*dq[48],d[56]*dq[56]) + // constants scaled things up by 1<<12; let's bring them back + // down, but keep 2 extra bits of precision + x0 += 512; x1 += 512; x2 += 512; x3 += 512; + v[ 0] = (x0+t3) >> 10; + v[56] = (x0-t3) >> 10; + v[ 8] = (x1+t2) >> 10; + v[48] = (x1-t2) >> 10; + v[16] = (x2+t1) >> 10; + v[40] = (x2-t1) >> 10; + v[24] = (x3+t0) >> 10; + v[32] = (x3-t0) >> 10; + } + } + + for (i=0, v=val, o=out; i < 8; ++i,v+=8,o+=out_stride) { + // no fast case since the first 1D IDCT spread components out + IDCT_1D(v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7]) + // constants scaled things up by 1<<12, plus we had 1<<2 from first + // loop, plus horizontal and vertical each scale by sqrt(8) so together + // we've got an extra 1<<3, so 1<<17 total we need to remove. + // so we want to round that, which means adding 0.5 * 1<<17, + // aka 65536. Also, we'll end up with -128 to 127 that we want + // to encode as 0..255 by adding 128, so we'll add that before the shift + x0 += 65536 + (128<<17); + x1 += 65536 + (128<<17); + x2 += 65536 + (128<<17); + x3 += 65536 + (128<<17); + // tried computing the shifts into temps, or'ing the temps to see + // if any were out of range, but that was slower + o[0] = clamp((x0+t3) >> 17); + o[7] = clamp((x0-t3) >> 17); + o[1] = clamp((x1+t2) >> 17); + o[6] = clamp((x1-t2) >> 17); + o[2] = clamp((x2+t1) >> 17); + o[5] = clamp((x2-t1) >> 17); + o[3] = clamp((x3+t0) >> 17); + o[4] = clamp((x3-t0) >> 17); + } +} + +#ifdef STBI_SIMD +static stbi_idct_8x8 stbi_idct_installed = idct_block; + +void stbi_install_idct(stbi_idct_8x8 func) +{ + stbi_idct_installed = func; +} +#endif + +#define MARKER_none 0xff +// if there's a pending marker from the entropy stream, return that +// otherwise, fetch from the stream and get a marker. if there's no +// marker, return 0xff, which is never a valid marker value +static uint8 get_marker(jpeg *j) +{ + uint8 x; + if (j->marker != MARKER_none) { x = j->marker; j->marker = MARKER_none; return x; } + x = get8u(j->s); + if (x != 0xff) return MARKER_none; + while (x == 0xff) + x = get8u(j->s); + return x; +} + +// in each scan, we'll have scan_n components, and the order +// of the components is specified by order[] +#define RESTART(x) ((x) >= 0xd0 && (x) <= 0xd7) + +// after a restart interval, reset the entropy decoder and +// the dc prediction +static void reset(jpeg *j) +{ + j->code_bits = 0; + j->code_buffer = 0; + j->nomore = 0; + j->img_comp[0].dc_pred = j->img_comp[1].dc_pred = j->img_comp[2].dc_pred = 0; + j->marker = MARKER_none; + j->todo = j->restart_interval ? j->restart_interval : 0x7fffffff; + // no more than 1<<31 MCUs if no restart_interal? that's plenty safe, + // since we don't even allow 1<<30 pixels +} + +static int parse_entropy_coded_data(jpeg *z) +{ + reset(z); + if (z->scan_n == 1) { + int i,j; + #ifdef STBI_SIMD + __declspec(align(16)) + #endif + short data[64]; + int n = z->order[0]; + // non-interleaved data, we just need to process one block at a time, + // in trivial scanline order + // number of blocks to do just depends on how many actual "pixels" this + // component has, independent of interleaved MCU blocking and such + int w = (z->img_comp[n].x+7) >> 3; + int h = (z->img_comp[n].y+7) >> 3; + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) { + if (!decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+z->img_comp[n].ha, n)) return 0; + #ifdef STBI_SIMD + stbi_idct_installed(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data, z->dequant2[z->img_comp[n].tq]); + #else + idct_block(z->img_comp[n].data+z->img_comp[n].w2*j*8+i*8, z->img_comp[n].w2, data, z->dequant[z->img_comp[n].tq]); + #endif + // every data block is an MCU, so countdown the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) grow_buffer_unsafe(z); + // if it's NOT a restart, then just bail, so we get corrupt data + // rather than no data + if (!RESTART(z->marker)) return 1; + reset(z); + } + } + } + } else { // interleaved! + int i,j,k,x,y; + short data[64]; + for (j=0; j < z->img_mcu_y; ++j) { + for (i=0; i < z->img_mcu_x; ++i) { + // scan an interleaved mcu... process scan_n components in order + for (k=0; k < z->scan_n; ++k) { + int n = z->order[k]; + // scan out an mcu's worth of this component; that's just determined + // by the basic H and V specified for the component + for (y=0; y < z->img_comp[n].v; ++y) { + for (x=0; x < z->img_comp[n].h; ++x) { + int x2 = (i*z->img_comp[n].h + x)*8; + int y2 = (j*z->img_comp[n].v + y)*8; + if (!decode_block(z, data, z->huff_dc+z->img_comp[n].hd, z->huff_ac+z->img_comp[n].ha, n)) return 0; + #ifdef STBI_SIMD + stbi_idct_installed(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data, z->dequant2[z->img_comp[n].tq]); + #else + idct_block(z->img_comp[n].data+z->img_comp[n].w2*y2+x2, z->img_comp[n].w2, data, z->dequant[z->img_comp[n].tq]); + #endif + } + } + } + // after all interleaved components, that's an interleaved MCU, + // so now count down the restart interval + if (--z->todo <= 0) { + if (z->code_bits < 24) grow_buffer_unsafe(z); + // if it's NOT a restart, then just bail, so we get corrupt data + // rather than no data + if (!RESTART(z->marker)) return 1; + reset(z); + } + } + } + } + return 1; +} + +static int process_marker(jpeg *z, int m) +{ + int L; + switch (m) { + case MARKER_none: // no marker found + return e("expected marker","Corrupt JPEG"); + + case 0xC2: // SOF - progressive + return e("progressive jpeg","JPEG format not supported (progressive)"); + + case 0xDD: // DRI - specify restart interval + if (get16(z->s) != 4) return e("bad DRI len","Corrupt JPEG"); + z->restart_interval = get16(z->s); + return 1; + + case 0xDB: // DQT - define quantization table + L = get16(z->s)-2; + while (L > 0) { + int q = get8(z->s); + int p = q >> 4; + int t = q & 15,i; + if (p != 0) return e("bad DQT type","Corrupt JPEG"); + if (t > 3) return e("bad DQT table","Corrupt JPEG"); + for (i=0; i < 64; ++i) + z->dequant[t][dezigzag[i]] = get8u(z->s); + #ifdef STBI_SIMD + for (i=0; i < 64; ++i) + z->dequant2[t][i] = z->dequant[t][i]; + #endif + L -= 65; + } + return L==0; + + case 0xC4: // DHT - define huffman table + L = get16(z->s)-2; + while (L > 0) { + uint8 *v; + int sizes[16],i,m=0; + int q = get8(z->s); + int tc = q >> 4; + int th = q & 15; + if (tc > 1 || th > 3) return e("bad DHT header","Corrupt JPEG"); + for (i=0; i < 16; ++i) { + sizes[i] = get8(z->s); + m += sizes[i]; + } + L -= 17; + if (tc == 0) { + if (!build_huffman(z->huff_dc+th, sizes)) return 0; + v = z->huff_dc[th].values; + } else { + if (!build_huffman(z->huff_ac+th, sizes)) return 0; + v = z->huff_ac[th].values; + } + for (i=0; i < m; ++i) + v[i] = get8u(z->s); + L -= m; + } + return L==0; + } + // check for comment block or APP blocks + if ((m >= 0xE0 && m <= 0xEF) || m == 0xFE) { + skip(z->s, get16(z->s)-2); + return 1; + } + return 0; +} + +// after we see SOS +static int process_scan_header(jpeg *z) +{ + int i; + int Ls = get16(z->s); + z->scan_n = get8(z->s); + if (z->scan_n < 1 || z->scan_n > 4 || z->scan_n > (int) z->s->img_n) return e("bad SOS component count","Corrupt JPEG"); + if (Ls != 6+2*z->scan_n) return e("bad SOS len","Corrupt JPEG"); + for (i=0; i < z->scan_n; ++i) { + int id = get8(z->s), which; + int q = get8(z->s); + for (which = 0; which < z->s->img_n; ++which) + if (z->img_comp[which].id == id) + break; + if (which == z->s->img_n) return 0; + z->img_comp[which].hd = q >> 4; if (z->img_comp[which].hd > 3) return e("bad DC huff","Corrupt JPEG"); + z->img_comp[which].ha = q & 15; if (z->img_comp[which].ha > 3) return e("bad AC huff","Corrupt JPEG"); + z->order[i] = which; + } + if (get8(z->s) != 0) return e("bad SOS","Corrupt JPEG"); + get8(z->s); // should be 63, but might be 0 + if (get8(z->s) != 0) return e("bad SOS","Corrupt JPEG"); + + return 1; +} + +static int process_frame_header(jpeg *z, int scan) +{ + stbi *s = z->s; + int Lf,p,i,q, h_max=1,v_max=1,c; + Lf = get16(s); if (Lf < 11) return e("bad SOF len","Corrupt JPEG"); // JPEG + p = get8(s); if (p != 8) return e("only 8-bit","JPEG format not supported: 8-bit only"); // JPEG baseline + s->img_y = get16(s); if (s->img_y == 0) return e("no header height", "JPEG format not supported: delayed height"); // Legal, but we don't handle it--but neither does IJG + s->img_x = get16(s); if (s->img_x == 0) return e("0 width","Corrupt JPEG"); // JPEG requires + c = get8(s); + if (c != 3 && c != 1) return e("bad component count","Corrupt JPEG"); // JFIF requires + s->img_n = c; + for (i=0; i < c; ++i) { + z->img_comp[i].data = NULL; + z->img_comp[i].linebuf = NULL; + } + + if (Lf != 8+3*s->img_n) return e("bad SOF len","Corrupt JPEG"); + + for (i=0; i < s->img_n; ++i) { + z->img_comp[i].id = get8(s); + if (z->img_comp[i].id != i+1) // JFIF requires + if (z->img_comp[i].id != i) // some version of jpegtran outputs non-JFIF-compliant files! + return e("bad component ID","Corrupt JPEG"); + q = get8(s); + z->img_comp[i].h = (q >> 4); if (!z->img_comp[i].h || z->img_comp[i].h > 4) return e("bad H","Corrupt JPEG"); + z->img_comp[i].v = q & 15; if (!z->img_comp[i].v || z->img_comp[i].v > 4) return e("bad V","Corrupt JPEG"); + z->img_comp[i].tq = get8(s); if (z->img_comp[i].tq > 3) return e("bad TQ","Corrupt JPEG"); + } + + if (scan != SCAN_load) return 1; + + if ((1 << 30) / s->img_x / s->img_n < s->img_y) return e("too large", "Image too large to decode"); + + for (i=0; i < s->img_n; ++i) { + if (z->img_comp[i].h > h_max) h_max = z->img_comp[i].h; + if (z->img_comp[i].v > v_max) v_max = z->img_comp[i].v; + } + + // compute interleaved mcu info + z->img_h_max = h_max; + z->img_v_max = v_max; + z->img_mcu_w = h_max * 8; + z->img_mcu_h = v_max * 8; + z->img_mcu_x = (s->img_x + z->img_mcu_w-1) / z->img_mcu_w; + z->img_mcu_y = (s->img_y + z->img_mcu_h-1) / z->img_mcu_h; + + for (i=0; i < s->img_n; ++i) { + // number of effective pixels (e.g. for non-interleaved MCU) + z->img_comp[i].x = (s->img_x * z->img_comp[i].h + h_max-1) / h_max; + z->img_comp[i].y = (s->img_y * z->img_comp[i].v + v_max-1) / v_max; + // to simplify generation, we'll allocate enough memory to decode + // the bogus oversized data from using interleaved MCUs and their + // big blocks (e.g. a 16x16 iMCU on an image of width 33); we won't + // discard the extra data until colorspace conversion + z->img_comp[i].w2 = z->img_mcu_x * z->img_comp[i].h * 8; + z->img_comp[i].h2 = z->img_mcu_y * z->img_comp[i].v * 8; + z->img_comp[i].raw_data = malloc(z->img_comp[i].w2 * z->img_comp[i].h2+15); + if (z->img_comp[i].raw_data == NULL) { + for(--i; i >= 0; --i) { + free(z->img_comp[i].raw_data); + z->img_comp[i].data = NULL; + } + return e("outofmem", "Out of memory"); + } + // align blocks for installable-idct using mmx/sse + z->img_comp[i].data = (uint8*) (((size_t) z->img_comp[i].raw_data + 15) & ~15); + z->img_comp[i].linebuf = NULL; + } + + return 1; +} + +// use comparisons since in some cases we handle more than one case (e.g. SOF) +#define DNL(x) ((x) == 0xdc) +#define SOI(x) ((x) == 0xd8) +#define EOI(x) ((x) == 0xd9) +#define SOF(x) ((x) == 0xc0 || (x) == 0xc1) +#define SOS(x) ((x) == 0xda) + +static int decode_jpeg_header(jpeg *z, int scan) +{ + int m; + z->marker = MARKER_none; // initialize cached marker to empty + m = get_marker(z); + if (!SOI(m)) return e("no SOI","Corrupt JPEG"); + if (scan == SCAN_type) return 1; + m = get_marker(z); + while (!SOF(m)) { + if (!process_marker(z,m)) return 0; + m = get_marker(z); + while (m == MARKER_none) { + // some files have extra padding after their blocks, so ok, we'll scan + if (at_eof(z->s)) return e("no SOF", "Corrupt JPEG"); + m = get_marker(z); + } + } + if (!process_frame_header(z, scan)) return 0; + return 1; +} + +static int decode_jpeg_image(jpeg *j) +{ + int m; + j->restart_interval = 0; + if (!decode_jpeg_header(j, SCAN_load)) return 0; + m = get_marker(j); + while (!EOI(m)) { + if (SOS(m)) { + if (!process_scan_header(j)) return 0; + if (!parse_entropy_coded_data(j)) return 0; + if (j->marker == MARKER_none ) { + // handle 0s at the end of image data from IP Kamera 9060 + while (!at_eof(j->s)) { + int x = get8(j->s); + if (x == 255) { + j->marker = get8u(j->s); + break; + } else if (x != 0) { + return 0; + } + } + // if we reach eof without hitting a marker, get_marker() below will fail and we'll eventually return 0 + } + } else { + if (!process_marker(j, m)) return 0; + } + m = get_marker(j); + } + return 1; +} + +// static jfif-centered resampling (across block boundaries) + +typedef uint8 *(*resample_row_func)(uint8 *out, uint8 *in0, uint8 *in1, + int w, int hs); + +#define div4(x) ((uint8) ((x) >> 2)) + +static uint8 *resample_row_1(uint8 *out, uint8 *in_near, uint8 *in_far, int w, int hs) +{ + STBI_NOTUSED(out); + STBI_NOTUSED(in_far); + STBI_NOTUSED(w); + STBI_NOTUSED(hs); + return in_near; +} + +static uint8* resample_row_v_2(uint8 *out, uint8 *in_near, uint8 *in_far, int w, int hs) +{ + // need to generate two samples vertically for every one in input + int i; + STBI_NOTUSED(hs); + for (i=0; i < w; ++i) + out[i] = div4(3*in_near[i] + in_far[i] + 2); + return out; +} + +static uint8* resample_row_h_2(uint8 *out, uint8 *in_near, uint8 *in_far, int w, int hs) +{ + // need to generate two samples horizontally for every one in input + int i; + uint8 *input = in_near; + + if (w == 1) { + // if only one sample, can't do any interpolation + out[0] = out[1] = input[0]; + return out; + } + + out[0] = input[0]; + out[1] = div4(input[0]*3 + input[1] + 2); + for (i=1; i < w-1; ++i) { + int n = 3*input[i]+2; + out[i*2+0] = div4(n+input[i-1]); + out[i*2+1] = div4(n+input[i+1]); + } + out[i*2+0] = div4(input[w-2]*3 + input[w-1] + 2); + out[i*2+1] = input[w-1]; + + STBI_NOTUSED(in_far); + STBI_NOTUSED(hs); + + return out; +} + +#define div16(x) ((uint8) ((x) >> 4)) + +static uint8 *resample_row_hv_2(uint8 *out, uint8 *in_near, uint8 *in_far, int w, int hs) +{ + // need to generate 2x2 samples for every one in input + int i,t0,t1; + if (w == 1) { + out[0] = out[1] = div4(3*in_near[0] + in_far[0] + 2); + return out; + } + + t1 = 3*in_near[0] + in_far[0]; + out[0] = div4(t1+2); + for (i=1; i < w; ++i) { + t0 = t1; + t1 = 3*in_near[i]+in_far[i]; + out[i*2-1] = div16(3*t0 + t1 + 8); + out[i*2 ] = div16(3*t1 + t0 + 8); + } + out[w*2-1] = div4(t1+2); + + STBI_NOTUSED(hs); + + return out; +} + +static uint8 *resample_row_generic(uint8 *out, uint8 *in_near, uint8 *in_far, int w, int hs) +{ + // resample with nearest-neighbor + int i,j; + // in_far = in_far; + for (i=0; i < w; ++i) + for (j=0; j < hs; ++j) + out[i*hs+j] = in_near[i]; + return out; +} + +#define float2fixed(x) ((int) ((x) * 65536 + 0.5)) + +// 0.38 seconds on 3*anemones.jpg (0.25 with processor = Pro) +// VC6 without processor=Pro is generating multiple LEAs per multiply! +static void YCbCr_to_RGB_row(uint8 *out, const uint8 *y, const uint8 *pcb, const uint8 *pcr, int count, int step) +{ + int i; + for (i=0; i < count; ++i) { + int y_fixed = (y[i] << 16) + 32768; // rounding + int r,g,b; + int cr = pcr[i] - 128; + int cb = pcb[i] - 128; + r = y_fixed + cr*float2fixed(1.40200f); + g = y_fixed - cr*float2fixed(0.71414f) - cb*float2fixed(0.34414f); + b = y_fixed + cb*float2fixed(1.77200f); + r >>= 16; + g >>= 16; + b >>= 16; + if ((unsigned) r > 255) { if (r < 0) r = 0; else r = 255; } + if ((unsigned) g > 255) { if (g < 0) g = 0; else g = 255; } + if ((unsigned) b > 255) { if (b < 0) b = 0; else b = 255; } + out[0] = (uint8)r; + out[1] = (uint8)g; + out[2] = (uint8)b; + out[3] = 255; + out += step; + } +} + +#ifdef STBI_SIMD +static stbi_YCbCr_to_RGB_run stbi_YCbCr_installed = YCbCr_to_RGB_row; + +void stbi_install_YCbCr_to_RGB(stbi_YCbCr_to_RGB_run func) +{ + stbi_YCbCr_installed = func; +} +#endif + + +// clean up the temporary component buffers +static void cleanup_jpeg(jpeg *j) +{ + int i; + for (i=0; i < j->s->img_n; ++i) { + if (j->img_comp[i].data) { + free(j->img_comp[i].raw_data); + j->img_comp[i].data = NULL; + } + if (j->img_comp[i].linebuf) { + free(j->img_comp[i].linebuf); + j->img_comp[i].linebuf = NULL; + } + } +} + +typedef struct +{ + resample_row_func resample; + uint8 *line0,*line1; + int hs,vs; // expansion factor in each axis + int w_lores; // horizontal pixels pre-expansion + int ystep; // how far through vertical expansion we are + int ypos; // which pre-expansion row we're on +} stbi_resample; + +static uint8 *load_jpeg_image(jpeg *z, int *out_x, int *out_y, int *comp, int req_comp) +{ + int n, decode_n; + // validate req_comp + if (req_comp < 0 || req_comp > 4) return epuc("bad req_comp", "Internal error"); + z->s->img_n = 0; + + // load a jpeg image from whichever source + if (!decode_jpeg_image(z)) { cleanup_jpeg(z); return NULL; } + + // determine actual number of components to generate + n = req_comp ? req_comp : z->s->img_n; + + if (z->s->img_n == 3 && n < 3) + decode_n = 1; + else + decode_n = z->s->img_n; + + // resample and color-convert + { + int k; + uint i,j; + uint8 *output; + uint8 *coutput[4]; + + stbi_resample res_comp[4]; + + for (k=0; k < decode_n; ++k) { + stbi_resample *r = &res_comp[k]; + + // allocate line buffer big enough for upsampling off the edges + // with upsample factor of 4 + z->img_comp[k].linebuf = (uint8 *) malloc(z->s->img_x + 3); + if (!z->img_comp[k].linebuf) { cleanup_jpeg(z); return epuc("outofmem", "Out of memory"); } + + r->hs = z->img_h_max / z->img_comp[k].h; + r->vs = z->img_v_max / z->img_comp[k].v; + r->ystep = r->vs >> 1; + r->w_lores = (z->s->img_x + r->hs-1) / r->hs; + r->ypos = 0; + r->line0 = r->line1 = z->img_comp[k].data; + + if (r->hs == 1 && r->vs == 1) r->resample = resample_row_1; + else if (r->hs == 1 && r->vs == 2) r->resample = resample_row_v_2; + else if (r->hs == 2 && r->vs == 1) r->resample = resample_row_h_2; + else if (r->hs == 2 && r->vs == 2) r->resample = resample_row_hv_2; + else r->resample = resample_row_generic; + } + + // can't error after this so, this is safe + output = (uint8 *) malloc(n * z->s->img_x * z->s->img_y + 1); + if (!output) { cleanup_jpeg(z); return epuc("outofmem", "Out of memory"); } + + // now go ahead and resample + for (j=0; j < z->s->img_y; ++j) { + uint8 *out = output + n * z->s->img_x * j; + for (k=0; k < decode_n; ++k) { + stbi_resample *r = &res_comp[k]; + int y_bot = r->ystep >= (r->vs >> 1); + coutput[k] = r->resample(z->img_comp[k].linebuf, + y_bot ? r->line1 : r->line0, + y_bot ? r->line0 : r->line1, + r->w_lores, r->hs); + if (++r->ystep >= r->vs) { + r->ystep = 0; + r->line0 = r->line1; + if (++r->ypos < z->img_comp[k].y) + r->line1 += z->img_comp[k].w2; + } + } + if (n >= 3) { + uint8 *y = coutput[0]; + if (z->s->img_n == 3) { + #ifdef STBI_SIMD + stbi_YCbCr_installed(out, y, coutput[1], coutput[2], z->s.img_x, n); + #else + YCbCr_to_RGB_row(out, y, coutput[1], coutput[2], z->s->img_x, n); + #endif + } else + for (i=0; i < z->s->img_x; ++i) { + out[0] = out[1] = out[2] = y[i]; + out[3] = 255; // not used if n==3 + out += n; + } + } else { + uint8 *y = coutput[0]; + if (n == 1) + for (i=0; i < z->s->img_x; ++i) out[i] = y[i]; + else + for (i=0; i < z->s->img_x; ++i) *out++ = y[i], *out++ = 255; + } + } + cleanup_jpeg(z); + *out_x = z->s->img_x; + *out_y = z->s->img_y; + if (comp) *comp = z->s->img_n; // report original components, not output + return output; + } +} + +static unsigned char *stbi_jpeg_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + jpeg j; + j.s = s; + return load_jpeg_image(&j, x,y,comp,req_comp); +} + +static int stbi_jpeg_test(stbi *s) +{ + int r; + jpeg j; + j.s = s; + r = decode_jpeg_header(&j, SCAN_type); + stbi_rewind(s); + return r; +} + +static int stbi_jpeg_info_raw(jpeg *j, int *x, int *y, int *comp) +{ + if (!decode_jpeg_header(j, SCAN_header)) { + stbi_rewind( j->s ); + return 0; + } + if (x) *x = j->s->img_x; + if (y) *y = j->s->img_y; + if (comp) *comp = j->s->img_n; + return 1; +} + +static int stbi_jpeg_info(stbi *s, int *x, int *y, int *comp) +{ + jpeg j; + j.s = s; + return stbi_jpeg_info_raw(&j, x, y, comp); +} + +// public domain zlib decode v0.2 Sean Barrett 2006-11-18 +// simple implementation +// - all input must be provided in an upfront buffer +// - all output is written to a single output buffer (can malloc/realloc) +// performance +// - fast huffman + +// fast-way is faster to check than jpeg huffman, but slow way is slower +#define ZFAST_BITS 9 // accelerate all cases in default tables +#define ZFAST_MASK ((1 << ZFAST_BITS) - 1) + +// zlib-style huffman encoding +// (jpegs packs from left, zlib from right, so can't share code) +typedef struct +{ + uint16 fast[1 << ZFAST_BITS]; + uint16 firstcode[16]; + int maxcode[17]; + uint16 firstsymbol[16]; + uint8 size[288]; + uint16 value[288]; +} zhuffman; + +stbi_inline static int bitreverse16(int n) +{ + n = ((n & 0xAAAA) >> 1) | ((n & 0x5555) << 1); + n = ((n & 0xCCCC) >> 2) | ((n & 0x3333) << 2); + n = ((n & 0xF0F0) >> 4) | ((n & 0x0F0F) << 4); + n = ((n & 0xFF00) >> 8) | ((n & 0x00FF) << 8); + return n; +} + +stbi_inline static int bit_reverse(int v, int bits) +{ + assert(bits <= 16); + // to bit reverse n bits, reverse 16 and shift + // e.g. 11 bits, bit reverse and shift away 5 + return bitreverse16(v) >> (16-bits); +} + +static int zbuild_huffman(zhuffman *z, uint8 *sizelist, int num) +{ + int i,k=0; + int code, next_code[16], sizes[17]; + + // DEFLATE spec for generating codes + memset(sizes, 0, sizeof(sizes)); + memset(z->fast, 255, sizeof(z->fast)); + for (i=0; i < num; ++i) + ++sizes[sizelist[i]]; + sizes[0] = 0; + for (i=1; i < 16; ++i) + assert(sizes[i] <= (1 << i)); + code = 0; + for (i=1; i < 16; ++i) { + next_code[i] = code; + z->firstcode[i] = (uint16) code; + z->firstsymbol[i] = (uint16) k; + code = (code + sizes[i]); + if (sizes[i]) + if (code-1 >= (1 << i)) return e("bad codelengths","Corrupt JPEG"); + z->maxcode[i] = code << (16-i); // preshift for inner loop + code <<= 1; + k += sizes[i]; + } + z->maxcode[16] = 0x10000; // sentinel + for (i=0; i < num; ++i) { + int s = sizelist[i]; + if (s) { + int c = next_code[s] - z->firstcode[s] + z->firstsymbol[s]; + z->size[c] = (uint8)s; + z->value[c] = (uint16)i; + if (s <= ZFAST_BITS) { + int k = bit_reverse(next_code[s],s); + while (k < (1 << ZFAST_BITS)) { + z->fast[k] = (uint16) c; + k += (1 << s); + } + } + ++next_code[s]; + } + } + return 1; +} + +// zlib-from-memory implementation for PNG reading +// because PNG allows splitting the zlib stream arbitrarily, +// and it's annoying structurally to have PNG call ZLIB call PNG, +// we require PNG read all the IDATs and combine them into a single +// memory buffer + +typedef struct +{ + uint8 *zbuffer, *zbuffer_end; + int num_bits; + uint32 code_buffer; + + char *zout; + char *zout_start; + char *zout_end; + int z_expandable; + + zhuffman z_length, z_distance; +} zbuf; + +stbi_inline static int zget8(zbuf *z) +{ + if (z->zbuffer >= z->zbuffer_end) return 0; + return *z->zbuffer++; +} + +static void fill_bits(zbuf *z) +{ + do { + assert(z->code_buffer < (1U << z->num_bits)); + z->code_buffer |= zget8(z) << z->num_bits; + z->num_bits += 8; + } while (z->num_bits <= 24); +} + +stbi_inline static unsigned int zreceive(zbuf *z, int n) +{ + unsigned int k; + if (z->num_bits < n) fill_bits(z); + k = z->code_buffer & ((1 << n) - 1); + z->code_buffer >>= n; + z->num_bits -= n; + return k; +} + +stbi_inline static int zhuffman_decode(zbuf *a, zhuffman *z) +{ + int b,s,k; + if (a->num_bits < 16) fill_bits(a); + b = z->fast[a->code_buffer & ZFAST_MASK]; + if (b < 0xffff) { + s = z->size[b]; + a->code_buffer >>= s; + a->num_bits -= s; + return z->value[b]; + } + + // not resolved by fast table, so compute it the slow way + // use jpeg approach, which requires MSbits at top + k = bit_reverse(a->code_buffer, 16); + for (s=ZFAST_BITS+1; ; ++s) + if (k < z->maxcode[s]) + break; + if (s == 16) return -1; // invalid code! + // code size is s, so: + b = (k >> (16-s)) - z->firstcode[s] + z->firstsymbol[s]; + assert(z->size[b] == s); + a->code_buffer >>= s; + a->num_bits -= s; + return z->value[b]; +} + +static int expand(zbuf *z, int n) // need to make room for n bytes +{ + char *q; + int cur, limit; + if (!z->z_expandable) return e("output buffer limit","Corrupt PNG"); + cur = (int) (z->zout - z->zout_start); + limit = (int) (z->zout_end - z->zout_start); + while (cur + n > limit) + limit *= 2; + q = (char *) realloc(z->zout_start, limit); + if (q == NULL) return e("outofmem", "Out of memory"); + z->zout_start = q; + z->zout = q + cur; + z->zout_end = q + limit; + return 1; +} + +static int length_base[31] = { + 3,4,5,6,7,8,9,10,11,13, + 15,17,19,23,27,31,35,43,51,59, + 67,83,99,115,131,163,195,227,258,0,0 }; + +static int length_extra[31]= +{ 0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,0,0 }; + +static int dist_base[32] = { 1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193, +257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577,0,0}; + +static int dist_extra[32] = +{ 0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; + +static int parse_huffman_block(zbuf *a) +{ + for(;;) { + int z = zhuffman_decode(a, &a->z_length); + if (z < 256) { + if (z < 0) return e("bad huffman code","Corrupt PNG"); // error in huffman codes + if (a->zout >= a->zout_end) if (!expand(a, 1)) return 0; + *a->zout++ = (char) z; + } else { + uint8 *p; + int len,dist; + if (z == 256) return 1; + z -= 257; + len = length_base[z]; + if (length_extra[z]) len += zreceive(a, length_extra[z]); + z = zhuffman_decode(a, &a->z_distance); + if (z < 0) return e("bad huffman code","Corrupt PNG"); + dist = dist_base[z]; + if (dist_extra[z]) dist += zreceive(a, dist_extra[z]); + if (a->zout - a->zout_start < dist) return e("bad dist","Corrupt PNG"); + if (a->zout + len > a->zout_end) if (!expand(a, len)) return 0; + p = (uint8 *) (a->zout - dist); + while (len--) + *a->zout++ = *p++; + } + } +} + +static int compute_huffman_codes(zbuf *a) +{ + static uint8 length_dezigzag[19] = { 16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15 }; + zhuffman z_codelength; + uint8 lencodes[286+32+137];//padding for maximum single op + uint8 codelength_sizes[19]; + int i,n; + + int hlit = zreceive(a,5) + 257; + int hdist = zreceive(a,5) + 1; + int hclen = zreceive(a,4) + 4; + + memset(codelength_sizes, 0, sizeof(codelength_sizes)); + for (i=0; i < hclen; ++i) { + int s = zreceive(a,3); + codelength_sizes[length_dezigzag[i]] = (uint8) s; + } + if (!zbuild_huffman(&z_codelength, codelength_sizes, 19)) return 0; + + n = 0; + while (n < hlit + hdist) { + int c = zhuffman_decode(a, &z_codelength); + assert(c >= 0 && c < 19); + if (c < 16) + lencodes[n++] = (uint8) c; + else if (c == 16) { + c = zreceive(a,2)+3; + memset(lencodes+n, lencodes[n-1], c); + n += c; + } else if (c == 17) { + c = zreceive(a,3)+3; + memset(lencodes+n, 0, c); + n += c; + } else { + assert(c == 18); + c = zreceive(a,7)+11; + memset(lencodes+n, 0, c); + n += c; + } + } + if (n != hlit+hdist) return e("bad codelengths","Corrupt PNG"); + if (!zbuild_huffman(&a->z_length, lencodes, hlit)) return 0; + if (!zbuild_huffman(&a->z_distance, lencodes+hlit, hdist)) return 0; + return 1; +} + +static int parse_uncompressed_block(zbuf *a) +{ + uint8 header[4]; + int len,nlen,k; + if (a->num_bits & 7) + zreceive(a, a->num_bits & 7); // discard + // drain the bit-packed data into header + k = 0; + while (a->num_bits > 0) { + header[k++] = (uint8) (a->code_buffer & 255); // wtf this warns? + a->code_buffer >>= 8; + a->num_bits -= 8; + } + assert(a->num_bits == 0); + // now fill header the normal way + while (k < 4) + header[k++] = (uint8) zget8(a); + len = header[1] * 256 + header[0]; + nlen = header[3] * 256 + header[2]; + if (nlen != (len ^ 0xffff)) return e("zlib corrupt","Corrupt PNG"); + if (a->zbuffer + len > a->zbuffer_end) return e("read past buffer","Corrupt PNG"); + if (a->zout + len > a->zout_end) + if (!expand(a, len)) return 0; + memcpy(a->zout, a->zbuffer, len); + a->zbuffer += len; + a->zout += len; + return 1; +} + +static int parse_zlib_header(zbuf *a) +{ + int cmf = zget8(a); + int cm = cmf & 15; + /* int cinfo = cmf >> 4; */ + int flg = zget8(a); + if ((cmf*256+flg) % 31 != 0) return e("bad zlib header","Corrupt PNG"); // zlib spec + if (flg & 32) return e("no preset dict","Corrupt PNG"); // preset dictionary not allowed in png + if (cm != 8) return e("bad compression","Corrupt PNG"); // DEFLATE required for png + // window = 1 << (8 + cinfo)... but who cares, we fully buffer output + return 1; +} + +// @TODO: should statically initialize these for optimal thread safety +static uint8 default_length[288], default_distance[32]; +static void init_defaults(void) +{ + int i; // use <= to match clearly with spec + for (i=0; i <= 143; ++i) default_length[i] = 8; + for ( ; i <= 255; ++i) default_length[i] = 9; + for ( ; i <= 279; ++i) default_length[i] = 7; + for ( ; i <= 287; ++i) default_length[i] = 8; + + for (i=0; i <= 31; ++i) default_distance[i] = 5; +} + +int stbi_png_partial; // a quick hack to only allow decoding some of a PNG... I should implement real streaming support instead +static int parse_zlib(zbuf *a, int parse_header) +{ + int final, type; + if (parse_header) + if (!parse_zlib_header(a)) return 0; + a->num_bits = 0; + a->code_buffer = 0; + do { + final = zreceive(a,1); + type = zreceive(a,2); + if (type == 0) { + if (!parse_uncompressed_block(a)) return 0; + } else if (type == 3) { + return 0; + } else { + if (type == 1) { + // use fixed code lengths + if (!default_distance[31]) init_defaults(); + if (!zbuild_huffman(&a->z_length , default_length , 288)) return 0; + if (!zbuild_huffman(&a->z_distance, default_distance, 32)) return 0; + } else { + if (!compute_huffman_codes(a)) return 0; + } + if (!parse_huffman_block(a)) return 0; + } + if (stbi_png_partial && a->zout - a->zout_start > 65536) + break; + } while (!final); + return 1; +} + +static int do_zlib(zbuf *a, char *obuf, int olen, int exp, int parse_header) +{ + a->zout_start = obuf; + a->zout = obuf; + a->zout_end = obuf + olen; + a->z_expandable = exp; + + return parse_zlib(a, parse_header); +} + +char *stbi_zlib_decode_malloc_guesssize(const char *buffer, int len, int initial_size, int *outlen) +{ + zbuf a; + char *p = (char *) malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (uint8 *) buffer; + a.zbuffer_end = (uint8 *) buffer + len; + if (do_zlib(&a, p, initial_size, 1, 1)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + free(a.zout_start); + return NULL; + } +} + +char *stbi_zlib_decode_malloc(char const *buffer, int len, int *outlen) +{ + return stbi_zlib_decode_malloc_guesssize(buffer, len, 16384, outlen); +} + +char *stbi_zlib_decode_malloc_guesssize_headerflag(const char *buffer, int len, int initial_size, int *outlen, int parse_header) +{ + zbuf a; + char *p = (char *) malloc(initial_size); + if (p == NULL) return NULL; + a.zbuffer = (uint8 *) buffer; + a.zbuffer_end = (uint8 *) buffer + len; + if (do_zlib(&a, p, initial_size, 1, parse_header)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + free(a.zout_start); + return NULL; + } +} + +int stbi_zlib_decode_buffer(char *obuffer, int olen, char const *ibuffer, int ilen) +{ + zbuf a; + a.zbuffer = (uint8 *) ibuffer; + a.zbuffer_end = (uint8 *) ibuffer + ilen; + if (do_zlib(&a, obuffer, olen, 0, 1)) + return (int) (a.zout - a.zout_start); + else + return -1; +} + +char *stbi_zlib_decode_noheader_malloc(char const *buffer, int len, int *outlen) +{ + zbuf a; + char *p = (char *) malloc(16384); + if (p == NULL) return NULL; + a.zbuffer = (uint8 *) buffer; + a.zbuffer_end = (uint8 *) buffer+len; + if (do_zlib(&a, p, 16384, 1, 0)) { + if (outlen) *outlen = (int) (a.zout - a.zout_start); + return a.zout_start; + } else { + free(a.zout_start); + return NULL; + } +} + +int stbi_zlib_decode_noheader_buffer(char *obuffer, int olen, const char *ibuffer, int ilen) +{ + zbuf a; + a.zbuffer = (uint8 *) ibuffer; + a.zbuffer_end = (uint8 *) ibuffer + ilen; + if (do_zlib(&a, obuffer, olen, 0, 0)) + return (int) (a.zout - a.zout_start); + else + return -1; +} + +// public domain "baseline" PNG decoder v0.10 Sean Barrett 2006-11-18 +// simple implementation +// - only 8-bit samples +// - no CRC checking +// - allocates lots of intermediate memory +// - avoids problem of streaming data between subsystems +// - avoids explicit window management +// performance +// - uses stb_zlib, a PD zlib implementation with fast huffman decoding + + +typedef struct +{ + uint32 length; + uint32 type; +} chunk; + +#define PNG_TYPE(a,b,c,d) (((a) << 24) + ((b) << 16) + ((c) << 8) + (d)) + +static chunk get_chunk_header(stbi *s) +{ + chunk c; + c.length = get32(s); + c.type = get32(s); + return c; +} + +static int check_png_header(stbi *s) +{ + static uint8 png_sig[8] = { 137,80,78,71,13,10,26,10 }; + int i; + for (i=0; i < 8; ++i) + if (get8u(s) != png_sig[i]) return e("bad png sig","Not a PNG"); + return 1; +} + +typedef struct +{ + stbi *s; + uint8 *idata, *expanded, *out; +} png; + + +enum { + F_none=0, F_sub=1, F_up=2, F_avg=3, F_paeth=4, + F_avg_first, F_paeth_first +}; + +static uint8 first_row_filter[5] = +{ + F_none, F_sub, F_none, F_avg_first, F_paeth_first +}; + +static int paeth(int a, int b, int c) +{ + int p = a + b - c; + int pa = abs(p-a); + int pb = abs(p-b); + int pc = abs(p-c); + if (pa <= pb && pa <= pc) return a; + if (pb <= pc) return b; + return c; +} + +// create the png data from post-deflated data +static int create_png_image_raw(png *a, uint8 *raw, uint32 raw_len, int out_n, uint32 x, uint32 y) +{ + stbi *s = a->s; + uint32 i,j,stride = x*out_n; + int k; + int img_n = s->img_n; // copy it into a local for later + assert(out_n == s->img_n || out_n == s->img_n+1); + if (stbi_png_partial) y = 1; + a->out = (uint8 *) malloc(x * y * out_n); + if (!a->out) return e("outofmem", "Out of memory"); + if (!stbi_png_partial) { + if (s->img_x == x && s->img_y == y) { + if (raw_len != (img_n * x + 1) * y) return e("not enough pixels","Corrupt PNG"); + } else { // interlaced: + if (raw_len < (img_n * x + 1) * y) return e("not enough pixels","Corrupt PNG"); + } + } + for (j=0; j < y; ++j) { + uint8 *cur = a->out + stride*j; + uint8 *prior = cur - stride; + int filter = *raw++; + if (filter > 4) return e("invalid filter","Corrupt PNG"); + // if first row, use special filter that doesn't sample previous row + if (j == 0) filter = first_row_filter[filter]; + // handle first pixel explicitly + for (k=0; k < img_n; ++k) { + switch (filter) { + case F_none : cur[k] = raw[k]; break; + case F_sub : cur[k] = raw[k]; break; + case F_up : cur[k] = raw[k] + prior[k]; break; + case F_avg : cur[k] = raw[k] + (prior[k]>>1); break; + case F_paeth : cur[k] = (uint8) (raw[k] + paeth(0,prior[k],0)); break; + case F_avg_first : cur[k] = raw[k]; break; + case F_paeth_first: cur[k] = raw[k]; break; + } + } + if (img_n != out_n) cur[img_n] = 255; + raw += img_n; + cur += out_n; + prior += out_n; + // this is a little gross, so that we don't switch per-pixel or per-component + if (img_n == out_n) { + #define CASE(f) \ + case f: \ + for (i=x-1; i >= 1; --i, raw+=img_n,cur+=img_n,prior+=img_n) \ + for (k=0; k < img_n; ++k) + switch (filter) { + CASE(F_none) cur[k] = raw[k]; break; + CASE(F_sub) cur[k] = raw[k] + cur[k-img_n]; break; + CASE(F_up) cur[k] = raw[k] + prior[k]; break; + CASE(F_avg) cur[k] = raw[k] + ((prior[k] + cur[k-img_n])>>1); break; + CASE(F_paeth) cur[k] = (uint8) (raw[k] + paeth(cur[k-img_n],prior[k],prior[k-img_n])); break; + CASE(F_avg_first) cur[k] = raw[k] + (cur[k-img_n] >> 1); break; + CASE(F_paeth_first) cur[k] = (uint8) (raw[k] + paeth(cur[k-img_n],0,0)); break; + } + #undef CASE + } else { + assert(img_n+1 == out_n); + #define CASE(f) \ + case f: \ + for (i=x-1; i >= 1; --i, cur[img_n]=255,raw+=img_n,cur+=out_n,prior+=out_n) \ + for (k=0; k < img_n; ++k) + switch (filter) { + CASE(F_none) cur[k] = raw[k]; break; + CASE(F_sub) cur[k] = raw[k] + cur[k-out_n]; break; + CASE(F_up) cur[k] = raw[k] + prior[k]; break; + CASE(F_avg) cur[k] = raw[k] + ((prior[k] + cur[k-out_n])>>1); break; + CASE(F_paeth) cur[k] = (uint8) (raw[k] + paeth(cur[k-out_n],prior[k],prior[k-out_n])); break; + CASE(F_avg_first) cur[k] = raw[k] + (cur[k-out_n] >> 1); break; + CASE(F_paeth_first) cur[k] = (uint8) (raw[k] + paeth(cur[k-out_n],0,0)); break; + } + #undef CASE + } + } + return 1; +} + +static int create_png_image(png *a, uint8 *raw, uint32 raw_len, int out_n, int interlaced) +{ + uint8 *final; + int p; + int save; + if (!interlaced) + return create_png_image_raw(a, raw, raw_len, out_n, a->s->img_x, a->s->img_y); + save = stbi_png_partial; + stbi_png_partial = 0; + + // de-interlacing + final = (uint8 *) malloc(a->s->img_x * a->s->img_y * out_n); + for (p=0; p < 7; ++p) { + int xorig[] = { 0,4,0,2,0,1,0 }; + int yorig[] = { 0,0,4,0,2,0,1 }; + int xspc[] = { 8,8,4,4,2,2,1 }; + int yspc[] = { 8,8,8,4,4,2,2 }; + int i,j,x,y; + // pass1_x[4] = 0, pass1_x[5] = 1, pass1_x[12] = 1 + x = (a->s->img_x - xorig[p] + xspc[p]-1) / xspc[p]; + y = (a->s->img_y - yorig[p] + yspc[p]-1) / yspc[p]; + if (x && y) { + if (!create_png_image_raw(a, raw, raw_len, out_n, x, y)) { + free(final); + return 0; + } + for (j=0; j < y; ++j) + for (i=0; i < x; ++i) + memcpy(final + (j*yspc[p]+yorig[p])*a->s->img_x*out_n + (i*xspc[p]+xorig[p])*out_n, + a->out + (j*x+i)*out_n, out_n); + free(a->out); + raw += (x*out_n+1)*y; + raw_len -= (x*out_n+1)*y; + } + } + a->out = final; + + stbi_png_partial = save; + return 1; +} + +static int compute_transparency(png *z, uint8 tc[3], int out_n) +{ + stbi *s = z->s; + uint32 i, pixel_count = s->img_x * s->img_y; + uint8 *p = z->out; + + // compute color-based transparency, assuming we've + // already got 255 as the alpha value in the output + assert(out_n == 2 || out_n == 4); + + if (out_n == 2) { + for (i=0; i < pixel_count; ++i) { + p[1] = (p[0] == tc[0] ? 0 : 255); + p += 2; + } + } else { + for (i=0; i < pixel_count; ++i) { + if (p[0] == tc[0] && p[1] == tc[1] && p[2] == tc[2]) + p[3] = 0; + p += 4; + } + } + return 1; +} + +static int expand_palette(png *a, uint8 *palette, int len, int pal_img_n) +{ + uint32 i, pixel_count = a->s->img_x * a->s->img_y; + uint8 *p, *temp_out, *orig = a->out; + + p = (uint8 *) malloc(pixel_count * pal_img_n); + if (p == NULL) return e("outofmem", "Out of memory"); + + // between here and free(out) below, exitting would leak + temp_out = p; + + if (pal_img_n == 3) { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p += 3; + } + } else { + for (i=0; i < pixel_count; ++i) { + int n = orig[i]*4; + p[0] = palette[n ]; + p[1] = palette[n+1]; + p[2] = palette[n+2]; + p[3] = palette[n+3]; + p += 4; + } + } + free(a->out); + a->out = temp_out; + + STBI_NOTUSED(len); + + return 1; +} + +static int stbi_unpremultiply_on_load = 0; +static int stbi_de_iphone_flag = 0; + +void stbi_set_unpremultiply_on_load(int flag_true_if_should_unpremultiply) +{ + stbi_unpremultiply_on_load = flag_true_if_should_unpremultiply; +} +void stbi_convert_iphone_png_to_rgb(int flag_true_if_should_convert) +{ + stbi_de_iphone_flag = flag_true_if_should_convert; +} + +static void stbi_de_iphone(png *z) +{ + stbi *s = z->s; + uint32 i, pixel_count = s->img_x * s->img_y; + uint8 *p = z->out; + + if (s->img_out_n == 3) { // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + uint8 t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 3; + } + } else { + assert(s->img_out_n == 4); + if (stbi_unpremultiply_on_load) { + // convert bgr to rgb and unpremultiply + for (i=0; i < pixel_count; ++i) { + uint8 a = p[3]; + uint8 t = p[0]; + if (a) { + p[0] = p[2] * 255 / a; + p[1] = p[1] * 255 / a; + p[2] = t * 255 / a; + } else { + p[0] = p[2]; + p[2] = t; + } + p += 4; + } + } else { + // convert bgr to rgb + for (i=0; i < pixel_count; ++i) { + uint8 t = p[0]; + p[0] = p[2]; + p[2] = t; + p += 4; + } + } + } +} + +static int parse_png_file(png *z, int scan, int req_comp) +{ + uint8 palette[1024], pal_img_n=0; + uint8 has_trans=0, tc[3]; + uint32 ioff=0, idata_limit=0, i, pal_len=0; + int first=1,k,interlace=0, iphone=0; + stbi *s = z->s; + + z->expanded = NULL; + z->idata = NULL; + z->out = NULL; + + if (!check_png_header(s)) return 0; + + if (scan == SCAN_type) return 1; + + for (;;) { + chunk c = get_chunk_header(s); + switch (c.type) { + case PNG_TYPE('C','g','B','I'): + iphone = stbi_de_iphone_flag; + skip(s, c.length); + break; + case PNG_TYPE('I','H','D','R'): { + int depth,color,comp,filter; + if (!first) return e("multiple IHDR","Corrupt PNG"); + first = 0; + if (c.length != 13) return e("bad IHDR len","Corrupt PNG"); + s->img_x = get32(s); if (s->img_x > (1 << 24)) return e("too large","Very large image (corrupt?)"); + s->img_y = get32(s); if (s->img_y > (1 << 24)) return e("too large","Very large image (corrupt?)"); + depth = get8(s); if (depth != 8) return e("8bit only","PNG not supported: 8-bit only"); + color = get8(s); if (color > 6) return e("bad ctype","Corrupt PNG"); + if (color == 3) pal_img_n = 3; else if (color & 1) return e("bad ctype","Corrupt PNG"); + comp = get8(s); if (comp) return e("bad comp method","Corrupt PNG"); + filter= get8(s); if (filter) return e("bad filter method","Corrupt PNG"); + interlace = get8(s); if (interlace>1) return e("bad interlace method","Corrupt PNG"); + if (!s->img_x || !s->img_y) return e("0-pixel image","Corrupt PNG"); + if (!pal_img_n) { + s->img_n = (color & 2 ? 3 : 1) + (color & 4 ? 1 : 0); + if ((1 << 30) / s->img_x / s->img_n < s->img_y) return e("too large", "Image too large to decode"); + if (scan == SCAN_header) return 1; + } else { + // if paletted, then pal_n is our final components, and + // img_n is # components to decompress/filter. + s->img_n = 1; + if ((1 << 30) / s->img_x / 4 < s->img_y) return e("too large","Corrupt PNG"); + // if SCAN_header, have to scan to see if we have a tRNS + } + break; + } + + case PNG_TYPE('P','L','T','E'): { + if (first) return e("first not IHDR", "Corrupt PNG"); + if (c.length > 256*3) return e("invalid PLTE","Corrupt PNG"); + pal_len = c.length / 3; + if (pal_len * 3 != c.length) return e("invalid PLTE","Corrupt PNG"); + for (i=0; i < pal_len; ++i) { + palette[i*4+0] = get8u(s); + palette[i*4+1] = get8u(s); + palette[i*4+2] = get8u(s); + palette[i*4+3] = 255; + } + break; + } + + case PNG_TYPE('t','R','N','S'): { + if (first) return e("first not IHDR", "Corrupt PNG"); + if (z->idata) return e("tRNS after IDAT","Corrupt PNG"); + if (pal_img_n) { + if (scan == SCAN_header) { s->img_n = 4; return 1; } + if (pal_len == 0) return e("tRNS before PLTE","Corrupt PNG"); + if (c.length > pal_len) return e("bad tRNS len","Corrupt PNG"); + pal_img_n = 4; + for (i=0; i < c.length; ++i) + palette[i*4+3] = get8u(s); + } else { + if (!(s->img_n & 1)) return e("tRNS with alpha","Corrupt PNG"); + if (c.length != (uint32) s->img_n*2) return e("bad tRNS len","Corrupt PNG"); + has_trans = 1; + for (k=0; k < s->img_n; ++k) + tc[k] = (uint8) get16(s); // non 8-bit images will be larger + } + break; + } + + case PNG_TYPE('I','D','A','T'): { + if (first) return e("first not IHDR", "Corrupt PNG"); + if (pal_img_n && !pal_len) return e("no PLTE","Corrupt PNG"); + if (scan == SCAN_header) { s->img_n = pal_img_n; return 1; } + if (ioff + c.length > idata_limit) { + uint8 *p; + if (idata_limit == 0) idata_limit = c.length > 4096 ? c.length : 4096; + while (ioff + c.length > idata_limit) + idata_limit *= 2; + p = (uint8 *) realloc(z->idata, idata_limit); if (p == NULL) return e("outofmem", "Out of memory"); + z->idata = p; + } + if (!getn(s, z->idata+ioff,c.length)) return e("outofdata","Corrupt PNG"); + ioff += c.length; + break; + } + + case PNG_TYPE('I','E','N','D'): { + uint32 raw_len; + if (first) return e("first not IHDR", "Corrupt PNG"); + if (scan != SCAN_load) return 1; + if (z->idata == NULL) return e("no IDAT","Corrupt PNG"); + z->expanded = (uint8 *) stbi_zlib_decode_malloc_guesssize_headerflag((char *) z->idata, ioff, 16384, (int *) &raw_len, !iphone); + if (z->expanded == NULL) return 0; // zlib should set error + free(z->idata); z->idata = NULL; + if ((req_comp == s->img_n+1 && req_comp != 3 && !pal_img_n) || has_trans) + s->img_out_n = s->img_n+1; + else + s->img_out_n = s->img_n; + if (!create_png_image(z, z->expanded, raw_len, s->img_out_n, interlace)) return 0; + if (has_trans) + if (!compute_transparency(z, tc, s->img_out_n)) return 0; + if (iphone && s->img_out_n > 2) + stbi_de_iphone(z); + if (pal_img_n) { + // pal_img_n == 3 or 4 + s->img_n = pal_img_n; // record the actual colors we had + s->img_out_n = pal_img_n; + if (req_comp >= 3) s->img_out_n = req_comp; + if (!expand_palette(z, palette, pal_len, s->img_out_n)) + return 0; + } + free(z->expanded); z->expanded = NULL; + return 1; + } + + default: + // if critical, fail + if (first) return e("first not IHDR", "Corrupt PNG"); + if ((c.type & (1 << 29)) == 0) { + #ifndef STBI_NO_FAILURE_STRINGS + // not threadsafe + static char invalid_chunk[] = "XXXX chunk not known"; + invalid_chunk[0] = (uint8) (c.type >> 24); + invalid_chunk[1] = (uint8) (c.type >> 16); + invalid_chunk[2] = (uint8) (c.type >> 8); + invalid_chunk[3] = (uint8) (c.type >> 0); + #endif + return e(invalid_chunk, "PNG not supported: unknown chunk type"); + } + skip(s, c.length); + break; + } + // end of chunk, read and skip CRC + get32(s); + } +} + +static unsigned char *do_png(png *p, int *x, int *y, int *n, int req_comp) +{ + unsigned char *result=NULL; + if (req_comp < 0 || req_comp > 4) return epuc("bad req_comp", "Internal error"); + if (parse_png_file(p, SCAN_load, req_comp)) { + result = p->out; + p->out = NULL; + if (req_comp && req_comp != p->s->img_out_n) { + result = convert_format(result, p->s->img_out_n, req_comp, p->s->img_x, p->s->img_y); + p->s->img_out_n = req_comp; + if (result == NULL) return result; + } + *x = p->s->img_x; + *y = p->s->img_y; + if (n) *n = p->s->img_n; + } + free(p->out); p->out = NULL; + free(p->expanded); p->expanded = NULL; + free(p->idata); p->idata = NULL; + + return result; +} + +static unsigned char *stbi_png_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + png p; + p.s = s; + return do_png(&p, x,y,comp,req_comp); +} + +static int stbi_png_test(stbi *s) +{ + int r; + r = check_png_header(s); + stbi_rewind(s); + return r; +} + +static int stbi_png_info_raw(png *p, int *x, int *y, int *comp) +{ + if (!parse_png_file(p, SCAN_header, 0)) { + stbi_rewind( p->s ); + return 0; + } + if (x) *x = p->s->img_x; + if (y) *y = p->s->img_y; + if (comp) *comp = p->s->img_n; + return 1; +} + +static int stbi_png_info(stbi *s, int *x, int *y, int *comp) +{ + png p; + p.s = s; + return stbi_png_info_raw(&p, x, y, comp); +} + +// Microsoft/Windows BMP image + +static int bmp_test(stbi *s) +{ + int sz; + if (get8(s) != 'B') return 0; + if (get8(s) != 'M') return 0; + get32le(s); // discard filesize + get16le(s); // discard reserved + get16le(s); // discard reserved + get32le(s); // discard data offset + sz = get32le(s); + if (sz == 12 || sz == 40 || sz == 56 || sz == 108) return 1; + return 0; +} + +static int stbi_bmp_test(stbi *s) +{ + int r = bmp_test(s); + stbi_rewind(s); + return r; +} + + +// returns 0..31 for the highest set bit +static int high_bit(unsigned int z) +{ + int n=0; + if (z == 0) return -1; + if (z >= 0x10000) n += 16, z >>= 16; + if (z >= 0x00100) n += 8, z >>= 8; + if (z >= 0x00010) n += 4, z >>= 4; + if (z >= 0x00004) n += 2, z >>= 2; + if (z >= 0x00002) n += 1, z >>= 1; + return n; +} + +static int bitcount(unsigned int a) +{ + a = (a & 0x55555555) + ((a >> 1) & 0x55555555); // max 2 + a = (a & 0x33333333) + ((a >> 2) & 0x33333333); // max 4 + a = (a + (a >> 4)) & 0x0f0f0f0f; // max 8 per 4, now 8 bits + a = (a + (a >> 8)); // max 16 per 8 bits + a = (a + (a >> 16)); // max 32 per 8 bits + return a & 0xff; +} + +static int shiftsigned(int v, int shift, int bits) +{ + int result; + int z=0; + + if (shift < 0) v <<= -shift; + else v >>= shift; + result = v; + + z = bits; + while (z < 8) { + result += v >> z; + z += bits; + } + return result; +} + +static stbi_uc *bmp_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + uint8 *out; + unsigned int mr=0,mg=0,mb=0,ma=0, fake_a=0; + stbi_uc pal[256][4]; + int psize=0,i,j,compress=0,width; + int bpp, flip_vertically, pad, target, offset, hsz; + if (get8(s) != 'B' || get8(s) != 'M') return epuc("not BMP", "Corrupt BMP"); + get32le(s); // discard filesize + get16le(s); // discard reserved + get16le(s); // discard reserved + offset = get32le(s); + hsz = get32le(s); + if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108) return epuc("unknown BMP", "BMP type not supported: unknown"); + if (hsz == 12) { + s->img_x = get16le(s); + s->img_y = get16le(s); + } else { + s->img_x = get32le(s); + s->img_y = get32le(s); + } + if (get16le(s) != 1) return epuc("bad BMP", "bad BMP"); + bpp = get16le(s); + if (bpp == 1) return epuc("monochrome", "BMP type not supported: 1-bit"); + flip_vertically = ((int) s->img_y) > 0; + s->img_y = abs((int) s->img_y); + if (hsz == 12) { + if (bpp < 24) + psize = (offset - 14 - 24) / 3; + } else { + compress = get32le(s); + if (compress == 1 || compress == 2) return epuc("BMP RLE", "BMP type not supported: RLE"); + get32le(s); // discard sizeof + get32le(s); // discard hres + get32le(s); // discard vres + get32le(s); // discard colorsused + get32le(s); // discard max important + if (hsz == 40 || hsz == 56) { + if (hsz == 56) { + get32le(s); + get32le(s); + get32le(s); + get32le(s); + } + if (bpp == 16 || bpp == 32) { + mr = mg = mb = 0; + if (compress == 0) { + if (bpp == 32) { + mr = 0xffu << 16; + mg = 0xffu << 8; + mb = 0xffu << 0; + ma = 0xffu << 24; + fake_a = 1; // @TODO: check for cases like alpha value is all 0 and switch it to 255 + } else { + mr = 31u << 10; + mg = 31u << 5; + mb = 31u << 0; + } + } else if (compress == 3) { + mr = get32le(s); + mg = get32le(s); + mb = get32le(s); + // not documented, but generated by photoshop and handled by mspaint + if (mr == mg && mg == mb) { + // ?!?!? + return epuc("bad BMP", "bad BMP"); + } + } else + return epuc("bad BMP", "bad BMP"); + } + } else { + assert(hsz == 108); + mr = get32le(s); + mg = get32le(s); + mb = get32le(s); + ma = get32le(s); + get32le(s); // discard color space + for (i=0; i < 12; ++i) + get32le(s); // discard color space parameters + } + if (bpp < 16) + psize = (offset - 14 - hsz) >> 2; + } + s->img_n = ma ? 4 : 3; + if (req_comp && req_comp >= 3) // we can directly decode 3 or 4 + target = req_comp; + else + target = s->img_n; // if they want monochrome, we'll post-convert + out = (stbi_uc *) malloc(target * s->img_x * s->img_y); + if (!out) return epuc("outofmem", "Out of memory"); + if (bpp < 16) { + int z=0; + if (psize == 0 || psize > 256) { free(out); return epuc("invalid", "Corrupt BMP"); } + for (i=0; i < psize; ++i) { + pal[i][2] = get8u(s); + pal[i][1] = get8u(s); + pal[i][0] = get8u(s); + if (hsz != 12) get8(s); + pal[i][3] = 255; + } + skip(s, offset - 14 - hsz - psize * (hsz == 12 ? 3 : 4)); + if (bpp == 4) width = (s->img_x + 1) >> 1; + else if (bpp == 8) width = s->img_x; + else { free(out); return epuc("bad bpp", "Corrupt BMP"); } + pad = (-width)&3; + for (j=0; j < (int) s->img_y; ++j) { + for (i=0; i < (int) s->img_x; i += 2) { + int v=get8(s),v2=0; + if (bpp == 4) { + v2 = v & 15; + v >>= 4; + } + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + if (i+1 == (int) s->img_x) break; + v = (bpp == 8) ? get8(s) : v2; + out[z++] = pal[v][0]; + out[z++] = pal[v][1]; + out[z++] = pal[v][2]; + if (target == 4) out[z++] = 255; + } + skip(s, pad); + } + } else { + int rshift=0,gshift=0,bshift=0,ashift=0,rcount=0,gcount=0,bcount=0,acount=0; + int z = 0; + int easy=0; + skip(s, offset - 14 - hsz); + if (bpp == 24) width = 3 * s->img_x; + else if (bpp == 16) width = 2*s->img_x; + else /* bpp = 32 and pad = 0 */ width=0; + pad = (-width) & 3; + if (bpp == 24) { + easy = 1; + } else if (bpp == 32) { + if (mb == 0xff && mg == 0xff00 && mr == 0x00ff0000 && ma == 0xff000000) + easy = 2; + } + if (!easy) { + if (!mr || !mg || !mb) { free(out); return epuc("bad masks", "Corrupt BMP"); } + // right shift amt to put high bit in position #7 + rshift = high_bit(mr)-7; rcount = bitcount(mr); + gshift = high_bit(mg)-7; gcount = bitcount(mr); + bshift = high_bit(mb)-7; bcount = bitcount(mr); + ashift = high_bit(ma)-7; acount = bitcount(mr); + } + for (j=0; j < (int) s->img_y; ++j) { + if (easy) { + for (i=0; i < (int) s->img_x; ++i) { + int a; + out[z+2] = get8u(s); + out[z+1] = get8u(s); + out[z+0] = get8u(s); + z += 3; + a = (easy == 2 ? get8(s) : 255); + if (target == 4) out[z++] = (uint8) a; + } + } else { + for (i=0; i < (int) s->img_x; ++i) { + uint32 v = (bpp == 16 ? get16le(s) : get32le(s)); + int a; + out[z++] = (uint8) shiftsigned(v & mr, rshift, rcount); + out[z++] = (uint8) shiftsigned(v & mg, gshift, gcount); + out[z++] = (uint8) shiftsigned(v & mb, bshift, bcount); + a = (ma ? shiftsigned(v & ma, ashift, acount) : 255); + if (target == 4) out[z++] = (uint8) a; + } + } + skip(s, pad); + } + } + if (flip_vertically) { + stbi_uc t; + for (j=0; j < (int) s->img_y>>1; ++j) { + stbi_uc *p1 = out + j *s->img_x*target; + stbi_uc *p2 = out + (s->img_y-1-j)*s->img_x*target; + for (i=0; i < (int) s->img_x*target; ++i) { + t = p1[i], p1[i] = p2[i], p2[i] = t; + } + } + } + + if (req_comp && req_comp != target) { + out = convert_format(out, target, req_comp, s->img_x, s->img_y); + if (out == NULL) return out; // convert_format frees input on failure + } + + *x = s->img_x; + *y = s->img_y; + if (comp) *comp = s->img_n; + return out; +} + +static stbi_uc *stbi_bmp_load(stbi *s,int *x, int *y, int *comp, int req_comp) +{ + return bmp_load(s, x,y,comp,req_comp); +} + + +// Targa Truevision - TGA +// by Jonathan Dummer + +static int tga_info(stbi *s, int *x, int *y, int *comp) +{ + int tga_w, tga_h, tga_comp; + int sz; + get8u(s); // discard Offset + sz = get8u(s); // color type + if( sz > 1 ) { + stbi_rewind(s); + return 0; // only RGB or indexed allowed + } + sz = get8u(s); // image type + // only RGB or grey allowed, +/- RLE + if ((sz != 1) && (sz != 2) && (sz != 3) && (sz != 9) && (sz != 10) && (sz != 11)) return 0; + skip(s,9); + tga_w = get16le(s); + if( tga_w < 1 ) { + stbi_rewind(s); + return 0; // test width + } + tga_h = get16le(s); + if( tga_h < 1 ) { + stbi_rewind(s); + return 0; // test height + } + sz = get8(s); // bits per pixel + // only RGB or RGBA or grey allowed + if ((sz != 8) && (sz != 16) && (sz != 24) && (sz != 32)) { + stbi_rewind(s); + return 0; + } + tga_comp = sz; + if (x) *x = tga_w; + if (y) *y = tga_h; + if (comp) *comp = tga_comp / 8; + return 1; // seems to have passed everything +} + +int stbi_tga_info(stbi *s, int *x, int *y, int *comp) +{ + return tga_info(s, x, y, comp); +} + +static int tga_test(stbi *s) +{ + int sz; + get8u(s); // discard Offset + sz = get8u(s); // color type + if ( sz > 1 ) return 0; // only RGB or indexed allowed + sz = get8u(s); // image type + if ( (sz != 1) && (sz != 2) && (sz != 3) && (sz != 9) && (sz != 10) && (sz != 11) ) return 0; // only RGB or grey allowed, +/- RLE + get16(s); // discard palette start + get16(s); // discard palette length + get8(s); // discard bits per palette color entry + get16(s); // discard x origin + get16(s); // discard y origin + if ( get16(s) < 1 ) return 0; // test width + if ( get16(s) < 1 ) return 0; // test height + sz = get8(s); // bits per pixel + if ( (sz != 8) && (sz != 16) && (sz != 24) && (sz != 32) ) return 0; // only RGB or RGBA or grey allowed + return 1; // seems to have passed everything +} + +static int stbi_tga_test(stbi *s) +{ + int res = tga_test(s); + stbi_rewind(s); + return res; +} + +static stbi_uc *tga_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + // read in the TGA header stuff + int tga_offset = get8u(s); + int tga_indexed = get8u(s); + int tga_image_type = get8u(s); + int tga_is_RLE = 0; + int tga_palette_start = get16le(s); + int tga_palette_len = get16le(s); + int tga_palette_bits = get8u(s); + int tga_x_origin = get16le(s); + int tga_y_origin = get16le(s); + int tga_width = get16le(s); + int tga_height = get16le(s); + int tga_bits_per_pixel = get8u(s); + int tga_inverted = get8u(s); + // image data + unsigned char *tga_data; + unsigned char *tga_palette = NULL; + int i, j; + unsigned char raw_data[4]; + unsigned char trans_data[4]; + int RLE_count = 0; + int RLE_repeating = 0; + int read_next_pixel = 1; + + // do a tiny bit of precessing + if ( tga_image_type >= 8 ) + { + tga_image_type -= 8; + tga_is_RLE = 1; + } + /* int tga_alpha_bits = tga_inverted & 15; */ + tga_inverted = 1 - ((tga_inverted >> 5) & 1); + + // error check + if ( //(tga_indexed) || + (tga_width < 1) || (tga_height < 1) || + (tga_image_type < 1) || (tga_image_type > 3) || + ((tga_bits_per_pixel != 8) && (tga_bits_per_pixel != 16) && + (tga_bits_per_pixel != 24) && (tga_bits_per_pixel != 32)) + ) + { + return NULL; // we don't report this as a bad TGA because we don't even know if it's TGA + } + + // If I'm paletted, then I'll use the number of bits from the palette + if ( tga_indexed ) + { + tga_bits_per_pixel = tga_palette_bits; + } + + // tga info + *x = tga_width; + *y = tga_height; + if ( (req_comp < 1) || (req_comp > 4) ) + { + // just use whatever the file was + req_comp = tga_bits_per_pixel / 8; + *comp = req_comp; + } else + { + // force a new number of components + *comp = tga_bits_per_pixel/8; + } + tga_data = (unsigned char*)malloc( tga_width * tga_height * req_comp ); + if (!tga_data) return epuc("outofmem", "Out of memory"); + + // skip to the data's starting position (offset usually = 0) + skip(s, tga_offset ); + // do I need to load a palette? + if ( tga_indexed ) + { + // any data to skip? (offset usually = 0) + skip(s, tga_palette_start ); + // load the palette + tga_palette = (unsigned char*)malloc( tga_palette_len * tga_palette_bits / 8 ); + if (!tga_palette) return epuc("outofmem", "Out of memory"); + if (!getn(s, tga_palette, tga_palette_len * tga_palette_bits / 8 )) { + free(tga_data); + free(tga_palette); + return epuc("bad palette", "Corrupt TGA"); + } + } + // load the data + trans_data[0] = trans_data[1] = trans_data[2] = trans_data[3] = 0; + for (i=0; i < tga_width * tga_height; ++i) + { + // if I'm in RLE mode, do I need to get a RLE chunk? + if ( tga_is_RLE ) + { + if ( RLE_count == 0 ) + { + // yep, get the next byte as a RLE command + int RLE_cmd = get8u(s); + RLE_count = 1 + (RLE_cmd & 127); + RLE_repeating = RLE_cmd >> 7; + read_next_pixel = 1; + } else if ( !RLE_repeating ) + { + read_next_pixel = 1; + } + } else + { + read_next_pixel = 1; + } + // OK, if I need to read a pixel, do it now + if ( read_next_pixel ) + { + // load however much data we did have + if ( tga_indexed ) + { + // read in 1 byte, then perform the lookup + int pal_idx = get8u(s); + if ( pal_idx >= tga_palette_len ) + { + // invalid index + pal_idx = 0; + } + pal_idx *= tga_bits_per_pixel / 8; + for (j = 0; j*8 < tga_bits_per_pixel; ++j) + { + raw_data[j] = tga_palette[pal_idx+j]; + } + } else + { + // read in the data raw + for (j = 0; j*8 < tga_bits_per_pixel; ++j) + { + raw_data[j] = get8u(s); + } + } + // convert raw to the intermediate format + switch (tga_bits_per_pixel) + { + case 8: + // Luminous => RGBA + trans_data[0] = raw_data[0]; + trans_data[1] = raw_data[0]; + trans_data[2] = raw_data[0]; + trans_data[3] = 255; + break; + case 16: + // Luminous,Alpha => RGBA + trans_data[0] = raw_data[0]; + trans_data[1] = raw_data[0]; + trans_data[2] = raw_data[0]; + trans_data[3] = raw_data[1]; + break; + case 24: + // BGR => RGBA + trans_data[0] = raw_data[2]; + trans_data[1] = raw_data[1]; + trans_data[2] = raw_data[0]; + trans_data[3] = 255; + break; + case 32: + // BGRA => RGBA + trans_data[0] = raw_data[2]; + trans_data[1] = raw_data[1]; + trans_data[2] = raw_data[0]; + trans_data[3] = raw_data[3]; + break; + } + // clear the reading flag for the next pixel + read_next_pixel = 0; + } // end of reading a pixel + // convert to final format + switch (req_comp) + { + case 1: + // RGBA => Luminance + tga_data[i*req_comp+0] = compute_y(trans_data[0],trans_data[1],trans_data[2]); + break; + case 2: + // RGBA => Luminance,Alpha + tga_data[i*req_comp+0] = compute_y(trans_data[0],trans_data[1],trans_data[2]); + tga_data[i*req_comp+1] = trans_data[3]; + break; + case 3: + // RGBA => RGB + tga_data[i*req_comp+0] = trans_data[0]; + tga_data[i*req_comp+1] = trans_data[1]; + tga_data[i*req_comp+2] = trans_data[2]; + break; + case 4: + // RGBA => RGBA + tga_data[i*req_comp+0] = trans_data[0]; + tga_data[i*req_comp+1] = trans_data[1]; + tga_data[i*req_comp+2] = trans_data[2]; + tga_data[i*req_comp+3] = trans_data[3]; + break; + } + // in case we're in RLE mode, keep counting down + --RLE_count; + } + // do I need to invert the image? + if ( tga_inverted ) + { + for (j = 0; j*2 < tga_height; ++j) + { + int index1 = j * tga_width * req_comp; + int index2 = (tga_height - 1 - j) * tga_width * req_comp; + for (i = tga_width * req_comp; i > 0; --i) + { + unsigned char temp = tga_data[index1]; + tga_data[index1] = tga_data[index2]; + tga_data[index2] = temp; + ++index1; + ++index2; + } + } + } + // clear my palette, if I had one + if ( tga_palette != NULL ) + { + free( tga_palette ); + } + // the things I do to get rid of an error message, and yet keep + // Microsoft's C compilers happy... [8^( + tga_palette_start = tga_palette_len = tga_palette_bits = + tga_x_origin = tga_y_origin = 0; + // OK, done + return tga_data; +} + +static stbi_uc *stbi_tga_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + return tga_load(s,x,y,comp,req_comp); +} + + +// ************************************************************************************************* +// Photoshop PSD loader -- PD by Thatcher Ulrich, integration by Nicolas Schulz, tweaked by STB + +static int psd_test(stbi *s) +{ + if (get32(s) != 0x38425053) return 0; // "8BPS" + else return 1; +} + +static int stbi_psd_test(stbi *s) +{ + int r = psd_test(s); + stbi_rewind(s); + return r; +} + +static stbi_uc *psd_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + int pixelCount; + int channelCount, compression; + int channel, i, count, len; + int w,h; + uint8 *out; + + // Check identifier + if (get32(s) != 0x38425053) // "8BPS" + return epuc("not PSD", "Corrupt PSD image"); + + // Check file type version. + if (get16(s) != 1) + return epuc("wrong version", "Unsupported version of PSD image"); + + // Skip 6 reserved bytes. + skip(s, 6 ); + + // Read the number of channels (R, G, B, A, etc). + channelCount = get16(s); + if (channelCount < 0 || channelCount > 16) + return epuc("wrong channel count", "Unsupported number of channels in PSD image"); + + // Read the rows and columns of the image. + h = get32(s); + w = get32(s); + + // Make sure the depth is 8 bits. + if (get16(s) != 8) + return epuc("unsupported bit depth", "PSD bit depth is not 8 bit"); + + // Make sure the color mode is RGB. + // Valid options are: + // 0: Bitmap + // 1: Grayscale + // 2: Indexed color + // 3: RGB color + // 4: CMYK color + // 7: Multichannel + // 8: Duotone + // 9: Lab color + if (get16(s) != 3) + return epuc("wrong color format", "PSD is not in RGB color format"); + + // Skip the Mode Data. (It's the palette for indexed color; other info for other modes.) + skip(s,get32(s) ); + + // Skip the image resources. (resolution, pen tool paths, etc) + skip(s, get32(s) ); + + // Skip the reserved data. + skip(s, get32(s) ); + + // Find out if the data is compressed. + // Known values: + // 0: no compression + // 1: RLE compressed + compression = get16(s); + if (compression > 1) + return epuc("bad compression", "PSD has an unknown compression format"); + + // Create the destination image. + out = (stbi_uc *) malloc(4 * w*h); + if (!out) return epuc("outofmem", "Out of memory"); + pixelCount = w*h; + + // Initialize the data to zero. + //memset( out, 0, pixelCount * 4 ); + + // Finally, the image data. + if (compression) { + // RLE as used by .PSD and .TIFF + // Loop until you get the number of unpacked bytes you are expecting: + // Read the next source byte into n. + // If n is between 0 and 127 inclusive, copy the next n+1 bytes literally. + // Else if n is between -127 and -1 inclusive, copy the next byte -n+1 times. + // Else if n is 128, noop. + // Endloop + + // The RLE-compressed data is preceeded by a 2-byte data count for each row in the data, + // which we're going to just skip. + skip(s, h * channelCount * 2 ); + + // Read the RLE data by channel. + for (channel = 0; channel < 4; channel++) { + uint8 *p; + + p = out+channel; + if (channel >= channelCount) { + // Fill this channel with default data. + for (i = 0; i < pixelCount; i++) *p = (channel == 3 ? 255 : 0), p += 4; + } else { + // Read the RLE data. + count = 0; + while (count < pixelCount) { + len = get8(s); + if (len == 128) { + // No-op. + } else if (len < 128) { + // Copy next len+1 bytes literally. + len++; + count += len; + while (len) { + *p = get8u(s); + p += 4; + len--; + } + } else if (len > 128) { + uint8 val; + // Next -len+1 bytes in the dest are replicated from next source byte. + // (Interpret len as a negative 8-bit int.) + len ^= 0x0FF; + len += 2; + val = get8u(s); + count += len; + while (len) { + *p = val; + p += 4; + len--; + } + } + } + } + } + + } else { + // We're at the raw image data. It's each channel in order (Red, Green, Blue, Alpha, ...) + // where each channel consists of an 8-bit value for each pixel in the image. + + // Read the data by channel. + for (channel = 0; channel < 4; channel++) { + uint8 *p; + + p = out + channel; + if (channel > channelCount) { + // Fill this channel with default data. + for (i = 0; i < pixelCount; i++) *p = channel == 3 ? 255 : 0, p += 4; + } else { + // Read the data. + for (i = 0; i < pixelCount; i++) + *p = get8u(s), p += 4; + } + } + } + + if (req_comp && req_comp != 4) { + out = convert_format(out, 4, req_comp, w, h); + if (out == NULL) return out; // convert_format frees input on failure + } + + if (comp) *comp = channelCount; + *y = h; + *x = w; + + return out; +} + +static stbi_uc *stbi_psd_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + return psd_load(s,x,y,comp,req_comp); +} + +// ************************************************************************************************* +// Softimage PIC loader +// by Tom Seddon +// +// See http://softimage.wiki.softimage.com/index.php/INFO:_PIC_file_format +// See http://ozviz.wasp.uwa.edu.au/~pbourke/dataformats/softimagepic/ + +static int pic_is4(stbi *s,const char *str) +{ + int i; + for (i=0; i<4; ++i) + if (get8(s) != (stbi_uc)str[i]) + return 0; + + return 1; +} + +static int pic_test(stbi *s) +{ + int i; + + if (!pic_is4(s,"\x53\x80\xF6\x34")) + return 0; + + for(i=0;i<84;++i) + get8(s); + + if (!pic_is4(s,"PICT")) + return 0; + + return 1; +} + +typedef struct +{ + stbi_uc size,type,channel; +} pic_packet_t; + +static stbi_uc *pic_readval(stbi *s, int channel, stbi_uc *dest) +{ + int mask=0x80, i; + + for (i=0; i<4; ++i, mask>>=1) { + if (channel & mask) { + if (at_eof(s)) return epuc("bad file","PIC file too short"); + dest[i]=get8u(s); + } + } + + return dest; +} + +static void pic_copyval(int channel,stbi_uc *dest,const stbi_uc *src) +{ + int mask=0x80,i; + + for (i=0;i<4; ++i, mask>>=1) + if (channel&mask) + dest[i]=src[i]; +} + +static stbi_uc *pic_load2(stbi *s,int width,int height,int *comp, stbi_uc *result) +{ + int act_comp=0,num_packets=0,y,chained; + pic_packet_t packets[10]; + + // this will (should...) cater for even some bizarre stuff like having data + // for the same channel in multiple packets. + do { + pic_packet_t *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return epuc("bad format","too many packets"); + + packet = &packets[num_packets++]; + + chained = get8(s); + packet->size = get8u(s); + packet->type = get8u(s); + packet->channel = get8u(s); + + act_comp |= packet->channel; + + if (at_eof(s)) return epuc("bad file","file too short (reading packets)"); + if (packet->size != 8) return epuc("bad format","packet isn't 8bpp"); + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); // has alpha channel? + + for(y=0; ytype) { + default: + return epuc("bad format","packet has bad compression type"); + + case 0: {//uncompressed + int x; + + for(x=0;xchannel,dest)) + return 0; + break; + } + + case 1://Pure RLE + { + int left=width, i; + + while (left>0) { + stbi_uc count,value[4]; + + count=get8u(s); + if (at_eof(s)) return epuc("bad file","file too short (pure read count)"); + + if (count > left) + count = (uint8) left; + + if (!pic_readval(s,packet->channel,value)) return 0; + + for(i=0; ichannel,dest,value); + left -= count; + } + } + break; + + case 2: {//Mixed RLE + int left=width; + while (left>0) { + int count = get8(s), i; + if (at_eof(s)) return epuc("bad file","file too short (mixed read count)"); + + if (count >= 128) { // Repeated + stbi_uc value[4]; + int i; + + if (count==128) + count = get16(s); + else + count -= 127; + if (count > left) + return epuc("bad file","scanline overrun"); + + if (!pic_readval(s,packet->channel,value)) + return 0; + + for(i=0;ichannel,dest,value); + } else { // Raw + ++count; + if (count>left) return epuc("bad file","scanline overrun"); + + for(i=0;ichannel,dest)) + return 0; + } + left-=count; + } + break; + } + } + } + } + + return result; +} + +static stbi_uc *pic_load(stbi *s,int *px,int *py,int *comp,int req_comp) +{ + stbi_uc *result; + int i, x,y; + + for (i=0; i<92; ++i) + get8(s); + + x = get16(s); + y = get16(s); + if (at_eof(s)) return epuc("bad file","file too short (pic header)"); + if ((1 << 28) / x < y) return epuc("too large", "Image too large to decode"); + + get32(s); //skip `ratio' + get16(s); //skip `fields' + get16(s); //skip `pad' + + // intermediate buffer is RGBA + result = (stbi_uc *) malloc(x*y*4); + memset(result, 0xff, x*y*4); + + if (!pic_load2(s,x,y,comp, result)) { + free(result); + result=0; + } + *px = x; + *py = y; + if (req_comp == 0) req_comp = *comp; + result=convert_format(result,4,req_comp,x,y); + + return result; +} + +static int stbi_pic_test(stbi *s) +{ + int r = pic_test(s); + stbi_rewind(s); + return r; +} + +static stbi_uc *stbi_pic_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + return pic_load(s,x,y,comp,req_comp); +} + +// ************************************************************************************************* +// GIF loader -- public domain by Jean-Marc Lienher -- simplified/shrunk by stb +typedef struct stbi_gif_lzw_struct { + int16 prefix; + uint8 first; + uint8 suffix; +} stbi_gif_lzw; + +typedef struct stbi_gif_struct +{ + int w,h; + stbi_uc *out; // output buffer (always 4 components) + int flags, bgindex, ratio, transparent, eflags; + uint8 pal[256][4]; + uint8 lpal[256][4]; + stbi_gif_lzw codes[4096]; + uint8 *color_table; + int parse, step; + int lflags; + int start_x, start_y; + int max_x, max_y; + int cur_x, cur_y; + int line_size; +} stbi_gif; + +static int gif_test(stbi *s) +{ + int sz; + if (get8(s) != 'G' || get8(s) != 'I' || get8(s) != 'F' || get8(s) != '8') return 0; + sz = get8(s); + if (sz != '9' && sz != '7') return 0; + if (get8(s) != 'a') return 0; + return 1; +} + +static int stbi_gif_test(stbi *s) +{ + int r = gif_test(s); + stbi_rewind(s); + return r; +} + +static void stbi_gif_parse_colortable(stbi *s, uint8 pal[256][4], int num_entries, int transp) +{ + int i; + for (i=0; i < num_entries; ++i) { + pal[i][2] = get8u(s); + pal[i][1] = get8u(s); + pal[i][0] = get8u(s); + pal[i][3] = transp ? 0 : 255; + } +} + +static int stbi_gif_header(stbi *s, stbi_gif *g, int *comp, int is_info) +{ + uint8 version; + if (get8(s) != 'G' || get8(s) != 'I' || get8(s) != 'F' || get8(s) != '8') + return e("not GIF", "Corrupt GIF"); + + version = get8u(s); + if (version != '7' && version != '9') return e("not GIF", "Corrupt GIF"); + if (get8(s) != 'a') return e("not GIF", "Corrupt GIF"); + + failure_reason = ""; + g->w = get16le(s); + g->h = get16le(s); + g->flags = get8(s); + g->bgindex = get8(s); + g->ratio = get8(s); + g->transparent = -1; + + if (comp != 0) *comp = 4; // can't actually tell whether it's 3 or 4 until we parse the comments + + if (is_info) return 1; + + if (g->flags & 0x80) + stbi_gif_parse_colortable(s,g->pal, 2 << (g->flags & 7), -1); + + return 1; +} + +static int stbi_gif_info_raw(stbi *s, int *x, int *y, int *comp) +{ + stbi_gif g; + if (!stbi_gif_header(s, &g, comp, 1)) { + stbi_rewind( s ); + return 0; + } + if (x) *x = g.w; + if (y) *y = g.h; + return 1; +} + +static void stbi_out_gif_code(stbi_gif *g, uint16 code) +{ + uint8 *p, *c; + + // recurse to decode the prefixes, since the linked-list is backwards, + // and working backwards through an interleaved image would be nasty + if (g->codes[code].prefix >= 0) + stbi_out_gif_code(g, g->codes[code].prefix); + + if (g->cur_y >= g->max_y) return; + + p = &g->out[g->cur_x + g->cur_y]; + c = &g->color_table[g->codes[code].suffix * 4]; + + if (c[3] >= 128) { + p[0] = c[2]; + p[1] = c[1]; + p[2] = c[0]; + p[3] = c[3]; + } + g->cur_x += 4; + + if (g->cur_x >= g->max_x) { + g->cur_x = g->start_x; + g->cur_y += g->step; + + while (g->cur_y >= g->max_y && g->parse > 0) { + g->step = (1 << g->parse) * g->line_size; + g->cur_y = g->start_y + (g->step >> 1); + --g->parse; + } + } +} + +static uint8 *stbi_process_gif_raster(stbi *s, stbi_gif *g) +{ + uint8 lzw_cs; + int32 len, code; + uint32 first; + int32 codesize, codemask, avail, oldcode, bits, valid_bits, clear; + stbi_gif_lzw *p; + + lzw_cs = get8u(s); + clear = 1 << lzw_cs; + first = 1; + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + bits = 0; + valid_bits = 0; + for (code = 0; code < clear; code++) { + g->codes[code].prefix = -1; + g->codes[code].first = (uint8) code; + g->codes[code].suffix = (uint8) code; + } + + // support no starting clear code + avail = clear+2; + oldcode = -1; + + len = 0; + for(;;) { + if (valid_bits < codesize) { + if (len == 0) { + len = get8(s); // start new block + if (len == 0) + return g->out; + } + --len; + bits |= (int32) get8(s) << valid_bits; + valid_bits += 8; + } else { + int32 code = bits & codemask; + bits >>= codesize; + valid_bits -= codesize; + // @OPTIMIZE: is there some way we can accelerate the non-clear path? + if (code == clear) { // clear code + codesize = lzw_cs + 1; + codemask = (1 << codesize) - 1; + avail = clear + 2; + oldcode = -1; + first = 0; + } else if (code == clear + 1) { // end of stream code + skip(s, len); + while ((len = get8(s)) > 0) + skip(s,len); + return g->out; + } else if (code <= avail) { + if (first) return epuc("no clear code", "Corrupt GIF"); + + if (oldcode >= 0) { + p = &g->codes[avail++]; + if (avail > 4096) return epuc("too many codes", "Corrupt GIF"); + p->prefix = (int16) oldcode; + p->first = g->codes[oldcode].first; + p->suffix = (code == avail) ? p->first : g->codes[code].first; + } else if (code == avail) + return epuc("illegal code in raster", "Corrupt GIF"); + + stbi_out_gif_code(g, (uint16) code); + + if ((avail & codemask) == 0 && avail <= 0x0FFF) { + codesize++; + codemask = (1 << codesize) - 1; + } + + oldcode = code; + } else { + return epuc("illegal code in raster", "Corrupt GIF"); + } + } + } +} + +static void stbi_fill_gif_background(stbi_gif *g) +{ + int i; + uint8 *c = g->pal[g->bgindex]; + // @OPTIMIZE: write a dword at a time + for (i = 0; i < g->w * g->h * 4; i += 4) { + uint8 *p = &g->out[i]; + p[0] = c[2]; + p[1] = c[1]; + p[2] = c[0]; + p[3] = c[3]; + } +} + +// this function is designed to support animated gifs, although stb_image doesn't support it +static uint8 *stbi_gif_load_next(stbi *s, stbi_gif *g, int *comp, int req_comp) +{ + int i; + uint8 *old_out = 0; + + if (g->out == 0) { + if (!stbi_gif_header(s, g, comp,0)) return 0; // failure_reason set by stbi_gif_header + g->out = (uint8 *) malloc(4 * g->w * g->h); + if (g->out == 0) return epuc("outofmem", "Out of memory"); + stbi_fill_gif_background(g); + } else { + // animated-gif-only path + if (((g->eflags & 0x1C) >> 2) == 3) { + old_out = g->out; + g->out = (uint8 *) malloc(4 * g->w * g->h); + if (g->out == 0) return epuc("outofmem", "Out of memory"); + memcpy(g->out, old_out, g->w*g->h*4); + } + } + + for (;;) { + switch (get8(s)) { + case 0x2C: /* Image Descriptor */ + { + int32 x, y, w, h; + uint8 *o; + + x = get16le(s); + y = get16le(s); + w = get16le(s); + h = get16le(s); + if (((x + w) > (g->w)) || ((y + h) > (g->h))) + return epuc("bad Image Descriptor", "Corrupt GIF"); + + g->line_size = g->w * 4; + g->start_x = x * 4; + g->start_y = y * g->line_size; + g->max_x = g->start_x + w * 4; + g->max_y = g->start_y + h * g->line_size; + g->cur_x = g->start_x; + g->cur_y = g->start_y; + + g->lflags = get8(s); + + if (g->lflags & 0x40) { + g->step = 8 * g->line_size; // first interlaced spacing + g->parse = 3; + } else { + g->step = g->line_size; + g->parse = 0; + } + + if (g->lflags & 0x80) { + stbi_gif_parse_colortable(s,g->lpal, 2 << (g->lflags & 7), g->eflags & 0x01 ? g->transparent : -1); + g->color_table = (uint8 *) g->lpal; + } else if (g->flags & 0x80) { + for (i=0; i < 256; ++i) // @OPTIMIZE: reset only the previous transparent + g->pal[i][3] = 255; + if (g->transparent >= 0 && (g->eflags & 0x01)) + g->pal[g->transparent][3] = 0; + g->color_table = (uint8 *) g->pal; + } else + return epuc("missing color table", "Corrupt GIF"); + + o = stbi_process_gif_raster(s, g); + if (o == NULL) return NULL; + + if (req_comp && req_comp != 4) + o = convert_format(o, 4, req_comp, g->w, g->h); + return o; + } + + case 0x21: // Comment Extension. + { + int len; + if (get8(s) == 0xF9) { // Graphic Control Extension. + len = get8(s); + if (len == 4) { + g->eflags = get8(s); + get16le(s); // delay + g->transparent = get8(s); + } else { + skip(s, len); + break; + } + } + while ((len = get8(s)) != 0) + skip(s, len); + break; + } + + case 0x3B: // gif stream termination code + return (uint8 *) 1; + + default: + return epuc("unknown code", "Corrupt GIF"); + } + } +} + +static stbi_uc *stbi_gif_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + uint8 *u = 0; + stbi_gif g={0}; + + u = stbi_gif_load_next(s, &g, comp, req_comp); + if (u == (void *) 1) u = 0; // end of animated gif marker + if (u) { + *x = g.w; + *y = g.h; + } + + return u; +} + +static int stbi_gif_info(stbi *s, int *x, int *y, int *comp) +{ + return stbi_gif_info_raw(s,x,y,comp); +} + + +// ************************************************************************************************* +// Radiance RGBE HDR loader +// originally by Nicolas Schulz +#ifndef STBI_NO_HDR +static int hdr_test(stbi *s) +{ + const char *signature = "#?RADIANCE\n"; + int i; + for (i=0; signature[i]; ++i) + if (get8(s) != signature[i]) + return 0; + return 1; +} + +static int stbi_hdr_test(stbi* s) +{ + int r = hdr_test(s); + stbi_rewind(s); + return r; +} + +#define HDR_BUFLEN 1024 +static char *hdr_gettoken(stbi *z, char *buffer) +{ + int len=0; + char c = '\0'; + + c = (char) get8(z); + + while (!at_eof(z) && c != '\n') { + buffer[len++] = c; + if (len == HDR_BUFLEN-1) { + // flush to end of line + while (!at_eof(z) && get8(z) != '\n') + ; + break; + } + c = (char) get8(z); + } + + buffer[len] = 0; + return buffer; +} + +static void hdr_convert(float *output, stbi_uc *input, int req_comp) +{ + if ( input[3] != 0 ) { + float f1; + // Exponent + f1 = (float) ldexp(1.0f, input[3] - (int)(128 + 8)); + if (req_comp <= 2) + output[0] = (input[0] + input[1] + input[2]) * f1 / 3; + else { + output[0] = input[0] * f1; + output[1] = input[1] * f1; + output[2] = input[2] * f1; + } + if (req_comp == 2) output[1] = 1; + if (req_comp == 4) output[3] = 1; + } else { + switch (req_comp) { + case 4: output[3] = 1; /* fallthrough */ + case 3: output[0] = output[1] = output[2] = 0; + break; + case 2: output[1] = 1; /* fallthrough */ + case 1: output[0] = 0; + break; + } + } +} + +static float *hdr_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + char buffer[HDR_BUFLEN]; + char *token; + int valid = 0; + int width, height; + stbi_uc *scanline; + float *hdr_data; + int len; + unsigned char count, value; + int i, j, k, c1,c2, z; + + + // Check identifier + if (strcmp(hdr_gettoken(s,buffer), "#?RADIANCE") != 0) + return epf("not HDR", "Corrupt HDR image"); + + // Parse header + for(;;) { + token = hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) return epf("unsupported format", "Unsupported HDR format"); + + // Parse width and height + // can't use sscanf() if we're not using stdio! + token = hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) return epf("unsupported data layout", "Unsupported HDR format"); + token += 3; + height = strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) return epf("unsupported data layout", "Unsupported HDR format"); + token += 3; + width = strtol(token, NULL, 10); + + *x = width; + *y = height; + + *comp = 3; + if (req_comp == 0) req_comp = 3; + + // Read data + hdr_data = (float *) malloc(height * width * req_comp * sizeof(float)); + + // Load image data + // image data is stored as some number of sca + if ( width < 8 || width >= 32768) { + // Read flat data + for (j=0; j < height; ++j) { + for (i=0; i < width; ++i) { + stbi_uc rgbe[4]; + main_decode_loop: + getn(s, rgbe, 4); + hdr_convert(hdr_data + j * width * req_comp + i * req_comp, rgbe, req_comp); + } + } + } else { + // Read RLE-encoded data + scanline = NULL; + + for (j = 0; j < height; ++j) { + c1 = get8(s); + c2 = get8(s); + len = get8(s); + if (c1 != 2 || c2 != 2 || (len & 0x80)) { + // not run-length encoded, so we have to actually use THIS data as a decoded + // pixel (note this can't be a valid pixel--one of RGB must be >= 128) + uint8 rgbe[4]; + rgbe[0] = (uint8) c1; + rgbe[1] = (uint8) c2; + rgbe[2] = (uint8) len; + rgbe[3] = (uint8) get8u(s); + hdr_convert(hdr_data, rgbe, req_comp); + i = 1; + j = 0; + free(scanline); + goto main_decode_loop; // yes, this makes no sense + } + len <<= 8; + len |= get8(s); + if (len != width) { free(hdr_data); free(scanline); return epf("invalid decoded scanline length", "corrupt HDR"); } + if (scanline == NULL) scanline = (stbi_uc *) malloc(width * 4); + + for (k = 0; k < 4; ++k) { + i = 0; + while (i < width) { + count = get8u(s); + if (count > 128) { + // Run + value = get8u(s); + count -= 128; + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = value; + } else { + // Dump + for (z = 0; z < count; ++z) + scanline[i++ * 4 + k] = get8u(s); + } + } + } + for (i=0; i < width; ++i) + hdr_convert(hdr_data+(j*width + i)*req_comp, scanline + i*4, req_comp); + } + free(scanline); + } + + return hdr_data; +} + +static float *stbi_hdr_load(stbi *s, int *x, int *y, int *comp, int req_comp) +{ + return hdr_load(s,x,y,comp,req_comp); +} + +static int stbi_hdr_info(stbi *s, int *x, int *y, int *comp) +{ + char buffer[HDR_BUFLEN]; + char *token; + int valid = 0; + + if (strcmp(hdr_gettoken(s,buffer), "#?RADIANCE") != 0) { + stbi_rewind( s ); + return 0; + } + + for(;;) { + token = hdr_gettoken(s,buffer); + if (token[0] == 0) break; + if (strcmp(token, "FORMAT=32-bit_rle_rgbe") == 0) valid = 1; + } + + if (!valid) { + stbi_rewind( s ); + return 0; + } + token = hdr_gettoken(s,buffer); + if (strncmp(token, "-Y ", 3)) { + stbi_rewind( s ); + return 0; + } + token += 3; + *y = strtol(token, &token, 10); + while (*token == ' ') ++token; + if (strncmp(token, "+X ", 3)) { + stbi_rewind( s ); + return 0; + } + token += 3; + *x = strtol(token, NULL, 10); + *comp = 3; + return 1; +} +#endif // STBI_NO_HDR + +static int stbi_bmp_info(stbi *s, int *x, int *y, int *comp) +{ + int hsz; + if (get8(s) != 'B' || get8(s) != 'M') { + stbi_rewind( s ); + return 0; + } + skip(s,12); + hsz = get32le(s); + if (hsz != 12 && hsz != 40 && hsz != 56 && hsz != 108) { + stbi_rewind( s ); + return 0; + } + if (hsz == 12) { + *x = get16le(s); + *y = get16le(s); + } else { + *x = get32le(s); + *y = get32le(s); + } + if (get16le(s) != 1) { + stbi_rewind( s ); + return 0; + } + *comp = get16le(s) / 8; + return 1; +} + +static int stbi_psd_info(stbi *s, int *x, int *y, int *comp) +{ + int channelCount; + if (get32(s) != 0x38425053) { + stbi_rewind( s ); + return 0; + } + if (get16(s) != 1) { + stbi_rewind( s ); + return 0; + } + skip(s, 6); + channelCount = get16(s); + if (channelCount < 0 || channelCount > 16) { + stbi_rewind( s ); + return 0; + } + *y = get32(s); + *x = get32(s); + if (get16(s) != 8) { + stbi_rewind( s ); + return 0; + } + if (get16(s) != 3) { + stbi_rewind( s ); + return 0; + } + *comp = 4; + return 1; +} + +static int stbi_pic_info(stbi *s, int *x, int *y, int *comp) +{ + int act_comp=0,num_packets=0,chained; + pic_packet_t packets[10]; + + skip(s, 92); + + *x = get16(s); + *y = get16(s); + if (at_eof(s)) return 0; + if ( (*x) != 0 && (1 << 28) / (*x) < (*y)) { + stbi_rewind( s ); + return 0; + } + + skip(s, 8); + + do { + pic_packet_t *packet; + + if (num_packets==sizeof(packets)/sizeof(packets[0])) + return 0; + + packet = &packets[num_packets++]; + chained = get8(s); + packet->size = get8u(s); + packet->type = get8u(s); + packet->channel = get8u(s); + act_comp |= packet->channel; + + if (at_eof(s)) { + stbi_rewind( s ); + return 0; + } + if (packet->size != 8) { + stbi_rewind( s ); + return 0; + } + } while (chained); + + *comp = (act_comp & 0x10 ? 4 : 3); + + return 1; +} + +static int stbi_info_main(stbi *s, int *x, int *y, int *comp) +{ + if (stbi_jpeg_info(s, x, y, comp)) + return 1; + if (stbi_png_info(s, x, y, comp)) + return 1; + if (stbi_gif_info(s, x, y, comp)) + return 1; + if (stbi_bmp_info(s, x, y, comp)) + return 1; + if (stbi_psd_info(s, x, y, comp)) + return 1; + if (stbi_pic_info(s, x, y, comp)) + return 1; + #ifndef STBI_NO_HDR + if (stbi_hdr_info(s, x, y, comp)) + return 1; + #endif + // test tga last because it's a crappy test! + if (stbi_tga_info(s, x, y, comp)) + return 1; + return e("unknown image type", "Image not of any known type, or corrupt"); +} + +#ifndef STBI_NO_STDIO +int stbi_info(char const *filename, int *x, int *y, int *comp) +{ + FILE *f = fopen(filename, "rb"); + int result; + if (!f) return e("can't fopen", "Unable to open file"); + result = stbi_info_from_file(f, x, y, comp); + fclose(f); + return result; +} + +int stbi_info_from_file(FILE *f, int *x, int *y, int *comp) +{ + int r; + stbi s; + long pos = ftell(f); + start_file(&s, f); + r = stbi_info_main(&s,x,y,comp); + fseek(f,pos,SEEK_SET); + return r; +} +#endif // !STBI_NO_STDIO + +int stbi_info_from_memory(stbi_uc const *buffer, int len, int *x, int *y, int *comp) +{ + stbi s; + start_mem(&s,buffer,len); + return stbi_info_main(&s,x,y,comp); +} + +int stbi_info_from_callbacks(stbi_io_callbacks const *c, void *user, int *x, int *y, int *comp) +{ + stbi s; + start_callbacks(&s, (stbi_io_callbacks *) c, user); + return stbi_info_main(&s,x,y,comp); +} + +#endif // STBI_HEADER_FILE_ONLY + +/* + revision history: + 1.33 (2011-07-14) + make stbi_is_hdr work in STBI_NO_HDR (as specified), minor compiler-friendly improvements + 1.32 (2011-07-13) + support for "info" function for all supported filetypes (SpartanJ) + 1.31 (2011-06-20) + a few more leak fixes, bug in PNG handling (SpartanJ) + 1.30 (2011-06-11) + added ability to load files via callbacks to accomidate custom input streams (Ben Wenger) + removed deprecated format-specific test/load functions + removed support for installable file formats (stbi_loader) -- would have been broken for IO callbacks anyway + error cases in bmp and tga give messages and don't leak (Raymond Barbiero, grisha) + fix inefficiency in decoding 32-bit BMP (David Woo) + 1.29 (2010-08-16) + various warning fixes from Aurelien Pocheville + 1.28 (2010-08-01) + fix bug in GIF palette transparency (SpartanJ) + 1.27 (2010-08-01) + cast-to-uint8 to fix warnings + 1.26 (2010-07-24) + fix bug in file buffering for PNG reported by SpartanJ + 1.25 (2010-07-17) + refix trans_data warning (Won Chun) + 1.24 (2010-07-12) + perf improvements reading from files on platforms with lock-heavy fgetc() + minor perf improvements for jpeg + deprecated type-specific functions so we'll get feedback if they're needed + attempt to fix trans_data warning (Won Chun) + 1.23 fixed bug in iPhone support + 1.22 (2010-07-10) + removed image *writing* support + stbi_info support from Jetro Lauha + GIF support from Jean-Marc Lienher + iPhone PNG-extensions from James Brown + warning-fixes from Nicolas Schulz and Janez Zemva (i.e. Janez (U+017D)emva) + 1.21 fix use of 'uint8' in header (reported by jon blow) + 1.20 added support for Softimage PIC, by Tom Seddon + 1.19 bug in interlaced PNG corruption check (found by ryg) + 1.18 2008-08-02 + fix a threading bug (local mutable static) + 1.17 support interlaced PNG + 1.16 major bugfix - convert_format converted one too many pixels + 1.15 initialize some fields for thread safety + 1.14 fix threadsafe conversion bug + header-file-only version (#define STBI_HEADER_FILE_ONLY before including) + 1.13 threadsafe + 1.12 const qualifiers in the API + 1.11 Support installable IDCT, colorspace conversion routines + 1.10 Fixes for 64-bit (don't use "unsigned long") + optimized upsampling by Fabian "ryg" Giesen + 1.09 Fix format-conversion for PSD code (bad global variables!) + 1.08 Thatcher Ulrich's PSD code integrated by Nicolas Schulz + 1.07 attempt to fix C++ warning/errors again + 1.06 attempt to fix C++ warning/errors again + 1.05 fix TGA loading to return correct *comp and use good luminance calc + 1.04 default float alpha is 1, not 255; use 'void *' for stbi_image_free + 1.03 bugfixes to STBI_NO_STDIO, STBI_NO_HDR + 1.02 support for (subset of) HDR files, float interface for preferred access to them + 1.01 fix bug: possible bug in handling right-side up bmps... not sure + fix bug: the stbi_bmp_load() and stbi_tga_load() functions didn't work at all + 1.00 interface to zlib that skips zlib header + 0.99 correct handling of alpha in palette + 0.98 TGA loader by lonesock; dynamically add loaders (untested) + 0.97 jpeg errors on too large a file; also catch another malloc failure + 0.96 fix detection of invalid v value - particleman@mollyrocket forum + 0.95 during header scan, seek to markers in case of padding + 0.94 STBI_NO_STDIO to disable stdio usage; rename all #defines the same + 0.93 handle jpegtran output; verbose errors + 0.92 read 4,8,16,24,32-bit BMP files of several formats + 0.91 output 24-bit Windows 3.0 BMP files + 0.90 fix a few more warnings; bump version number to approach 1.0 + 0.61 bugfixes due to Marc LeBlanc, Christopher Lloyd + 0.60 fix compiling as c++ + 0.59 fix warnings: merge Dave Moore's -Wall fixes + 0.58 fix bug: zlib uncompressed mode len/nlen was wrong endian + 0.57 fix bug: jpg last huffman symbol before marker was >9 bits but less than 16 available + 0.56 fix bug: zlib uncompressed mode len vs. nlen + 0.55 fix bug: restart_interval not initialized to 0 + 0.54 allow NULL for 'int *comp' + 0.53 fix bug in png 3->4; speedup png decoding + 0.52 png handles req_comp=3,4 directly; minor cleanup; jpeg comments + 0.51 obey req_comp requests, 1-component jpegs return as 1-component, + on 'test' only check type, not whether we support this variant + 0.50 first released version +*/ diff --git a/src/third_party/stb/stb_truetype.h b/src/third_party/stb/stb_truetype.h new file mode 100644 index 0000000..3252e9b --- /dev/null +++ b/src/third_party/stb/stb_truetype.h @@ -0,0 +1,2066 @@ +// stb_truetype.h - v0.7 - public domain +// authored from 2009-2013 by Sean Barrett / RAD Game Tools +// +// This library processes TrueType files: +// parse files +// extract glyph metrics +// extract glyph shapes +// render glyphs to one-channel bitmaps with antialiasing (box filter) +// +// Todo: +// non-MS cmaps +// crashproof on bad data +// hinting? (no longer patented) +// cleartype-style AA? +// optimize: use simple memory allocator for intermediates +// optimize: build edge-list directly from curves +// optimize: rasterize directly from curves? +// +// ADDITIONAL CONTRIBUTORS +// +// Mikko Mononen: compound shape support, more cmap formats +// Tor Andersson: kerning, subpixel rendering +// +// Bug/warning reports: +// "Zer" on mollyrocket (with fix) +// Cass Everitt +// stoiko (Haemimont Games) +// Brian Hook +// Walter van Niftrik +// +// VERSION HISTORY +// +// 0.7 (2013-09-25) bugfix: subpixel glyph bug fixed in 0.5 had come back +// 0.6c (2012-07-24) improve documentation +// 0.6b (2012-07-20) fix a few more warnings +// 0.6 (2012-07-17) fix warnings; added stbtt_ScaleForMappingEmToPixels, +// stbtt_GetFontBoundingBox, stbtt_IsGlyphEmpty +// 0.5 (2011-12-09) bugfixes: +// subpixel glyph renderer computed wrong bounding box +// first vertex of shape can be off-curve (FreeSans) +// 0.4b (2011-12-03) fixed an error in the font baking example +// 0.4 (2011-12-01) kerning, subpixel rendering (tor) +// bugfixes for: +// codepoint-to-glyph conversion using table fmt=12 +// codepoint-to-glyph conversion using table fmt=4 +// stbtt_GetBakedQuad with non-square texture (Zer) +// updated Hello World! sample to use kerning and subpixel +// fixed some warnings +// 0.3 (2009-06-24) cmap fmt=12, compound shapes (MM) +// userdata, malloc-from-userdata, non-zero fill (STB) +// 0.2 (2009-03-11) Fix unsigned/signed char warnings +// 0.1 (2009-03-09) First public release +// +// LICENSE +// +// This software is in the public domain. Where that dedication is not +// recognized, you are granted a perpetual, irrevokable license to copy +// and modify this file as you see fit. +// +// USAGE +// +// Include this file in whatever places neeed to refer to it. In ONE C/C++ +// file, write: +// #define STB_TRUETYPE_IMPLEMENTATION +// before the #include of this file. This expands out the actual +// implementation into that C/C++ file. +// +// Simple 3D API (don't ship this, but it's fine for tools and quick start, +// and you can cut and paste from it to move to more advanced) +// stbtt_BakeFontBitmap() -- bake a font to a bitmap for use as texture +// stbtt_GetBakedQuad() -- compute quad to draw for a given char +// +// "Load" a font file from a memory buffer (you have to keep the buffer loaded) +// stbtt_InitFont() +// stbtt_GetFontOffsetForIndex() -- use for TTC font collections +// +// Render a unicode codepoint to a bitmap +// stbtt_GetCodepointBitmap() -- allocates and returns a bitmap +// stbtt_MakeCodepointBitmap() -- renders into bitmap you provide +// stbtt_GetCodepointBitmapBox() -- how big the bitmap must be +// +// Character advance/positioning +// stbtt_GetCodepointHMetrics() +// stbtt_GetFontVMetrics() +// stbtt_GetCodepointKernAdvance() +// +// ADDITIONAL DOCUMENTATION +// +// Immediately after this block comment are a series of sample programs. +// +// After the sample programs is the "header file" section. This section +// includes documentation for each API function. +// +// Some important concepts to understand to use this library: +// +// Codepoint +// Characters are defined by unicode codepoints, e.g. 65 is +// uppercase A, 231 is lowercase c with a cedilla, 0x7e30 is +// the hiragana for "ma". +// +// Glyph +// A visual character shape (every codepoint is rendered as +// some glyph) +// +// Glyph index +// A font-specific integer ID representing a glyph +// +// Baseline +// Glyph shapes are defined relative to a baseline, which is the +// bottom of uppercase characters. Characters extend both above +// and below the baseline. +// +// Current Point +// As you draw text to the screen, you keep track of a "current point" +// which is the origin of each character. The current point's vertical +// position is the baseline. Even "baked fonts" use this model. +// +// Vertical Font Metrics +// The vertical qualities of the font, used to vertically position +// and space the characters. See docs for stbtt_GetFontVMetrics. +// +// Font Size in Pixels or Points +// The preferred interface for specifying font sizes in stb_truetype +// is to specify how tall the font's vertical extent should be in pixels. +// If that sounds good enough, skip the next paragraph. +// +// Most font APIs instead use "points", which are a common typographic +// measurement for describing font size, defined as 72 points per inch. +// stb_truetype provides a point API for compatibility. However, true +// "per inch" conventions don't make much sense on computer displays +// since they different monitors have different number of pixels per +// inch. For example, Windows traditionally uses a convention that +// there are 96 pixels per inch, thus making 'inch' measurements have +// nothing to do with inches, and thus effectively defining a point to +// be 1.333 pixels. Additionally, the TrueType font data provides +// an explicit scale factor to scale a given font's glyphs to points, +// but the author has observed that this scale factor is often wrong +// for non-commercial fonts, thus making fonts scaled in points +// according to the TrueType spec incoherently sized in practice. +// +// ADVANCED USAGE +// +// Quality: +// +// - Use the functions with Subpixel at the end to allow your characters +// to have subpixel positioning. Since the font is anti-aliased, not +// hinted, this is very import for quality. (This is not possible with +// baked fonts.) +// +// - Kerning is now supported, and if you're supporting subpixel rendering +// then kerning is worth using to give your text a polished look. +// +// Performance: +// +// - Convert Unicode codepoints to glyph indexes and operate on the glyphs; +// if you don't do this, stb_truetype is forced to do the conversion on +// every call. +// +// - There are a lot of memory allocations. We should modify it to take +// a temp buffer and allocate from the temp buffer (without freeing), +// should help performance a lot. +// +// NOTES +// +// The system uses the raw data found in the .ttf file without changing it +// and without building auxiliary data structures. This is a bit inefficient +// on little-endian systems (the data is big-endian), but assuming you're +// caching the bitmaps or glyph shapes this shouldn't be a big deal. +// +// It appears to be very hard to programmatically determine what font a +// given file is in a general way. I provide an API for this, but I don't +// recommend it. +// +// +// SOURCE STATISTICS (based on v0.6c, 2050 LOC) +// +// Documentation & header file 520 LOC \___ 660 LOC documentation +// Sample code 140 LOC / +// Truetype parsing 620 LOC ---- 620 LOC TrueType +// Software rasterization 240 LOC \ . +// Curve tesselation 120 LOC \__ 550 LOC Bitmap creation +// Bitmap management 100 LOC / +// Baked bitmap interface 70 LOC / +// Font name matching & access 150 LOC ---- 150 +// C runtime library abstraction 60 LOC ---- 60 + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// SAMPLE PROGRAMS +//// +// +// Incomplete text-in-3d-api example, which draws quads properly aligned to be lossless +// +#if 0 +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +char ttf_buffer[1<<20]; +unsigned char temp_bitmap[512*512]; + +stbtt_bakedchar cdata[96]; // ASCII 32..126 is 95 glyphs +GLstbtt_uint ftex; + +void my_stbtt_initfont(void) +{ + fread(ttf_buffer, 1, 1<<20, fopen("c:/windows/fonts/times.ttf", "rb")); + stbtt_BakeFontBitmap(data,0, 32.0, temp_bitmap,512,512, 32,96, cdata); // no guarantee this fits! + // can free ttf_buffer at this point + glGenTextures(1, &ftex); + glBindTexture(GL_TEXTURE_2D, ftex); + glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, 512,512, 0, GL_ALPHA, GL_UNSIGNED_BYTE, temp_bitmap); + // can free temp_bitmap at this point + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); +} + +void my_stbtt_print(float x, float y, char *text) +{ + // assume orthographic projection with units = screen pixels, origin at top left + glBindTexture(GL_TEXTURE_2D, ftex); + glBegin(GL_QUADS); + while (*text) { + if (*text >= 32 && *text < 128) { + stbtt_aligned_quad q; + stbtt_GetBakedQuad(cdata, 512,512, *text-32, &x,&y,&q,1);//1=opengl,0=old d3d + glTexCoord2f(q.s0,q.t1); glVertex2f(q.x0,q.y0); + glTexCoord2f(q.s1,q.t1); glVertex2f(q.x1,q.y0); + glTexCoord2f(q.s1,q.t0); glVertex2f(q.x1,q.y1); + glTexCoord2f(q.s0,q.t0); glVertex2f(q.x0,q.y1); + } + ++text; + } + glEnd(); +} +#endif +// +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program (this compiles): get a single bitmap, print as ASCII art +// +#if 0 +#include +#define STB_TRUETYPE_IMPLEMENTATION // force following include to generate implementation +#include "stb_truetype.h" + +char ttf_buffer[1<<25]; + +int main(int argc, char **argv) +{ + stbtt_fontinfo font; + unsigned char *bitmap; + int w,h,i,j,c = (argc > 1 ? atoi(argv[1]) : 'a'), s = (argc > 2 ? atoi(argv[2]) : 20); + + fread(ttf_buffer, 1, 1<<25, fopen(argc > 3 ? argv[3] : "c:/windows/fonts/arialbd.ttf", "rb")); + + stbtt_InitFont(&font, ttf_buffer, stbtt_GetFontOffsetForIndex(ttf_buffer,0)); + bitmap = stbtt_GetCodepointBitmap(&font, 0,stbtt_ScaleForPixelHeight(&font, s), c, &w, &h, 0,0); + + for (j=0; j < h; ++j) { + for (i=0; i < w; ++i) + putchar(" .:ioVM@"[bitmap[j*w+i]>>5]); + putchar('\n'); + } + return 0; +} +#endif +// +// Output: +// +// .ii. +// @@@@@@. +// V@Mio@@o +// :i. V@V +// :oM@@M +// :@@@MM@M +// @@o o@M +// :@@. M@M +// @@@o@@@@ +// :M@@V:@@. +// +////////////////////////////////////////////////////////////////////////////// +// +// Complete program: print "Hello World!" banner, with bugs +// +#if 0 +char buffer[24<<20]; +unsigned char screen[20][79]; + +int main(int arg, char **argv) +{ + stbtt_fontinfo font; + int i,j,ascent,baseline,ch=0; + float scale, xpos=0; + char *text = "Heljo World!"; + + fread(buffer, 1, 1000000, fopen("c:/windows/fonts/arialbd.ttf", "rb")); + stbtt_InitFont(&font, buffer, 0); + + scale = stbtt_ScaleForPixelHeight(&font, 15); + stbtt_GetFontVMetrics(&font, &ascent,0,0); + baseline = (int) (ascent*scale); + + while (text[ch]) { + int advance,lsb,x0,y0,x1,y1; + float x_shift = xpos - (float) floor(xpos); + stbtt_GetCodepointHMetrics(&font, text[ch], &advance, &lsb); + stbtt_GetCodepointBitmapBoxSubpixel(&font, text[ch], scale,scale,x_shift,0, &x0,&y0,&x1,&y1); + stbtt_MakeCodepointBitmapSubpixel(&font, &screen[baseline + y0][(int) xpos + x0], x1-x0,y1-y0, 79, scale,scale,x_shift,0, text[ch]); + // note that this stomps the old data, so where character boxes overlap (e.g. 'lj') it's wrong + // because this API is really for baking character bitmaps into textures. if you want to render + // a sequence of characters, you really need to render each bitmap to a temp buffer, then + // "alpha blend" that into the working buffer + xpos += (advance * scale); + if (text[ch+1]) + xpos += scale*stbtt_GetCodepointKernAdvance(&font, text[ch],text[ch+1]); + ++ch; + } + + for (j=0; j < 20; ++j) { + for (i=0; i < 78; ++i) + putchar(" .:ioVM@"[screen[j][i]>>5]); + putchar('\n'); + } + + return 0; +} +#endif + + +////////////////////////////////////////////////////////////////////////////// +////////////////////////////////////////////////////////////////////////////// +//// +//// INTEGRATION WITH YOUR CODEBASE +//// +//// The following sections allow you to supply alternate definitions +//// of C library functions used by stb_truetype. + +#ifdef STB_TRUETYPE_IMPLEMENTATION + // #define your own (u)stbtt_int8/16/32 before including to override this + #ifndef stbtt_uint8 + typedef unsigned char stbtt_uint8; + typedef signed char stbtt_int8; + typedef unsigned short stbtt_uint16; + typedef signed short stbtt_int16; + typedef unsigned int stbtt_uint32; + typedef signed int stbtt_int32; + #endif + + typedef char stbtt__check_size32[sizeof(stbtt_int32)==4 ? 1 : -1]; + typedef char stbtt__check_size16[sizeof(stbtt_int16)==2 ? 1 : -1]; + + // #define your own STBTT_sort() to override this to avoid qsort + #ifndef STBTT_sort + #include + #define STBTT_sort(data,num_items,item_size,compare_func) qsort(data,num_items,item_size,compare_func) + #endif + + // #define your own STBTT_ifloor/STBTT_iceil() to avoid math.h + #ifndef STBTT_ifloor + #include + #define STBTT_ifloor(x) ((int) floor(x)) + #define STBTT_iceil(x) ((int) ceil(x)) + #endif + + // #define your own functions "STBTT_malloc" / "STBTT_free" to avoid malloc.h + #ifndef STBTT_malloc + #include + #define STBTT_malloc(x,u) malloc(x) + #define STBTT_free(x,u) free(x) + #endif + + #ifndef STBTT_assert + #include + #define STBTT_assert(x) assert(x) + #endif + + #ifndef STBTT_strlen + #include + #define STBTT_strlen(x) strlen(x) + #endif + + #ifndef STBTT_memcpy + #include + #define STBTT_memcpy memcpy + #define STBTT_memset memset + #endif +#endif + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// INTERFACE +//// +//// + +#ifndef __STB_INCLUDE_STB_TRUETYPE_H__ +#define __STB_INCLUDE_STB_TRUETYPE_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +////////////////////////////////////////////////////////////////////////////// +// +// TEXTURE BAKING API +// +// If you use this API, you only have to call two functions ever. +// + +typedef struct +{ + unsigned short x0,y0,x1,y1; // coordinates of bbox in bitmap + float xoff,yoff,xadvance; +} stbtt_bakedchar; + +extern int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata); // you allocate this, it's num_chars long +// if return is positive, the first unused row of the bitmap +// if return is negative, returns the negative of the number of characters that fit +// if return is 0, no characters fit and no rows were used +// This uses a very crappy packing. + +typedef struct +{ + float x0,y0,s0,t0; // top-left + float x1,y1,s1,t1; // bottom-right +} stbtt_aligned_quad; + +extern void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, // same data as above + int char_index, // character to display + float *xpos, float *ypos, // pointers to current position in screen pixel space + stbtt_aligned_quad *q, // output: quad to draw + int opengl_fillrule); // true if opengl fill rule; false if DX9 or earlier +// Call GetBakedQuad with char_index = 'character - first_char', and it +// creates the quad you need to draw and advances the current position. +// +// The coordinate system used assumes y increases downwards. +// +// Characters will extend both above and below the current position; +// see discussion of "BASELINE" above. +// +// It's inefficient; you might want to c&p it and optimize it. + + +////////////////////////////////////////////////////////////////////////////// +// +// FONT LOADING +// +// + +extern int stbtt_GetFontOffsetForIndex(const unsigned char *data, int index); +// Each .ttf/.ttc file may have more than one font. Each font has a sequential +// index number starting from 0. Call this function to get the font offset for +// a given index; it returns -1 if the index is out of range. A regular .ttf +// file will only define one font and it always be at offset 0, so it will +// return '0' for index 0, and -1 for all other indices. You can just skip +// this step if you know it's that kind of font. + + +// The following structure is defined publically so you can declare one on +// the stack or as a global or etc, but you should treat it as opaque. +typedef struct stbtt_fontinfo +{ + void * userdata; + unsigned char * data; // pointer to .ttf file + int fontstart; // offset of start of font + + int numGlyphs; // number of glyphs, needed for range checking + + int loca,head,glyf,hhea,hmtx,kern; // table locations as offset from start of .ttf + int index_map; // a cmap mapping for our chosen character encoding + int indexToLocFormat; // format needed to map from glyph index to glyph +} stbtt_fontinfo; + +extern int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data, int offset); +// Given an offset into the file that defines a font, this function builds +// the necessary cached info for the rest of the system. You must allocate +// the stbtt_fontinfo yourself, and stbtt_InitFont will fill it out. You don't +// need to do anything special to free it, because the contents are pure +// value data with no additional data structures. Returns 0 on failure. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER TO GLYPH-INDEX CONVERSIOn + +int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint); +// If you're going to perform multiple operations on the same character +// and you want a speed-up, call this function with the character you're +// going to process, then use glyph-based functions instead of the +// codepoint-based functions. + + +////////////////////////////////////////////////////////////////////////////// +// +// CHARACTER PROPERTIES +// + +extern float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose "height" is 'pixels' tall. +// Height is measured as the distance from the highest ascender to the lowest +// descender; in other words, it's equivalent to calling stbtt_GetFontVMetrics +// and computing: +// scale = pixels / (ascent - descent) +// so if you prefer to measure height by the ascent only, use a similar calculation. + +extern float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels); +// computes a scale factor to produce a font whose EM size is mapped to +// 'pixels' tall. This is probably what traditional APIs compute, but +// I'm not positive. + +extern void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap); +// ascent is the coordinate above the baseline the font extends; descent +// is the coordinate below the baseline the font extends (i.e. it is typically negative) +// lineGap is the spacing between one row's descent and the next row's ascent... +// so you should advance the vertical position by "*ascent - *descent + *lineGap" +// these are expressed in unscaled coordinates, so you must multiply by +// the scale factor for a given size + +extern void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1); +// the bounding box around all possible characters + +extern void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing); +// leftSideBearing is the offset from the current horizontal position to the left edge of the character +// advanceWidth is the offset from the current horizontal position to the next horizontal position +// these are expressed in unscaled coordinates + +extern int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2); +// an additional amount to add to the 'advance' value between ch1 and ch2 +// @TODO; for now always returns 0! + +extern int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1); +// Gets the bounding box of the visible part of the glyph, in unscaled coordinates + +extern void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing); +extern int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2); +extern int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1); +// as above, but takes one or more glyph indices for greater efficiency + + +////////////////////////////////////////////////////////////////////////////// +// +// GLYPH SHAPES (you probably don't need these, but they have to go before +// the bitmaps for C declaration-order reasons) +// + +#ifndef STBTT_vmove // you can predefine these to use different values (but why?) + enum { + STBTT_vmove=1, + STBTT_vline, + STBTT_vcurve + }; +#endif + +#ifndef stbtt_vertex // you can predefine this to use different values + // (we share this with other code at RAD) + #define stbtt_vertex_type short // can't use stbtt_int16 because that's not visible in the header file + typedef struct + { + stbtt_vertex_type x,y,cx,cy; + unsigned char type,padding; + } stbtt_vertex; +#endif + +extern int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index); +// returns non-zero if nothing is drawn for this glyph + +extern int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices); +extern int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **vertices); +// returns # of vertices and fills *vertices with the pointer to them +// these are expressed in "unscaled" coordinates + +extern void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *vertices); +// frees the data allocated above + +////////////////////////////////////////////////////////////////////////////// +// +// BITMAP RENDERING +// + +extern void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata); +// frees the bitmap allocated below + +extern unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// allocates a large-enough single-channel 8bpp bitmap and renders the +// specified character/glyph at the specified scale into it, with +// antialiasing. 0 is no coverage (transparent), 255 is fully covered (opaque). +// *width & *height are filled out with the width & height of the bitmap, +// which is stored left-to-right, top-to-bottom. +// +// xoff/yoff are the offset it pixel space from the glyph origin to the top-left of the bitmap + +extern unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff); +// the same as stbtt_GetCodepoitnBitmap, but you can specify a subpixel +// shift for the character + +extern void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint); +// the same as stbtt_GetCodepointBitmap, but you pass in storage for the bitmap +// in the form of 'output', with row spacing of 'out_stride' bytes. the bitmap +// is clipped to out_w/out_h bytes. Call stbtt_GetCodepointBitmapBox to get the +// width and height and positioning info for it first. + +extern void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint); +// same as stbtt_MakeCodepointBitmap, but you can specify a subpixel +// shift for the character + +extern void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +// get the bbox of the bitmap centered around the glyph origin; so the +// bitmap width is ix1-ix0, height is iy1-iy0, and location to place +// the bitmap top left is (leftSideBearing*scale,iy0). +// (Note that the bitmap uses y-increases-down, but the shape uses +// y-increases-up, so CodepointBitmapBox and CodepointBox are inverted.) + +extern void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); +// same as stbtt_GetCodepointBitmapBox, but you can specify a subpixel +// shift for the character + +// the following functions are equivalent to the above functions, but operate +// on glyph indices instead of Unicode codepoints (for efficiency) +extern unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff); +extern unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff); +extern void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph); +extern void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph); +extern void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1); +extern void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1); + + +// @TODO: don't expose this structure +typedef struct +{ + int w,h,stride; + unsigned char *pixels; +} stbtt__bitmap; + +extern void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata); + +////////////////////////////////////////////////////////////////////////////// +// +// Finding the right font... +// +// You should really just solve this offline, keep your own tables +// of what font is what, and don't try to get it out of the .ttf file. +// That's because getting it out of the .ttf file is really hard, because +// the names in the file can appear in many possible encodings, in many +// possible languages, and e.g. if you need a case-insensitive comparison, +// the details of that depend on the encoding & language in a complex way +// (actually underspecified in truetype, but also gigantic). +// +// But you can use the provided functions in two possible ways: +// stbtt_FindMatchingFont() will use *case-sensitive* comparisons on +// unicode-encoded names to try to find the font you want; +// you can run this before calling stbtt_InitFont() +// +// stbtt_GetFontNameString() lets you get any of the various strings +// from the file yourself and do your own comparisons on them. +// You have to have called stbtt_InitFont() first. + + +extern int stbtt_FindMatchingFont(const unsigned char *fontdata, const char *name, int flags); +// returns the offset (not index) of the font that matches, or -1 if none +// if you use STBTT_MACSTYLE_DONTCARE, use a font name like "Arial Bold". +// if you use any other flag, use a font name like "Arial"; this checks +// the 'macStyle' header field; i don't know if fonts set this consistently +#define STBTT_MACSTYLE_DONTCARE 0 +#define STBTT_MACSTYLE_BOLD 1 +#define STBTT_MACSTYLE_ITALIC 2 +#define STBTT_MACSTYLE_UNDERSCORE 4 +#define STBTT_MACSTYLE_NONE 8 // <= not same as 0, this makes us check the bitfield is 0 + +extern int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2); +// returns 1/0 whether the first string interpreted as utf8 is identical to +// the second string interpreted as big-endian utf16... useful for strings from next func + +extern const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID); +// returns the string (which may be big-endian double byte, e.g. for unicode) +// and puts the length in bytes in *length. +// +// some of the values for the IDs are below; for more see the truetype spec: +// http://developer.apple.com/textfonts/TTRefMan/RM06/Chap6name.html +// http://www.microsoft.com/typography/otspec/name.htm + +enum { // platformID + STBTT_PLATFORM_ID_UNICODE =0, + STBTT_PLATFORM_ID_MAC =1, + STBTT_PLATFORM_ID_ISO =2, + STBTT_PLATFORM_ID_MICROSOFT =3 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_UNICODE + STBTT_UNICODE_EID_UNICODE_1_0 =0, + STBTT_UNICODE_EID_UNICODE_1_1 =1, + STBTT_UNICODE_EID_ISO_10646 =2, + STBTT_UNICODE_EID_UNICODE_2_0_BMP=3, + STBTT_UNICODE_EID_UNICODE_2_0_FULL=4 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MICROSOFT + STBTT_MS_EID_SYMBOL =0, + STBTT_MS_EID_UNICODE_BMP =1, + STBTT_MS_EID_SHIFTJIS =2, + STBTT_MS_EID_UNICODE_FULL =10 +}; + +enum { // encodingID for STBTT_PLATFORM_ID_MAC; same as Script Manager codes + STBTT_MAC_EID_ROMAN =0, STBTT_MAC_EID_ARABIC =4, + STBTT_MAC_EID_JAPANESE =1, STBTT_MAC_EID_HEBREW =5, + STBTT_MAC_EID_CHINESE_TRAD =2, STBTT_MAC_EID_GREEK =6, + STBTT_MAC_EID_KOREAN =3, STBTT_MAC_EID_RUSSIAN =7 +}; + +enum { // languageID for STBTT_PLATFORM_ID_MICROSOFT; same as LCID... + // problematic because there are e.g. 16 english LCIDs and 16 arabic LCIDs + STBTT_MS_LANG_ENGLISH =0x0409, STBTT_MS_LANG_ITALIAN =0x0410, + STBTT_MS_LANG_CHINESE =0x0804, STBTT_MS_LANG_JAPANESE =0x0411, + STBTT_MS_LANG_DUTCH =0x0413, STBTT_MS_LANG_KOREAN =0x0412, + STBTT_MS_LANG_FRENCH =0x040c, STBTT_MS_LANG_RUSSIAN =0x0419, + STBTT_MS_LANG_GERMAN =0x0407, STBTT_MS_LANG_SPANISH =0x0409, + STBTT_MS_LANG_HEBREW =0x040d, STBTT_MS_LANG_SWEDISH =0x041D +}; + +enum { // languageID for STBTT_PLATFORM_ID_MAC + STBTT_MAC_LANG_ENGLISH =0 , STBTT_MAC_LANG_JAPANESE =11, + STBTT_MAC_LANG_ARABIC =12, STBTT_MAC_LANG_KOREAN =23, + STBTT_MAC_LANG_DUTCH =4 , STBTT_MAC_LANG_RUSSIAN =32, + STBTT_MAC_LANG_FRENCH =1 , STBTT_MAC_LANG_SPANISH =6 , + STBTT_MAC_LANG_GERMAN =2 , STBTT_MAC_LANG_SWEDISH =5 , + STBTT_MAC_LANG_HEBREW =10, STBTT_MAC_LANG_CHINESE_SIMPLIFIED =33, + STBTT_MAC_LANG_ITALIAN =3 , STBTT_MAC_LANG_CHINESE_TRAD =19 +}; + +#ifdef __cplusplus +} +#endif + +#endif // __STB_INCLUDE_STB_TRUETYPE_H__ + +/////////////////////////////////////////////////////////////////////////////// +/////////////////////////////////////////////////////////////////////////////// +//// +//// IMPLEMENTATION +//// +//// + +#ifdef STB_TRUETYPE_IMPLEMENTATION + +////////////////////////////////////////////////////////////////////////// +// +// accessors to parse data from file +// + +// on platforms that don't allow misaligned reads, if we want to allow +// truetype fonts that aren't padded to alignment, define ALLOW_UNALIGNED_TRUETYPE + +#define ttBYTE(p) (* (stbtt_uint8 *) (p)) +#define ttCHAR(p) (* (stbtt_int8 *) (p)) +#define ttFixed(p) ttLONG(p) + +#if defined(STB_TRUETYPE_BIGENDIAN) && !defined(ALLOW_UNALIGNED_TRUETYPE) + + #define ttUSHORT(p) (* (stbtt_uint16 *) (p)) + #define ttSHORT(p) (* (stbtt_int16 *) (p)) + #define ttULONG(p) (* (stbtt_uint32 *) (p)) + #define ttLONG(p) (* (stbtt_int32 *) (p)) + +#else + + stbtt_uint16 ttUSHORT(const stbtt_uint8 *p) { return p[0]*256 + p[1]; } + stbtt_int16 ttSHORT(const stbtt_uint8 *p) { return p[0]*256 + p[1]; } + stbtt_uint32 ttULONG(const stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + stbtt_int32 ttLONG(const stbtt_uint8 *p) { return (p[0]<<24) + (p[1]<<16) + (p[2]<<8) + p[3]; } + +#endif + +#define stbtt_tag4(p,c0,c1,c2,c3) ((p)[0] == (c0) && (p)[1] == (c1) && (p)[2] == (c2) && (p)[3] == (c3)) +#define stbtt_tag(p,str) stbtt_tag4(p,str[0],str[1],str[2],str[3]) + +static int stbtt__isfont(const stbtt_uint8 *font) +{ + // check the version number + if (stbtt_tag4(font, '1',0,0,0)) return 1; // TrueType 1 + if (stbtt_tag(font, "typ1")) return 1; // TrueType with type 1 font -- we don't support this! + if (stbtt_tag(font, "OTTO")) return 1; // OpenType with CFF + if (stbtt_tag4(font, 0,1,0,0)) return 1; // OpenType 1.0 + return 0; +} + +// @OPTIMIZE: binary search +static stbtt_uint32 stbtt__find_table(stbtt_uint8 *data, stbtt_uint32 fontstart, const char *tag) +{ + stbtt_int32 num_tables = ttUSHORT(data+fontstart+4); + stbtt_uint32 tabledir = fontstart + 12; + stbtt_int32 i; + for (i=0; i < num_tables; ++i) { + stbtt_uint32 loc = tabledir + 16*i; + if (stbtt_tag(data+loc+0, tag)) + return ttULONG(data+loc+8); + } + return 0; +} + +int stbtt_GetFontOffsetForIndex(const unsigned char *font_collection, int index) +{ + // if it's just a font, there's only one valid index + if (stbtt__isfont(font_collection)) + return index == 0 ? 0 : -1; + + // check if it's a TTC + if (stbtt_tag(font_collection, "ttcf")) { + // version 1? + if (ttULONG(font_collection+4) == 0x00010000 || ttULONG(font_collection+4) == 0x00020000) { + stbtt_int32 n = ttLONG(font_collection+8); + if (index >= n) + return -1; + return ttULONG(font_collection+12+index*14); + } + } + return -1; +} + +int stbtt_InitFont(stbtt_fontinfo *info, const unsigned char *data2, int fontstart) +{ + stbtt_uint8 *data = (stbtt_uint8 *) data2; + stbtt_uint32 cmap, t; + stbtt_int32 i,numTables; + + info->data = data; + info->fontstart = fontstart; + + cmap = stbtt__find_table(data, fontstart, "cmap"); // required + info->loca = stbtt__find_table(data, fontstart, "loca"); // required + info->head = stbtt__find_table(data, fontstart, "head"); // required + info->glyf = stbtt__find_table(data, fontstart, "glyf"); // required + info->hhea = stbtt__find_table(data, fontstart, "hhea"); // required + info->hmtx = stbtt__find_table(data, fontstart, "hmtx"); // required + info->kern = stbtt__find_table(data, fontstart, "kern"); // not required + if (!cmap || !info->loca || !info->head || !info->glyf || !info->hhea || !info->hmtx) + return 0; + + t = stbtt__find_table(data, fontstart, "maxp"); + if (t) + info->numGlyphs = ttUSHORT(data+t+4); + else + info->numGlyphs = 0xffff; + + // find a cmap encoding table we understand *now* to avoid searching + // later. (todo: could make this installable) + // the same regardless of glyph. + numTables = ttUSHORT(data + cmap + 2); + info->index_map = 0; + for (i=0; i < numTables; ++i) { + stbtt_uint32 encoding_record = cmap + 4 + 8 * i; + // find an encoding we understand: + switch(ttUSHORT(data+encoding_record)) { + case STBTT_PLATFORM_ID_MICROSOFT: + switch (ttUSHORT(data+encoding_record+2)) { + case STBTT_MS_EID_UNICODE_BMP: + case STBTT_MS_EID_UNICODE_FULL: + // MS/Unicode + info->index_map = cmap + ttULONG(data+encoding_record+4); + break; + } + break; + } + } + if (info->index_map == 0) + return 0; + + info->indexToLocFormat = ttUSHORT(data+info->head + 50); + return 1; +} + +int stbtt_FindGlyphIndex(const stbtt_fontinfo *info, int unicode_codepoint) +{ + stbtt_uint8 *data = info->data; + stbtt_uint32 index_map = info->index_map; + + stbtt_uint16 format = ttUSHORT(data + index_map + 0); + if (format == 0) { // apple byte encoding + stbtt_int32 bytes = ttUSHORT(data + index_map + 2); + if (unicode_codepoint < bytes-6) + return ttBYTE(data + index_map + 6 + unicode_codepoint); + return 0; + } else if (format == 6) { + stbtt_uint32 first = ttUSHORT(data + index_map + 6); + stbtt_uint32 count = ttUSHORT(data + index_map + 8); + if ((stbtt_uint32) unicode_codepoint >= first && (stbtt_uint32) unicode_codepoint < first+count) + return ttUSHORT(data + index_map + 10 + (unicode_codepoint - first)*2); + return 0; + } else if (format == 2) { + STBTT_assert(0); // @TODO: high-byte mapping for japanese/chinese/korean + return 0; + } else if (format == 4) { // standard mapping for windows fonts: binary search collection of ranges + stbtt_uint16 segcount = ttUSHORT(data+index_map+6) >> 1; + stbtt_uint16 searchRange = ttUSHORT(data+index_map+8) >> 1; + stbtt_uint16 entrySelector = ttUSHORT(data+index_map+10); + stbtt_uint16 rangeShift = ttUSHORT(data+index_map+12) >> 1; + stbtt_uint16 item, offset, start, end; + + // do a binary search of the segments + stbtt_uint32 endCount = index_map + 14; + stbtt_uint32 search = endCount; + + if (unicode_codepoint > 0xffff) + return 0; + + // they lie from endCount .. endCount + segCount + // but searchRange is the nearest power of two, so... + if (unicode_codepoint >= ttUSHORT(data + search + rangeShift*2)) + search += rangeShift*2; + + // now decrement to bias correctly to find smallest + search -= 2; + while (entrySelector) { + stbtt_uint16 start, end; + searchRange >>= 1; + start = ttUSHORT(data + search + 2 + segcount*2 + 2); + end = ttUSHORT(data + search + 2); + start = ttUSHORT(data + search + searchRange*2 + segcount*2 + 2); + end = ttUSHORT(data + search + searchRange*2); + if (unicode_codepoint > end) + search += searchRange*2; + --entrySelector; + } + search += 2; + + item = (stbtt_uint16) ((search - endCount) >> 1); + + STBTT_assert(unicode_codepoint <= ttUSHORT(data + endCount + 2*item)); + start = ttUSHORT(data + index_map + 14 + segcount*2 + 2 + 2*item); + end = ttUSHORT(data + index_map + 14 + 2 + 2*item); + if (unicode_codepoint < start) + return 0; + + offset = ttUSHORT(data + index_map + 14 + segcount*6 + 2 + 2*item); + if (offset == 0) + return (stbtt_uint16) (unicode_codepoint + ttSHORT(data + index_map + 14 + segcount*4 + 2 + 2*item)); + + return ttUSHORT(data + offset + (unicode_codepoint-start)*2 + index_map + 14 + segcount*6 + 2 + 2*item); + } else if (format == 12 || format == 13) { + stbtt_uint32 ngroups = ttULONG(data+index_map+12); + stbtt_int32 low,high; + low = 0; high = (stbtt_int32)ngroups; + // Binary search the right group. + while (low < high) { + stbtt_int32 mid = low + ((high-low) >> 1); // rounds down, so low <= mid < high + stbtt_uint32 start_char = ttULONG(data+index_map+16+mid*12); + stbtt_uint32 end_char = ttULONG(data+index_map+16+mid*12+4); + if ((stbtt_uint32) unicode_codepoint < start_char) + high = mid; + else if ((stbtt_uint32) unicode_codepoint > end_char) + low = mid+1; + else { + stbtt_uint32 start_glyph = ttULONG(data+index_map+16+mid*12+8); + if (format == 12) + return start_glyph + unicode_codepoint-start_char; + else // format == 13 + return start_glyph; + } + } + return 0; // not found + } + // @TODO + STBTT_assert(0); + return 0; +} + +int stbtt_GetCodepointShape(const stbtt_fontinfo *info, int unicode_codepoint, stbtt_vertex **vertices) +{ + return stbtt_GetGlyphShape(info, stbtt_FindGlyphIndex(info, unicode_codepoint), vertices); +} + +static void stbtt_setvertex(stbtt_vertex *v, stbtt_uint8 type, stbtt_int32 x, stbtt_int32 y, stbtt_int32 cx, stbtt_int32 cy) +{ + v->type = type; + v->x = (stbtt_int16) x; + v->y = (stbtt_int16) y; + v->cx = (stbtt_int16) cx; + v->cy = (stbtt_int16) cy; +} + +static int stbtt__GetGlyfOffset(const stbtt_fontinfo *info, int glyph_index) +{ + int g1,g2; + + if (glyph_index >= info->numGlyphs) return -1; // glyph index out of range + if (info->indexToLocFormat >= 2) return -1; // unknown index->glyph map format + + if (info->indexToLocFormat == 0) { + g1 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2) * 2; + g2 = info->glyf + ttUSHORT(info->data + info->loca + glyph_index * 2 + 2) * 2; + } else { + g1 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4); + g2 = info->glyf + ttULONG (info->data + info->loca + glyph_index * 4 + 4); + } + + return g1==g2 ? -1 : g1; // if length is 0, return -1 +} + +int stbtt_GetGlyphBox(const stbtt_fontinfo *info, int glyph_index, int *x0, int *y0, int *x1, int *y1) +{ + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 0; + + if (x0) *x0 = ttSHORT(info->data + g + 2); + if (y0) *y0 = ttSHORT(info->data + g + 4); + if (x1) *x1 = ttSHORT(info->data + g + 6); + if (y1) *y1 = ttSHORT(info->data + g + 8); + return 1; +} + +int stbtt_GetCodepointBox(const stbtt_fontinfo *info, int codepoint, int *x0, int *y0, int *x1, int *y1) +{ + return stbtt_GetGlyphBox(info, stbtt_FindGlyphIndex(info,codepoint), x0,y0,x1,y1); +} + +int stbtt_IsGlyphEmpty(const stbtt_fontinfo *info, int glyph_index) +{ + stbtt_int16 numberOfContours; + int g = stbtt__GetGlyfOffset(info, glyph_index); + if (g < 0) return 1; + numberOfContours = ttSHORT(info->data + g); + return numberOfContours == 0; +} + +static int stbtt__close_shape(stbtt_vertex *vertices, int num_vertices, int was_off, int start_off, + stbtt_int32 sx, stbtt_int32 sy, stbtt_int32 scx, stbtt_int32 scy, stbtt_int32 cx, stbtt_int32 cy) +{ + if (start_off) { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+scx)>>1, (cy+scy)>>1, cx,cy); + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, sx,sy,scx,scy); + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve,sx,sy,cx,cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline,sx,sy,0,0); + } + return num_vertices; +} + +int stbtt_GetGlyphShape(const stbtt_fontinfo *info, int glyph_index, stbtt_vertex **pvertices) +{ + stbtt_int16 numberOfContours; + stbtt_uint8 *endPtsOfContours; + stbtt_uint8 *data = info->data; + stbtt_vertex *vertices=0; + int num_vertices=0; + int g = stbtt__GetGlyfOffset(info, glyph_index); + + *pvertices = NULL; + + if (g < 0) return 0; + + numberOfContours = ttSHORT(data + g); + + if (numberOfContours > 0) { + stbtt_uint8 flags=0,flagcount; + stbtt_int32 ins, i,j=0,m,n, next_move, was_off=0, off, start_off=0; + stbtt_int32 x,y,cx,cy,sx,sy, scx,scy; + stbtt_uint8 *points; + endPtsOfContours = (data + g + 10); + ins = ttUSHORT(data + g + 10 + numberOfContours * 2); + points = data + g + 10 + numberOfContours * 2 + 2 + ins; + + n = 1+ttUSHORT(endPtsOfContours + numberOfContours*2-2); + + m = n + 2*numberOfContours; // a loose bound on how many vertices we might need + vertices = (stbtt_vertex *) STBTT_malloc(m * sizeof(vertices[0]), info->userdata); + if (vertices == 0) + return 0; + + next_move = 0; + flagcount=0; + + // in first pass, we load uninterpreted data into the allocated array + // above, shifted to the end of the array so we won't overwrite it when + // we create our final data starting from the front + + off = m - n; // starting offset for uninterpreted data, regardless of how m ends up being calculated + + // first load flags + + for (i=0; i < n; ++i) { + if (flagcount == 0) { + flags = *points++; + if (flags & 8) + flagcount = *points++; + } else + --flagcount; + vertices[off+i].type = flags; + } + + // now load x coordinates + x=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 2) { + stbtt_int16 dx = *points++; + x += (flags & 16) ? dx : -dx; // ??? + } else { + if (!(flags & 16)) { + x = x + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].x = (stbtt_int16) x; + } + + // now load y coordinates + y=0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + if (flags & 4) { + stbtt_int16 dy = *points++; + y += (flags & 32) ? dy : -dy; // ??? + } else { + if (!(flags & 32)) { + y = y + (stbtt_int16) (points[0]*256 + points[1]); + points += 2; + } + } + vertices[off+i].y = (stbtt_int16) y; + } + + // now convert them to our format + num_vertices=0; + sx = sy = cx = cy = scx = scy = 0; + for (i=0; i < n; ++i) { + flags = vertices[off+i].type; + x = (stbtt_int16) vertices[off+i].x; + y = (stbtt_int16) vertices[off+i].y; + + if (next_move == i) { + if (i != 0) + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + + // now start the new one + start_off = !(flags & 1); + if (start_off) { + // if we start off with an off-curve point, then when we need to find a point on the curve + // where we can start, and we need to save some state for when we wraparound. + scx = x; + scy = y; + if (!(vertices[off+i+1].type & 1)) { + // next point is also a curve point, so interpolate an on-point curve + sx = (x + (stbtt_int32) vertices[off+i+1].x) >> 1; + sy = (y + (stbtt_int32) vertices[off+i+1].y) >> 1; + } else { + // otherwise just use the next point as our start point + sx = (stbtt_int32) vertices[off+i+1].x; + sy = (stbtt_int32) vertices[off+i+1].y; + ++i; // we're using point i+1 as the starting point, so skip it + } + } else { + sx = x; + sy = y; + } + stbtt_setvertex(&vertices[num_vertices++], STBTT_vmove,sx,sy,0,0); + was_off = 0; + next_move = 1 + ttUSHORT(endPtsOfContours+j*2); + ++j; + } else { + if (!(flags & 1)) { // if it's a curve + if (was_off) // two off-curve control points in a row means interpolate an on-curve midpoint + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, (cx+x)>>1, (cy+y)>>1, cx, cy); + cx = x; + cy = y; + was_off = 1; + } else { + if (was_off) + stbtt_setvertex(&vertices[num_vertices++], STBTT_vcurve, x,y, cx, cy); + else + stbtt_setvertex(&vertices[num_vertices++], STBTT_vline, x,y,0,0); + was_off = 0; + } + } + } + num_vertices = stbtt__close_shape(vertices, num_vertices, was_off, start_off, sx,sy,scx,scy,cx,cy); + } else if (numberOfContours == -1) { + // Compound shapes. + int more = 1; + stbtt_uint8 *comp = data + g + 10; + num_vertices = 0; + vertices = 0; + while (more) { + stbtt_uint16 flags, gidx; + int comp_num_verts = 0, i; + stbtt_vertex *comp_verts = 0, *tmp = 0; + float mtx[6] = {1,0,0,1,0,0}, m, n; + + flags = ttSHORT(comp); comp+=2; + gidx = ttSHORT(comp); comp+=2; + + if (flags & 2) { // XY values + if (flags & 1) { // shorts + mtx[4] = ttSHORT(comp); comp+=2; + mtx[5] = ttSHORT(comp); comp+=2; + } else { + mtx[4] = ttCHAR(comp); comp+=1; + mtx[5] = ttCHAR(comp); comp+=1; + } + } + else { + // @TODO handle matching point + STBTT_assert(0); + } + if (flags & (1<<3)) { // WE_HAVE_A_SCALE + mtx[0] = mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + } else if (flags & (1<<6)) { // WE_HAVE_AN_X_AND_YSCALE + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = mtx[2] = 0; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } else if (flags & (1<<7)) { // WE_HAVE_A_TWO_BY_TWO + mtx[0] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[1] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[2] = ttSHORT(comp)/16384.0f; comp+=2; + mtx[3] = ttSHORT(comp)/16384.0f; comp+=2; + } + + // Find transformation scales. + m = (float) sqrt(mtx[0]*mtx[0] + mtx[1]*mtx[1]); + n = (float) sqrt(mtx[2]*mtx[2] + mtx[3]*mtx[3]); + + // Get indexed glyph. + comp_num_verts = stbtt_GetGlyphShape(info, gidx, &comp_verts); + if (comp_num_verts > 0) { + // Transform vertices. + for (i = 0; i < comp_num_verts; ++i) { + stbtt_vertex* v = &comp_verts[i]; + stbtt_vertex_type x,y; + x=v->x; y=v->y; + v->x = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->y = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + x=v->cx; y=v->cy; + v->cx = (stbtt_vertex_type)(m * (mtx[0]*x + mtx[2]*y + mtx[4])); + v->cy = (stbtt_vertex_type)(n * (mtx[1]*x + mtx[3]*y + mtx[5])); + } + // Append vertices. + tmp = (stbtt_vertex*)STBTT_malloc((num_vertices+comp_num_verts)*sizeof(stbtt_vertex), info->userdata); + if (!tmp) { + if (vertices) STBTT_free(vertices, info->userdata); + if (comp_verts) STBTT_free(comp_verts, info->userdata); + return 0; + } + if (num_vertices > 0) memcpy(tmp, vertices, num_vertices*sizeof(stbtt_vertex)); + memcpy(tmp+num_vertices, comp_verts, comp_num_verts*sizeof(stbtt_vertex)); + if (vertices) STBTT_free(vertices, info->userdata); + vertices = tmp; + STBTT_free(comp_verts, info->userdata); + num_vertices += comp_num_verts; + } + // More components ? + more = flags & (1<<5); + } + } else if (numberOfContours < 0) { + // @TODO other compound variations? + STBTT_assert(0); + } else { + // numberOfCounters == 0, do nothing + } + + *pvertices = vertices; + return num_vertices; +} + +void stbtt_GetGlyphHMetrics(const stbtt_fontinfo *info, int glyph_index, int *advanceWidth, int *leftSideBearing) +{ + stbtt_uint16 numOfLongHorMetrics = ttUSHORT(info->data+info->hhea + 34); + if (glyph_index < numOfLongHorMetrics) { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*glyph_index); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*glyph_index + 2); + } else { + if (advanceWidth) *advanceWidth = ttSHORT(info->data + info->hmtx + 4*(numOfLongHorMetrics-1)); + if (leftSideBearing) *leftSideBearing = ttSHORT(info->data + info->hmtx + 4*numOfLongHorMetrics + 2*(glyph_index - numOfLongHorMetrics)); + } +} + +int stbtt_GetGlyphKernAdvance(const stbtt_fontinfo *info, int glyph1, int glyph2) +{ + stbtt_uint8 *data = info->data + info->kern; + stbtt_uint32 needle, straw; + int l, r, m; + + // we only look at the first table. it must be 'horizontal' and format 0. + if (!info->kern) + return 0; + if (ttUSHORT(data+2) < 1) // number of tables, need at least 1 + return 0; + if (ttUSHORT(data+8) != 1) // horizontal flag must be set in format + return 0; + + l = 0; + r = ttUSHORT(data+10) - 1; + needle = glyph1 << 16 | glyph2; + while (l <= r) { + m = (l + r) >> 1; + straw = ttULONG(data+18+(m*6)); // note: unaligned read + if (needle < straw) + r = m - 1; + else if (needle > straw) + l = m + 1; + else + return ttSHORT(data+22+(m*6)); + } + return 0; +} + +int stbtt_GetCodepointKernAdvance(const stbtt_fontinfo *info, int ch1, int ch2) +{ + if (!info->kern) // if no kerning table, don't waste time looking up both codepoint->glyphs + return 0; + return stbtt_GetGlyphKernAdvance(info, stbtt_FindGlyphIndex(info,ch1), stbtt_FindGlyphIndex(info,ch2)); +} + +void stbtt_GetCodepointHMetrics(const stbtt_fontinfo *info, int codepoint, int *advanceWidth, int *leftSideBearing) +{ + stbtt_GetGlyphHMetrics(info, stbtt_FindGlyphIndex(info,codepoint), advanceWidth, leftSideBearing); +} + +void stbtt_GetFontVMetrics(const stbtt_fontinfo *info, int *ascent, int *descent, int *lineGap) +{ + if (ascent ) *ascent = ttSHORT(info->data+info->hhea + 4); + if (descent) *descent = ttSHORT(info->data+info->hhea + 6); + if (lineGap) *lineGap = ttSHORT(info->data+info->hhea + 8); +} + +void stbtt_GetFontBoundingBox(const stbtt_fontinfo *info, int *x0, int *y0, int *x1, int *y1) +{ + *x0 = ttSHORT(info->data + info->head + 36); + *y0 = ttSHORT(info->data + info->head + 38); + *x1 = ttSHORT(info->data + info->head + 40); + *y1 = ttSHORT(info->data + info->head + 42); +} + +float stbtt_ScaleForPixelHeight(const stbtt_fontinfo *info, float height) +{ + int fheight = ttSHORT(info->data + info->hhea + 4) - ttSHORT(info->data + info->hhea + 6); + return (float) height / fheight; +} + +float stbtt_ScaleForMappingEmToPixels(const stbtt_fontinfo *info, float pixels) +{ + int unitsPerEm = ttUSHORT(info->data + info->head + 18); + return pixels / unitsPerEm; +} + +void stbtt_FreeShape(const stbtt_fontinfo *info, stbtt_vertex *v) +{ + STBTT_free(v, info->userdata); +} + +////////////////////////////////////////////////////////////////////////////// +// +// antialiasing software rasterizer +// + +void stbtt_GetGlyphBitmapBoxSubpixel(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y,float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + int x0,y0,x1,y1; + if (!stbtt_GetGlyphBox(font, glyph, &x0,&y0,&x1,&y1)) + x0=y0=x1=y1=0; // e.g. space character + // now move to integral bboxes (treating pixels as little squares, what pixels get touched)? + if (ix0) *ix0 = STBTT_ifloor(x0 * scale_x + shift_x); + if (iy0) *iy0 = -STBTT_iceil (y1 * scale_y + shift_y); + if (ix1) *ix1 = STBTT_iceil (x1 * scale_x + shift_x); + if (iy1) *iy1 = -STBTT_ifloor(y0 * scale_y + shift_y); +} +void stbtt_GetGlyphBitmapBox(const stbtt_fontinfo *font, int glyph, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, glyph, scale_x, scale_y,0.0f,0.0f, ix0, iy0, ix1, iy1); +} + +void stbtt_GetCodepointBitmapBoxSubpixel(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, float shift_x, float shift_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetGlyphBitmapBoxSubpixel(font, stbtt_FindGlyphIndex(font,codepoint), scale_x, scale_y,shift_x,shift_y, ix0,iy0,ix1,iy1); +} + +void stbtt_GetCodepointBitmapBox(const stbtt_fontinfo *font, int codepoint, float scale_x, float scale_y, int *ix0, int *iy0, int *ix1, int *iy1) +{ + stbtt_GetCodepointBitmapBoxSubpixel(font, codepoint, scale_x, scale_y,0.0f,0.0f, ix0,iy0,ix1,iy1); +} + +typedef struct stbtt__edge { + float x0,y0, x1,y1; + int invert; +} stbtt__edge; + +typedef struct stbtt__active_edge +{ + int x,dx; + float ey; + struct stbtt__active_edge *next; + int valid; +} stbtt__active_edge; + +#define FIXSHIFT 10 +#define FIX (1 << FIXSHIFT) +#define FIXMASK (FIX-1) + +static stbtt__active_edge *new_active(stbtt__edge *e, int off_x, float start_point, void *userdata) +{ + stbtt__active_edge *z = (stbtt__active_edge *) STBTT_malloc(sizeof(*z), userdata); // @TODO: make a pool of these!!! + float dxdy = (e->x1 - e->x0) / (e->y1 - e->y0); + STBTT_assert(e->y0 <= start_point); + if (!z) return z; + // round dx down to avoid going too far + if (dxdy < 0) + z->dx = -STBTT_ifloor(FIX * -dxdy); + else + z->dx = STBTT_ifloor(FIX * dxdy); + z->x = STBTT_ifloor(FIX * (e->x0 + dxdy * (start_point - e->y0))); + z->x -= off_x * FIX; + z->ey = e->y1; + z->next = 0; + z->valid = e->invert ? 1 : -1; + return z; +} + +// note: this routine clips fills that extend off the edges... ideally this +// wouldn't happen, but it could happen if the truetype glyph bounding boxes +// are wrong, or if the user supplies a too-small bitmap +static void stbtt__fill_active_edges(unsigned char *scanline, int len, stbtt__active_edge *e, int max_weight) +{ + // non-zero winding fill + int x0=0, w=0; + + while (e) { + if (w == 0) { + // if we're currently at zero, we need to record the edge start point + x0 = e->x; w += e->valid; + } else { + int x1 = e->x; w += e->valid; + // if we went to zero, we need to draw + if (w == 0) { + int i = x0 >> FIXSHIFT; + int j = x1 >> FIXSHIFT; + + if (i < len && j >= 0) { + if (i == j) { + // x0,x1 are the same pixel, so compute combined coverage + scanline[i] = scanline[i] + (stbtt_uint8) ((x1 - x0) * max_weight >> FIXSHIFT); + } else { + if (i >= 0) // add antialiasing for x0 + scanline[i] = scanline[i] + (stbtt_uint8) (((FIX - (x0 & FIXMASK)) * max_weight) >> FIXSHIFT); + else + i = -1; // clip + + if (j < len) // add antialiasing for x1 + scanline[j] = scanline[j] + (stbtt_uint8) (((x1 & FIXMASK) * max_weight) >> FIXSHIFT); + else + j = len; // clip + + for (++i; i < j; ++i) // fill pixels between x0 and x1 + scanline[i] = scanline[i] + (stbtt_uint8) max_weight; + } + } + } + } + + e = e->next; + } +} + +static void stbtt__rasterize_sorted_edges(stbtt__bitmap *result, stbtt__edge *e, int n, int vsubsample, int off_x, int off_y, void *userdata) +{ + stbtt__active_edge *active = NULL; + int y,j=0; + int max_weight = (255 / vsubsample); // weight per vertical scanline + int s; // vertical subsample index + unsigned char scanline_data[512], *scanline; + + if (result->w > 512) + scanline = (unsigned char *) STBTT_malloc(result->w, userdata); + else + scanline = scanline_data; + + y = off_y * vsubsample; + e[n].y0 = (off_y + result->h) * (float) vsubsample + 1; + + while (j < result->h) { + STBTT_memset(scanline, 0, result->w); + for (s=0; s < vsubsample; ++s) { + // find center of pixel for this scanline + float scan_y = y + 0.5f; + stbtt__active_edge **step = &active; + + // update all active edges; + // remove all active edges that terminate before the center of this scanline + while (*step) { + stbtt__active_edge * z = *step; + if (z->ey <= scan_y) { + *step = z->next; // delete from list + STBTT_assert(z->valid); + z->valid = 0; + STBTT_free(z, userdata); + } else { + z->x += z->dx; // advance to position for current scanline + step = &((*step)->next); // advance through list + } + } + + // resort the list if needed + for(;;) { + int changed=0; + step = &active; + while (*step && (*step)->next) { + if ((*step)->x > (*step)->next->x) { + stbtt__active_edge *t = *step; + stbtt__active_edge *q = t->next; + + t->next = q->next; + q->next = t; + *step = q; + changed = 1; + } + step = &(*step)->next; + } + if (!changed) break; + } + + // insert all edges that start before the center of this scanline -- omit ones that also end on this scanline + while (e->y0 <= scan_y) { + if (e->y1 > scan_y) { + stbtt__active_edge *z = new_active(e, off_x, scan_y, userdata); + // find insertion point + if (active == NULL) + active = z; + else if (z->x < active->x) { + // insert at front + z->next = active; + active = z; + } else { + // find thing to insert AFTER + stbtt__active_edge *p = active; + while (p->next && p->next->x < z->x) + p = p->next; + // at this point, p->next->x is NOT < z->x + z->next = p->next; + p->next = z; + } + } + ++e; + } + + // now process all active edges in XOR fashion + if (active) + stbtt__fill_active_edges(scanline, result->w, active, max_weight); + + ++y; + } + STBTT_memcpy(result->pixels + j * result->stride, scanline, result->w); + ++j; + } + + while (active) { + stbtt__active_edge *z = active; + active = active->next; + STBTT_free(z, userdata); + } + + if (scanline != scanline_data) + STBTT_free(scanline, userdata); +} + +static int stbtt__edge_compare(const void *p, const void *q) +{ + stbtt__edge *a = (stbtt__edge *) p; + stbtt__edge *b = (stbtt__edge *) q; + + if (a->y0 < b->y0) return -1; + if (a->y0 > b->y0) return 1; + return 0; +} + +typedef struct +{ + float x,y; +} stbtt__point; + +static void stbtt__rasterize(stbtt__bitmap *result, stbtt__point *pts, int *wcount, int windings, float scale_x, float scale_y, float shift_x, float shift_y, int off_x, int off_y, int invert, void *userdata) +{ + float y_scale_inv = invert ? -scale_y : scale_y; + stbtt__edge *e; + int n,i,j,k,m; + int vsubsample = result->h < 8 ? 15 : 5; + // vsubsample should divide 255 evenly; otherwise we won't reach full opacity + + // now we have to blow out the windings into explicit edge lists + n = 0; + for (i=0; i < windings; ++i) + n += wcount[i]; + + e = (stbtt__edge *) STBTT_malloc(sizeof(*e) * (n+1), userdata); // add an extra one as a sentinel + if (e == 0) return; + n = 0; + + m=0; + for (i=0; i < windings; ++i) { + stbtt__point *p = pts + m; + m += wcount[i]; + j = wcount[i]-1; + for (k=0; k < wcount[i]; j=k++) { + int a=k,b=j; + // skip the edge if horizontal + if (p[j].y == p[k].y) + continue; + // add edge from j to k to the list + e[n].invert = 0; + if (invert ? p[j].y > p[k].y : p[j].y < p[k].y) { + e[n].invert = 1; + a=j,b=k; + } + e[n].x0 = p[a].x * scale_x + shift_x; + e[n].y0 = p[a].y * y_scale_inv * vsubsample + shift_y; + e[n].x1 = p[b].x * scale_x + shift_x; + e[n].y1 = p[b].y * y_scale_inv * vsubsample + shift_y; + ++n; + } + } + + // now sort the edges by their highest point (should snap to integer, and then by x) + STBTT_sort(e, n, sizeof(e[0]), stbtt__edge_compare); + + // now, traverse the scanlines and find the intersections on each scanline, use xor winding rule + stbtt__rasterize_sorted_edges(result, e, n, vsubsample, off_x, off_y, userdata); + + STBTT_free(e, userdata); +} + +static void stbtt__add_point(stbtt__point *points, int n, float x, float y) +{ + if (!points) return; // during first pass, it's unallocated + points[n].x = x; + points[n].y = y; +} + +// tesselate until threshhold p is happy... @TODO warped to compensate for non-linear stretching +static int stbtt__tesselate_curve(stbtt__point *points, int *num_points, float x0, float y0, float x1, float y1, float x2, float y2, float objspace_flatness_squared, int n) +{ + // midpoint + float mx = (x0 + 2*x1 + x2)/4; + float my = (y0 + 2*y1 + y2)/4; + // versus directly drawn line + float dx = (x0+x2)/2 - mx; + float dy = (y0+y2)/2 - my; + if (n > 16) // 65536 segments on one curve better be enough! + return 1; + if (dx*dx+dy*dy > objspace_flatness_squared) { // half-pixel error allowed... need to be smaller if AA + stbtt__tesselate_curve(points, num_points, x0,y0, (x0+x1)/2.0f,(y0+y1)/2.0f, mx,my, objspace_flatness_squared,n+1); + stbtt__tesselate_curve(points, num_points, mx,my, (x1+x2)/2.0f,(y1+y2)/2.0f, x2,y2, objspace_flatness_squared,n+1); + } else { + stbtt__add_point(points, *num_points,x2,y2); + *num_points = *num_points+1; + } + return 1; +} + +// returns number of contours +stbtt__point *stbtt_FlattenCurves(stbtt_vertex *vertices, int num_verts, float objspace_flatness, int **contour_lengths, int *num_contours, void *userdata) +{ + stbtt__point *points=0; + int num_points=0; + + float objspace_flatness_squared = objspace_flatness * objspace_flatness; + int i,n=0,start=0, pass; + + // count how many "moves" there are to get the contour count + for (i=0; i < num_verts; ++i) + if (vertices[i].type == STBTT_vmove) + ++n; + + *num_contours = n; + if (n == 0) return 0; + + *contour_lengths = (int *) STBTT_malloc(sizeof(**contour_lengths) * n, userdata); + + if (*contour_lengths == 0) { + *num_contours = 0; + return 0; + } + + // make two passes through the points so we don't need to realloc + for (pass=0; pass < 2; ++pass) { + float x=0,y=0; + if (pass == 1) { + points = (stbtt__point *) STBTT_malloc(num_points * sizeof(points[0]), userdata); + if (points == NULL) goto error; + } + num_points = 0; + n= -1; + for (i=0; i < num_verts; ++i) { + switch (vertices[i].type) { + case STBTT_vmove: + // start the next contour + if (n >= 0) + (*contour_lengths)[n] = num_points - start; + ++n; + start = num_points; + + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x,y); + break; + case STBTT_vline: + x = vertices[i].x, y = vertices[i].y; + stbtt__add_point(points, num_points++, x, y); + break; + case STBTT_vcurve: + stbtt__tesselate_curve(points, &num_points, x,y, + vertices[i].cx, vertices[i].cy, + vertices[i].x, vertices[i].y, + objspace_flatness_squared, 0); + x = vertices[i].x, y = vertices[i].y; + break; + } + } + (*contour_lengths)[n] = num_points - start; + } + + return points; +error: + STBTT_free(points, userdata); + STBTT_free(*contour_lengths, userdata); + *contour_lengths = 0; + *num_contours = 0; + return NULL; +} + +void stbtt_Rasterize(stbtt__bitmap *result, float flatness_in_pixels, stbtt_vertex *vertices, int num_verts, float scale_x, float scale_y, float shift_x, float shift_y, int x_off, int y_off, int invert, void *userdata) +{ + float scale = scale_x > scale_y ? scale_y : scale_x; + int winding_count, *winding_lengths; + stbtt__point *windings = stbtt_FlattenCurves(vertices, num_verts, flatness_in_pixels / scale, &winding_lengths, &winding_count, userdata); + if (windings) { + stbtt__rasterize(result, windings, winding_lengths, winding_count, scale_x, scale_y, shift_x, shift_y, x_off, y_off, invert, userdata); + STBTT_free(winding_lengths, userdata); + STBTT_free(windings, userdata); + } +} + +void stbtt_FreeBitmap(unsigned char *bitmap, void *userdata) +{ + STBTT_free(bitmap, userdata); +} + +unsigned char *stbtt_GetGlyphBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + int ix0,iy0,ix1,iy1; + stbtt__bitmap gbm; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + + if (scale_x == 0) scale_x = scale_y; + if (scale_y == 0) { + if (scale_x == 0) return NULL; + scale_y = scale_x; + } + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,&ix1,&iy1); + + // now we get the size + gbm.w = (ix1 - ix0); + gbm.h = (iy1 - iy0); + gbm.pixels = NULL; // in case we error + + if (width ) *width = gbm.w; + if (height) *height = gbm.h; + if (xoff ) *xoff = ix0; + if (yoff ) *yoff = iy0; + + if (gbm.w && gbm.h) { + gbm.pixels = (unsigned char *) STBTT_malloc(gbm.w * gbm.h, info->userdata); + if (gbm.pixels) { + gbm.stride = gbm.w; + + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0, iy0, 1, info->userdata); + } + } + STBTT_free(vertices, info->userdata); + return gbm.pixels; +} + +unsigned char *stbtt_GetGlyphBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int glyph, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y, 0.0f, 0.0f, glyph, width, height, xoff, yoff); +} + +void stbtt_MakeGlyphBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int glyph) +{ + int ix0,iy0; + stbtt_vertex *vertices; + int num_verts = stbtt_GetGlyphShape(info, glyph, &vertices); + stbtt__bitmap gbm; + + stbtt_GetGlyphBitmapBoxSubpixel(info, glyph, scale_x, scale_y, shift_x, shift_y, &ix0,&iy0,0,0); + gbm.pixels = output; + gbm.w = out_w; + gbm.h = out_h; + gbm.stride = out_stride; + + if (gbm.w && gbm.h) + stbtt_Rasterize(&gbm, 0.35f, vertices, num_verts, scale_x, scale_y, shift_x, shift_y, ix0,iy0, 1, info->userdata); + + STBTT_free(vertices, info->userdata); +} + +void stbtt_MakeGlyphBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int glyph) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, glyph); +} + +unsigned char *stbtt_GetCodepointBitmapSubpixel(const stbtt_fontinfo *info, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetGlyphBitmapSubpixel(info, scale_x, scale_y,shift_x,shift_y, stbtt_FindGlyphIndex(info,codepoint), width,height,xoff,yoff); +} + +void stbtt_MakeCodepointBitmapSubpixel(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, float shift_x, float shift_y, int codepoint) +{ + stbtt_MakeGlyphBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, shift_x, shift_y, stbtt_FindGlyphIndex(info,codepoint)); +} + +unsigned char *stbtt_GetCodepointBitmap(const stbtt_fontinfo *info, float scale_x, float scale_y, int codepoint, int *width, int *height, int *xoff, int *yoff) +{ + return stbtt_GetCodepointBitmapSubpixel(info, scale_x, scale_y, 0.0f,0.0f, codepoint, width,height,xoff,yoff); +} + +void stbtt_MakeCodepointBitmap(const stbtt_fontinfo *info, unsigned char *output, int out_w, int out_h, int out_stride, float scale_x, float scale_y, int codepoint) +{ + stbtt_MakeCodepointBitmapSubpixel(info, output, out_w, out_h, out_stride, scale_x, scale_y, 0.0f,0.0f, codepoint); +} + +////////////////////////////////////////////////////////////////////////////// +// +// bitmap baking +// +// This is SUPER-CRAPPY packing to keep source code small + +extern int stbtt_BakeFontBitmap(const unsigned char *data, int offset, // font location (use offset=0 for plain .ttf) + float pixel_height, // height of font in pixels + unsigned char *pixels, int pw, int ph, // bitmap to be filled in + int first_char, int num_chars, // characters to bake + stbtt_bakedchar *chardata) +{ + float scale; + int x,y,bottom_y, i; + stbtt_fontinfo f; + stbtt_InitFont(&f, data, offset); + STBTT_memset(pixels, 0, pw*ph); // background of 0 around pixels + x=y=1; + bottom_y = 1; + + scale = stbtt_ScaleForPixelHeight(&f, pixel_height); + + for (i=0; i < num_chars; ++i) { + int advance, lsb, x0,y0,x1,y1,gw,gh; + int g = stbtt_FindGlyphIndex(&f, first_char + i); + stbtt_GetGlyphHMetrics(&f, g, &advance, &lsb); + stbtt_GetGlyphBitmapBox(&f, g, scale,scale, &x0,&y0,&x1,&y1); + gw = x1-x0; + gh = y1-y0; + if (x + gw + 1 >= pw) + y = bottom_y, x = 1; // advance to next row + if (y + gh + 1 >= ph) // check if it fits vertically AFTER potentially moving to next row + return -i; + STBTT_assert(x+gw < pw); + STBTT_assert(y+gh < ph); + stbtt_MakeGlyphBitmap(&f, pixels+x+y*pw, gw,gh,pw, scale,scale, g); + chardata[i].x0 = (stbtt_int16) x; + chardata[i].y0 = (stbtt_int16) y; + chardata[i].x1 = (stbtt_int16) (x + gw); + chardata[i].y1 = (stbtt_int16) (y + gh); + chardata[i].xadvance = scale * advance; + chardata[i].xoff = (float) x0; + chardata[i].yoff = (float) y0; + x = x + gw + 2; + if (y+gh+2 > bottom_y) + bottom_y = y+gh+2; + } + return bottom_y; +} + +void stbtt_GetBakedQuad(const stbtt_bakedchar *chardata, int pw, int ph, int char_index, float *xpos, float *ypos, stbtt_aligned_quad *q, int opengl_fillrule) +{ + float d3d_bias = opengl_fillrule ? 0 : -0.5f; + float ipw = 1.0f / pw, iph = 1.0f / ph; + const stbtt_bakedchar *b = chardata + char_index; + int round_x = STBTT_ifloor((*xpos + b->xoff) + 0.5); + int round_y = STBTT_ifloor((*ypos + b->yoff) + 0.5); + + q->x0 = round_x + d3d_bias; + q->y0 = round_y + d3d_bias; + q->x1 = round_x + b->x1 - b->x0 + d3d_bias; + q->y1 = round_y + b->y1 - b->y0 + d3d_bias; + + q->s0 = b->x0 * ipw; + q->t0 = b->y0 * iph; + q->s1 = b->x1 * ipw; + q->t1 = b->y1 * iph; + + *xpos += b->xadvance; +} + +////////////////////////////////////////////////////////////////////////////// +// +// font name matching -- recommended not to use this +// + +// check if a utf8 string contains a prefix which is the utf16 string; if so return length of matching utf8 string +static stbtt_int32 stbtt__CompareUTF8toUTF16_bigendian_prefix(const stbtt_uint8 *s1, stbtt_int32 len1, const stbtt_uint8 *s2, stbtt_int32 len2) +{ + stbtt_int32 i=0; + + // convert utf16 to utf8 and compare the results while converting + while (len2) { + stbtt_uint16 ch = s2[0]*256 + s2[1]; + if (ch < 0x80) { + if (i >= len1) return -1; + if (s1[i++] != ch) return -1; + } else if (ch < 0x800) { + if (i+1 >= len1) return -1; + if (s1[i++] != 0xc0 + (ch >> 6)) return -1; + if (s1[i++] != 0x80 + (ch & 0x3f)) return -1; + } else if (ch >= 0xd800 && ch < 0xdc00) { + stbtt_uint32 c; + stbtt_uint16 ch2 = s2[2]*256 + s2[3]; + if (i+3 >= len1) return -1; + c = ((ch - 0xd800) << 10) + (ch2 - 0xdc00) + 0x10000; + if (s1[i++] != 0xf0 + (c >> 18)) return -1; + if (s1[i++] != 0x80 + ((c >> 12) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((c ) & 0x3f)) return -1; + s2 += 2; // plus another 2 below + len2 -= 2; + } else if (ch >= 0xdc00 && ch < 0xe000) { + return -1; + } else { + if (i+2 >= len1) return -1; + if (s1[i++] != 0xe0 + (ch >> 12)) return -1; + if (s1[i++] != 0x80 + ((ch >> 6) & 0x3f)) return -1; + if (s1[i++] != 0x80 + ((ch ) & 0x3f)) return -1; + } + s2 += 2; + len2 -= 2; + } + return i; +} + +int stbtt_CompareUTF8toUTF16_bigendian(const char *s1, int len1, const char *s2, int len2) +{ + return len1 == stbtt__CompareUTF8toUTF16_bigendian_prefix((const stbtt_uint8*) s1, len1, (const stbtt_uint8*) s2, len2); +} + +// returns results in whatever encoding you request... but note that 2-byte encodings +// will be BIG-ENDIAN... use stbtt_CompareUTF8toUTF16_bigendian() to compare +const char *stbtt_GetFontNameString(const stbtt_fontinfo *font, int *length, int platformID, int encodingID, int languageID, int nameID) +{ + stbtt_int32 i,count,stringOffset; + stbtt_uint8 *fc = font->data; + stbtt_uint32 offset = font->fontstart; + stbtt_uint32 nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return NULL; + + count = ttUSHORT(fc+nm+2); + stringOffset = nm + ttUSHORT(fc+nm+4); + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + if (platformID == ttUSHORT(fc+loc+0) && encodingID == ttUSHORT(fc+loc+2) + && languageID == ttUSHORT(fc+loc+4) && nameID == ttUSHORT(fc+loc+6)) { + *length = ttUSHORT(fc+loc+8); + return (const char *) (fc+stringOffset+ttUSHORT(fc+loc+10)); + } + } + return NULL; +} + +static int stbtt__matchpair(stbtt_uint8 *fc, stbtt_uint32 nm, stbtt_uint8 *name, stbtt_int32 nlen, stbtt_int32 target_id, stbtt_int32 next_id) +{ + stbtt_int32 i; + stbtt_int32 count = ttUSHORT(fc+nm+2); + stbtt_int32 stringOffset = nm + ttUSHORT(fc+nm+4); + + for (i=0; i < count; ++i) { + stbtt_uint32 loc = nm + 6 + 12 * i; + stbtt_int32 id = ttUSHORT(fc+loc+6); + if (id == target_id) { + // find the encoding + stbtt_int32 platform = ttUSHORT(fc+loc+0), encoding = ttUSHORT(fc+loc+2), language = ttUSHORT(fc+loc+4); + + // is this a Unicode encoding? + if (platform == 0 || (platform == 3 && encoding == 1) || (platform == 3 && encoding == 10)) { + stbtt_int32 slen = ttUSHORT(fc+loc+8), off = ttUSHORT(fc+loc+10); + + // check if there's a prefix match + stbtt_int32 matchlen = stbtt__CompareUTF8toUTF16_bigendian_prefix(name, nlen, fc+stringOffset+off,slen); + if (matchlen >= 0) { + // check for target_id+1 immediately following, with same encoding & language + if (i+1 < count && ttUSHORT(fc+loc+12+6) == next_id && ttUSHORT(fc+loc+12) == platform && ttUSHORT(fc+loc+12+2) == encoding && ttUSHORT(fc+loc+12+4) == language) { + stbtt_int32 slen = ttUSHORT(fc+loc+12+8), off = ttUSHORT(fc+loc+12+10); + if (slen == 0) { + if (matchlen == nlen) + return 1; + } else if (matchlen < nlen && name[matchlen] == ' ') { + ++matchlen; + if (stbtt_CompareUTF8toUTF16_bigendian((char*) (name+matchlen), nlen-matchlen, (char*)(fc+stringOffset+off),slen)) + return 1; + } + } else { + // if nothing immediately following + if (matchlen == nlen) + return 1; + } + } + } + + // @TODO handle other encodings + } + } + return 0; +} + +static int stbtt__matches(stbtt_uint8 *fc, stbtt_uint32 offset, stbtt_uint8 *name, stbtt_int32 flags) +{ + stbtt_int32 nlen = (stbtt_int32) STBTT_strlen((char *) name); + stbtt_uint32 nm,hd; + if (!stbtt__isfont(fc+offset)) return 0; + + // check italics/bold/underline flags in macStyle... + if (flags) { + hd = stbtt__find_table(fc, offset, "head"); + if ((ttUSHORT(fc+hd+44) & 7) != (flags & 7)) return 0; + } + + nm = stbtt__find_table(fc, offset, "name"); + if (!nm) return 0; + + if (flags) { + // if we checked the macStyle flags, then just check the family and ignore the subfamily + if (stbtt__matchpair(fc, nm, name, nlen, 16, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, -1)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } else { + if (stbtt__matchpair(fc, nm, name, nlen, 16, 17)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 1, 2)) return 1; + if (stbtt__matchpair(fc, nm, name, nlen, 3, -1)) return 1; + } + + return 0; +} + +int stbtt_FindMatchingFont(const unsigned char *font_collection, const char *name_utf8, stbtt_int32 flags) +{ + stbtt_int32 i; + for (i=0;;++i) { + stbtt_int32 off = stbtt_GetFontOffsetForIndex(font_collection, i); + if (off < 0) return off; + if (stbtt__matches((stbtt_uint8 *) font_collection, off, (stbtt_uint8*) name_utf8, flags)) + return off; + } +} + +#endif // STB_TRUETYPE_IMPLEMENTATION diff --git a/src/third_party/texture_compressor/dxt_encoder.cc b/src/third_party/texture_compressor/dxt_encoder.cc new file mode 100644 index 0000000..40df3b8 --- /dev/null +++ b/src/third_party/texture_compressor/dxt_encoder.cc @@ -0,0 +1,760 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is based on the public domain code "stb_dxt.h" originally written +// by Fabian Giesen and Sean Barrett. +// +// The following changes were made: +// - Added support for ATC format. +// - Replaced the Principal Component Analysis based calculation to find the +// initial base colors with a much simpler bounding box implementation for low +// quality only. +// - Removed dithering support. +// - Some minor optimizations. +// - Reformatted the code (mainly with clang-format). +// - Swapped red and blue channels in the output to match Skia (Android only). + +#include "dxt_encoder.h" + +#include +#include +#include +#include +#include + +#include "dxt_encoder_internals.h" + +namespace { + +struct TYPE_ATC_GENERIC : public TYPE_ATC { + typedef TYPE_ATC BASE_TYPE; + static const int kRemap[8]; + static const int kW1Table[4]; + static const int kProds[4]; +}; + +struct TYPE_DXT_GENERIC : public TYPE_DXT { + typedef TYPE_DXT BASE_TYPE; + static const int kRemap[8]; + static const int kW1Table[4]; + static const int kProds[4]; +}; + +const int TYPE_ATC_GENERIC::kRemap[8] = + {0 << 30, 1 << 30, 0 << 30, 1 << 30, 2 << 30, 2 << 30, 3 << 30, 3 << 30}; +const int TYPE_ATC_GENERIC::kW1Table[4] = {3, 2, 1, 0}; +const int TYPE_ATC_GENERIC::kProds[4] = {0x090000, + 0x040102, + 0x010402, + 0x000900}; + +const int TYPE_DXT_GENERIC::kRemap[8] = + {0 << 30, 2 << 30, 0 << 30, 2 << 30, 3 << 30, 3 << 30, 1 << 30, 1 << 30}; +const int TYPE_DXT_GENERIC::kW1Table[4] = {3, 0, 2, 1}; +const int TYPE_DXT_GENERIC::kProds[4] = {0x090000, + 0x000900, + 0x040102, + 0x010402}; + +inline int Mul8Bit(int a, int b) { + int t = a * b + 128; + return (t + (t >> 8)) >> 8; +} + +// Linear interpolation at 1/3 point between a and b, using desired rounding +// type. +inline int Lerp13(int a, int b) { + // Without rounding bias. + // (2 * a + b) / 3; + return ((2 * a + b) * 0xaaab) >> 17; +} + +inline void Lerp13RGB(uint8_t* out, const uint8_t* p1, const uint8_t* p2) { + out[0] = Lerp13(p1[0], p2[0]); + out[1] = Lerp13(p1[1], p2[1]); + out[2] = Lerp13(p1[2], p2[2]); +} + +inline uint16_t As16Bit(int r, int g, int b) { + return (Mul8Bit(r, 31) << 11) + (Mul8Bit(g, 63) << 5) + Mul8Bit(b, 31); +} + +inline int sclamp(float y, int p0, int p1) { + int x = static_cast(y); + if (x < p0) { + return p0; + } + if (x > p1) { + return p1; + } + return x; +} + +// Take two 16-bit base colors and generate 4 32-bit RGBX colors where: +// 0 = c0 +// 1 = c1 +// 2 = (2 * c0 + c1) / 3 +// 3 = (2 * c1 + c0) / 3 +// +// The base colors are expanded by reusing the top bits at the end. That makes +// sure that white is still white after being quantized and converted back. +// +// params: +// color (output) 4 RGBA pixels. +// c0 base color 0 (16 bit RGB) +// c1 base color 1 (16 bit RGB) +inline void EvalColors(uint8_t* color, uint16_t c0, uint16_t c1) { + // Expand the two base colors to 32-bit + // From: [00000000][00000000][rrrrrggg][gggbbbbb] + // To: [00000000][bbbbbxxx][ggggggxx][rrrrrxxx] + // Where x means repeat the upper bits for that color component. + + // Take shortcut if either color is zero. Both will never be zero. + assert(c0 | c1); + if (c0 && c1) { + // Combine the two base colors into one register to allow operating on both + // pixels at the same time. + // [rrrrrggg][gggbbbbb][RRRRRGGG][GGGBBBBB] + uint32_t c01 = c1 | (c0 << 16); + + // Mask out the red components and shift it down one channel to avoid some + // shifts when combining the channels. + // [00000000][rrrrr000][00000000][RRRRR000] + uint32_t c01_r = (c01 & 0xf800f800) >> 8; + // Extend to be 8-bit by reusing top bits at the end. + // Note that we leave some extra garbage bits in the other channels, but + // that's ok since we mask that off when we combine the different + // components. + // [00000000][rrrrrrrr][xx000000][RRRRRRRR] + c01_r |= (c01_r >> 5); + + // Mask out the green components. + // [00000ggg][ggg00000][00000GGG][GGG00000] + uint32_t c01_g = c01 & 0x07e007e0; + // Shift it into place and extend. + // [gggggggg][xxxx0000][GGGGGGGG][xxxx0000] + c01_g = ((c01_g << 5) | (c01_g >> 1)); + + // Mask out the blue components. + // [00000000][000bbbbb][00000000][000BBBBB] + uint32_t c01_b = c01 & 0x001f001f; + // Shift it into place and extend. + // [bbbbbbbb][xx000000][BBBBBBBB][xx000000] + c01_b = ((c01_b << 11) | (c01_b << 6)); + + // Combine the components into base color 0. + // Shift the components into place and mask of each channel. + // [00000000][bbbbbbbb][gggggggg][rrrrrrrr] + *reinterpret_cast(color + 0) = ((c01_r >> 16) & 0x000000ff) | + ((c01_g >> 16) & 0x0000ff00) | + ((c01_b >> 8) & 0x00ff0000); + + // Combine the components into base color 1. + // [00000000][BBBBBBBB][GGGGGGGG][RRRRRRRR] + *reinterpret_cast(color + 4) = (c01_r & 0x000000ff) | + (c01_g & 0x0000ff00) | + ((c01_b << 8) & 0x00ff0000); + + Lerp13RGB(color + 8, color, color + 4); + Lerp13RGB(color + 12, color + 4, color); + } else { + // Combine the two base colors into one register, one of them will be zero. + // [00000000][00000000][rrrrrggg][gggbbbbb] + uint32_t c = c0 | c1; + + // Mask out the red components and shift it down one channel to avoid some + // shifts when combining the channels. + // [00000000][00000000][00000000][rrrrr000] + uint32_t c_r = (c & 0xf800) >> 8; + // Extend to be 8-bit by reusing top bits at the end. + // [00000000][00000000][00000000][rrrrrrrr] + c_r |= c_r >> 5; + + // Mask out the green components. + // [00000000][00000000][00000ggg][ggg00000] + uint32_t c_g = c & 0x07e0; + // Shift it into place and extend. Then mask off garbage bits. + // [00000000][00000000][gggggggg][xxxx0000] + c_g = ((c_g << 5) | (c_g >> 1)) & 0x0000ff00; + + // Mask out the blue components. + // [00000000][00000000][00000000][000bbbbb] + uint32_t c_b = c & 0x001f; + // Shift it into place and extend. Then mask off garbage bits. + // [00000000][bbbbbbbb][xx000000][00000000] + c_b = ((c_b << 19) | (c_b << 14)) & 0x00ff0000; + + size_t zero_offset = !!c0 * 4; + size_t nonzero_offset = !!c1 * 4; + + // Combine the components into non zero base color. + // [00000000][bbbbbbbb][gggggggg][rrrrrrrr] + *reinterpret_cast(color + nonzero_offset) = c_r | c_g | c_b; + + // We already know that the other base color is zero. + *reinterpret_cast(color + zero_offset) = 0; + + color[8 + nonzero_offset + 0] = + (color[nonzero_offset + 0] * (2 * 0xaaab)) >> 17; + color[8 + nonzero_offset + 1] = + (color[nonzero_offset + 1] * (2 * 0xaaab)) >> 17; + color[8 + nonzero_offset + 2] = + (color[nonzero_offset + 2] * (2 * 0xaaab)) >> 17; + + color[8 + zero_offset + 0] = (color[nonzero_offset + 0] * 0xaaab) >> 17; + color[8 + zero_offset + 1] = (color[nonzero_offset + 1] * 0xaaab) >> 17; + color[8 + zero_offset + 2] = (color[nonzero_offset + 2] * 0xaaab) >> 17; + } +} + +// The color matching function. +template +uint32_t MatchColorsBlock(const uint8_t* block, uint8_t* color) { + int dirr = color[0 * 4 + 0] - color[1 * 4 + 0]; + int dirg = color[0 * 4 + 1] - color[1 * 4 + 1]; + int dirb = color[0 * 4 + 2] - color[1 * 4 + 2]; + + int stops[4]; + for (int i = 0; i < 4; ++i) { + stops[i] = color[i * 4 + 0] * dirr + color[i * 4 + 1] * dirg + + color[i * 4 + 2] * dirb; + } + + // Think of the colors as arranged on a line; project point onto that line, + // then choose next color out of available ones. we compute the crossover + // points for "best color in top half"/"best in bottom half" and then the same + // inside that subinterval. + // + // Relying on this 1d approximation isn't always optimal in terms of euclidean + // distance, but it's very close and a lot faster. + // http://cbloomrants.blogspot.com/2008/12/12-08-08-dxtc-summary.html + + int c0_point = (stops[1] + stops[3]) >> 1; + int half_point = (stops[3] + stops[2]) >> 1; + int c3_point = (stops[2] + stops[0]) >> 1; + + uint32_t mask = 0; + for (int i = 0; i < 16; i++) { + int dot = block[i * 4 + 0] * dirr + block[i * 4 + 1] * dirg + + block[i * 4 + 2] * dirb; + + int bits = ((dot < half_point) ? 4 : 0) | ((dot < c0_point) ? 2 : 0) | + ((dot < c3_point) ? 1 : 0); + + mask >>= 2; + mask |= T::kRemap[bits]; + } + + return mask; +} + +void GetBaseColors(const uint8_t* block, + int v_r, + int v_g, + int v_b, + uint16_t* pmax16, + uint16_t* pmin16) { + // Pick colors at extreme points. +#ifdef VERIFY_RESULTS + // Rewritten to match the SIMD implementation, not as efficient. + int dots[16]; + for (int i = 0; i < 16; ++i) { + dots[i] = block[i * 4 + 0] * v_r + block[i * 4 + 1] * v_g + + block[i * 4 + 2] * v_b; + } + int max_dot = dots[0]; + int min_dot = dots[0]; + for (int i = 1; i < 16; ++i) { + if (dots[i] > max_dot) + max_dot = dots[i]; + if (dots[i] < min_dot) + min_dot = dots[i]; + } + uint32_t max_pixels[16]; + uint32_t min_pixels[16]; + for (int i = 0; i < 16; ++i) { + const uint32_t* p = reinterpret_cast(block) + i; + max_pixels[i] = (dots[i] == max_dot) ? *p : 0; + min_pixels[i] = (dots[i] == min_dot) ? *p : 0; + } + uint32_t max_pixel = max_pixels[0]; + uint32_t min_pixel = min_pixels[0]; + for (int i = 1; i < 16; ++i) { + if (max_pixels[i] > max_pixel) { + max_pixel = max_pixels[i]; + } + if (min_pixels[i] > min_pixel) { + min_pixel = min_pixels[i]; + } + } + uint8_t* maxp = reinterpret_cast(&max_pixel); + uint8_t* minp = reinterpret_cast(&min_pixel); +#else + int mind = 0x7fffffff; + int maxd = -0x7fffffff; + const uint8_t* minp = block; + const uint8_t* maxp = block; + for (int i = 0; i < 16; ++i) { + int dot = block[i * 4 + 0] * v_r + block[i * 4 + 1] * v_g + + block[i * 4 + 2] * v_b; + + if (dot < mind) { + mind = dot; + minp = block + i * 4; + } + + if (dot > maxd) { + maxd = dot; + maxp = block + i * 4; + } + } +#endif + + *pmax16 = As16Bit(maxp[0], maxp[1], maxp[2]); + *pmin16 = As16Bit(minp[0], minp[1], minp[2]); +} + +// Figure out the two base colors to use from a block of 16 pixels +// by Primary Component Analysis and map along principal axis. +// +// params: +// block 16 32-bit RGBX colors. +// pmax16 (output) base color 0 (minimum value), 16-bit RGB +// pmin16 (output) base color 1 (maximum value), 16-bit RGB +void OptimizeColorsBlock(const uint8_t* block, + uint16_t* pmax16, + uint16_t* pmin16) { + // Determine color distribution. + int mu[3]; + int min[3]; + int max[3]; + for (int ch = 0; ch < 3; ++ch) { + const uint8_t* bp = block + ch; + int muv = bp[0]; + int minv = muv; + int maxv = muv; + for (int i = 4; i < 64; i += 4) { + int pixel = bp[i]; + muv += pixel; + if (pixel < minv) { + minv = pixel; + } else if (pixel > maxv) { + maxv = pixel; + } + } + + mu[ch] = (muv + 8) >> 4; + min[ch] = minv; + max[ch] = maxv; + } + + // Determine covariance matrix. + int cov[6] = {0, 0, 0, 0, 0, 0}; + for (int i = 0; i < 16; ++i) { + int r = block[i * 4 + 0] - mu[0]; + int g = block[i * 4 + 1] - mu[1]; + int b = block[i * 4 + 2] - mu[2]; + + cov[0] += r * r; + cov[1] += r * g; + cov[2] += r * b; + cov[3] += g * g; + cov[4] += g * b; + cov[5] += b * b; + } + + // Convert covariance matrix to float, find principal axis via power iter. + float covf[6]; + for (int i = 0; i < 6; ++i) { + covf[i] = cov[i] / 255.0f; + } + + float vfr = static_cast(max[0] - min[0]); + float vfg = static_cast(max[1] - min[1]); + float vfb = static_cast(max[2] - min[2]); + + // Iterate to the power of 4. + for (int iter = 0; iter < 4; ++iter) { + float r = vfr * covf[0] + vfg * covf[1] + vfb * covf[2]; + float g = vfr * covf[1] + vfg * covf[3] + vfb * covf[4]; + float b = vfr * covf[2] + vfg * covf[4] + vfb * covf[5]; + + vfr = r; + vfg = g; + vfb = b; + } + + double magn = std::abs(vfr); + double mag_g = std::abs(vfg); + double mag_b = std::abs(vfb); + if (mag_g > magn) { + magn = mag_g; + } + if (mag_b > magn) { + magn = mag_b; + } + + int v_r = 299; // JPEG YCbCr luma coefs, scaled by 1000. + int v_g = 587; + int v_b = 114; + if (magn >= 4.0f) { // Too small, default to luminance. + magn = 512.0 / magn; + v_r = static_cast(vfr * magn); + v_g = static_cast(vfg * magn); + v_b = static_cast(vfb * magn); + } + + GetBaseColors(block, v_r, v_g, v_b, pmax16, pmin16); +} + +// Figure out the two base colors simply using a direction vector between min +// and max colors. +// +// params: +// block 16 32-bit RGBX colors. +// pmax16 (output) base color 0 (minimum value), 16-bit RGB +// pmin16 (output) base color 1 (maximum value), 16-bit RGB +void GetApproximateBaseColors(const uint8_t* block, + uint16_t* pmax16, + uint16_t* pmin16) { + uint8_t dir[3]; + for (int ch = 0; ch < 3; ++ch) { + const uint8_t* bp = block + ch; + uint8_t minv = bp[0]; + uint8_t maxv = bp[0]; + for (int i = 4; i < 64; i += 4) { + uint8_t pixel = bp[i]; + if (pixel < minv) { + minv = pixel; + } else if (pixel > maxv) { + maxv = pixel; + } + } + + dir[ch] = maxv - minv; + } + + GetBaseColors(block, dir[0], dir[1], dir[2], pmax16, pmin16); +} + +// The refinement function. +// Tries to optimize colors to suit block contents better. +// (By solving a least squares system via normal equations+Cramer's rule) +// +// params: +// block 16 32-bit RGBX colors. +// pmax16 (output) base color 0 (minimum value), 16-bit RGB +// pmin16 (output) base color 1 (maximum value), 16-bit RGB +// mask 16 2-bit color indices. +template +int RefineBlock(const uint8_t* block, + uint16_t* pmax16, + uint16_t* pmin16, + uint32_t mask) { + uint16_t min16 = 0; + uint16_t max16 = 0; + if ((mask ^ (mask << 2)) < 4) { // All pixels have the same index? + // Yes, linear system would be singular; solve using optimal + // single-color match on average color. + int r = 8; + int g = 8; + int b = 8; + for (int i = 0; i < 16; ++i) { + r += block[i * 4 + 0]; + g += block[i * 4 + 1]; + b += block[i * 4 + 2]; + } + + r >>= 4; + g >>= 4; + b >>= 4; + + max16 = Match8BitColorMax(r, g, b); + min16 = Match8BitColorMin(r, g, b); + } else { + int at1_r = 0; + int at1_g = 0; + int at1_b = 0; + int at2_r = 0; + int at2_g = 0; + int at2_b = 0; + int akku = 0; + uint32_t cm = mask; + for (int i = 0; i < 16; ++i, cm >>= 2) { + int step = cm & 3; + + int w1 = T::kW1Table[step]; + int r = block[i * 4 + 0]; + int g = block[i * 4 + 1]; + int b = block[i * 4 + 2]; + + // Some magic to save a lot of multiplies in the accumulating loop... + // (Precomputed products of weights for least squares system, accumulated + // inside one 32-bit register.) + akku += T::kProds[step]; + at1_r += w1 * r; + at1_g += w1 * g; + at1_b += w1 * b; + at2_r += r; + at2_g += g; + at2_b += b; + } + + at2_r = 3 * at2_r - at1_r; + at2_g = 3 * at2_g - at1_g; + at2_b = 3 * at2_b - at1_b; + + // Extract solutions and decide solvability. + int xx = akku >> 16; + int yy = (akku >> 8) & 0xff; + int xy = (akku >> 0) & 0xff; + + float frb = 3.0f * 31.0f / 255.0f / (xx * yy - xy * xy); + float fg = frb * 63.0f / 31.0f; + + // Solve. + max16 = sclamp((at1_r * yy - at2_r * xy) * frb + 0.5f, 0, 31) << 11; + max16 |= sclamp((at1_g * yy - at2_g * xy) * fg + 0.5f, 0, 63) << 5; + max16 |= sclamp((at1_b * yy - at2_b * xy) * frb + 0.5f, 0, 31) << 0; + + min16 = sclamp((at2_r * xx - at1_r * xy) * frb + 0.5f, 0, 31) << 11; + min16 |= sclamp((at2_g * xx - at1_g * xy) * fg + 0.5f, 0, 63) << 5; + min16 |= sclamp((at2_b * xx - at1_b * xy) * frb + 0.5f, 0, 31) << 0; + } + + uint16_t oldMin = *pmin16; + uint16_t oldMax = *pmax16; + *pmin16 = min16; + *pmax16 = max16; + return oldMin != min16 || oldMax != max16; +} + +// Color block compression. +template +void CompressColorBlock(uint8_t* dst, + const uint8_t* block, + TextureCompressor::Quality quality) { + // Check if block is constant. + int i = 1; + uint32_t first_pixel = + reinterpret_cast(block)[0] & 0x00ffffff; + for (; i < 16; ++i) { + if ((reinterpret_cast(block)[i] & 0x00ffffff) != + first_pixel) { + break; + } + } + + uint32_t mask = 0; + uint16_t max16 = 0; + uint16_t min16 = 0; + if (i == 16) { // Constant color + int r = block[0]; + int g = block[1]; + int b = block[2]; + max16 = Match8BitColorMax(r, g, b); + min16 = Match8BitColorMin(r, g, b); + mask = T::kConstantColorIndices; + } else { + if (quality == TextureCompressor::kQualityLow) { + GetApproximateBaseColors(block, &max16, &min16); + } else { + // Do Primary Component Analysis and map along principal axis. + OptimizeColorsBlock(block, &max16, &min16); + } + + if (max16 != min16) { + uint8_t color[4 * 4]; + EvalColors(color, max16, min16); + mask = MatchColorsBlock(block, color); + } + + if (quality == TextureCompressor::kQualityHigh) { + // Refine (multiple times if requested). + for (int i = 0; i < kNumRefinements; ++i) { + uint32_t last_mask = mask; + + if (RefineBlock(block, &max16, &min16, mask)) { + if (max16 != min16) { + uint8_t color[4 * 4]; + EvalColors(color, max16, min16); + mask = MatchColorsBlock(block, color); + } else { + mask = 0; + break; + } + } + + if (mask == last_mask) { + break; + } + } + } + } + + FormatFixup(&max16, &min16, &mask); + + uint32_t* dst32 = reinterpret_cast(dst); + dst32[0] = max16 | (min16 << 16); + dst32[1] = mask; +} + +// Alpha block compression. +void CompressAlphaBlock(uint8_t* dst, const uint8_t* src) { + // Find min/max alpha. + int mn = src[3]; + int mx = mn; + for (int i = 1; i < 16; ++i) { + int alpha = src[i * 4 + 3]; + if (alpha < mn) { + mn = alpha; + } else if (alpha > mx) { + mx = alpha; + } + } + + // Encode them. + dst[0] = mx; + dst[1] = mn; + dst += 2; + + if (mx == mn) { + memset(dst, 0, 6); + } else { + // Determine bias and emit color indices. + // Given the choice of mx/mn, these indices are optimal: + // http://fgiesen.wordpress.com/2009/12/15/dxt5-alpha-block-index-determination/ + int dist = mx - mn; + int dist4 = dist * 4; + int dist2 = dist * 2; + int bias = (dist < 8) ? (dist - 1) : (dist / 2 + 2); + bias -= mn * 7; + int bits = 0; + int mask = 0; + + for (int i = 0; i < 16; ++i) { + int a = src[i * 4 + 3] * 7 + bias; + + // Select index. this is a "linear scale" lerp factor between 0 (val=min) + // and 7 (val=max). + int t = (a >= dist4) ? -1 : 0; + int ind = t & 4; + a -= dist4 & t; + + t = (a >= dist2) ? -1 : 0; + ind += t & 2; + a -= dist2 & t; + + ind += (a >= dist); + + // Turn linear scale into DXT index (0/1 are extremal pts). + ind = -ind & 7; + ind ^= (2 > ind); + + // Write index. + mask |= ind << bits; + if ((bits += 3) >= 8) { + *dst++ = mask; + mask >>= 8; + bits -= 8; + } + } + } +} + +// Extract up to a 4x4 block of pixels and "de-swizzle" them into 16x1. +// If 'num_columns' or 'num_rows' are less than 4 then it fills out the rest +// of the block by taking a copy of the last valid column or row. +void ExtractBlock(uint8_t* dst, + const uint8_t* src, + int num_columns, + int num_rows, + int width) { + uint32_t* wdst = reinterpret_cast(dst); + const uint32_t* wsrc = reinterpret_cast(src); + + for (int y = 0; y < num_rows; ++y) { + for (int x = 0; x < num_columns; ++x) + *wdst++ = *wsrc++; + // Fill remaining columns with values from last column. + uint32_t* padding = wdst - 1; + for (int x = num_columns; x < 4; ++x) + *wdst++ = *padding; + wsrc += width - num_columns; + } + + // Fill remaining rows with values from last row. + const uint32_t* last_row = wdst - 4; + for (int y = num_rows; y < 4; ++y) { + const uint32_t* padding = last_row; + for (int x = 0; x < 4; ++x) + *wdst++ = *padding++; + } +} + +} // namespace + +void CompressATC(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality) { + assert(quality >= TextureCompressor::kQualityLow && + quality <= TextureCompressor::kQualityHigh); + + // The format works on blocks of 4x4 pixels. + // If the size is misaligned then we need to be careful when extracting the + // block of source texels. + + for (int y = 0; y < height; y += 4, src += width * 4 * 4) { + // Figure out if we need to skip the last few rows or not. + int num_rows = std::min(height - y, 4); + + for (int x = 0; x < width; x += 4) { + // Figure out of we need to skip the last few columns or not. + int num_columns = std::min(width - x, 4); + + uint8_t block[64]; + ExtractBlock(block, src + x * 4, num_columns, num_rows, width); + + if (!opaque) { + CompressAlphaBlock(dst, block); + dst += 8; + } + + CompressColorBlock(dst, block, quality); + dst += 8; + } + } +} + +void CompressDXT(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality) { + assert(quality >= TextureCompressor::kQualityLow && + quality <= TextureCompressor::kQualityHigh); + + for (int y = 0; y < height; y += 4, src += width * 4 * 4) { + int num_rows = std::min(height - y, 4); + + for (int x = 0; x < width; x += 4) { + int num_columns = std::min(width - x, 4); + + uint8_t block[64]; + ExtractBlock(block, src + x * 4, num_columns, num_rows, width); + + if (!opaque) { + CompressAlphaBlock(dst, block); + dst += 8; + } + + CompressColorBlock(dst, block, quality); + dst += 8; + } + } +} diff --git a/src/third_party/texture_compressor/dxt_encoder.h b/src/third_party/texture_compressor/dxt_encoder.h new file mode 100644 index 0000000..294b6d8 --- /dev/null +++ b/src/third_party/texture_compressor/dxt_encoder.h @@ -0,0 +1,30 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DXT_ENCODER_H_ +#define DXT_ENCODER_H_ + +#include + +#include "texture_compressor.h" + +// ATC compression works on blocks of 4 by 4 texels. Width and height of the +// source image must be multiple of 4. +void CompressATC(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality); + +// DXT compression works on blocks of 4 by 4 texels. Width and height of the +// source image must be multiple of 4. +void CompressDXT(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality); + +#endif // DXT_ENCODER_H_ diff --git a/src/third_party/texture_compressor/dxt_encoder_implementation_autogen.h b/src/third_party/texture_compressor/dxt_encoder_implementation_autogen.h new file mode 100644 index 0000000..abc6053 --- /dev/null +++ b/src/third_party/texture_compressor/dxt_encoder_implementation_autogen.h @@ -0,0 +1,785 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// This file is auto-generated from +// build_constant_color_dxt_tables.py +// It's formatted by clang-format using chromium coding style: +// clang-format -i -style=chromium filename +// DO NOT EDIT! + +#ifndef DXT_ENCODER_IMPLEMENTATION_AUTOGEN_H_ +#define DXT_ENCODER_IMPLEMENTATION_AUTOGEN_H_ + +const uint8_t kDXTConstantColors55[256][2] = {{0, 0}, + {0, 0}, + {0, 1}, + {0, 1}, + {1, 0}, + {1, 0}, + {1, 0}, + {1, 1}, + {1, 1}, + {2, 0}, + {2, 0}, + {0, 4}, + {2, 1}, + {2, 1}, + {2, 1}, + {3, 0}, + {3, 0}, + {3, 0}, + {3, 1}, + {1, 5}, + {3, 2}, + {3, 2}, + {4, 0}, + {4, 0}, + {4, 1}, + {4, 1}, + {4, 2}, + {4, 2}, + {4, 2}, + {3, 5}, + {5, 1}, + {5, 1}, + {5, 2}, + {4, 4}, + {5, 3}, + {5, 3}, + {5, 3}, + {6, 2}, + {6, 2}, + {6, 2}, + {6, 3}, + {5, 5}, + {6, 4}, + {6, 4}, + {4, 8}, + {7, 3}, + {7, 3}, + {7, 3}, + {7, 4}, + {7, 4}, + {7, 4}, + {7, 5}, + {5, 9}, + {7, 6}, + {7, 6}, + {8, 4}, + {8, 4}, + {8, 5}, + {8, 5}, + {8, 6}, + {8, 6}, + {8, 6}, + {7, 9}, + {9, 5}, + {9, 5}, + {9, 6}, + {8, 8}, + {9, 7}, + {9, 7}, + {9, 7}, + {10, 6}, + {10, 6}, + {10, 6}, + {10, 7}, + {9, 9}, + {10, 8}, + {10, 8}, + {8, 12}, + {11, 7}, + {11, 7}, + {11, 7}, + {11, 8}, + {11, 8}, + {11, 8}, + {11, 9}, + {9, 13}, + {11, 10}, + {11, 10}, + {12, 8}, + {12, 8}, + {12, 9}, + {12, 9}, + {12, 10}, + {12, 10}, + {12, 10}, + {11, 13}, + {13, 9}, + {13, 9}, + {13, 10}, + {12, 12}, + {13, 11}, + {13, 11}, + {13, 11}, + {14, 10}, + {14, 10}, + {14, 10}, + {14, 11}, + {13, 13}, + {14, 12}, + {14, 12}, + {12, 16}, + {15, 11}, + {15, 11}, + {15, 11}, + {15, 12}, + {15, 12}, + {15, 12}, + {15, 13}, + {13, 17}, + {15, 14}, + {15, 14}, + {16, 12}, + {16, 12}, + {16, 13}, + {16, 13}, + {16, 14}, + {16, 14}, + {16, 14}, + {15, 17}, + {17, 13}, + {17, 13}, + {17, 14}, + {16, 16}, + {17, 15}, + {17, 15}, + {17, 15}, + {18, 14}, + {18, 14}, + {18, 14}, + {18, 15}, + {17, 17}, + {18, 16}, + {18, 16}, + {16, 20}, + {19, 15}, + {19, 15}, + {19, 15}, + {19, 16}, + {19, 16}, + {19, 16}, + {19, 17}, + {17, 21}, + {19, 18}, + {19, 18}, + {20, 16}, + {20, 16}, + {20, 17}, + {20, 17}, + {20, 18}, + {20, 18}, + {20, 18}, + {19, 21}, + {21, 17}, + {21, 17}, + {21, 18}, + {20, 20}, + {21, 19}, + {21, 19}, + {21, 19}, + {22, 18}, + {22, 18}, + {22, 18}, + {22, 19}, + {21, 21}, + {22, 20}, + {22, 20}, + {20, 24}, + {23, 19}, + {23, 19}, + {23, 19}, + {23, 20}, + {23, 20}, + {23, 20}, + {23, 21}, + {21, 25}, + {23, 22}, + {23, 22}, + {24, 20}, + {24, 20}, + {24, 21}, + {24, 21}, + {24, 22}, + {24, 22}, + {24, 22}, + {23, 25}, + {25, 21}, + {25, 21}, + {25, 22}, + {24, 24}, + {25, 23}, + {25, 23}, + {25, 23}, + {26, 22}, + {26, 22}, + {26, 22}, + {26, 23}, + {25, 25}, + {26, 24}, + {26, 24}, + {24, 28}, + {27, 23}, + {27, 23}, + {27, 23}, + {27, 24}, + {27, 24}, + {27, 24}, + {27, 25}, + {25, 29}, + {27, 26}, + {27, 26}, + {28, 24}, + {28, 24}, + {28, 25}, + {28, 25}, + {28, 26}, + {28, 26}, + {28, 26}, + {27, 29}, + {29, 25}, + {29, 25}, + {29, 26}, + {28, 28}, + {29, 27}, + {29, 27}, + {29, 27}, + {30, 26}, + {30, 26}, + {30, 26}, + {30, 27}, + {29, 29}, + {30, 28}, + {30, 28}, + {30, 28}, + {31, 27}, + {31, 27}, + {31, 27}, + {31, 28}, + {31, 28}, + {31, 28}, + {31, 29}, + {31, 29}, + {31, 30}, + {31, 30}, + {31, 30}, + {31, 31}, + {31, 31}}; + +const uint8_t kDXTConstantColors56[256][2] = {{0, 0}, + {0, 1}, + {0, 2}, + {0, 2}, + {0, 3}, + {1, 0}, + {1, 1}, + {1, 1}, + {1, 2}, + {1, 3}, + {2, 0}, + {2, 0}, + {2, 1}, + {2, 2}, + {2, 3}, + {3, 0}, + {3, 0}, + {3, 1}, + {3, 2}, + {3, 2}, + {3, 3}, + {3, 4}, + {4, 0}, + {4, 1}, + {4, 2}, + {3, 7}, + {4, 3}, + {4, 4}, + {4, 5}, + {3, 10}, + {5, 2}, + {5, 3}, + {5, 4}, + {3, 13}, + {5, 5}, + {5, 6}, + {5, 7}, + {6, 4}, + {6, 4}, + {6, 5}, + {6, 6}, + {6, 6}, + {6, 7}, + {6, 8}, + {6, 9}, + {8, 1}, + {7, 6}, + {7, 7}, + {7, 8}, + {5, 16}, + {7, 9}, + {7, 10}, + {7, 11}, + {9, 3}, + {7, 12}, + {7, 13}, + {8, 9}, + {8, 10}, + {8, 11}, + {7, 16}, + {8, 12}, + {8, 13}, + {8, 14}, + {7, 19}, + {9, 11}, + {9, 12}, + {9, 13}, + {8, 17}, + {9, 14}, + {9, 15}, + {11, 8}, + {9, 16}, + {10, 13}, + {10, 14}, + {10, 15}, + {9, 19}, + {10, 16}, + {10, 17}, + {12, 9}, + {10, 18}, + {11, 15}, + {11, 16}, + {12, 12}, + {11, 17}, + {11, 18}, + {11, 19}, + {13, 11}, + {11, 20}, + {11, 21}, + {12, 17}, + {12, 18}, + {12, 19}, + {11, 24}, + {12, 20}, + {12, 21}, + {12, 22}, + {11, 27}, + {13, 19}, + {13, 20}, + {13, 21}, + {11, 30}, + {13, 22}, + {13, 23}, + {13, 24}, + {14, 21}, + {14, 21}, + {14, 22}, + {14, 23}, + {14, 23}, + {14, 24}, + {14, 25}, + {14, 26}, + {16, 18}, + {15, 23}, + {15, 24}, + {15, 25}, + {13, 33}, + {15, 26}, + {15, 27}, + {15, 28}, + {14, 32}, + {16, 25}, + {15, 30}, + {16, 26}, + {16, 27}, + {16, 28}, + {15, 33}, + {16, 29}, + {16, 30}, + {17, 27}, + {15, 36}, + {17, 28}, + {17, 29}, + {17, 30}, + {16, 34}, + {17, 31}, + {17, 32}, + {18, 29}, + {17, 33}, + {18, 30}, + {18, 31}, + {19, 28}, + {18, 32}, + {18, 33}, + {18, 34}, + {19, 31}, + {18, 35}, + {19, 32}, + {19, 33}, + {20, 29}, + {19, 34}, + {19, 35}, + {19, 36}, + {21, 28}, + {20, 33}, + {19, 38}, + {20, 34}, + {20, 35}, + {20, 36}, + {19, 41}, + {20, 37}, + {20, 38}, + {21, 35}, + {19, 44}, + {21, 36}, + {21, 37}, + {21, 38}, + {23, 31}, + {21, 39}, + {21, 40}, + {22, 37}, + {22, 37}, + {22, 38}, + {22, 39}, + {22, 40}, + {20, 48}, + {22, 41}, + {22, 42}, + {23, 39}, + {24, 35}, + {23, 40}, + {23, 41}, + {23, 42}, + {21, 50}, + {23, 43}, + {23, 44}, + {23, 45}, + {24, 41}, + {24, 42}, + {23, 47}, + {24, 43}, + {24, 44}, + {24, 45}, + {23, 50}, + {24, 46}, + {25, 43}, + {25, 44}, + {24, 48}, + {25, 45}, + {25, 46}, + {25, 47}, + {24, 51}, + {25, 48}, + {26, 45}, + {26, 46}, + {25, 50}, + {26, 47}, + {26, 48}, + {27, 45}, + {26, 49}, + {26, 50}, + {27, 47}, + {28, 43}, + {27, 48}, + {27, 49}, + {27, 50}, + {28, 46}, + {27, 51}, + {27, 52}, + {27, 53}, + {28, 49}, + {28, 50}, + {27, 55}, + {28, 51}, + {28, 52}, + {28, 53}, + {27, 58}, + {28, 54}, + {29, 51}, + {29, 52}, + {27, 61}, + {29, 53}, + {29, 54}, + {29, 55}, + {29, 55}, + {29, 56}, + {30, 53}, + {30, 54}, + {30, 54}, + {30, 55}, + {30, 56}, + {30, 57}, + {30, 57}, + {30, 58}, + {31, 55}, + {31, 56}, + {31, 56}, + {31, 57}, + {31, 58}, + {31, 59}, + {31, 59}, + {31, 60}, + {31, 61}, + {31, 62}, + {31, 62}, + {31, 63}}; + +const uint8_t kDXTConstantColors66[256][2] = {{0, 0}, + {0, 1}, + {1, 0}, + {1, 0}, + {1, 1}, + {2, 0}, + {2, 1}, + {3, 0}, + {3, 0}, + {3, 1}, + {4, 0}, + {4, 0}, + {4, 1}, + {5, 0}, + {5, 1}, + {6, 0}, + {6, 0}, + {6, 1}, + {7, 0}, + {7, 0}, + {7, 1}, + {8, 0}, + {8, 1}, + {8, 1}, + {8, 2}, + {9, 1}, + {9, 2}, + {9, 2}, + {9, 3}, + {10, 2}, + {10, 3}, + {10, 3}, + {10, 4}, + {11, 3}, + {11, 4}, + {11, 4}, + {11, 5}, + {12, 4}, + {12, 5}, + {12, 5}, + {12, 6}, + {13, 5}, + {13, 6}, + {8, 16}, + {13, 7}, + {14, 6}, + {14, 7}, + {9, 17}, + {14, 8}, + {15, 7}, + {15, 8}, + {11, 16}, + {15, 9}, + {15, 10}, + {16, 8}, + {16, 9}, + {16, 10}, + {15, 13}, + {17, 9}, + {17, 10}, + {17, 11}, + {15, 16}, + {18, 10}, + {18, 11}, + {18, 12}, + {16, 16}, + {19, 11}, + {19, 12}, + {19, 13}, + {17, 17}, + {20, 12}, + {20, 13}, + {20, 14}, + {19, 16}, + {21, 13}, + {21, 14}, + {21, 15}, + {20, 17}, + {22, 14}, + {22, 15}, + {25, 10}, + {22, 16}, + {23, 15}, + {23, 16}, + {26, 11}, + {23, 17}, + {24, 16}, + {24, 17}, + {27, 12}, + {24, 18}, + {25, 17}, + {25, 18}, + {28, 13}, + {25, 19}, + {26, 18}, + {26, 19}, + {29, 14}, + {26, 20}, + {27, 19}, + {27, 20}, + {30, 15}, + {27, 21}, + {28, 20}, + {28, 21}, + {28, 21}, + {28, 22}, + {29, 21}, + {29, 22}, + {24, 32}, + {29, 23}, + {30, 22}, + {30, 23}, + {25, 33}, + {30, 24}, + {31, 23}, + {31, 24}, + {27, 32}, + {31, 25}, + {31, 26}, + {32, 24}, + {32, 25}, + {32, 26}, + {31, 29}, + {33, 25}, + {33, 26}, + {33, 27}, + {31, 32}, + {34, 26}, + {34, 27}, + {34, 28}, + {32, 32}, + {35, 27}, + {35, 28}, + {35, 29}, + {33, 33}, + {36, 28}, + {36, 29}, + {36, 30}, + {35, 32}, + {37, 29}, + {37, 30}, + {37, 31}, + {36, 33}, + {38, 30}, + {38, 31}, + {41, 26}, + {38, 32}, + {39, 31}, + {39, 32}, + {42, 27}, + {39, 33}, + {40, 32}, + {40, 33}, + {43, 28}, + {40, 34}, + {41, 33}, + {41, 34}, + {44, 29}, + {41, 35}, + {42, 34}, + {42, 35}, + {45, 30}, + {42, 36}, + {43, 35}, + {43, 36}, + {46, 31}, + {43, 37}, + {44, 36}, + {44, 37}, + {44, 37}, + {44, 38}, + {45, 37}, + {45, 38}, + {40, 48}, + {45, 39}, + {46, 38}, + {46, 39}, + {41, 49}, + {46, 40}, + {47, 39}, + {47, 40}, + {43, 48}, + {47, 41}, + {47, 42}, + {48, 40}, + {48, 41}, + {48, 42}, + {47, 45}, + {49, 41}, + {49, 42}, + {49, 43}, + {47, 48}, + {50, 42}, + {50, 43}, + {50, 44}, + {48, 48}, + {51, 43}, + {51, 44}, + {51, 45}, + {49, 49}, + {52, 44}, + {52, 45}, + {52, 46}, + {51, 48}, + {53, 45}, + {53, 46}, + {53, 47}, + {52, 49}, + {54, 46}, + {54, 47}, + {57, 42}, + {54, 48}, + {55, 47}, + {55, 48}, + {58, 43}, + {55, 49}, + {56, 48}, + {56, 49}, + {59, 44}, + {56, 50}, + {57, 49}, + {57, 50}, + {60, 45}, + {57, 51}, + {58, 50}, + {58, 51}, + {61, 46}, + {58, 52}, + {59, 51}, + {59, 52}, + {62, 47}, + {59, 53}, + {60, 52}, + {60, 53}, + {60, 53}, + {60, 54}, + {61, 53}, + {61, 54}, + {61, 54}, + {61, 55}, + {62, 54}, + {62, 55}, + {62, 55}, + {62, 56}, + {63, 55}, + {63, 56}, + {63, 56}, + {63, 57}, + {63, 58}, + {63, 59}, + {63, 59}, + {63, 60}, + {63, 61}, + {63, 62}, + {63, 62}, + {63, 63}}; + +#endif // DXT_ENCODER_IMPLEMENTATION_AUTOGEN_H_ diff --git a/src/third_party/texture_compressor/dxt_encoder_internals.cc b/src/third_party/texture_compressor/dxt_encoder_internals.cc new file mode 100644 index 0000000..dcc7cd4 --- /dev/null +++ b/src/third_party/texture_compressor/dxt_encoder_internals.cc @@ -0,0 +1,7 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "dxt_encoder_internals.h" + +#include "dxt_encoder_implementation_autogen.h" diff --git a/src/third_party/texture_compressor/dxt_encoder_internals.h b/src/third_party/texture_compressor/dxt_encoder_internals.h new file mode 100644 index 0000000..3cf4cae --- /dev/null +++ b/src/third_party/texture_compressor/dxt_encoder_internals.h @@ -0,0 +1,85 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DXT_ENCODER_INTERNALS_H_ +#define DXT_ENCODER_INTERNALS_H_ + +#include + +extern const uint8_t kDXTConstantColors55[256][2]; +extern const uint8_t kDXTConstantColors66[256][2]; +extern const uint8_t kDXTConstantColors56[256][2]; + +// Types used to explicitly instantiate template functions. +struct TYPE_ATC { + static const uint32_t kConstantColorIndices = 0x55555555; +}; + +struct TYPE_DXT { + static const uint32_t kConstantColorIndices = 0xaaaaaaaa; +}; + +// Returns max and min base colors matching the given 8-bit color channels when +// solved via linear interpolation. Output format differs for ATC and DXT. See +// explicitly instantiated template functions below. +template +inline uint16_t Match8BitColorMax(int r, int g, int b); +template +inline uint16_t Match8BitColorMin(int r, int g, int b); + +template <> +inline uint16_t Match8BitColorMax(int r, int g, int b) { + return (kDXTConstantColors55[r][0] << 11) | + (kDXTConstantColors56[g][0] << 6) | kDXTConstantColors55[b][0]; +} + +template <> +inline uint16_t Match8BitColorMin(int r, int g, int b) { + return (kDXTConstantColors55[r][1] << 11) | + (kDXTConstantColors56[g][1] << 5) | kDXTConstantColors55[b][1]; +} + +template <> +inline uint16_t Match8BitColorMax(int r, int g, int b) { + return (kDXTConstantColors55[r][0] << 11) | + (kDXTConstantColors66[g][0] << 5) | kDXTConstantColors55[b][0]; +} + +template <> +inline uint16_t Match8BitColorMin(int r, int g, int b) { + return (kDXTConstantColors55[r][1] << 11) | + (kDXTConstantColors66[g][1] << 5) | kDXTConstantColors55[b][1]; +} + +// This converts the output data to either ATC or DXT format. +// See explicitly instantiated template functions below. +template +inline void FormatFixup(uint16_t* max16, uint16_t* min16, uint32_t* mask); + +template <> +inline void FormatFixup(uint16_t* max16, + uint16_t* min16, + uint32_t* mask) { + // First color in ATC format is 555. + *max16 = (*max16 & 0x001f) | ((*max16 & 0xffC0) >> 1); +} + +template <> +inline void FormatFixup(uint16_t* max16, + uint16_t* min16, + uint32_t* mask) { + // Swap min/max colors if necessary. + if (*max16 < *min16) { + uint16_t t = *min16; + *min16 = *max16; + *max16 = t; + *mask ^= 0x55555555; + } +} + +// Number of passes over the block that's done to refine the base colors. +// Only applies to high quality compression mode. +const int kNumRefinements = 2; + +#endif // DXT_ENCODER_INTERNALS_H_ diff --git a/src/third_party/texture_compressor/dxt_encoder_neon.cc b/src/third_party/texture_compressor/dxt_encoder_neon.cc new file mode 100644 index 0000000..6f389c8 --- /dev/null +++ b/src/third_party/texture_compressor/dxt_encoder_neon.cc @@ -0,0 +1,1268 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// See the links below for detailed descriptions of the algorithms used. +// http://cbloomrants.blogspot.se/2008/12/12-08-08-dxtc-summary.html +// http://fgiesen.wordpress.com/2009/12/15/dxt5-alpha-block-index-determination + +#include "dxt_encoder_neon.h" + +#include + +#include +#include + +#include "dxt_encoder_internals.h" + +#if defined(__GNUC__) +#define ALWAYS_INLINE __attribute__((always_inline)) +#else +#define ALWAYS_INLINE inline +#endif + +#define ALIGNAS(X) __attribute__ ((aligned (X))) + +namespace { + +struct TYPE_ATC_NEON : public TYPE_ATC { + typedef TYPE_ATC BASE_TYPE; + static const uint8x8_t kRemap; + static const uint64_t kProds[3]; +}; + +struct TYPE_DXT_NEON : public TYPE_DXT { + typedef TYPE_DXT BASE_TYPE; + static const uint8x8_t kRemap; + static const int8x8_t kW1Table; + static const uint64_t kProds[3]; +}; + +const uint8x8_t TYPE_ATC_NEON::kRemap = {0, 1, 0, 1, 2, 2, 3, 3}; +const uint64_t TYPE_ATC_NEON::kProds[3] = {0x00010409, 0x09040100, 0x00020200}; + +const uint8x8_t TYPE_DXT_NEON::kRemap = {0, 2, 0, 2, 3, 3, 1, 1}; +const int8x8_t TYPE_DXT_NEON::kW1Table = {3, 0, 2, 1, 0, 0, 0, 0}; +const uint64_t TYPE_DXT_NEON::kProds[3] = {0x01040009, 0x04010900, 0x02020000}; + +template +ALWAYS_INLINE int8x16_t DoW1TableLookup(uint8x16_t indices); + +template <> +ALWAYS_INLINE int8x16_t DoW1TableLookup(uint8x16_t indices) { + // Take a shortcut for ATC which gives the same result as the table lookup. + // {0, 1, 2, 3} -> {3, 2, 1, 0} + return veorq_s8(vreinterpretq_s8_u8(indices), vdupq_n_s8(3)); +} + +template <> +ALWAYS_INLINE int8x16_t DoW1TableLookup(uint8x16_t indices) { + // Do table lookup for each color index. + return vcombine_s8(vtbl1_s8(TYPE_DXT_NEON::kW1Table, + vreinterpret_s8_u8(vget_low_u8(indices))), + vtbl1_s8(TYPE_DXT_NEON::kW1Table, + vreinterpret_s8_u8(vget_high_u8(indices)))); +} + +// Returns max and min base color components matching the given 8-bit color +// component when solved via linear interpolation. Output format differs for ATC +// and DXT. See explicitly instantiated template functions below. +template +ALWAYS_INLINE uint16_t Match8BitComponentMax(int g); +template +ALWAYS_INLINE uint16_t Match8BitComponentMin(int g); + +template <> +ALWAYS_INLINE uint16_t Match8BitComponentMax(int g) { + return kDXTConstantColors56[g][0] << 1; +} + +template <> +ALWAYS_INLINE uint16_t Match8BitComponentMin(int g) { + return kDXTConstantColors56[g][1]; +} + +template <> +ALWAYS_INLINE uint16_t Match8BitComponentMax(int g) { + return kDXTConstantColors66[g][0]; +} + +template <> +ALWAYS_INLINE uint16_t Match8BitComponentMin(int g) { + return kDXTConstantColors66[g][1]; +} + +// This converts the output data to either ATC or DXT format. +// See explicitly instantiated template functions below. +template +ALWAYS_INLINE void FormatFixupIdx(uint16x4_t* base_colors, uint64x1_t* indices); + +template <> +ALWAYS_INLINE void FormatFixupIdx(uint16x4_t* base_colors, + uint64x1_t* indices) { + // First color in ATC format is 555. + *base_colors = vorr_u16( + vand_u16(*base_colors, vreinterpret_u16_u64(vdup_n_u64(0xffff001f))), + vshr_n_u16( + vand_u16(*base_colors, vreinterpret_u16_u64(vdup_n_u64(0x0000ffC0))), + 1)); +} + +template <> +ALWAYS_INLINE void FormatFixupIdx(uint16x4_t* base_colors, + uint64x1_t* indices) { + // Swap min/max colors if necessary. + uint16x4_t max = vdup_lane_u16(*base_colors, 0); + uint16x4_t min = vdup_lane_u16(*base_colors, 1); + uint16x4_t cmp = vclt_u16(max, min); + *base_colors = + vorr_u16(vand_u16(vbsl_u16(cmp, min, max), + vreinterpret_u16_u64(vdup_n_u64(0x0000ffff))), + vand_u16(vbsl_u16(cmp, max, min), + vreinterpret_u16_u64(vdup_n_u64(0xffff0000)))); + *indices = vbsl_u64(vreinterpret_u64_u16(cmp), + veor_u64(*indices, vdup_n_u64(0x55555555)), *indices); +} + +// Check if all the 8 bits elements in the given quad register are equal. +ALWAYS_INLINE bool ElementsEqual(uint8x16_t elements) { + uint8x16_t first = vdupq_lane_u8(vget_low_u8(elements), 0); + uint8x16_t eq = vceqq_u8(elements, first); + uint8x8_t tst = vand_u8(vget_low_u8(eq), vget_high_u8(eq)); + return vget_lane_u64(vreinterpret_u64_u8(tst), 0) == 0xffffffffffffffff; +} + +ALWAYS_INLINE bool Equal(uint8x16_t e1, uint8x16_t e2) { + uint8x16_t eq = vceqq_u8(e1, e2); + uint8x8_t tst = vand_u8(vget_low_u8(eq), vget_high_u8(eq)); + return vget_lane_u64(vreinterpret_u64_u8(tst), 0) == 0xffffffffffffffff; +} + +ALWAYS_INLINE bool Equal(uint16x8_t e1, uint16x8_t e2) { + uint16x8_t eq = vceqq_u16(e1, e2); + uint16x4_t tst = vand_u16(vget_low_u16(eq), vget_high_u16(eq)); + return vget_lane_u64(vreinterpret_u64_u16(tst), 0) == 0xffffffffffffffff; +} + +ALWAYS_INLINE bool Equal(uint16x4_t e1, uint16x4_t e2) { + uint16x4_t eq = vceq_u16(e1, e2); + return vget_lane_u64(vreinterpret_u64_u16(eq), 0) == 0xffffffffffffffff; +} + +ALWAYS_INLINE int16x8x2_t ExpandRGBATo16(const uint8x16_t& channel) { + int16x8x2_t result; + result.val[0] = vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(channel))); + result.val[1] = vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(channel))); + return result; +} + +ALWAYS_INLINE int32x4x4_t ExpandRGBATo32(const uint8x16_t& channel) { + uint16x8_t lo = vmovl_u8(vget_low_u8(channel)); + uint16x8_t hi = vmovl_u8(vget_high_u8(channel)); + int32x4x4_t result; + result.val[0] = vreinterpretq_s32_u32(vmovl_u16(vget_low_u16(lo))); + result.val[1] = vreinterpretq_s32_u32(vmovl_u16(vget_high_u16(lo))); + result.val[2] = vreinterpretq_s32_u32(vmovl_u16(vget_low_u16(hi))); + result.val[3] = vreinterpretq_s32_u32(vmovl_u16(vget_high_u16(hi))); + return result; +} + +// NEON doesn't have support for division. +// Instead it's recommended to use Newton-Raphson refinement to get a close +// approximation. +template +ALWAYS_INLINE float32x4_t Divide(float32x4_t a, float32x4_t b) { +#ifdef VERIFY_RESULTS + ALIGNAS(8) float a_[4]; + ALIGNAS(8) float b_[4]; + vst1q_f32(a_, a); + vst1q_f32(b_, b); + for (int i = 0; i < 4; ++i) + a_[i] /= b_[i]; + return vld1q_f32(a_); +#else + // Get an initial estimate of 1/b. + float32x4_t reciprocal = vrecpeq_f32(b); + // Use a number of Newton-Raphson steps to refine the estimate. + for (int i = 0; i < REFINEMENT_STEPS; ++i) + reciprocal = vmulq_f32(vrecpsq_f32(b, reciprocal), reciprocal); + // Calculate the final estimate. + return vmulq_f32(a, reciprocal); +#endif +} + +namespace vec_ops { + +struct Max { + ALWAYS_INLINE int32x4_t Calc(int32x4_t a, int32x4_t b) { + return vmaxq_s32(a, b); + } + + ALWAYS_INLINE uint32x4_t Calc(uint32x4_t a, uint32x4_t b) { + return vmaxq_u32(a, b); + } + + ALWAYS_INLINE uint8x8_t Fold(uint8x8_t a, uint8x8_t b) { + return vpmax_u8(a, b); + } + + ALWAYS_INLINE int32x2_t Fold(int32x2_t a, int32x2_t b) { + return vpmax_s32(a, b); + } + + ALWAYS_INLINE uint32x2_t Fold(uint32x2_t a, uint32x2_t b) { + return vpmax_u32(a, b); + } +}; + +struct Min { + ALWAYS_INLINE int32x4_t Calc(int32x4_t a, int32x4_t b) { + return vminq_s32(a, b); + } + + ALWAYS_INLINE uint32x4_t Calc(uint32x4_t a, uint32x4_t b) { + return vminq_u32(a, b); + } + + ALWAYS_INLINE uint8x8_t Fold(uint8x8_t a, uint8x8_t b) { + return vpmin_u8(a, b); + } + + ALWAYS_INLINE int32x2_t Fold(int32x2_t a, int32x2_t b) { + return vpmin_s32(a, b); + } + + ALWAYS_INLINE uint32x2_t Fold(uint32x2_t a, uint32x2_t b) { + return vpmin_u32(a, b); + } +}; + +} // namespace vec_ops + +template +ALWAYS_INLINE uint8x8_t FoldRGBA(const uint8x16x4_t& src) { + Operator op; + + // Fold each adjacent pair. + uint8x8_t r = op.Fold(vget_low_u8(src.val[0]), vget_high_u8(src.val[0])); + uint8x8_t g = op.Fold(vget_low_u8(src.val[1]), vget_high_u8(src.val[1])); + uint8x8_t b = op.Fold(vget_low_u8(src.val[2]), vget_high_u8(src.val[2])); + uint8x8_t a = op.Fold(vget_low_u8(src.val[3]), vget_high_u8(src.val[3])); + + // Do both red and green channels at the same time. + uint8x8_t rg = op.Fold(r, g); + + // Do both blue and alpha channels at the same time. + uint8x8_t ba = op.Fold(b, a); + + // Do all the channels at the same time. + uint8x8_t rgba = op.Fold(rg, ba); + + // Finally, we need to pad it to get the final reduction. + return op.Fold(rgba, rgba); +} + +template +ALWAYS_INLINE int32x2_t Fold(const int32x4x4_t& src) { + Operator op; + + int32x4_t fold0 = op.Calc(src.val[0], src.val[1]); + int32x4_t fold1 = op.Calc(src.val[2], src.val[3]); + int32x4_t fold01 = op.Calc(fold0, fold1); + int32x2_t fold0123 = op.Fold(vget_low_s32(fold01), vget_high_s32(fold01)); + return op.Fold(fold0123, vdup_n_s32(0)); +} + +template +ALWAYS_INLINE uint32x2_t Fold(const uint32x4x4_t& src) { + Operator op; + + uint32x4_t fold0 = op.Calc(src.val[0], src.val[1]); + uint32x4_t fold1 = op.Calc(src.val[2], src.val[3]); + uint32x4_t fold01 = op.Calc(fold0, fold1); + uint32x2_t fold0123 = op.Fold(vget_low_u32(fold01), vget_high_u32(fold01)); + return op.Fold(fold0123, vdup_n_u32(0)); +} + +template +ALWAYS_INLINE int32x4_t FoldDup(const int32x4x4_t& src) { + return vdupq_lane_s32(Fold(src), 0); +} + +ALWAYS_INLINE uint16x4_t SumRGB(const uint8x16x4_t& src) { + // Add up all red values for 16 pixels. + uint16x8_t r = vpaddlq_u8(src.val[0]); + uint16x4_t r2 = vpadd_u16(vget_low_u16(r), vget_high_u16(r)); + + // Add up all green values for 16 pixels. + uint16x8_t g = vpaddlq_u8(src.val[1]); + uint16x4_t g2 = vpadd_u16(vget_low_u16(g), vget_high_u16(g)); + + uint16x4_t rg = vpadd_u16(r2, g2); + + // Add up all blue values for 16 pixels. + uint16x8_t b = vpaddlq_u8(src.val[2]); + uint16x4_t b2 = vpadd_u16(vget_low_u16(b), vget_high_u16(b)); + + uint16x4_t ba = vpadd_u16(b2, vdup_n_u16(0)); + + return vpadd_u16(rg, ba); +} + +ALWAYS_INLINE int32x4_t SumRGB(const int16x8x4_t& src) { + // Add up all red values for 8 pixels. + int32x4_t r = vpaddlq_s16(src.val[0]); + int32x2_t r2 = vpadd_s32(vget_low_s32(r), vget_high_s32(r)); + + // Add up all green values for 8 pixels. + int32x4_t g = vpaddlq_s16(src.val[1]); + int32x2_t g2 = vpadd_s32(vget_low_s32(g), vget_high_s32(g)); + + int32x2_t rg = vpadd_s32(r2, g2); + + // Add up all blue values for 8 pixels. + int32x4_t b = vpaddlq_s16(src.val[2]); + int32x2_t b2 = vpadd_s32(vget_low_s32(b), vget_high_s32(b)); + + int32x2_t ba = vpadd_s32(b2, vdup_n_s32(0)); + + return vcombine_s32(rg, ba); +} + +ALWAYS_INLINE int32x4_t SumRGB(const int32x4x4_t& src) { + // Add up all red values for 4 pixels. + int32x2_t r = vmovn_s64(vpaddlq_s32(src.val[0])); + + // Add up all green values for 4 pixels. + int32x2_t g = vmovn_s64(vpaddlq_s32(src.val[1])); + + int32x2_t rg = vpadd_s32(r, g); + + // Add up all blue values for 4 pixels. + int32x2_t b = vmovn_s64(vpaddlq_s32(src.val[2])); + + int32x2_t ba = vpadd_s32(b, vdup_n_s32(0)); + + return vcombine_s32(rg, ba); +} + +ALWAYS_INLINE int32x4_t DotProduct(int32x4_t r, + int32x4_t g, + int32x4_t b, + int32x4_t dir_r, + int32x4_t dir_g, + int32x4_t dir_b) { + // Multiply and accumulate each 32 bits element. + int32x4_t dots = vmulq_s32(r, dir_r); + dots = vmlaq_s32(dots, g, dir_g); + dots = vmlaq_s32(dots, b, dir_b); + return dots; +} + +ALWAYS_INLINE int32x4x4_t CalculateDots(const int32x4x4_t& r, + const int32x4x4_t& g, + const int32x4x4_t& b, + const int32x4_t& v_vec) { + // Duplicate the red, green and blue luminance values. + int32x4_t r_vec = vdupq_n_s32(vgetq_lane_s32(v_vec, 0)); + int32x4_t g_vec = vdupq_n_s32(vgetq_lane_s32(v_vec, 1)); + int32x4_t b_vec = vdupq_n_s32(vgetq_lane_s32(v_vec, 2)); + + int32x4x4_t result; + result.val[0] = DotProduct(r.val[0], g.val[0], b.val[0], r_vec, g_vec, b_vec); + result.val[1] = DotProduct(r.val[1], g.val[1], b.val[1], r_vec, g_vec, b_vec); + result.val[2] = DotProduct(r.val[2], g.val[2], b.val[2], r_vec, g_vec, b_vec); + result.val[3] = DotProduct(r.val[3], g.val[3], b.val[3], r_vec, g_vec, b_vec); + return result; +} + +// Quantize given colors from 888rgb to 565rgb. +// in: [min_r min_g min_b 0 max_r max_g max_b 0] +// out: [min_r5 min_g6 min_b5 0][max_r5 max_g6 max_b5 0] +ALWAYS_INLINE uint16x8_t QuantizeTo565(uint8x8_t pixels) { + // Expand the components to signed 16 bit. + uint16x8_t pixels16 = vmovl_u8(pixels); + + // {31, 63, 31, 0, 31, 63, 31, 0}; + const uint16x8_t kMultiply = vreinterpretq_u16_u64(vdupq_n_u64(0x1f003f001f)); + uint16x8_t pixel0 = vmulq_u16(pixels16, kMultiply); + + // {128, 128, 128, 0, 128, 128, 128, 0}; + const uint16x8_t kAdd = vreinterpretq_u16_u64(vdupq_n_u64(0x8000800080)); + uint16x8_t pixel1 = vaddq_u16(pixel0, kAdd); + + // Create a shifted copy. + uint16x8_t pixel2 = vsraq_n_u16(pixel1, pixel1, 8); + + // Shift and return. + return vshrq_n_u16(pixel2, 8); +} + +// Combine the components of base colors in to 16 bits. +// in: [max_r5 max_g6 max_b5 0][min_r5 min_g6 min_b5 0] +// out: [max_rgb565 min_rgb565 0 0] +ALWAYS_INLINE uint16x4_t PackBaseColors(uint16x8_t base_colors) { + // Shift to pack RGB565 in 16-bit. + uint64x2_t r = + vshlq_u64(vreinterpretq_u64_u16(base_colors), vdupq_n_s64(-32)); + uint64x2_t g = + vshlq_u64(vreinterpretq_u64_u16(base_colors), vdupq_n_s64(-11)); + uint64x2_t b = vshlq_u64(vreinterpretq_u64_u16(base_colors), vdupq_n_s64(11)); + uint64x2_t base_colors_16 = vorrq_u64(r, vorrq_u64(g, b)); + + // Shift to pack 16-bit base colors in 32-bit and return. + return vreinterpret_u16_u64( + vorr_u64(vshl_n_u64(vget_high_u64(base_colors_16), 16), + vand_u64(vget_low_u64(base_colors_16), vdup_n_u64(0xffff)))); +} + +// Combine the given color indices. +// +// Params: +// S Size of an index in bits. +// indices Indices to be combined. Each of 8 bits element represents an index. +template +ALWAYS_INLINE uint64x1_t PackIndices(uint8x16_t indices) { + uint64x2_t ind = vshlq_n_u64(vreinterpretq_u64_u8(indices), 8 - S); + const uint64x2_t mask = vdupq_n_u64(0xff00000000000000); + uint64x2_t ind2 = vandq_u64(vshlq_n_u64(ind, 56), mask); + ind2 = vorrq_u64(vshrq_n_u64(ind2, S), vandq_u64(vshlq_n_u64(ind, 48), mask)); + ind2 = vorrq_u64(vshrq_n_u64(ind2, S), vandq_u64(vshlq_n_u64(ind, 40), mask)); + ind2 = vorrq_u64(vshrq_n_u64(ind2, S), vandq_u64(vshlq_n_u64(ind, 32), mask)); + ind2 = vorrq_u64(vshrq_n_u64(ind2, S), vandq_u64(vshlq_n_u64(ind, 24), mask)); + ind2 = vorrq_u64(vshrq_n_u64(ind2, S), vandq_u64(vshlq_n_u64(ind, 16), mask)); + ind2 = vorrq_u64(vshrq_n_u64(ind2, S), vandq_u64(vshlq_n_u64(ind, 8), mask)); + ind2 = vorrq_u64(vshrq_n_u64(ind2, S), vandq_u64(ind, mask)); + return vshr_n_u64( + vorr_u64(vshr_n_u64(vget_low_u64(ind2), (8 * S)), vget_high_u64(ind2)), + 64 - 16 * S); +} + +ALWAYS_INLINE int32x4_t +CovarianceChannels(const int16x8x2_t& ch1, const int16x8x2_t& ch2) { + // Multiply and accumulate. + int32x4_t cov; + cov = vmull_s16(vget_low_s16(ch1.val[0]), vget_low_s16(ch2.val[0])); + cov = vmlal_s16(cov, vget_high_s16(ch1.val[0]), vget_high_s16(ch2.val[0])); + cov = vmlal_s16(cov, vget_low_s16(ch1.val[1]), vget_low_s16(ch2.val[1])); + cov = vmlal_s16(cov, vget_high_s16(ch1.val[1]), vget_high_s16(ch2.val[1])); + return cov; +} + +ALWAYS_INLINE int32x4x2_t +Covariance(uint16x4_t average_rgb, const uint8x16x4_t& pixels_scattered) { + int16x8_t average_r = vreinterpretq_s16_u16(vdupq_lane_u16(average_rgb, 0)); + int16x8_t average_g = vreinterpretq_s16_u16(vdupq_lane_u16(average_rgb, 1)); + int16x8_t average_b = vreinterpretq_s16_u16(vdupq_lane_u16(average_rgb, 2)); + + // Subtract red values from the average red. + int16x8x2_t diff_r; + diff_r.val[0] = vsubq_s16( + vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(pixels_scattered.val[0]))), + average_r); + diff_r.val[1] = vsubq_s16( + vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(pixels_scattered.val[0]))), + average_r); + + // Subtract green values from the average green. + int16x8x2_t diff_g; + diff_g.val[0] = vsubq_s16( + vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(pixels_scattered.val[1]))), + average_g); + diff_g.val[1] = vsubq_s16( + vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(pixels_scattered.val[1]))), + average_g); + + // Subtract blue values from the average blue. + int16x8x2_t diff_b; + diff_b.val[0] = vsubq_s16( + vreinterpretq_s16_u16(vmovl_u8(vget_low_u8(pixels_scattered.val[2]))), + average_b); + diff_b.val[1] = vsubq_s16( + vreinterpretq_s16_u16(vmovl_u8(vget_high_u8(pixels_scattered.val[2]))), + average_b); + + int32x4x4_t cov1; + cov1.val[0] = CovarianceChannels(diff_r, diff_r); + cov1.val[1] = CovarianceChannels(diff_r, diff_g); + cov1.val[2] = CovarianceChannels(diff_r, diff_b); + cov1.val[3] = vdupq_n_s32(0); + + int32x4x4_t cov2; + cov2.val[0] = CovarianceChannels(diff_g, diff_g); + cov2.val[1] = CovarianceChannels(diff_g, diff_b); + cov2.val[2] = CovarianceChannels(diff_b, diff_b); + cov2.val[3] = vdupq_n_s32(0); + + int32x4x2_t covariance; + covariance.val[0] = SumRGB(cov1); + covariance.val[1] = SumRGB(cov2); + return covariance; +} + +ALWAYS_INLINE uint32x2_t MaskOutPixel(const uint8x16x4_t& pixels_linear, + const int32x4x4_t& dots, + int32x4_t max_dot_vec) { + // Mask out any of the 16 pixels where the dot product matches exactly. + uint32x4x4_t pixels; + pixels.val[0] = vandq_u32(vceqq_s32(dots.val[0], max_dot_vec), + vreinterpretq_u32_u8(pixels_linear.val[0])); + + pixels.val[1] = vandq_u32(vceqq_s32(dots.val[1], max_dot_vec), + vreinterpretq_u32_u8(pixels_linear.val[1])); + + pixels.val[2] = vandq_u32(vceqq_s32(dots.val[2], max_dot_vec), + vreinterpretq_u32_u8(pixels_linear.val[2])); + + pixels.val[3] = vandq_u32(vceqq_s32(dots.val[3], max_dot_vec), + vreinterpretq_u32_u8(pixels_linear.val[3])); + + // Fold it down. + return Fold(pixels); +} + +ALWAYS_INLINE uint16x8_t GetBaseColors(const uint8x16x4_t& pixels_linear, + const uint8x16x4_t& pixels_scattered, + int32x4_t dir) { + // Expand all pixels to signed 32-bit integers. + int32x4x4_t r = ExpandRGBATo32(pixels_scattered.val[0]); + int32x4x4_t g = ExpandRGBATo32(pixels_scattered.val[1]); + int32x4x4_t b = ExpandRGBATo32(pixels_scattered.val[2]); + + int32x4x4_t dots = CalculateDots(r, g, b, dir); + + // Mask out the pixel(s) that matches the max dot. + uint32x2_t max_pixel = + MaskOutPixel(pixels_linear, dots, FoldDup(dots)); + + // Mask out the pixel(s) that matches the min dot. + uint32x2_t min_pixel = + MaskOutPixel(pixels_linear, dots, FoldDup(dots)); + + return QuantizeTo565( + vreinterpret_u8_u32(vzip_u32(max_pixel, min_pixel).val[0])); +} + +// Figure out the two base colors to use from a block of 16 pixels +// by Primary Component Analysis and map along principal axis. +ALWAYS_INLINE uint16x8_t +OptimizeColorsBlock(const uint8x16x4_t& pixels_linear, + const uint8x16x4_t& pixels_scattered, + uint16x4_t sum_rgb, + uint8x8_t min_rgba, + uint8x8_t max_rgba) { + // min_rgba: [min_r min_g min_b min_a x x x x] + // max_rgba: [max_r max_g max_b max_a x x x x] + + // Determine color distribution. We already have the max and min, now we need + // the average of the 16 pixels. Divide sum_rgb with rounding. + uint16x4_t average_rgb = vrshr_n_u16(sum_rgb, 4); + + // Determine covariance matrix. + int32x4x2_t covariance = Covariance(average_rgb, pixels_scattered); + + // Convert covariance matrix to float, find principal axis via power + // iteration. + float32x4x2_t covariance_float; + const float32x4_t kInv255 = vdupq_n_f32(1.0f / 255.0f); + covariance_float.val[0] = + vmulq_f32(vcvtq_f32_s32(covariance.val[0]), kInv255); + covariance_float.val[1] = + vmulq_f32(vcvtq_f32_s32(covariance.val[1]), kInv255); + + int16x4_t max_16 = vreinterpret_s16_u16(vget_low_u16(vmovl_u8(max_rgba))); + int16x4_t min_16 = vreinterpret_s16_u16(vget_low_u16(vmovl_u8(min_rgba))); + float32x4_t vf4 = vcvtq_f32_s32(vsubl_s16(max_16, min_16)); + + for (int i = 0; i < 4; ++i) { + float32x4_t vfr4 = vdupq_n_f32(vgetq_lane_f32(vf4, 0)); + float32x4_t vfg4 = vdupq_n_f32(vgetq_lane_f32(vf4, 1)); + float32x4_t vfb4 = vdupq_n_f32(vgetq_lane_f32(vf4, 2)); + + // from: [0 1 2 x] [3 4 5 x] + // to: [1 3 4 x] + float32x4_t cov_134 = + vextq_f32(covariance_float.val[1], covariance_float.val[1], 3); + cov_134 = + vsetq_lane_f32(vgetq_lane_f32(covariance_float.val[0], 1), cov_134, 0); + + // from: [0 1 2 x] [3 4 5 x] + // to: [2 4 5 x] + float32x4_t cov_245 = vsetq_lane_f32( + vgetq_lane_f32(covariance_float.val[0], 2), covariance_float.val[1], 0); + + vf4 = vmulq_f32(vfr4, covariance_float.val[0]); + vf4 = vmlaq_f32(vf4, vfg4, cov_134); + vf4 = vmlaq_f32(vf4, vfb4, cov_245); + } + + float32x4_t magnitude = vabsq_f32(vf4); + magnitude = vsetq_lane_f32(0.0f, magnitude, 3); // Null out alpha. + float32x4_t mag4 = vdupq_lane_f32( + vpmax_f32(vpmax_f32(vget_low_f32(magnitude), vget_high_f32(magnitude)), + vdup_n_f32(0.0f)), + 0); + + const int32x4_t kLuminance = {299, 587, 114, 0}; + + // Note that this quite often means dividing by zero. The math still works + // when comparing with Inf though. + float32x4_t inv_magnitude = Divide<2>(vdupq_n_f32(512.0f), mag4); + + int32x4_t vf4_mag = vcvtq_s32_f32(vmulq_f32(vf4, inv_magnitude)); + int32x4_t v = + vbslq_s32(vcltq_f32(mag4, vdupq_n_f32(4.0f)), kLuminance, vf4_mag); + + return GetBaseColors(pixels_linear, pixels_scattered, v); +} + +ALWAYS_INLINE uint16x8_t +GetApproximateBaseColors(const uint8x16x4_t& pixels_linear, + const uint8x16x4_t& pixels_scattered, + uint8x8_t min_rgba, + uint8x8_t max_rgba) { + // min_rgba: [min_r min_g min_b min_a x x x x] + // max_rgba: [max_r max_g max_b max_a x x x x] + + // Get direction vector and expand to 32-bit. + int16x4_t max_16 = vreinterpret_s16_u16(vget_low_u16(vmovl_u8(max_rgba))); + int16x4_t min_16 = vreinterpret_s16_u16(vget_low_u16(vmovl_u8(min_rgba))); + int32x4_t v = vsubl_s16(max_16, min_16); + + return GetBaseColors(pixels_linear, pixels_scattered, v); +} + +// Take two base colors and generate 4 RGBX colors where: +// 0 = baseColor0 +// 1 = baseColor1 +// 2 = (2 * baseColor0 + baseColor1) / 3 +// 3 = (2 * baseColor1 + baseColor0) / 3 +ALWAYS_INLINE uint16x4x4_t EvalColors(const uint16x8_t& base_colors) { + // The base colors are expanded by reusing the top bits at the end. That makes + // sure that white is still white after being quantized and converted back. + // + // [(r<<3 | r>>2) (g<<2 | g>>4) (b<<3 | b>>2) 0] + + // The upper shift values for each component. + // {3, 2, 3, 0, 3, 2, 3, 0}; + const int16x8_t kShiftUp = vreinterpretq_s16_u64(vdupq_n_u64(0x300020003)); + uint16x8_t pixels_up = vshlq_u16(base_colors, kShiftUp); + // [r0<<3 g0<<2 b0<<3 0] [r1<<3 g1<<2 b1<<3 0] + + // The lower shift values for each component. + // Note that we need to use negative values to shift right. + // {-2, -4, -2, 0, -2, -4, -2, 0}; + const int16x8_t kShiftDown = + vreinterpretq_s16_u64(vdupq_n_u64(0xfffefffcfffe)); + uint16x8_t pixels_down = vshlq_u16(base_colors, kShiftDown); + // [r0>>2 g0>>4 b0>>2 0] [r1>>2 g1>>4 b1>>2 0] + + uint16x8_t pixels = vorrq_u16(pixels_up, pixels_down); + // [(r0<<3 | r0>>2) (g0<<2 | g0>>4) (b0<<3 | b0>>2) 0] + // [(r1<<3 | r1>>2) (g1<<2 | g1>>4) (b1<<3 | b1>>2) 0] + + // Linear interpolate the two other colors: + // (2 * max + min) / 3 + // (2 * min + max) / 3 + + uint16x8_t pixels_mul2 = vaddq_u16(pixels, pixels); + + uint16x8_t swapped = vreinterpretq_u16_u64(vextq_u64( + vreinterpretq_u64_u16(pixels), vreinterpretq_u64_u16(pixels), 1)); + int16x8_t output = vreinterpretq_s16_u16(vaddq_u16(pixels_mul2, swapped)); + + // There's no division in NEON, but we can use "x * ((1 << 16) / 3 + 1))" + // instead. + output = vqdmulhq_s16(output, vdupq_n_s16(((1 << 16) / 3 + 1) >> 1)); + + uint16x4x4_t colors; + colors.val[0] = vget_low_u16(pixels); + colors.val[1] = vget_high_u16(pixels); + colors.val[2] = vreinterpret_u16_s16(vget_low_s16(output)); + colors.val[3] = vreinterpret_u16_s16(vget_high_s16(output)); + return colors; +} + +ALWAYS_INLINE uint8x8_t GetRemapIndices(int32x4_t dots, + int32x4_t half_point, + int32x4_t c0_point, + int32x4_t c3_point) { + // bits = (dot < half_point ? 4 : 0) + // | (dot < c0_point ? 2 : 0) + // | (dot < c3_point ? 1 : 0) + int32x4_t cmp0 = vreinterpretq_s32_u32( + vandq_u32(vcgtq_s32(half_point, dots), vdupq_n_u32(4))); + int32x4_t cmp1 = vreinterpretq_s32_u32( + vandq_u32(vcgtq_s32(c0_point, dots), vdupq_n_u32(2))); + int32x4_t cmp2 = vreinterpretq_s32_u32( + vandq_u32(vcgtq_s32(c3_point, dots), vdupq_n_u32(1))); + int32x4_t bits = vorrq_s32(vorrq_s32(cmp0, cmp1), cmp2); + + // Narrow it down to unsigned 8 bits and return. + return vqmovn_u16(vcombine_u16(vqmovun_s32(bits), vdup_n_u16(0))); +} + +// dots: Dot products for each pixel. +// points: Crossover points. +template +ALWAYS_INLINE uint8x16_t GetColorIndices(int32x4x4_t dots, int32x4_t points) { + // Crossover points for "best color in top half"/"best in bottom half" and + // the same inside that subinterval. + int32x4_t c0_point = vdupq_lane_s32(vget_low_s32(points), 1); + int32x4_t half_point = vdupq_lane_s32(vget_low_s32(points), 0); + int32x4_t c3_point = vdupq_lane_s32(vget_high_s32(points), 0); + + // Get kRemap table indices. + uint8x8x4_t ind; + ind.val[0] = GetRemapIndices(dots.val[0], half_point, c0_point, c3_point); + ind.val[1] = GetRemapIndices(dots.val[1], half_point, c0_point, c3_point); + ind.val[2] = GetRemapIndices(dots.val[2], half_point, c0_point, c3_point); + ind.val[3] = GetRemapIndices(dots.val[3], half_point, c0_point, c3_point); + + // Combine indices. + uint8x8_t indices_lo = + vreinterpret_u8_u32(vzip_u32(vreinterpret_u32_u8(ind.val[0]), + vreinterpret_u32_u8(ind.val[1])).val[0]); + uint8x8_t indices_hi = + vreinterpret_u8_u32(vzip_u32(vreinterpret_u32_u8(ind.val[2]), + vreinterpret_u32_u8(ind.val[3])).val[0]); + // Do table lookup and return 2-bit color indices. + return vcombine_u8(vtbl1_u8(T::kRemap, indices_lo), + vtbl1_u8(T::kRemap, indices_hi)); +} + +template +ALWAYS_INLINE uint8x16_t +MatchColorsBlock(const uint8x16x4_t& pixels_scattered, uint16x4x4_t colors) { + // Get direction vector and expand to 32-bit. + int32x4_t dir = vsubl_s16(vreinterpret_s16_u16(colors.val[0]), + vreinterpret_s16_u16(colors.val[1])); + // Duplicate r g b elements of direction into different registers. + int32x4_t dir_r = vdupq_lane_s32(vget_low_s32(dir), 0); + int32x4_t dir_g = vdupq_lane_s32(vget_low_s32(dir), 1); + int32x4_t dir_b = vdupq_lane_s32(vget_high_s32(dir), 0); + + // Transpose to separate red, green, blue and alpha channels into 4 different + // registers. Alpha is ignored. + uint16x4x2_t trn_lo = vtrn_u16(colors.val[0], colors.val[1]); + uint16x4x2_t trn_hi = vtrn_u16(colors.val[2], colors.val[3]); + uint32x4x2_t transposed_colors = vtrnq_u32( + vreinterpretq_u32_u16(vcombine_u16(trn_lo.val[0], trn_lo.val[1])), + vreinterpretq_u32_u16(vcombine_u16(trn_hi.val[0], trn_hi.val[1]))); + + // Expand to 32-bit. + int32x4_t colors_r = + vmovl_s16(vget_low_s16(vreinterpretq_s16_u32(transposed_colors.val[0]))); + int32x4_t colors_g = + vmovl_s16(vget_high_s16(vreinterpretq_s16_u32(transposed_colors.val[0]))); + int32x4_t colors_b = + vmovl_s16(vget_low_s16(vreinterpretq_s16_u32(transposed_colors.val[1]))); + + // Get dot products. + int32x4_t stops = + DotProduct(colors_r, colors_g, colors_b, dir_r, dir_g, dir_b); + + // Build a register containing 4th, 2nd and 3rd elements of stops respectively + // in each 32 bits element. + int32x4_t points1 = vsetq_lane_s32(vgetq_lane_s32(stops, 3), stops, 0); + // Build a register containing 3rd, 4th and 1st elements of stops respectively + // in each 32 bits element. + int32x4_t points2 = vreinterpretq_s32_s64( + vextq_s64(vreinterpretq_s64_s32(stops), vreinterpretq_s64_s32(stops), 1)); + // Add and divide by 2. + int32x4_t points = vshrq_n_s32(vaddq_s32(points1, points2), 1); + + // Expand all pixels to signed 32-bit integers. + int32x4x4_t r = ExpandRGBATo32(pixels_scattered.val[0]); + int32x4x4_t g = ExpandRGBATo32(pixels_scattered.val[1]); + int32x4x4_t b = ExpandRGBATo32(pixels_scattered.val[2]); + + int32x4x4_t dots = CalculateDots(r, g, b, dir); + + // Get 2-bit color indices. + return GetColorIndices(dots, points); +} + +template +ALWAYS_INLINE uint32x4x2_t DoProdsTableLookup(uint8x8_t indices) { + // Do table lookup for each color index. The values in the table are 3 bytes + // big so we do it in 3 steps. + uint16x8_t lookup1 = vmovl_u8(vtbl1_u8(vcreate_u8(T::kProds[0]), indices)); + uint16x8_t lookup2 = vmovl_u8(vtbl1_u8(vcreate_u8(T::kProds[1]), indices)); + uint16x8_t lookup3 = vmovl_u8(vtbl1_u8(vcreate_u8(T::kProds[2]), indices)); + // Expand to 32-bit. + uint32x4_t lookup1_lo = vmovl_u16(vget_low_u16(lookup1)); + uint32x4_t lookup1_hi = vmovl_u16(vget_high_u16(lookup1)); + uint32x4_t lookup2_lo = vmovl_u16(vget_low_u16(lookup2)); + uint32x4_t lookup2_hi = vmovl_u16(vget_high_u16(lookup2)); + uint32x4_t lookup3_lo = vmovl_u16(vget_low_u16(lookup3)); + uint32x4_t lookup3_hi = vmovl_u16(vget_high_u16(lookup3)); + // Combine results by shifting and or-ing to obtain the actual table value. + uint32x4x2_t result; + result.val[0] = vorrq_u32(lookup3_lo, vorrq_u32(vshlq_n_u32(lookup2_lo, 8), + vshlq_n_u32(lookup1_lo, 16))); + result.val[1] = vorrq_u32(lookup3_hi, vorrq_u32(vshlq_n_u32(lookup2_hi, 8), + vshlq_n_u32(lookup1_hi, 16))); + return result; +} + +// Tries to optimize colors to suit block contents better. +// Done by solving a least squares system via normal equations+Cramer's rule. +template +ALWAYS_INLINE uint16x8_t RefineBlock(const uint8x16x4_t& pixels_scattered, + uint16x4_t sum_rgb, + uint16x8_t base_colors, + uint8x16_t indices) { + if (ElementsEqual(indices)) { // Do all pixels have the same index? + // Yes, linear system would be singular; solve using optimal single-color + // match on average color. + + // Get the average of the 16 pixels with rounding. + uint16x4_t average_rgb = vrshr_n_u16(sum_rgb, 4); + + ALIGNAS(8) uint16_t rgb[4]; + vst1_u16(rgb, average_rgb); + // Look up optimal values instead of trying to calculate. + uint16_t colors[8] = {kDXTConstantColors55[rgb[0]][0], + Match8BitComponentMax(rgb[1]), + kDXTConstantColors55[rgb[2]][0], + 0, + kDXTConstantColors55[rgb[0]][1], + Match8BitComponentMin(rgb[1]), + kDXTConstantColors55[rgb[2]][1], + 0}; + return vld1q_u16(colors); + } else { + // Expand to 16-bit. + int16x8x2_t r = ExpandRGBATo16(pixels_scattered.val[0]); + int16x8x2_t g = ExpandRGBATo16(pixels_scattered.val[1]); + int16x8x2_t b = ExpandRGBATo16(pixels_scattered.val[2]); + + // Do table lookup for each color index. + int8x16_t w1 = DoW1TableLookup(indices); + // Expand to 16-bit. + int16x8_t w1_lo = vmovl_s8(vget_low_s8(w1)); + int16x8_t w1_hi = vmovl_s8(vget_high_s8(w1)); + // Multiply and accumulate. + int16x8x4_t at1_rgb; + at1_rgb.val[0] = vmulq_s16(w1_lo, r.val[0]); + at1_rgb.val[0] = vmlaq_s16(at1_rgb.val[0], w1_hi, r.val[1]); + at1_rgb.val[1] = vmulq_s16(w1_lo, g.val[0]); + at1_rgb.val[1] = vmlaq_s16(at1_rgb.val[1], w1_hi, g.val[1]); + at1_rgb.val[2] = vmulq_s16(w1_lo, b.val[0]); + at1_rgb.val[2] = vmlaq_s16(at1_rgb.val[2], w1_hi, b.val[1]); + // [r][g][b][] + int32x4_t at1 = SumRGB(at1_rgb); + + // [r][g][b][] + int32x4_t at2 = vreinterpretq_s32_u32(vmovl_u16(sum_rgb)); + // at2 = 3 * at2 - at1; + at2 = vsubq_s32(vmulq_s32(at2, vdupq_n_s32(3)), at1); + + // Do table lookup for each color index. + uint32x4x2_t akku1 = DoProdsTableLookup(vget_low_u8(indices)); + uint32x4x2_t akku2 = DoProdsTableLookup(vget_high_u8(indices)); + uint32x4_t sum_akku = vaddq_u32( + vaddq_u32(vaddq_u32(akku1.val[0], akku1.val[1]), akku2.val[0]), + akku2.val[1]); + // Pairwise add and accumulate. + uint64x1_t akku = vpaddl_u32(vget_low_u32(sum_akku)); + akku = vpadal_u32(akku, vget_high_u32(sum_akku)); + + // Extract solutions and decide solvability. + + // [akku >> 16]x4 + int32x4_t xx = + vdupq_lane_s32(vreinterpret_s32_u64(vshr_n_u64(akku, 16)), 0); + // [(akku >> 8) & 0xff]x4 + const uint64x1_t kFF = vdup_n_u64(0xff); + int32x4_t yy = vdupq_lane_s32( + vreinterpret_s32_u64(vand_u64(vshr_n_u64(akku, 8), kFF)), 0); + // [akku & 0xff]x4 + int32x4_t xy = vdupq_lane_s32(vreinterpret_s32_u64(vand_u64(akku, kFF)), 0); + + // ((3.0f * 31.0f) / 255.0f) / (xx * yy - xy * xy) + float32x4_t frb = Divide<2>( + vdupq_n_f32((3.0f * 31.0f) / 255.0f), + vcvtq_f32_s32(vsubq_s32(vmulq_s32(xx, yy), vmulq_s32(xy, xy)))); + // frb * 63.0f / 31.0f + float32x4_t fg = vmulq_f32(vmulq_f32(frb, vdupq_n_f32(63.0f)), + vdupq_n_f32(1.0f / 31.0f)); + + // Solve. + + // [frb][fg][frb][] + float32x4_t frb_fg_frb = vsetq_lane_f32(vgetq_lane_f32(fg, 0), frb, 1); + // [31][63][31][] + const int32x4_t kClamp565_vec = {31, 63, 31, 0}; + + // (at1_r * yy - at2_r * xy) * frb + 0.5f + int32x4_t base0_rgb32 = vcvtq_s32_f32(vaddq_f32( + vmulq_f32( + vcvtq_f32_s32(vsubq_s32(vmulq_s32(at1, yy), vmulq_s32(at2, xy))), + frb_fg_frb), + vdupq_n_f32(0.5f))); + // Clamp and saturate. + uint16x4_t base0_rgb16 = vqmovun_s32(vbslq_s32( + vcgeq_s32(base0_rgb32, kClamp565_vec), kClamp565_vec, base0_rgb32)); + + // (at2_r * xx - at1_r * xy) * frb + 0.5f + int32x4_t base1_rgb32 = vcvtq_s32_f32(vaddq_f32( + vmulq_f32( + vcvtq_f32_s32(vsubq_s32(vmulq_s32(at2, xx), vmulq_s32(at1, xy))), + frb_fg_frb), + vdupq_n_f32(0.5f))); + // Clamp and saturate. + uint16x4_t base1_rgb16 = vqmovun_s32(vbslq_s32( + vcgeq_s32(base1_rgb32, kClamp565_vec), kClamp565_vec, base1_rgb32)); + + return vcombine_u16(base0_rgb16, base1_rgb16); + } +} + +template +ALWAYS_INLINE void CompressColorBlock(uint8_t* dst, + const uint8x16x4_t& pixels_linear, + const uint8x16x4_t& pixels_scattered, + uint8x8_t min_rgba, + uint8x8_t max_rgba, + TextureCompressor::Quality quality) { + // Take a shortcut if the block is constant (disregarding alpha). + uint32_t min32 = vget_lane_u32(vreinterpret_u32_u8(min_rgba), 0); + uint32_t max32 = vget_lane_u32(vreinterpret_u32_u8(max_rgba), 0); + if ((min32 & 0x00ffffff) == (max32 & 0x00ffffff)) { + int r = min32 & 0xff; + int g = (min32 >> 8) & 0xff; + int b = (min32 >> 16) & 0xff; + + uint16_t max16 = Match8BitColorMax(r, g, b); + uint16_t min16 = Match8BitColorMin(r, g, b); + uint32_t indices = T::kConstantColorIndices; + FormatFixup(&max16, &min16, &indices); + + uint32_t* dst32 = reinterpret_cast(dst); + dst32[0] = max16 | (min16 << 16); + dst32[1] = indices; + } else { + uint16x4_t sum_rgb = vdup_n_u16(0); + uint16x8_t base_colors; + + if (quality == TextureCompressor::kQualityLow) { + base_colors = GetApproximateBaseColors(pixels_linear, pixels_scattered, + min_rgba, max_rgba); + } else { + sum_rgb = SumRGB(pixels_scattered); + // Do Primary Component Analysis and map along principal axis. + base_colors = OptimizeColorsBlock(pixels_linear, pixels_scattered, + sum_rgb, min_rgba, max_rgba); + } + + // Check if the two base colors are the same. + uint8x16_t indices; + if (!Equal(vget_low_u16(base_colors), vget_high_u16(base_colors))) { + // Calculate the two intermediate colors as well. + uint16x4x4_t colors = EvalColors(base_colors); + + // Do a first pass to find good index candicates for all 16 of the pixels + // in the block. + indices = MatchColorsBlock(pixels_scattered, colors); + } else { + // Any indices can be used here. + indices = vdupq_n_u8(0); + } + + if (quality == TextureCompressor::kQualityHigh) { + // Refine the base colors and indices multiple times if requested. + for (int i = 0; i < kNumRefinements; ++i) { + uint8x16_t last_indices = indices; + uint16x8_t last_base_colors = base_colors; + + base_colors = + RefineBlock(pixels_scattered, sum_rgb, base_colors, indices); + if (!Equal(last_base_colors, base_colors)) { + if (!Equal(vget_low_u16(base_colors), vget_high_u16(base_colors))) { + uint16x4x4_t colors = EvalColors(base_colors); + indices = MatchColorsBlock(pixels_scattered, colors); + } else { + // We ended up with two identical base colors, can't refine this + // further. + indices = vdupq_n_u8(0); + break; + } + } + + if (Equal(indices, last_indices)) { + // There's no need to do another refinement pass if we didn't get any + // improvements this pass. + break; + } + } + } + + // Prepare the final block by converting the base colors to 16-bit and + // packing the pixel indices. + uint16x4_t base_colors_16 = PackBaseColors(base_colors); + uint64x1_t indices_2x16 = PackIndices<2>(indices); + FormatFixupIdx(&base_colors_16, &indices_2x16); + uint64x1_t output = vorr_u64(vshl_n_u64(indices_2x16, 32), + vreinterpret_u64_u16(base_colors_16)); + vst1_u64(reinterpret_cast(dst), output); + } +} + +// alpha: 8x8-bit alpha values. +// dist: Distance between max and min alpha in the color block. +// bias: Rounding bias. +ALWAYS_INLINE uint8x8_t +GetAlphaIndices(uint8x8_t alpha, int16x8_t dist, int16x8_t bias) { + // Expand to signed 16-bit. + int16x8_t alpha_16 = vreinterpretq_s16_u16(vmovl_u8(alpha)); + + // Multiply each alpha value by 7 and add bias. + int16x8_t a = vaddq_s16(vmulq_s16(alpha_16, vdupq_n_s16(7)), bias); + + int16x8_t dist4 = vmulq_s16(dist, vdupq_n_s16(4)); + int16x8_t dist2 = vmulq_s16(dist, vdupq_n_s16(2)); + + // Select index. This is a "linear scale" lerp factor between 0 (val=min) + // and 7 (val=max). + // t = (a >= dist4) ? -1 : 0 + int16x8_t t = + vandq_s16(vreinterpretq_s16_u16(vcgeq_s16(a, dist4)), vdupq_n_s16(-1)); + // ind1 = t & 4; + int16x8_t ind1 = vandq_s16(t, vdupq_n_s16(4)); + // a1 = a - (dist4 & t); + int16x8_t a1 = vsubq_s16(a, vandq_s16(dist4, t)); + + // t = (a1 >= dist2) ? -1 : 0; + t = vandq_s16(vreinterpretq_s16_u16(vcgeq_s16(a1, dist2)), vdupq_n_s16(-1)); + // ind2 = t & 2; + int16x8_t ind2 = vandq_s16(t, vdupq_n_s16(2)); + // a2 = a1 - (dist2 & t); + int16x8_t a2 = vsubq_s16(a1, vandq_s16(dist2, t)); + + // ind3 = (a2 >= dist) + int16x8_t ind3 = + vandq_s16(vreinterpretq_s16_u16(vcgeq_s16(a2, dist)), vdupq_n_s16(1)); + + // indices = ind1 + ind2 + ind3 + int16x8_t indices = vaddq_s16(ind1, vaddq_s16(ind2, ind3)); + + // Turn linear scale into alpha index (0/1 are extremal pts). + // ind = -indices & 7 + int16x8_t ind = vandq_s16(vnegq_s16(indices), vdupq_n_s16(7)); + // indices = ind ^ (2 > ind) + indices = veorq_s16( + ind, vandq_s16(vreinterpretq_s16_u16(vcgtq_s16(vdupq_n_s16(2), ind)), + vdupq_n_s16(1))); + // Narrow it down to unsigned 8 bits and return. + return vqmovun_s16(indices); +} + +ALWAYS_INLINE void CompressAlphaBlock(uint8_t* dst, + uint8x16_t pixels_alpha, + uint8x8_t min_rgba, + uint8x8_t max_rgba) { + // Take a shortcut if the block is constant. + uint8_t min_alpha = vget_lane_u8(min_rgba, 3); + uint8_t max_alpha = vget_lane_u8(max_rgba, 3); + if (min_alpha == max_alpha) { + dst[0] = max_alpha; + dst[1] = min_alpha; + // All indices are the same, any value will do. + *reinterpret_cast(dst + 2) = 0; + *reinterpret_cast(dst + 4) = 0; + } else { + // [max - min]x8 + int16x8_t dist = vdupq_lane_s16( + vreinterpret_s16_u16(vget_low_u16(vsubl_u8(max_rgba, min_rgba))), 3); + // bias = (dist < 8) ? (dist - 1) : (dist / 2 + 2) + int16x8_t bias = vbslq_s16(vcltq_s16(dist, vdupq_n_s16(8)), + vsubq_s16(dist, vdupq_n_s16(1)), + vaddq_s16(vshrq_n_s16(dist, 1), vdupq_n_s16(2))); + // bias -= min * 7; + bias = vsubq_s16( + bias, + vmulq_s16( + vdupq_lane_s16( + vreinterpret_s16_u16(vget_low_u16(vmovl_u8(min_rgba))), 3), + vdupq_n_s16(7))); + + uint8x8_t indices_lo = + GetAlphaIndices(vget_low_u8(pixels_alpha), dist, bias); + uint8x8_t indices_hi = + GetAlphaIndices(vget_high_u8(pixels_alpha), dist, bias); + + // Prepare the final block by combining the base alpha values and packing + // the alpha indices. + uint8x8_t max_min_alpha = vzip_u8(max_rgba, min_rgba).val[0]; + uint64x1_t indices = PackIndices<3>(vcombine_u8(indices_lo, indices_hi)); + uint64x1_t output = + vorr_u64(vshl_n_u64(indices, 16), + vshr_n_u64(vreinterpret_u64_u8(max_min_alpha), 48)); + vst1_u64(reinterpret_cast(dst), output); + } +} + +void ExtractMisalignedBlock(uint8_t* dst, + const uint8_t* src, + int num_columns, + int num_rows, + int width) { + uint32_t* wdst = reinterpret_cast(dst); + const uint32_t* wsrc = reinterpret_cast(src); + + for (int y = 0; y < num_rows; ++y) { + for (int x = 0; x < num_columns; ++x) + *wdst++ = *wsrc++; + // Fill remaining columns with values from last column. + uint32_t* padding = wdst - 1; + for (int x = num_columns; x < 4; ++x) + *wdst++ = *padding; + wsrc += width - num_columns; + } + + // Fill remaining rows with values from last row. + const uint32_t* last_row = wdst - 4; + for (int y = num_rows; y < 4; ++y) { + const uint32_t* padding = last_row; + for (int x = 0; x < 4; ++x) + *wdst++ = *padding++; + } +} + +} // namespace + +void CompressATC_NEON(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality) { + assert(quality >= TextureCompressor::kQualityLow && + quality <= TextureCompressor::kQualityHigh); + + // The format works on blocks of 4x4 pixels. + // If the size is misaligned then we need to be careful when extracting the + // block of source texels. + + for (int y = 0; y < height; y += 4, src += width * 4 * 4) { + // Number of rows to read in this iteration. + int num_rows = std::min(height - y, 4); + + for (int x = 0; x < width; x += 4) { + // Number of columns to read in this iteration. + int num_columns = std::min(width - x, 4); + + // Load the four rows of pixels. + uint8x16x4_t pixels_linear; + if (num_rows + num_columns != 8) { + // We're at the border and don't have a full block to work with. Extend + // the last column and row to fill out the block. + uint64_t aligned_buffer[8]; + uint8_t* buffer = reinterpret_cast(aligned_buffer); + ExtractMisalignedBlock(buffer, src + x * 4, num_columns, num_rows, + width); + pixels_linear.val[0] = vld1q_u8(buffer + 0 * 16); + pixels_linear.val[1] = vld1q_u8(buffer + 1 * 16); + pixels_linear.val[2] = vld1q_u8(buffer + 2 * 16); + pixels_linear.val[3] = vld1q_u8(buffer + 3 * 16); + } else { + pixels_linear.val[0] = vld1q_u8(src + (x + 0 * width) * 4); + pixels_linear.val[1] = vld1q_u8(src + (x + 1 * width) * 4); + pixels_linear.val[2] = vld1q_u8(src + (x + 2 * width) * 4); + pixels_linear.val[3] = vld1q_u8(src + (x + 3 * width) * 4); + } + + // Transpose/scatter the red, green, blue and alpha channels into + // separate registers. + ALIGNAS(8) uint8_t block[64]; + vst1q_u8(block + 0 * 16, pixels_linear.val[0]); + vst1q_u8(block + 1 * 16, pixels_linear.val[1]); + vst1q_u8(block + 2 * 16, pixels_linear.val[2]); + vst1q_u8(block + 3 * 16, pixels_linear.val[3]); + uint8x16x4_t pixels_scattered = vld4q_u8(block); + + // We need the min and max values both to detect solid blocks and when + // computing the base colors. + uint8x8_t min_rgba = FoldRGBA(pixels_scattered); + uint8x8_t max_rgba = FoldRGBA(pixels_scattered); + + if (!opaque) { + CompressAlphaBlock(dst, pixels_scattered.val[3], min_rgba, max_rgba); + dst += 8; + } + + CompressColorBlock(dst, pixels_linear, pixels_scattered, + min_rgba, max_rgba, quality); + dst += 8; + } + } +} + +void CompressDXT_NEON(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality) { + assert(quality >= TextureCompressor::kQualityLow && + quality <= TextureCompressor::kQualityHigh); + + // The format works on blocks of 4x4 pixels. + // If the size is misaligned then we need to be careful when extracting the + // block of source texels. + + for (int y = 0; y < height; y += 4, src += width * 4 * 4) { + // Number of rows to read in this iteration. + int num_rows = std::min(height - y, 4); + + for (int x = 0; x < width; x += 4) { + // Number of columns to read in this iteration. + int num_columns = std::min(width - x, 4); + + // Load the four rows of pixels. + uint8x16x4_t pixels_linear; + if (num_rows + num_columns != 8) { + // We're at the border and don't have a full block to work with. Extend + // the last column and row to fill out the block. + uint64_t aligned_buffer[8]; + uint8_t* buffer = reinterpret_cast(aligned_buffer); + ExtractMisalignedBlock(buffer, src + x * 4, num_columns, num_rows, + width); + pixels_linear.val[0] = vld1q_u8(buffer + 0 * 16); + pixels_linear.val[1] = vld1q_u8(buffer + 1 * 16); + pixels_linear.val[2] = vld1q_u8(buffer + 2 * 16); + pixels_linear.val[3] = vld1q_u8(buffer + 3 * 16); + } else { + pixels_linear.val[0] = vld1q_u8(src + (x + 0 * width) * 4); + pixels_linear.val[1] = vld1q_u8(src + (x + 1 * width) * 4); + pixels_linear.val[2] = vld1q_u8(src + (x + 2 * width) * 4); + pixels_linear.val[3] = vld1q_u8(src + (x + 3 * width) * 4); + } + + // Transpose/scatter the red, green, blue and alpha channels into + // separate registers. + ALIGNAS(8) uint8_t block[64]; + vst1q_u8(block + 0 * 16, pixels_linear.val[0]); + vst1q_u8(block + 1 * 16, pixels_linear.val[1]); + vst1q_u8(block + 2 * 16, pixels_linear.val[2]); + vst1q_u8(block + 3 * 16, pixels_linear.val[3]); + uint8x16x4_t pixels_scattered = vld4q_u8(block); + + // We need the min and max values both to detect solid blocks and when + // computing the base colors. + uint8x8_t min_rgba = FoldRGBA(pixels_scattered); + uint8x8_t max_rgba = FoldRGBA(pixels_scattered); + + if (!opaque) { + CompressAlphaBlock(dst, pixels_scattered.val[3], min_rgba, max_rgba); + dst += 8; + } + + CompressColorBlock(dst, pixels_linear, pixels_scattered, + min_rgba, max_rgba, quality); + dst += 8; + } + } +} diff --git a/src/third_party/texture_compressor/dxt_encoder_neon.h b/src/third_party/texture_compressor/dxt_encoder_neon.h new file mode 100644 index 0000000..f18ad18 --- /dev/null +++ b/src/third_party/texture_compressor/dxt_encoder_neon.h @@ -0,0 +1,30 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef DXT_ENCODER_NEON_H_ +#define DXT_ENCODER_NEON_H_ + +#include + +#include "texture_compressor.h" + +// ATC compression works on blocks of 4 by 4 texels. Width and height of the +// source image must be multiple of 4. +void CompressATC_NEON(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality); + +// DXT compression works on blocks of 4 by 4 texels. Width and height of the +// source image must be multiple of 4. +void CompressDXT_NEON(const uint8_t* src, + uint8_t* dst, + int width, + int height, + bool opaque, + TextureCompressor::Quality quality); + +#endif // DXT_ENCODER_NEON_H_ diff --git a/src/third_party/texture_compressor/texture_compressor.cc b/src/third_party/texture_compressor/texture_compressor.cc new file mode 100644 index 0000000..c3b9816 --- /dev/null +++ b/src/third_party/texture_compressor/texture_compressor.cc @@ -0,0 +1,151 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "texture_compressor.h" + +#include "dxt_encoder.h" +#include "texture_compressor_etc1.h" + +#if defined(__ANDROID__) +#if defined(__ARMEL__) || defined(__aarch64__) || defined(_M_ARM64) +#define ANDROID_NEON +#include +#include "dxt_encoder_neon.h" +#include "texture_compressor_etc1_neon.h" +#endif +#endif + +class TextureCompressorATC : public TextureCompressor { + public: + /** + * Creates an ATC encoder. It may create either an ATC or ATCIA encoder, + * depending on whether opacity support is needed. + */ + explicit TextureCompressorATC(bool supports_opacity) + : supports_opacity_(supports_opacity) { + format_ = supports_opacity_ ? kFormatATCIA : kFormatATC; + } + + void Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) { + CompressATC(src, dst, width, height, !supports_opacity_, quality); + } + + private: + bool supports_opacity_; +}; + +#ifdef ANDROID_NEON + +class TextureCompressorATC_NEON : public TextureCompressor { + public: + /** + * Creates an ATC encoder. It may create either an ATC or ATCIA encoder, + * depending on whether opacity support is needed. + */ + explicit TextureCompressorATC_NEON(bool supports_opacity) + : supports_opacity_(supports_opacity) { + format_ = supports_opacity_ ? kFormatATCIA : kFormatATC; + } + + void Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) { + CompressATC_NEON(src, dst, width, height, !supports_opacity_, quality); + } + + private: + bool supports_opacity_; +}; + +#endif + +class TextureCompressorDXT : public TextureCompressor { + public: + /** + * Creates an ATC encoder. It may create either an ATC or ATCIA encoder, + * depending on whether opacity support is needed. + */ + explicit TextureCompressorDXT(bool supports_opacity) + : supports_opacity_(supports_opacity) { + format_ = supports_opacity_ ? kFormatDXT5 : kFormatDXT1; + } + + void Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) { + CompressDXT(src, dst, width, height, !supports_opacity_, quality); + } + + private: + bool supports_opacity_; +}; + +#ifdef ANDROID_NEON + +class TextureCompressorDXT_NEON : public TextureCompressor { + public: + /** + * Creates an ATC encoder. It may create either an ATC or ATCIA encoder, + * depending on whether opacity support is needed. + */ + explicit TextureCompressorDXT_NEON(bool supports_opacity) + : supports_opacity_(supports_opacity) { + format_ = supports_opacity_ ? kFormatDXT5 : kFormatDXT1; + } + + void Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) { + CompressDXT_NEON(src, dst, width, height, !supports_opacity_, quality); + } + + private: + bool supports_opacity_; +}; + +#endif + +std::unique_ptr TextureCompressor::Create(Format format) { + switch (format) { + case kFormatATC: + case kFormatATCIA: +#ifdef ANDROID_NEON + if ((android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0) { + return std::make_unique( + format == kFormatATCIA); + } +#endif + return std::make_unique(format == kFormatATCIA); + + case kFormatDXT1: + case kFormatDXT5: +#ifdef ANDROID_NEON + if ((android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0) { + return std::make_unique( + format == kFormatDXT5); + } +#endif + return std::make_unique(format == kFormatDXT5); + + case kFormatETC1: +#ifdef ANDROID_NEON + if ((android_getCpuFeatures() & ANDROID_CPU_ARM_FEATURE_NEON) != 0) { + return std::make_unique(); + } +#endif + return std::make_unique(); + } + + return nullptr; +} diff --git a/src/third_party/texture_compressor/texture_compressor.h b/src/third_party/texture_compressor/texture_compressor.h new file mode 100644 index 0000000..3f9b400 --- /dev/null +++ b/src/third_party/texture_compressor/texture_compressor.h @@ -0,0 +1,44 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TEXTURE_COMPRESSOR_H_ +#define TEXTURE_COMPRESSOR_H_ + +#include +#include + +class TextureCompressor { + public: + enum Format { + kFormatATC, + kFormatATCIA, + kFormatDXT1, + kFormatDXT5, + kFormatETC1, + }; + + enum Quality { + kQualityLow, + kQualityMedium, + kQualityHigh, + }; + + static std::unique_ptr Create(Format format); + virtual ~TextureCompressor() {} + + virtual void Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) = 0; + + Format format() { return format_; } + + protected: + TextureCompressor() {} + + Format format_; +}; + +#endif // TEXTURE_COMPRESSOR_H_ diff --git a/src/third_party/texture_compressor/texture_compressor_etc1.cc b/src/third_party/texture_compressor/texture_compressor_etc1.cc new file mode 100644 index 0000000..722b90d --- /dev/null +++ b/src/third_party/texture_compressor/texture_compressor_etc1.cc @@ -0,0 +1,368 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// See the following specification for details on the ETC1 format: +// https://www.khronos.org/registry/gles/extensions/OES/OES_compressed_ETC1_RGB8_texture.txt + +#include "texture_compressor_etc1.h" + +#include +#include + +#include +#include + +// Defining the following macro will cause the error metric function to weigh +// each color channel differently depending on how the human eye can perceive +// them. This can give a slight improvement in image quality at the cost of a +// performance hit. +// #define USE_PERCEIVED_ERROR_METRIC + +namespace { + +// Constructs a color from a given base color and luminance value. +inline Color MakeColor(const Color& base, int16_t lum) { + int b = static_cast(base.channels.b) + lum; + int g = static_cast(base.channels.g) + lum; + int r = static_cast(base.channels.r) + lum; + Color color; + color.channels.b = static_cast(clamp(b, 0, 255)); + color.channels.g = static_cast(clamp(g, 0, 255)); + color.channels.r = static_cast(clamp(r, 0, 255)); + return color; +} + +// Calculates the error metric for two colors. A small error signals that the +// colors are similar to each other, a large error the signals the opposite. +inline uint32_t GetColorError(const Color& u, const Color& v) { +#ifdef USE_PERCEIVED_ERROR_METRIC + float delta_b = static_cast(u.channels.b) - v.channels.b; + float delta_g = static_cast(u.channels.g) - v.channels.g; + float delta_r = static_cast(u.channels.r) - v.channels.r; + return static_cast(0.299f * delta_b * delta_b + + 0.587f * delta_g * delta_g + + 0.114f * delta_r * delta_r); +#else + int delta_b = static_cast(u.channels.b) - v.channels.b; + int delta_g = static_cast(u.channels.g) - v.channels.g; + int delta_r = static_cast(u.channels.r) - v.channels.r; + return delta_b * delta_b + delta_g * delta_g + delta_r * delta_r; +#endif +} + +void GetAverageColor(const Color* src, float* avg_color) { + uint32_t sum_b = 0, sum_g = 0, sum_r = 0; + + for (unsigned int i = 0; i < 8; ++i) { + sum_b += src[i].channels.b; + sum_g += src[i].channels.g; + sum_r += src[i].channels.r; + } + + const float kInv8 = 1.0f / 8.0f; + avg_color[0] = static_cast(sum_b) * kInv8; + avg_color[1] = static_cast(sum_g) * kInv8; + avg_color[2] = static_cast(sum_r) * kInv8; +} + +void ComputeLuminance(uint8_t* block, + const Color* src, + const Color& base, + int sub_block_id, + const uint8_t* idx_to_num_tab) { + uint32_t best_tbl_err = std::numeric_limits::max(); + uint8_t best_tbl_idx = 0; + uint8_t best_mod_idx[8][8]; // [table][texel] + + // Try all codeword tables to find the one giving the best results for this + // block. + for (unsigned int tbl_idx = 0; tbl_idx < 8; ++tbl_idx) { + // Pre-compute all the candidate colors; combinations of the base color and + // all available luminance values. + Color candidate_color[4]; // [modifier] + for (unsigned int mod_idx = 0; mod_idx < 4; ++mod_idx) { + int16_t lum = g_codeword_tables[tbl_idx][mod_idx]; + candidate_color[mod_idx] = MakeColor(base, lum); + } + + uint32_t tbl_err = 0; + + for (unsigned int i = 0; i < 8; ++i) { + // Try all modifiers in the current table to find which one gives the + // smallest error. + uint32_t best_mod_err = std::numeric_limits::max(); + for (unsigned int mod_idx = 0; mod_idx < 4; ++mod_idx) { + const Color& color = candidate_color[mod_idx]; + + uint32_t mod_err = GetColorError(src[i], color); + if (mod_err < best_mod_err) { + best_mod_idx[tbl_idx][i] = mod_idx; + best_mod_err = mod_err; + + if (mod_err == 0) + break; // We cannot do any better than this. + } + } + + tbl_err += best_mod_err; + if (tbl_err > best_tbl_err) + break; // We're already doing worse than the best table so skip. + } + + if (tbl_err < best_tbl_err) { + best_tbl_err = tbl_err; + best_tbl_idx = tbl_idx; + + if (tbl_err == 0) + break; // We cannot do any better than this. + } + } + + WriteCodewordTable(block, sub_block_id, best_tbl_idx); + + uint32_t pix_data = 0; + + for (unsigned int i = 0; i < 8; ++i) { + uint8_t mod_idx = best_mod_idx[best_tbl_idx][i]; + uint8_t pix_idx = g_mod_to_pix[mod_idx]; + + uint32_t lsb = pix_idx & 0x1; + uint32_t msb = pix_idx >> 1; + + // Obtain the texel number as specified in the standard. + int texel_num = idx_to_num_tab[i]; + pix_data |= msb << (texel_num + 16); + pix_data |= lsb << (texel_num); + } + + WritePixelData(block, pix_data); +} + +/** + * Tries to compress the block under the assumption that it's a single color + * block. If it's not the function will bail out without writing anything to + * the destination buffer. + */ +bool TryCompressSolidBlock(uint8_t* dst, const Color* src) { + for (unsigned int i = 1; i < 16; ++i) { + if (src[i].bits != src[0].bits) + return false; + } + + // Clear destination buffer so that we can "or" in the results. + memset(dst, 0, 8); + + float src_color_float[3] = {static_cast(src->channels.b), + static_cast(src->channels.g), + static_cast(src->channels.r)}; + Color base = MakeColor555(src_color_float); + + WriteDiff(dst, true); + WriteFlip(dst, false); + WriteColors555(dst, base, base); + + uint8_t best_tbl_idx = 0; + uint8_t best_mod_idx = 0; + uint32_t best_mod_err = std::numeric_limits::max(); + + // Try all codeword tables to find the one giving the best results for this + // block. + for (unsigned int tbl_idx = 0; tbl_idx < 8; ++tbl_idx) { + // Try all modifiers in the current table to find which one gives the + // smallest error. + for (unsigned int mod_idx = 0; mod_idx < 4; ++mod_idx) { + int16_t lum = g_codeword_tables[tbl_idx][mod_idx]; + const Color& color = MakeColor(base, lum); + + uint32_t mod_err = GetColorError(*src, color); + if (mod_err < best_mod_err) { + best_tbl_idx = tbl_idx; + best_mod_idx = mod_idx; + best_mod_err = mod_err; + + if (mod_err == 0) + break; // We cannot do any better than this. + } + } + + if (best_mod_err == 0) + break; + } + + WriteCodewordTable(dst, 0, best_tbl_idx); + WriteCodewordTable(dst, 1, best_tbl_idx); + + uint8_t pix_idx = g_mod_to_pix[best_mod_idx]; + uint32_t lsb = pix_idx & 0x1; + uint32_t msb = pix_idx >> 1; + + uint32_t pix_data = 0; + for (unsigned int i = 0; i < 2; ++i) { + for (unsigned int j = 0; j < 8; ++j) { + // Obtain the texel number as specified in the standard. + int texel_num = g_idx_to_num[i][j]; + pix_data |= msb << (texel_num + 16); + pix_data |= lsb << (texel_num); + } + } + + WritePixelData(dst, pix_data); + return true; +} + +void CompressBlock(uint8_t* dst, const Color* ver_src, const Color* hor_src) { + if (TryCompressSolidBlock(dst, ver_src)) + return; + + const Color* sub_block_src[4] = {ver_src, ver_src + 8, hor_src, hor_src + 8}; + + Color sub_block_avg[4]; + bool use_differential[2] = {true, true}; + + // Compute the average color for each sub block and determine if differential + // coding can be used. + for (unsigned int i = 0, j = 1; i < 4; i += 2, j += 2) { + float avg_color_0[3]; + GetAverageColor(sub_block_src[i], avg_color_0); + Color avg_color_555_0 = MakeColor555(avg_color_0); + + float avg_color_1[3]; + GetAverageColor(sub_block_src[j], avg_color_1); + Color avg_color_555_1 = MakeColor555(avg_color_1); + + for (unsigned int light_idx = 0; light_idx < 3; ++light_idx) { + int u = avg_color_555_0.components[light_idx] >> 3; + int v = avg_color_555_1.components[light_idx] >> 3; + + int component_diff = v - u; + if (component_diff < -4 || component_diff > 3) { + use_differential[i / 2] = false; + sub_block_avg[i] = MakeColor444(avg_color_0); + sub_block_avg[j] = MakeColor444(avg_color_1); + } else { + sub_block_avg[i] = avg_color_555_0; + sub_block_avg[j] = avg_color_555_1; + } + } + } + + // Compute the error of each sub block before adjusting for luminance. These + // error values are later used for determining if we should flip the sub + // block or not. + uint32_t sub_block_err[4] = {0}; + for (unsigned int i = 0; i < 4; ++i) { + for (unsigned int j = 0; j < 8; ++j) { + sub_block_err[i] += GetColorError(sub_block_avg[i], sub_block_src[i][j]); + } + } + + bool flip = + sub_block_err[2] + sub_block_err[3] < sub_block_err[0] + sub_block_err[1]; + + // Clear destination buffer so that we can "or" in the results. + memset(dst, 0, 8); + + WriteDiff(dst, use_differential[!!flip]); + WriteFlip(dst, flip); + + uint8_t sub_block_off_0 = flip ? 2 : 0; + uint8_t sub_block_off_1 = sub_block_off_0 + 1; + + if (use_differential[!!flip]) { + WriteColors555(dst, sub_block_avg[sub_block_off_0], + sub_block_avg[sub_block_off_1]); + } else { + WriteColors444(dst, sub_block_avg[sub_block_off_0], + sub_block_avg[sub_block_off_1]); + } + + // Compute luminance for the first sub block. + ComputeLuminance(dst, sub_block_src[sub_block_off_0], + sub_block_avg[sub_block_off_0], 0, + g_idx_to_num[sub_block_off_0]); + // Compute luminance for the second sub block. + ComputeLuminance(dst, sub_block_src[sub_block_off_1], + sub_block_avg[sub_block_off_1], 1, + g_idx_to_num[sub_block_off_1]); +} + +// Extract up to a 4x4 block of pixels and "de-swizzle" them into 16x1. +// If 'num_columns' or 'num_rows' are less than 4 then it fills out the rest +// of the block by taking a copy of the last valid column or row. +void ExtractMisalignedBlock(uint8_t* dst, + const uint8_t* src, + int num_columns, + int num_rows, + int width) { + uint32_t* wdst = reinterpret_cast(dst); + const uint32_t* wsrc = reinterpret_cast(src); + + for (int y = 0; y < num_rows; ++y) { + for (int x = 0; x < num_columns; ++x) + *wdst++ = *wsrc++; + // Fill remaining columns with values from last column. + uint32_t* padding = wdst - 1; + for (int x = num_columns; x < 4; ++x) + *wdst++ = *padding; + wsrc += width - num_columns; + } + + // Fill remaining rows with values from last row. + const uint32_t* last_row = wdst - 4; + for (int y = num_rows; y < 4; ++y) { + const uint32_t* padding = last_row; + for (int x = 0; x < 4; ++x) + *wdst++ = *padding++; + } +} + +} // namespace + +void TextureCompressorETC1::Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) { + Color ver_blocks[16]; + Color hor_blocks[16]; + uint8_t block_buffer[64]; + + for (int y = 0; y < height; y += 4, src += width * 4 * 4) { + int num_rows = std::min(height - y, 4); + for (int x = 0; x < width; x += 4, dst += 8) { + int num_columns = std::min(width - x, 4); + + const Color* row0; + int stride; + if (num_rows + num_columns != 8) { + ExtractMisalignedBlock(block_buffer, src + x * 4, num_columns, num_rows, + width); + row0 = reinterpret_cast(block_buffer); + stride = 4; + } else { + row0 = reinterpret_cast(src + x * 4); + stride = width; + } + const Color* row1 = row0 + stride; + const Color* row2 = row1 + stride; + const Color* row3 = row2 + stride; + + memcpy(ver_blocks, row0, 8); + memcpy(ver_blocks + 2, row1, 8); + memcpy(ver_blocks + 4, row2, 8); + memcpy(ver_blocks + 6, row3, 8); + memcpy(ver_blocks + 8, row0 + 2, 8); + memcpy(ver_blocks + 10, row1 + 2, 8); + memcpy(ver_blocks + 12, row2 + 2, 8); + memcpy(ver_blocks + 14, row3 + 2, 8); + + memcpy(hor_blocks, row0, 16); + memcpy(hor_blocks + 4, row1, 16); + memcpy(hor_blocks + 8, row2, 16); + memcpy(hor_blocks + 12, row3, 16); + + CompressBlock(dst, ver_blocks, hor_blocks); + } + } +} diff --git a/src/third_party/texture_compressor/texture_compressor_etc1.h b/src/third_party/texture_compressor/texture_compressor_etc1.h new file mode 100644 index 0000000..8222984 --- /dev/null +++ b/src/third_party/texture_compressor/texture_compressor_etc1.h @@ -0,0 +1,186 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TEXTURE_COMPRESSOR_ETC1_H_ +#define TEXTURE_COMPRESSOR_ETC1_H_ + +#include "texture_compressor.h" + +#include + +template +inline T clamp(T val, T min, T max) { + return val < min ? min : (val > max ? max : val); +} + +inline uint8_t round_to_5_bits(float val) { + return clamp(val * 31.0f / 255.0f + 0.5f, 0, 31); +} + +inline uint8_t round_to_4_bits(float val) { + return clamp(val * 15.0f / 255.0f + 0.5f, 0, 15); +} + +union Color { + struct BgraColorType { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t a; + } channels; + uint8_t components[4]; + uint32_t bits; +}; + +// Codeword tables. +// See: Table 3.17.2 +alignas(16) static const int16_t g_codeword_tables[8][4] = { + {-8, -2, 2, 8}, {-17, -5, 5, 17}, {-29, -9, 9, 29}, + {-42, -13, 13, 42}, {-60, -18, 18, 60}, {-80, -24, 24, 80}, + {-106, -33, 33, 106}, {-183, -47, 47, 183}}; + +// Maps modifier indices to pixel index values. +// See: Table 3.17.3 +static const uint8_t g_mod_to_pix[4] = {3, 2, 0, 1}; + +// The ETC1 specification index texels as follows: +// [a][e][i][m] [ 0][ 4][ 8][12] +// [b][f][j][n] <-> [ 1][ 5][ 9][13] +// [c][g][k][o] [ 2][ 6][10][14] +// [d][h][l][p] [ 3][ 7][11][15] + +// [ 0][ 1][ 2][ 3] [ 0][ 1][ 4][ 5] +// [ 4][ 5][ 6][ 7] <-> [ 8][ 9][12][13] +// [ 8][ 9][10][11] [ 2][ 3][ 6][ 7] +// [12][13][14][15] [10][11][14][15] + +// However, when extracting sub blocks from BGRA data the natural array +// indexing order ends up different: +// vertical0: [a][e][b][f] horizontal0: [a][e][i][m] +// [c][g][d][h] [b][f][j][n] +// vertical1: [i][m][j][n] horizontal1: [c][g][k][o] +// [k][o][l][p] [d][h][l][p] + +// In order to translate from the natural array indices in a sub block to the +// indices (number) used by specification and hardware we use this table. +static const uint8_t g_idx_to_num[4][8] = { + {0, 4, 1, 5, 2, 6, 3, 7}, // Vertical block 0. + {8, 12, 9, 13, 10, 14, 11, 15}, // Vertical block 1. + {0, 4, 8, 12, 1, 5, 9, 13}, // Horizontal block 0. + {2, 6, 10, 14, 3, 7, 11, 15} // Horizontal block 1. +}; + +inline void WriteColors444(uint8_t* block, + const Color& color0, + const Color& color1) { + // Write output color for BGRA textures. + block[0] = (color0.channels.b & 0xf0) | (color1.channels.b >> 4); + block[1] = (color0.channels.g & 0xf0) | (color1.channels.g >> 4); + block[2] = (color0.channels.r & 0xf0) | (color1.channels.r >> 4); +} + +inline void WriteColors555(uint8_t* block, + const Color& color0, + const Color& color1) { + // Table for conversion to 3-bit two complement format. + static const uint8_t two_compl_trans_table[8] = { + 4, // -4 (100b) + 5, // -3 (101b) + 6, // -2 (110b) + 7, // -1 (111b) + 0, // 0 (000b) + 1, // 1 (001b) + 2, // 2 (010b) + 3, // 3 (011b) + }; + + int16_t delta_r = + static_cast(color1.channels.r >> 3) - (color0.channels.r >> 3); + int16_t delta_g = + static_cast(color1.channels.g >> 3) - (color0.channels.g >> 3); + int16_t delta_b = + static_cast(color1.channels.b >> 3) - (color0.channels.b >> 3); + + // Write output color for BGRA textures. + block[0] = (color0.channels.b & 0xf8) | two_compl_trans_table[delta_b + 4]; + block[1] = (color0.channels.g & 0xf8) | two_compl_trans_table[delta_g + 4]; + block[2] = (color0.channels.r & 0xf8) | two_compl_trans_table[delta_r + 4]; +} + +inline void WriteCodewordTable(uint8_t* block, + uint8_t sub_block_id, + uint8_t table) { + uint8_t shift = (2 + (3 - sub_block_id * 3)); + block[3] &= ~(0x07 << shift); + block[3] |= table << shift; +} + +inline void WritePixelData(uint8_t* block, uint32_t pixel_data) { + block[4] |= pixel_data >> 24; + block[5] |= (pixel_data >> 16) & 0xff; + block[6] |= (pixel_data >> 8) & 0xff; + block[7] |= pixel_data & 0xff; +} + +inline void WriteFlip(uint8_t* block, bool flip) { + block[3] &= ~0x01; + block[3] |= static_cast(flip); +} + +inline void WriteDiff(uint8_t* block, bool diff) { + block[3] &= ~0x02; + block[3] |= static_cast(diff) << 1; +} + +// Compress and rounds BGR888 into BGR444. The resulting BGR444 color is +// expanded to BGR888 as it would be in hardware after decompression. The +// actual 444-bit data is available in the four most significant bits of each +// channel. +inline Color MakeColor444(const float* bgr) { + uint8_t b4 = round_to_4_bits(bgr[0]); + uint8_t g4 = round_to_4_bits(bgr[1]); + uint8_t r4 = round_to_4_bits(bgr[2]); + Color bgr444; + bgr444.channels.b = (b4 << 4) | b4; + bgr444.channels.g = (g4 << 4) | g4; + bgr444.channels.r = (r4 << 4) | r4; + // Added to distinguish between expanded 555 and 444 colors. + bgr444.channels.a = 0x44; + return bgr444; +} + +// Compress and rounds BGR888 into BGR555. The resulting BGR555 color is +// expanded to BGR888 as it would be in hardware after decompression. The +// actual 555-bit data is available in the five most significant bits of each +// channel. +inline Color MakeColor555(const float* bgr) { + uint8_t b5 = round_to_5_bits(bgr[0]); + uint8_t g5 = round_to_5_bits(bgr[1]); + uint8_t r5 = round_to_5_bits(bgr[2]); + Color bgr555; + bgr555.channels.b = (b5 << 3) | (b5 >> 2); + bgr555.channels.g = (g5 << 3) | (g5 >> 2); + bgr555.channels.r = (r5 << 3) | (r5 >> 2); + // Added to distinguish between expanded 555 and 444 colors. + bgr555.channels.a = 0x55; + return bgr555; +} + +class TextureCompressorETC1 : public TextureCompressor { + public: + TextureCompressorETC1() { + format_ = kFormatETC1; + } + + // Compress a texture using ETC1. Note that the |quality| parameter is + // ignored. The current implementation does not support different quality + // settings. + void Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) override; +}; + +#endif // TEXTURE_COMPRESSOR_ETC1_H_ diff --git a/src/third_party/texture_compressor/texture_compressor_etc1_neon.cc b/src/third_party/texture_compressor/texture_compressor_etc1_neon.cc new file mode 100644 index 0000000..a41a0eb --- /dev/null +++ b/src/third_party/texture_compressor/texture_compressor_etc1_neon.cc @@ -0,0 +1,672 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// See the following specification for details on the ETC1 format: +// https://www.khronos.org/registry/gles/extensions/OES/OES_compressed_ETC1_RGB8_texture.txt + +#include "texture_compressor_etc1_neon.h" + +#include + +#include +#include +#include + +// GCC 4.6 suffers from a bug, raising an internal error when mixing +// interleaved load instructions with linear load instructions. By fiddling +// with variable declaration order this problem can be avoided which is done +// when the following macro is defined. +#if (__GNUC__ == 4) && (__GNUC_MINOR__ == 6) +#define GCC46_INTERNAL_ERROR_WORKAROUND +#endif + +#define ALIGNAS(X) __attribute__ ((aligned (X))) + +namespace { + +template +inline T clamp(T val, T min, T max) { + return val < min ? min : (val > max ? max : val); +} + +inline uint8_t round_to_5_bits(float val) { + return clamp(val * 31.0f / 255.0f + 0.5f, 0, 31); +} + +inline uint8_t round_to_4_bits(float val) { + return clamp(val * 15.0f / 255.0f + 0.5f, 0, 15); +} + +union Color { + struct { + uint8_t b; + uint8_t g; + uint8_t r; + uint8_t a; + }; + uint8_t components[4]; + uint32_t bits; +}; + +/* + * NEON optimized codeword tables. + * + * It allows for a single table entry to be loaded into a 64-bit register + * without duplication and with the alpha channel already cleared. + * + * See: Table 3.17.2 + */ +ALIGNAS(8) static const int16_t g_codeword_tables_neon_opt[8][16] = { + {-8, -8, -8, 0, -2, -2, -2, 0, 2, 2, 2, 0, 8, 8, 8, 0}, + {-17, -17, -17, 0, -5, -5, -5, 0, 5, 5, 5, 0, 17, 17, 17, 0}, + {-29, -29, -29, 0, -9, -9, -9, 0, 9, 9, 9, 0, 29, 29, 29, 0}, + {-42, -42, -42, 0, -13, -13, -13, 0, 13, 13, 13, 0, 42, 42, 42, 0}, + {-60, -60, -60, 0, -18, -18, -18, 0, 18, 18, 18, 0, 60, 60, 60, 0}, + {-80, -80, -80, 0, -24, -24, -24, 0, 24, 24, 24, 0, 80, 80, 80, 0}, + {-106, -106, -106, 0, -33, -33, -33, 0, 33, 33, 33, 0, 106, 106, 106, 0}, + {-183, -183, -183, 0, -47, -47, -47, 0, 47, 47, 47, 0, 183, 183, 183, 0}}; + +/* + * Maps modifier indices to pixel index values. + * See: Table 3.17.3 + */ +static const uint8_t g_mod_to_pix[4] = {3, 2, 0, 1}; + +/* + * The ETC1 specification index texels as follows: + * + * [a][e][i][m] [ 0][ 4][ 8][12] + * [b][f][j][n] <-> [ 1][ 5][ 9][13] + * [c][g][k][o] [ 2][ 6][10][14] + * [d][h][l][p] [ 3][ 7][11][15] + * + * However, when extracting sub blocks from BGRA data the natural array + * indexing order ends up different: + * + * vertical0: [a][b][c][d] horizontal0: [a][e][i][m] + * [e][f][g][h] [b][f][j][n] + * vertical1: [i][j][k][l] horizontal1: [c][g][k][o] + * [m][n][o][p] [d][h][l][p] + * + * In order to translate from the natural array indices in a sub block to the + * indices (numbers) used by specification and hardware we use this table. + * + * NOTE: Since we can efficiently transpose matrixes using NEON we end up with + * near perfect indexing for vertical sub blocks. + */ +static const uint8_t g_idx_to_num[4][8] = { + {0, 1, 2, 3, 4, 5, 6, 7}, // Vertical block 0. + {8, 9, 10, 11, 12, 13, 14, 15}, // Vertical block 1. + {0, 4, 8, 12, 1, 5, 9, 13}, // Horizontal block 0. + {2, 6, 10, 14, 3, 7, 11, 15} // Horizontal block 1. +}; + +inline void WriteColors444(uint8_t* block, + const Color& color0, + const Color& color1) { + block[0] = (color0.b & 0xf0) | (color1.b >> 4); + block[1] = (color0.g & 0xf0) | (color1.g >> 4); + block[2] = (color0.r & 0xf0) | (color1.r >> 4); +} + +inline void WriteColors555(uint8_t* block, + const Color& color0, + const Color& color1) { + // Table for conversion to 3-bit two complement format. + static const uint8_t two_compl_trans_table[8] = { + 4, // -4 (100b) + 5, // -3 (101b) + 6, // -2 (110b) + 7, // -1 (111b) + 0, // 0 (000b) + 1, // 1 (001b) + 2, // 2 (010b) + 3, // 3 (011b) + }; + + int16_t delta_r = static_cast(color1.r >> 3) - (color0.r >> 3); + int16_t delta_g = static_cast(color1.g >> 3) - (color0.g >> 3); + int16_t delta_b = static_cast(color1.b >> 3) - (color0.b >> 3); + assert(delta_r >= -4 && delta_r <= 3); + assert(delta_g >= -4 && delta_g <= 3); + assert(delta_b >= -4 && delta_b <= 3); + + block[0] = (color0.b & 0xf8) | two_compl_trans_table[delta_b + 4]; + block[1] = (color0.g & 0xf8) | two_compl_trans_table[delta_g + 4]; + block[2] = (color0.r & 0xf8) | two_compl_trans_table[delta_r + 4]; +} + +inline void WriteCodewordTable(uint8_t* block, + uint8_t sub_block_id, + uint8_t table) { + uint8_t shift = (2 + (3 - sub_block_id * 3)); + block[3] &= ~(0x07 << shift); + block[3] |= table << shift; +} + +inline void WritePixelData(uint8_t* block, uint32_t pixel_data) { + block[4] |= pixel_data >> 24; + block[5] |= (pixel_data >> 16) & 0xff; + block[6] |= (pixel_data >> 8) & 0xff; + block[7] |= pixel_data & 0xff; +} + +inline void WriteFlip(uint8_t* block, bool flip) { + block[3] &= ~0x01; + block[3] |= static_cast(flip); +} + +inline void WriteDiff(uint8_t* block, bool diff) { + block[3] &= ~0x02; + block[3] |= static_cast(diff) << 1; +} + +/** + * Compress and rounds BGR888 into BGR444. The resulting BGR444 color is + * expanded to BGR888 as it would be in hardware after decompression. The + * actual 444-bit data is available in the four most significant bits of each + * channel. + */ +inline Color MakeColor444(const float* bgr) { + uint8_t b4 = round_to_4_bits(bgr[0]); + uint8_t g4 = round_to_4_bits(bgr[1]); + uint8_t r4 = round_to_4_bits(bgr[2]); + Color bgr444; + bgr444.b = (b4 << 4) | b4; + bgr444.g = (g4 << 4) | g4; + bgr444.r = (r4 << 4) | r4; + return bgr444; +} + +/** + * Compress and rounds BGR888 into BGR555. The resulting BGR555 color is + * expanded to BGR888 as it would be in hardware after decompression. The + * actual 555-bit data is available in the five most significant bits of each + * channel. + */ +inline Color MakeColor555(const float* bgr) { + uint8_t b5 = round_to_5_bits(bgr[0]); + uint8_t g5 = round_to_5_bits(bgr[1]); + uint8_t r5 = round_to_5_bits(bgr[2]); + Color bgr555; + bgr555.b = (b5 << 3) | (b5 >> 2); + bgr555.g = (g5 << 3) | (g5 >> 2); + bgr555.r = (r5 << 3) | (r5 >> 2); + return bgr555; +} + +/** + * Calculates the error metric for two colors. A small error signals that the + * colors are similar to each other, a large error the signals the opposite. + * + * IMPORTANT: This function call has been inlined and NEON optimized in the + * ComputeLuminance() function. The inlined version should be kept + * in sync with this function implementation. + */ +inline uint32_t GetColorError(const Color& u, const Color& v) { + int delta_b = static_cast(u.b) - v.b; + int delta_g = static_cast(u.g) - v.g; + int delta_r = static_cast(u.r) - v.r; + return delta_b * delta_b + delta_g * delta_g + delta_r * delta_r; +} + +void GetAverageColor(const Color* src, float* avg_color_bgr) { + const uint8_t* src_ptr = reinterpret_cast(src); +#ifdef GCC46_INTERNAL_ERROR_WORKAROUND + uint8x8x4_t src0; + src0 = vld4_u8(src_ptr); +#else + uint8x8x4_t src0 = vld4_u8(src_ptr); +#endif + + uint64x1_t sum_b0 = vpaddl_u32(vpaddl_u16(vpaddl_u8(src0.val[0]))); + uint64x1_t sum_g0 = vpaddl_u32(vpaddl_u16(vpaddl_u8(src0.val[1]))); + uint64x1_t sum_r0 = vpaddl_u32(vpaddl_u16(vpaddl_u8(src0.val[2]))); + + ALIGNAS(8) uint64_t sum_b, sum_g, sum_r; + vst1_u64(&sum_b, sum_b0); + vst1_u64(&sum_g, sum_g0); + vst1_u64(&sum_r, sum_r0); + + const float kInv8 = 1.0f / 8.0f; + avg_color_bgr[0] = static_cast(sum_b) * kInv8; + avg_color_bgr[1] = static_cast(sum_g) * kInv8; + avg_color_bgr[2] = static_cast(sum_r) * kInv8; +} + +void ComputeLuminance(uint8_t* block, + const Color* src, + const Color& base, + int sub_block_id, + const uint8_t* idx_to_num_tab) { + uint32_t best_tbl_err = std::numeric_limits::max(); + uint8_t best_tbl_idx = 0; + uint8_t best_mod_idxs[8][8]; // [table][texel] + + // Load immutable data that is shared through iteration. + ALIGNAS(8) const int16_t base_color_ptr[4] = {base.b, base.g, base.r, 0x00}; + int16x8_t base_color = + vcombine_s16(vld1_s16(base_color_ptr), vld1_s16(base_color_ptr)); + + ALIGNAS(8) const uint32_t idx_mask_ptr[4] = {0x00, 0x01, 0x02, 0x03}; + uint32x4_t idx_mask = vld1q_u32(idx_mask_ptr); + + // Preload source color registers. + uint8x16_t src_color[8]; + for (unsigned int i = 0; i < 8; ++i) { + const uint32_t* src_ptr = reinterpret_cast(&src[i]); + src_color[i] = vreinterpretq_u8_u32(vld1q_dup_u32(src_ptr)); + } + + // Try all codeword tables to find the one giving the best results for this + // block. + for (unsigned int tbl_idx = 0; tbl_idx < 8; ++tbl_idx) { + uint32_t tbl_err = 0; + + // For the current table, compute the candidate color: base + lum for all + // four luminance entries. + const int16_t* lum_ptr = g_codeword_tables_neon_opt[tbl_idx]; + int16x8_t lum01 = vld1q_s16(lum_ptr); + int16x8_t lum23 = vld1q_s16(lum_ptr + 8); + + int16x8_t color01 = vaddq_s16(base_color, lum01); + int16x8_t color23 = vaddq_s16(base_color, lum23); + + // Clamp the candidate colors to [0, 255]. + color01 = vminq_s16(color01, vdupq_n_s16(255)); + color01 = vmaxq_s16(color01, vdupq_n_s16(0)); + color23 = vminq_s16(color23, vdupq_n_s16(255)); + color23 = vmaxq_s16(color23, vdupq_n_s16(0)); + + uint8x16_t candidate_color = + vcombine_u8(vmovn_u16(vreinterpretq_u16_s16(color01)), + vmovn_u16(vreinterpretq_u16_s16(color23))); + + for (unsigned int i = 0; i < 8; ++i) { + // Compute the squared distance between the source and candidate colors. + uint8x16_t diff = vabdq_u8(src_color[i], candidate_color); + uint8x8_t diff01 = vget_low_u8(diff); + uint8x8_t diff23 = vget_high_u8(diff); + + uint16x8_t square01 = vmull_u8(diff01, diff01); + uint16x8_t square23 = vmull_u8(diff23, diff23); + + uint32x4_t psum01 = vpaddlq_u16(square01); + uint32x4_t psum23 = vpaddlq_u16(square23); + uint32x2_t err01 = vpadd_u32(vget_low_u32(psum01), vget_high_u32(psum01)); + uint32x2_t err23 = vpadd_u32(vget_low_u32(psum23), vget_high_u32(psum23)); + uint32x4_t errs = vcombine_u32(err01, err23); + + // Find the minimum error. + uint32x2_t min_err = vpmin_u32(err01, err23); + min_err = vpmin_u32(min_err, min_err); + + // Find the modifier index which produced the minimum error. This is + // essentially the lane number of the lane containing the minimum error. + uint32x4_t min_mask = vceqq_u32(vcombine_u32(min_err, min_err), errs); + uint32x4_t idxs = vbslq_u32(min_mask, idx_mask, vdupq_n_u32(0xffffffff)); + + uint32x2_t min_idx = vpmin_u32(vget_low_u32(idxs), vget_high_u32(idxs)); + min_idx = vpmin_u32(min_idx, min_idx); + + uint32_t best_mod_err = vget_lane_u32(min_err, 0); + uint32_t best_mod_idx = vget_lane_u32(min_idx, 0); + + best_mod_idxs[tbl_idx][i] = best_mod_idx; + + tbl_err += best_mod_err; + if (tbl_err > best_tbl_err) + break; // We're already doing worse than the best table so skip. + } + + if (tbl_err < best_tbl_err) { + best_tbl_err = tbl_err; + best_tbl_idx = tbl_idx; + + if (tbl_err == 0) + break; // We cannot do any better than this. + } + } + + WriteCodewordTable(block, sub_block_id, best_tbl_idx); + + uint32_t pix_data = 0; + + for (unsigned int i = 0; i < 8; ++i) { + uint8_t mod_idx = best_mod_idxs[best_tbl_idx][i]; + uint8_t pix_idx = g_mod_to_pix[mod_idx]; + + uint32_t lsb = pix_idx & 0x1; + uint32_t msb = pix_idx >> 1; + + // Obtain the texel number as specified in the standard. + int texel_num = idx_to_num_tab[i]; + pix_data |= msb << (texel_num + 16); + pix_data |= lsb << (texel_num); + } + + WritePixelData(block, pix_data); +} + +/** + * Compress a solid, single colored block. + */ +void CompressSolidBlock(uint8_t* dst, Color src) { + // Clear destination buffer so that we can "or" in the results. + memset(dst, 0, 8); + + float src_color_float[3] = {static_cast(src.b), + static_cast(src.g), + static_cast(src.r)}; + Color base = MakeColor555(src_color_float); + + WriteDiff(dst, true); + WriteFlip(dst, false); + WriteColors555(dst, base, base); + + uint32_t best_tbl_err = std::numeric_limits::max(); + uint8_t best_tbl_idx = 0; + uint8_t best_mod_idx = 0; + + // Load immutable data that is shared through iteration. + ALIGNAS(8) const int16_t base_color_ptr[4] = {base.b, base.g, base.r, 0x00}; + int16x8_t base_color = + vcombine_s16(vld1_s16(base_color_ptr), vld1_s16(base_color_ptr)); + + ALIGNAS(8) const uint32_t idx_mask_ptr[4] = {0x00, 0x01, 0x02, 0x03}; + uint32x4_t idx_mask = vld1q_u32(idx_mask_ptr); + + // Preload source color registers. + src.a = 0x00; + uint8x16_t src_color = vreinterpretq_u8_u32( + vld1q_dup_u32(reinterpret_cast(&src))); + + // Try all codeword tables to find the one giving the best results for this + // block. + for (unsigned int tbl_idx = 0; tbl_idx < 8; ++tbl_idx) { + // For the current table, compute the candidate color: base + lum for all + // four luminance entries. + const int16_t* lum_ptr = g_codeword_tables_neon_opt[tbl_idx]; + int16x8_t lum01 = vld1q_s16(lum_ptr); + int16x8_t lum23 = vld1q_s16(lum_ptr + 8); + + int16x8_t color01 = vaddq_s16(base_color, lum01); + int16x8_t color23 = vaddq_s16(base_color, lum23); + + // Clamp the candidate colors to [0, 255]. + color01 = vminq_s16(color01, vdupq_n_s16(255)); + color01 = vmaxq_s16(color01, vdupq_n_s16(0)); + color23 = vminq_s16(color23, vdupq_n_s16(255)); + color23 = vmaxq_s16(color23, vdupq_n_s16(0)); + + uint8x16_t candidate_color = + vcombine_u8(vmovn_u16(vreinterpretq_u16_s16(color01)), + vmovn_u16(vreinterpretq_u16_s16(color23))); + + // Compute the squared distance between the source and candidate colors. + uint8x16_t diff = vabdq_u8(src_color, candidate_color); + uint8x8_t diff01 = vget_low_u8(diff); + uint8x8_t diff23 = vget_high_u8(diff); + + uint16x8_t square01 = vmull_u8(diff01, diff01); + uint16x8_t square23 = vmull_u8(diff23, diff23); + + uint32x4_t psum01 = vpaddlq_u16(square01); + uint32x4_t psum23 = vpaddlq_u16(square23); + uint32x2_t err01 = vpadd_u32(vget_low_u32(psum01), vget_high_u32(psum01)); + uint32x2_t err23 = vpadd_u32(vget_low_u32(psum23), vget_high_u32(psum23)); + uint32x4_t errs = vcombine_u32(err01, err23); + + // Find the minimum error. + uint32x2_t min_err = vpmin_u32(err01, err23); + min_err = vpmin_u32(min_err, min_err); + + // Find the modifier index which produced the minimum error. This is + // essentially the lane number of the lane containing the minimum error. + uint32x4_t min_mask = vceqq_u32(vcombine_u32(min_err, min_err), errs); + uint32x4_t idxs = vbslq_u32(min_mask, idx_mask, vdupq_n_u32(0xffffffff)); + + uint32x2_t min_idx = vpmin_u32(vget_low_u32(idxs), vget_high_u32(idxs)); + min_idx = vpmin_u32(min_idx, min_idx); + + uint32_t cur_best_mod_err = vget_lane_u32(min_err, 0); + uint32_t cur_best_mod_idx = vget_lane_u32(min_idx, 0); + + uint32_t tbl_err = cur_best_mod_err; + if (tbl_err < best_tbl_err) { + best_tbl_err = tbl_err; + best_tbl_idx = tbl_idx; + best_mod_idx = cur_best_mod_idx; + + if (tbl_err == 0) + break; // We cannot do any better than this. + } + } + + WriteCodewordTable(dst, 0, best_tbl_idx); + WriteCodewordTable(dst, 1, best_tbl_idx); + + uint8_t pix_idx = g_mod_to_pix[best_mod_idx]; + uint32_t lsb = pix_idx & 0x1; + uint32_t msb = pix_idx >> 1; + + uint32_t pix_data = 0; + for (unsigned int i = 0; i < 2; ++i) { + for (unsigned int j = 0; j < 8; ++j) { + // Obtain the texel number as specified in the standard. + int texel_num = g_idx_to_num[i][j]; + pix_data |= msb << (texel_num + 16); + pix_data |= lsb << (texel_num); + } + } + + WritePixelData(dst, pix_data); +} + +void CompressBlock(uint8_t* dst, const Color* ver_src, const Color* hor_src) { + ALIGNAS(8) const Color* sub_block_src[4] = { + ver_src, ver_src + 8, hor_src, hor_src + 8}; + + Color sub_block_avg[4]; + bool use_differential[2] = {true, true}; + + // Compute the average color for each sub block and determine if differential + // coding can be used. + for (unsigned int i = 0, j = 1; i < 4; i += 2, j += 2) { + float avg_color_0[3]; + GetAverageColor(sub_block_src[i], avg_color_0); + Color avg_color_555_0 = MakeColor555(avg_color_0); + + float avg_color_1[3]; + GetAverageColor(sub_block_src[j], avg_color_1); + Color avg_color_555_1 = MakeColor555(avg_color_1); + + for (unsigned int light_idx = 0; light_idx < 3; ++light_idx) { + int u = avg_color_555_0.components[light_idx] >> 3; + int v = avg_color_555_1.components[light_idx] >> 3; + + int component_diff = v - u; + if (component_diff < -4 || component_diff > 3) { + use_differential[i / 2] = false; + sub_block_avg[i] = MakeColor444(avg_color_0); + sub_block_avg[j] = MakeColor444(avg_color_1); + } else { + sub_block_avg[i] = avg_color_555_0; + sub_block_avg[j] = avg_color_555_1; + } + } + } + + // Compute the error of each sub block before adjusting for luminance. These + // error values are later used for determining if we should flip the sub + // block or not. + uint32_t sub_block_err[4] = {0}; + for (unsigned int i = 0; i < 4; ++i) { + for (unsigned int j = 0; j < 8; ++j) { + sub_block_err[i] += GetColorError(sub_block_avg[i], sub_block_src[i][j]); + } + } + + bool flip = + sub_block_err[2] + sub_block_err[3] < sub_block_err[0] + sub_block_err[1]; + + // Clear destination buffer so that we can "or" in the results. + memset(dst, 0, 8); + + WriteDiff(dst, use_differential[!!flip]); + WriteFlip(dst, flip); + + uint8_t sub_block_off_0 = flip ? 2 : 0; + uint8_t sub_block_off_1 = sub_block_off_0 + 1; + + if (use_differential[!!flip]) { + WriteColors555(dst, sub_block_avg[sub_block_off_0], + sub_block_avg[sub_block_off_1]); + } else { + WriteColors444(dst, sub_block_avg[sub_block_off_0], + sub_block_avg[sub_block_off_1]); + } + + // Compute luminance for the first sub block. + ComputeLuminance(dst, sub_block_src[sub_block_off_0], + sub_block_avg[sub_block_off_0], 0, + g_idx_to_num[sub_block_off_0]); + // Compute luminance for the second sub block. + ComputeLuminance(dst, sub_block_src[sub_block_off_1], + sub_block_avg[sub_block_off_1], 1, + g_idx_to_num[sub_block_off_1]); +} + +// Extract up to a 4x4 block of pixels and "de-swizzle" them into 16x1. +// If 'num_columns' or 'num_rows' are less than 4 then it fills out the rest +// of the block by taking a copy of the last valid column or row. +void ExtractMisalignedBlock(uint8_t* dst, + const uint8_t* src, + int num_columns, + int num_rows, + int width) { + uint32_t* wdst = reinterpret_cast(dst); + const uint32_t* wsrc = reinterpret_cast(src); + + for (int y = 0; y < num_rows; ++y) { + for (int x = 0; x < num_columns; ++x) + *wdst++ = *wsrc++; + // Fill remaining columns with values from last column. + uint32_t* padding = wdst - 1; + for (int x = num_columns; x < 4; ++x) + *wdst++ = *padding; + wsrc += width - num_columns; + } + + // Fill remaining rows with values from last row. + const uint32_t* last_row = wdst - 4; + for (int y = num_rows; y < 4; ++y) { + const uint32_t* padding = last_row; + for (int x = 0; x < 4; ++x) + *wdst++ = *padding++; + } +} + +} // namespace + +void TextureCompressorETC1_NEON::Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) { + ALIGNAS(8) uint32_t ver_blocks[16]; + ALIGNAS(8) uint32_t hor_blocks[16]; + + uint64_t aligned_buffer[8]; + uint8_t* block_buffer = reinterpret_cast(aligned_buffer); + + // Mask for clearing the alpha channel. + ALIGNAS(8) const uint32_t clear_mask_ptr[4] = { + 0xff000000, 0xff000000, 0xff000000, 0xff000000}; + uint32x4_t clear_mask = vld1q_u32(clear_mask_ptr); + + for (int y = 0; y < height; y += 4, src += width * 4 * 4) { + int num_rows = std::min(height - y, 4); + for (int x = 0; x < width; x += 4, dst += 8) { + int num_columns = std::min(width - x, 4); + + const uint32_t* row0; + int stride; + if (num_rows + num_columns != 8) { + ExtractMisalignedBlock(block_buffer, src + x * 4, num_columns, num_rows, + width); + row0 = reinterpret_cast(block_buffer); + stride = 4; + } else { + row0 = reinterpret_cast(src + x * 4); + stride = width; + } + const uint32_t* row1 = row0 + stride; + const uint32_t* row2 = row1 + stride; + const uint32_t* row3 = row2 + stride; + +#ifdef GCC46_INTERNAL_ERROR_WORKAROUND + uint32x4x4_t block_transposed; +#endif + ALIGNAS(8) uint32x4_t block[4]; + block[0] = vld1q_u32(row0); + block[1] = vld1q_u32(row1); + block[2] = vld1q_u32(row2); + block[3] = vld1q_u32(row3); + + // Clear alpha channel. + for (unsigned int i = 0; i < 4; ++i) { + block[i] = vbicq_u32(block[i], clear_mask); + } + + // Check if the block is solid. + uint32x4_t solid = vbicq_u32(vdupq_n_u32(*row0), clear_mask); + + uint16x4_t eq0 = vmovn_u32(vceqq_u32(block[0], solid)); + uint16x4_t eq1 = vmovn_u32(vceqq_u32(block[1], solid)); + uint16x4_t eq2 = vmovn_u32(vceqq_u32(block[2], solid)); + uint16x4_t eq3 = vmovn_u32(vceqq_u32(block[3], solid)); + uint16x4_t tst = vand_u16(vand_u16(eq0, eq1), vand_u16(eq2, eq3)); + + ALIGNAS(8) uint64_t solid_block_tst_bits; + vst1_u64(&solid_block_tst_bits, vreinterpret_u64_u16(tst)); + + if (solid_block_tst_bits == 0xffffffffffffffff) { + CompressSolidBlock(dst, *reinterpret_cast(row0)); + continue; + } + + vst1q_u32(hor_blocks, block[0]); + vst1q_u32(hor_blocks + 4, block[1]); + vst1q_u32(hor_blocks + 8, block[2]); + vst1q_u32(hor_blocks + 12, block[3]); + + // Texel ordering according to specification: + // [ 0][ 4][ 8][12] + // [ 1][ 5][ 9][13] + // [ 2][ 6][10][14] + // [ 3][ 7][11][15] + // + // To access the vertical blocks using C-style indexing we + // transpose the block: + // [ 0][ 1][ 2][ 3] + // [ 4][ 5][ 6][ 7] + // [ 8][ 9][10][11] + // [12][13][14][15] +#ifdef GCC46_INTERNAL_ERROR_WORKAROUND + block_transposed = vld4q_u32(hor_blocks); +#else + uint32x4x4_t block_transposed = vld4q_u32(hor_blocks); +#endif + + vst1q_u32(ver_blocks, block_transposed.val[0]); + vst1q_u32(ver_blocks + 4, block_transposed.val[1]); + vst1q_u32(ver_blocks + 8, block_transposed.val[2]); + vst1q_u32(ver_blocks + 12, block_transposed.val[3]); + + CompressBlock(dst, reinterpret_cast(ver_blocks), + reinterpret_cast(hor_blocks)); + } + } +} diff --git a/src/third_party/texture_compressor/texture_compressor_etc1_neon.h b/src/third_party/texture_compressor/texture_compressor_etc1_neon.h new file mode 100644 index 0000000..76d69a4 --- /dev/null +++ b/src/third_party/texture_compressor/texture_compressor_etc1_neon.h @@ -0,0 +1,28 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef TEXTURE_COMPRESSOR_ETC1_NEON_H_ +#define TEXTURE_COMPRESSOR_ETC1_NEON_H_ + +#include + +#include "texture_compressor.h" + +class TextureCompressorETC1_NEON : public TextureCompressor { + public: + TextureCompressorETC1_NEON() { + format_ = kFormatETC1; + } + + // Compress a texture using ETC1. Note that the |quality| parameter is + // ignored. The current implementation does not support different quality + // settings. + void Compress(const uint8_t* src, + uint8_t* dst, + int width, + int height, + Quality quality) override; +}; + +#endif // TEXTURE_COMPRESSOR_ETC1_NEON_H_