Unified · Split
| @@ -0,0 +1,36 @@ | |||
| 1 | #include <memory> | ||
| 2 | #include <vector> | ||
| 3 | #include "third_party/googletest/src/include/gtest/gtest.h" | ||
| 4 | #include "vp9/simple_encode.h" | ||
| 5 | |||
| 6 | namespace { | ||
| 7 | |||
| 8 | TEST(SimpleEncode, ComputeFirstPassStats) { | ||
| 9 | int w = 352; | ||
| 10 | int h = 288; | ||
| 11 | int frame_rate_num = 30; | ||
| 12 | int frame_rate_den = 1; | ||
| 13 | int target_bitrate = 200; | ||
| 14 | int num_frames = 17; | ||
| 15 | // TODO(angiebird): Figure out how to upload test video to our codebase | ||
| 16 | FILE *file = fopen("bus_352x288_420_f20_b8.yuv", "r"); | ||
| 17 | SimpleEncode simple_encode(w, h, frame_rate_num, frame_rate_den, | ||
| 18 | target_bitrate, num_frames, file); | ||
| 19 | simple_encode.ComputeFirstPassStats(); | ||
| 20 | std::vector<std::vector<double>> frame_stats = | ||
| 21 | simple_encode.ObserveFirstPassStats(); | ||
| 22 | EXPECT_EQ(frame_stats.size(), static_cast<size_t>(num_frames)); | ||
| 23 | size_t data_num = frame_stats[0].size(); | ||
| 24 | // Read ObserveFirstPassStats before changing FIRSTPASS_STATS. | ||
| 25 | EXPECT_EQ(data_num, static_cast<size_t>(25)); | ||
| 26 | for (size_t i = 0; i < frame_stats.size(); ++i) { | ||
| 27 | EXPECT_EQ(frame_stats[i].size(), data_num); | ||
| 28 | // FIRSTPASS_STATS's first element is frame | ||
| 29 | EXPECT_EQ(frame_stats[i][0], i); | ||
| 30 | // FIRSTPASS_STATS's last element is count, and the count is 1 for single | ||
| 31 | // frame stats | ||
| 32 | EXPECT_EQ(frame_stats[i][data_num - 1], 1); | ||
| 33 | } | ||
| 34 | } | ||
| 35 | |||
| 36 | } // namespace | ||
| @@ -26,6 +26,7 @@ LIBVPX_TEST_DATA-$(CONFIG_VP9_ENCODER) += noisy_clip_640_360.y4m · 25 unmodified lines | |||
| 26 | LIBVPX_TEST_DATA-$(CONFIG_VP9_ENCODER) += rush_hour_444.y4m | 26 | LIBVPX_TEST_DATA-$(CONFIG_VP9_ENCODER) += rush_hour_444.y4m |
| 27 | LIBVPX_TEST_DATA-$(CONFIG_VP9_ENCODER) += screendata.y4m | 27 | LIBVPX_TEST_DATA-$(CONFIG_VP9_ENCODER) += screendata.y4m |
| 28 | LIBVPX_TEST_DATA-$(CONFIG_VP9_ENCODER) += niklas_640_480_30.yuv | 28 | LIBVPX_TEST_DATA-$(CONFIG_VP9_ENCODER) += niklas_640_480_30.yuv |
| 29 | LIBVPX_TEST_DATA-$(CONFIG_RATE_CTRL) += bus_352x288_420_f20_b8.yuv | ||
| 29 | 30 | ||
| 30 | # Test vectors | 31 | # Test vectors |
| 31 | LIBVPX_TEST_DATA-$(CONFIG_VP8_DECODER) += vp80-00-comprehensive-001.ivf | 32 | LIBVPX_TEST_DATA-$(CONFIG_VP8_DECODER) += vp80-00-comprehensive-001.ivf |
| @@ -1,3 +1,4 @@ | |||
| 1 | 3eaf216d9fc8b4b9bb8c3956311f49a85974806c *bus_352x288_420_f20_b8.yuv | ||
| 1 | d5dfb0151c9051f8c85999255645d7a23916d3c0 *hantro_collage_w352h288.yuv | 2 | d5dfb0151c9051f8c85999255645d7a23916d3c0 *hantro_collage_w352h288.yuv |
| 2 | b87815bf86020c592ccc7a846ba2e28ec8043902 *hantro_odd.yuv | 3 | b87815bf86020c592ccc7a846ba2e28ec8043902 *hantro_odd.yuv |
| 3 | 76024eb753cdac6a5e5703aaea189d35c3c30ac7 *invalid-vp90-2-00-quantizer-00.webm.ivf.s5861_r01-05_b6-.v2.ivf | 4 | 76024eb753cdac6a5e5703aaea189d35c3c30ac7 *invalid-vp90-2-00-quantizer-00.webm.ivf.s5861_r01-05_b6-.v2.ivf |
| @@ -58,6 +58,7 @@ LIBVPX_TEST_SRCS-$(CONFIG_VP9_ENCODER) += svc_test.cc · 57 unmodified lines | |||
| 58 | LIBVPX_TEST_SRCS-$(CONFIG_VP9_ENCODER) += svc_test.h | 58 | LIBVPX_TEST_SRCS-$(CONFIG_VP9_ENCODER) += svc_test.h |
| 59 | LIBVPX_TEST_SRCS-$(CONFIG_VP9_ENCODER) += svc_end_to_end_test.cc | 59 | LIBVPX_TEST_SRCS-$(CONFIG_VP9_ENCODER) += svc_end_to_end_test.cc |
| 60 | LIBVPX_TEST_SRCS-$(CONFIG_VP9_ENCODER) += timestamp_test.cc | 60 | LIBVPX_TEST_SRCS-$(CONFIG_VP9_ENCODER) += timestamp_test.cc |
| 61 | LIBVPX_TEST_SRCS-$(CONFIG_RATE_CTRL) += simple_encode_test.cc | ||
| 61 | 62 | ||
| 62 | LIBVPX_TEST_SRCS-yes += decode_test_driver.cc | 63 | LIBVPX_TEST_SRCS-yes += decode_test_driver.cc |
| 63 | LIBVPX_TEST_SRCS-yes += decode_test_driver.h | 64 | LIBVPX_TEST_SRCS-yes += decode_test_driver.h |
| @@ -3685,3 +3685,10 @@ int vp9_get_coding_frame_num(const struct VP9EncoderConfig *oxcf, · 3684 unmodified lines | |||
| 3685 | return coding_frame_num; | 3685 | return coding_frame_num; |
| 3686 | } | 3686 | } |
| 3687 | #endif | 3687 | #endif |
| 3688 | |||
| 3689 | FIRSTPASS_STATS vp9_get_frame_stats(const TWO_PASS *twopass) { | ||
| 3690 | return twopass->this_frame_stats; | ||
| 3691 | } | ||
| 3692 | FIRSTPASS_STATS vp9_get_total_stats(const TWO_PASS *twopass) { | ||
| 3693 | return twopass->total_stats; | ||
| 3694 | } | ||
| @@ -256,6 +256,9 @@ int vp9_get_coding_frame_num(const struct VP9EncoderConfig *oxcf, · 255 unmodified lines | |||
| 256 | int multi_layer_arf, int allow_alt_ref); | 256 | int multi_layer_arf, int allow_alt_ref); |
| 257 | #endif | 257 | #endif |
| 258 | 258 | ||
| 259 | FIRSTPASS_STATS vp9_get_frame_stats(const TWO_PASS *two_pass); | ||
| 260 | FIRSTPASS_STATS vp9_get_total_stats(const TWO_PASS *two_pass); | ||
| 261 | |||
| 259 | #ifdef __cplusplus | 262 | #ifdef __cplusplus |
| 260 | } // extern "C" | 263 | } // extern "C" |
| 261 | #endif | 264 | #endif |
| @@ -1,27 +1,173 @@ | |||
| 1 | #include <vector> | ||
| 1 | #include "vp9/common/vp9_onyxc_int.h" | 2 | #include "vp9/common/vp9_onyxc_int.h" |
| 3 | #include "vp9/vp9_iface_common.h" | ||
| 2 | #include "vp9/encoder/vp9_encoder.h" | 4 | #include "vp9/encoder/vp9_encoder.h" |
| 5 | #include "vp9/encoder/vp9_firstpass.h" | ||
| 3 | #include "vp9/simple_encode.h" | 6 | #include "vp9/simple_encode.h" |
| 4 | #include "vp9/vp9_cx_iface.h" | 7 | #include "vp9/vp9_cx_iface.h" |
| 5 | 8 | ||
| 9 | // TODO(angiebird): Merge this function with vpx_img_plane_width() | ||
| 10 | static int img_plane_width(const vpx_image_t *img, int plane) { | ||
| 11 | if (plane > 0 && img->x_chroma_shift > 0) | ||
| 12 | return (img->d_w + 1) >> img->x_chroma_shift; | ||
| 13 | else | ||
| 14 | return img->d_w; | ||
| 15 | } | ||
| 16 | |||
| 17 | // TODO(angiebird): Merge this function with vpx_img_plane_height() | ||
| 18 | static int img_plane_height(const vpx_image_t *img, int plane) { | ||
| 19 | if (plane > 0 && img->y_chroma_shift > 0) | ||
| 20 | return (img->d_h + 1) >> img->y_chroma_shift; | ||
| 21 | else | ||
| 22 | return img->d_h; | ||
| 23 | } | ||
| 24 | |||
| 25 | // TODO(angiebird): Merge this function with vpx_img_read() | ||
| 26 | static int img_read(vpx_image_t *img, FILE *file) { | ||
| 27 | int plane; | ||
| 28 | |||
| 29 | for (plane = 0; plane < 3; ++plane) { | ||
| 30 | unsigned char *buf = img->planes[plane]; | ||
| 31 | const int stride = img->stride[plane]; | ||
| 32 | const int w = img_plane_width(img, plane) * | ||
| 33 | ((img->fmt & VPX_IMG_FMT_HIGHBITDEPTH) ? 2 : 1); | ||
| 34 | const int h = img_plane_height(img, plane); | ||
| 35 | int y; | ||
| 36 | |||
| 37 | for (y = 0; y < h; ++y) { | ||
| 38 | if (fread(buf, 1, w, file) != (size_t)w) return 0; | ||
| 39 | buf += stride; | ||
| 40 | } | ||
| 41 | } | ||
| 42 | |||
| 43 | return 1; | ||
| 44 | } | ||
| 45 | |||
| 6 | class SimpleEncode::impl { | 46 | class SimpleEncode::impl { |
| 7 | public: | 47 | public: |
| 8 | VP9_COMP *cpi; | 48 | VP9_COMP *cpi; |
| 9 | BufferPool *buffer_pool; | 49 | std::vector<FIRSTPASS_STATS> frame_stats; |
| 10 | }; | 50 | }; |
| 11 | 51 | ||
| 52 | static VP9_COMP *init_encoder(const VP9EncoderConfig *oxcf, | ||
| 53 | vpx_img_fmt_t img_fmt) { | ||
| 54 | VP9_COMP *cpi; | ||
| 55 | BufferPool *buffer_pool = (BufferPool *)vpx_calloc(1, sizeof(*buffer_pool)); | ||
| 56 | vp9_initialize_enc(); | ||
| 57 | cpi = vp9_create_compressor(oxcf, buffer_pool); | ||
| 58 | vp9_update_compressor_with_img_fmt(cpi, img_fmt); | ||
| 59 | return cpi; | ||
| 60 | } | ||
| 61 | |||
| 62 | static void free_encoder(VP9_COMP *cpi) { | ||
| 63 | vpx_free(cpi->common.buffer_pool); | ||
| 64 | vp9_remove_compressor(cpi); | ||
| 65 | } | ||
| 66 | |||
| 67 | static INLINE vpx_rational_t make_vpx_rational(int num, int den) { | ||
| 68 | vpx_rational_t v; | ||
| 69 | v.num = num; | ||
| 70 | v.den = den; | ||
| 71 | return v; | ||
| 72 | } | ||
| 73 | |||
| 12 | SimpleEncode::SimpleEncode(int frame_width, int frame_height, | 74 | SimpleEncode::SimpleEncode(int frame_width, int frame_height, |
| 13 | vpx_rational_t frame_rate, int target_bitrate) | 75 | int frame_rate_num, int frame_rate_den, |
| 76 | int target_bitrate, int num_frames, FILE *file) | ||
| 14 | : pimpl{ std::unique_ptr<impl>(new impl()) } { | 77 | : pimpl{ std::unique_ptr<impl>(new impl()) } { |
| 78 | this->frame_width = frame_width; | ||
| 79 | this->frame_height = frame_height; | ||
| 80 | this->frame_rate_num = frame_rate_num; | ||
| 81 | this->frame_rate_den = frame_rate_den; | ||
| 82 | this->target_bitrate = target_bitrate; | ||
| 83 | this->num_frames = num_frames; | ||
| 84 | this->file = file; | ||
| 85 | pimpl->cpi = NULL; | ||
| 86 | } | ||
| 87 | |||
| 88 | void SimpleEncode::ComputeFirstPassStats() { | ||
| 89 | const vpx_img_fmt_t img_fmt = VPX_IMG_FMT_I420; | ||
| 90 | vpx_rational_t frame_rate = make_vpx_rational(frame_rate_num, frame_rate_den); | ||
| 91 | const VP9EncoderConfig oxcf = vp9_get_encoder_config( | ||
| 92 | frame_width, frame_height, frame_rate, target_bitrate, VPX_RC_FIRST_PASS); | ||
| 93 | VP9_COMP *cpi = init_encoder(&oxcf, img_fmt); | ||
| 94 | struct lookahead_ctx *lookahead = cpi->lookahead; | ||
| 95 | int i; | ||
| 96 | int use_highbitdepth = 0; | ||
| 97 | #if CONFIG_VP9_HIGHBITDEPTH | ||
| 98 | use_highbitdepth = cpi->common.use_highbitdepth; | ||
| 99 | #endif | ||
| 100 | vpx_image_t img; | ||
| 101 | vpx_img_alloc(&img, img_fmt, frame_width, frame_height, 1); | ||
| 102 | rewind(file); | ||
| 103 | pimpl->frame_stats.clear(); | ||
| 104 | for (i = 0; i < num_frames; ++i) { | ||
| 105 | assert(!vp9_lookahead_full(lookahead)); | ||
| 106 | if (img_read(&img, file)) { | ||
| 107 | int next_show_idx = vp9_lookahead_next_show_idx(lookahead); | ||
| 108 | int64_t ts_start = | ||
| 109 | timebase_units_to_ticks(&oxcf.g_timebase_in_ts, next_show_idx); | ||
| 110 | int64_t ts_end = | ||
| 111 | timebase_units_to_ticks(&oxcf.g_timebase_in_ts, next_show_idx + 1); | ||
| 112 | YV12_BUFFER_CONFIG sd; | ||
| 113 | image2yuvconfig(&img, &sd); | ||
| 114 | vp9_lookahead_push(lookahead, &sd, ts_start, ts_end, use_highbitdepth, 0); | ||
| 115 | { | ||
| 116 | int64_t time_stamp; | ||
| 117 | int64_t time_end; | ||
| 118 | int flush = 1; // Make vp9_get_compressed_data process a frame | ||
| 119 | size_t size; | ||
| 120 | unsigned int frame_flags = 0; | ||
| 121 | // TODO(angiebird): Call vp9_first_pass directly | ||
| 122 | vp9_get_compressed_data(cpi, &frame_flags, &size, NULL, &time_stamp, | ||
| 123 | &time_end, flush); | ||
| 124 | // vp9_get_compressed_data only generates first pass stats not | ||
| 125 | // compresses data | ||
| 126 | assert(size == 0); | ||
| 127 | } | ||
| 128 | pimpl->frame_stats.push_back(vp9_get_frame_stats(&cpi->twopass)); | ||
| 129 | } | ||
| 130 | } | ||
| 131 | vp9_end_first_pass(cpi); | ||
| 132 | // TODO(angiebird): Store the total_stats apart form frame_stats | ||
| 133 | pimpl->frame_stats.push_back(vp9_get_total_stats(&cpi->twopass)); | ||
| 134 | free_encoder(cpi); | ||
| 135 | rewind(file); | ||
| 136 | vpx_img_free(&img); | ||
| 137 | } | ||
| 138 | |||
| 139 | std::vector<std::vector<double>> SimpleEncode::ObserveFirstPassStats() { | ||
| 140 | std::vector<std::vector<double>> output_stats; | ||
| 141 | // TODO(angiebird): This function make several assumptions of | ||
| 142 | // FIRSTPASS_STATS. 1) All elements in FIRSTPASS_STATS are double except the | ||
| 143 | // last one. 2) The last entry of frame_stats is the total_stats. | ||
| 144 | // Change the code structure, so that we don't have to make these assumptions | ||
| 145 | |||
| 146 | // Note the last entry of frame_stats is the total_stats, we don't need it. | ||
| 147 | for (size_t i = 0; i < pimpl->frame_stats.size() - 1; ++i) { | ||
| 148 | double *buf_start = reinterpret_cast<double *>(&pimpl->frame_stats[i]); | ||
| 149 | // We use - 1 here because the last member in FIRSTPASS_STATS is not double | ||
| 150 | double *buf_end = | ||
| 151 | buf_start + sizeof(pimpl->frame_stats[i]) / sizeof(*buf_end) - 1; | ||
| 152 | std::vector<double> this_stats(buf_start, buf_end); | ||
| 153 | output_stats.push_back(this_stats); | ||
| 154 | } | ||
| 155 | return output_stats; | ||
| 156 | } | ||
| 157 | |||
| 158 | void SimpleEncode::StartEncode() { | ||
| 159 | vpx_rational_t frame_rate = make_vpx_rational(frame_rate_num, frame_rate_den); | ||
| 15 | VP9EncoderConfig oxcf = vp9_get_encoder_config( | 160 | VP9EncoderConfig oxcf = vp9_get_encoder_config( |
| 16 | frame_width, frame_height, frame_rate, target_bitrate, VPX_RC_LAST_PASS); | 161 | frame_width, frame_height, frame_rate, target_bitrate, VPX_RC_LAST_PASS); |
| 17 | pimpl->buffer_pool = (BufferPool *)vpx_calloc(1, sizeof(*pimpl->buffer_pool)); | 162 | assert(pimpl->cpi == NULL); |
| 18 | vp9_initialize_enc(); | 163 | pimpl->cpi = init_encoder(&oxcf, VPX_IMG_FMT_I420); |
| 19 | pimpl->cpi = vp9_create_compressor(&oxcf, pimpl->buffer_pool); | 164 | rewind(file); |
| 20 | vp9_update_compressor_with_img_fmt(pimpl->cpi, VPX_IMG_FMT_I420); | ||
| 21 | } | 165 | } |
| 22 | 166 | ||
| 23 | SimpleEncode::~SimpleEncode() { | 167 | void SimpleEncode::EndEncode() { |
| 24 | vpx_free(pimpl->buffer_pool); | 168 | free_encoder(pimpl->cpi); |
| 25 | vp9_remove_compressor(pimpl->cpi); | ||
| 26 | pimpl->cpi = nullptr; | 169 | pimpl->cpi = nullptr; |
| 170 | rewind(file); | ||
| 27 | } | 171 | } |
| 172 | |||
| 173 | SimpleEncode::~SimpleEncode() {} | ||
| @@ -1,13 +1,35 @@ | |||
| 1 | #include <memory> | 1 | #include <memory> |
| 2 | #include <vector> | ||
| 2 | class SimpleEncode { | 3 | class SimpleEncode { |
| 3 | public: | 4 | public: |
| 4 | SimpleEncode(int frame_width, int frame_height, vpx_rational_t frame_rate, | 5 | SimpleEncode(int frame_width, int frame_height, int frame_rate_num, |
| 5 | int target_bitrate); | 6 | int frame_rate_den, int target_bitrate, int num_frames, |
| 7 | FILE *file); | ||
| 6 | ~SimpleEncode(); | 8 | ~SimpleEncode(); |
| 7 | SimpleEncode(SimpleEncode &&) = delete; | 9 | SimpleEncode(SimpleEncode &&) = delete; |
| 8 | SimpleEncode &operator=(SimpleEncode &&) = delete; | 10 | SimpleEncode &operator=(SimpleEncode &&) = delete; |
| 9 | 11 | ||
| 12 | // Make encoder compute the first pass stats and store it internally for | ||
| 13 | // future encode | ||
| 14 | void ComputeFirstPassStats(); | ||
| 15 | |||
| 16 | // Output the first pass stats | ||
| 17 | std::vector<std::vector<double>> ObserveFirstPassStats(); | ||
| 18 | |||
| 19 | // Initialize the encoder for actual encoding | ||
| 20 | void StartEncode(); | ||
| 21 | |||
| 22 | // Free the encoder | ||
| 23 | void EndEncode(); | ||
| 24 | |||
| 10 | private: | 25 | private: |
| 11 | class impl; | 26 | class impl; |
| 27 | int frame_width; | ||
| 28 | int frame_height; | ||
| 29 | int frame_rate_num; | ||
| 30 | int frame_rate_den; | ||
| 31 | int target_bitrate; | ||
| 32 | int num_frames; | ||
| 33 | FILE *file; | ||
| 12 | std::unique_ptr<impl> pimpl; | 34 | std::unique_ptr<impl> pimpl; |
| 13 | }; | 35 | }; |
| @@ -22,6 +22,7 @@ · 21 unmodified lines | |||
| 22 | #include "vp9/encoder/vp9_encoder.h" | 22 | #include "vp9/encoder/vp9_encoder.h" |
| 23 | #include "vpx/vp8cx.h" | 23 | #include "vpx/vp8cx.h" |
| 24 | #include "vp9/common/vp9_alloccommon.h" | 24 | #include "vp9/common/vp9_alloccommon.h" |
| 25 | #include "vp9/vp9_cx_iface.h" | ||
| 25 | #include "vp9/encoder/vp9_firstpass.h" | 26 | #include "vp9/encoder/vp9_firstpass.h" |
| 26 | #include "vp9/encoder/vp9_lookahead.h" | 27 | #include "vp9/encoder/vp9_lookahead.h" |
| 27 | #include "vp9/vp9_cx_iface.h" | 28 | #include "vp9/vp9_cx_iface.h" |
| @@ -1086,18 +1087,6 @@ static int write_superframe_index(vpx_codec_alg_priv_t *ctx) { · 1058 unmodified lines | |||
| 1086 | return index_sz; | 1087 | return index_sz; |
| 1087 | } | 1088 | } |
| 1088 | 1089 | ||
| 1089 | static int64_t timebase_units_to_ticks(const vpx_rational64_t *timestamp_ratio, | ||
| 1090 | int64_t n) { | ||
| 1091 | return n * timestamp_ratio->num / timestamp_ratio->den; | ||
| 1092 | } | ||
| 1093 | |||
| 1094 | static int64_t ticks_to_timebase_units(const vpx_rational64_t *timestamp_ratio, | ||
| 1095 | int64_t n) { | ||
| 1096 | int64_t round = timestamp_ratio->num / 2; | ||
| 1097 | if (round > 0) --round; | ||
| 1098 | return (n * timestamp_ratio->den + round) / timestamp_ratio->num; | ||
| 1099 | } | ||
| 1100 | |||
| 1101 | static vpx_codec_frame_flags_t get_frame_pkt_flags(const VP9_COMP *cpi, | 1090 | static vpx_codec_frame_flags_t get_frame_pkt_flags(const VP9_COMP *cpi, |
| 1102 | unsigned int lib_flags) { | 1091 | unsigned int lib_flags) { |
| 1103 | vpx_codec_frame_flags_t flags = lib_flags << 16; | 1092 | vpx_codec_frame_flags_t flags = lib_flags << 16; |
| @@ -23,6 +23,18 @@ VP9EncoderConfig vp9_get_encoder_config(int frame_width, int frame_height, · 22 unmodified lines | |||
| 23 | vpx_enc_pass enc_pass); | 23 | vpx_enc_pass enc_pass); |
| 24 | FRAME_INFO vp9_get_frame_info(const VP9EncoderConfig *oxcf); | 24 | FRAME_INFO vp9_get_frame_info(const VP9EncoderConfig *oxcf); |
| 25 | 25 | ||
| 26 | static INLINE int64_t | ||
| 27 | timebase_units_to_ticks(const vpx_rational64_t *timestamp_ratio, int64_t n) { | ||
| 28 | return n * timestamp_ratio->num / timestamp_ratio->den; | ||
| 29 | } | ||
| 30 | |||
| 31 | static INLINE int64_t | ||
| 32 | ticks_to_timebase_units(const vpx_rational64_t *timestamp_ratio, int64_t n) { | ||
| 33 | int64_t round = timestamp_ratio->num / 2; | ||
| 34 | if (round > 0) --round; | ||
| 35 | return (n * timestamp_ratio->den + round) / timestamp_ratio->num; | ||
| 36 | } | ||
| 37 | |||
| 26 | #ifdef __cplusplus | 38 | #ifdef __cplusplus |
| 27 | } // extern "C" | 39 | } // extern "C" |
| 28 | #endif | 40 | #endif |