From 0e4ea14ba10bb2e7d7fde715b6c722d0d8089431 Mon Sep 17 00:00:00 2001 From: Hans Petter Selasky Date: Sat, 21 Mar 2020 10:55:12 +0100 Subject: [PATCH] libobs: Implement and use better scaling function for 64-bit integers As os_gettime_ns() gets large the current scaling methods, mostly by casting to uint64_t, may lead to numerical overflows. Sweep the code and use util_mul_div64() where applicable. Signed-off-by: Hans Petter Selasky --- UI/window-basic-auto-config-test.cpp | 9 ++++--- libobs/CMakeLists.txt | 1 + libobs/audio-monitoring/win32/wasapi-output.c | 9 +++---- libobs/media-io/audio-io.c | 7 ++---- libobs/media-io/audio-io.h | 12 +++------- libobs/media-io/video-io.c | 5 ++-- libobs/obs-audio.c | 7 +++--- libobs/obs-encoder.c | 8 +++---- libobs/obs-output.c | 8 ++++--- libobs/obs-scene.c | 9 +++---- libobs/obs-source-transition.c | 3 ++- libobs/obs-source.c | 7 +++--- libobs/util/util_uint64.h | 24 +++++++++++++++++++ plugins/decklink/decklink-device-instance.cpp | 7 +++--- plugins/decklink/decklink-output.cpp | 8 ++++--- plugins/linux-alsa/alsa-input.c | 6 +++-- plugins/linux-pulseaudio/pulse-input.c | 3 ++- plugins/obs-filters/async-delay-filter.c | 4 +++- plugins/obs-filters/gpu-delay.c | 4 ++-- plugins/win-capture/game-capture.c | 4 +++- plugins/win-wasapi/win-wasapi.cpp | 5 ++-- test/test-input/sync-pair-aud.c | 4 +++- 22 files changed, 97 insertions(+), 57 deletions(-) create mode 100644 libobs/util/util_uint64.h diff --git a/UI/window-basic-auto-config-test.cpp b/UI/window-basic-auto-config-test.cpp index e62acd36f..fd4374dd6 100644 --- a/UI/window-basic-auto-config-test.cpp +++ b/UI/window-basic-auto-config-test.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -413,12 +414,14 @@ void AutoConfigTestPage::TestBandwidthThread() cv.wait(ul); uint64_t total_time = os_gettime_ns() - t_start; + if (total_time == 0) + total_time = 1; int total_bytes = (int)obs_output_get_total_bytes(output) - start_bytes; - uint64_t bitrate = (uint64_t)total_bytes * 8 * 1000000000 / - total_time / 1000; - + uint64_t bitrate = util_mul_div64( + total_bytes, 8ULL * 1000000000ULL / 1000ULL, + total_time); if (obs_output_get_frames_dropped(output) || (int)bitrate < (wiz->startingBitrate * 75 / 100)) { server.bitrate = (int)bitrate * 70 / 100; diff --git a/libobs/CMakeLists.txt b/libobs/CMakeLists.txt index 84889e614..20593ac6e 100644 --- a/libobs/CMakeLists.txt +++ b/libobs/CMakeLists.txt @@ -360,6 +360,7 @@ set(libobs_util_HEADERS util/text-lookup.h util/bmem.h util/c99defs.h + util/util_uint64.h util/util_uint128.h util/cf-parser.h util/threading.h diff --git a/libobs/audio-monitoring/win32/wasapi-output.c b/libobs/audio-monitoring/win32/wasapi-output.c index 1247a1bca..04d991de1 100644 --- a/libobs/audio-monitoring/win32/wasapi-output.c +++ b/libobs/audio-monitoring/win32/wasapi-output.c @@ -2,6 +2,7 @@ #include "../../util/circlebuf.h" #include "../../util/platform.h" #include "../../util/darray.h" +#include "../../util/util_uint64.h" #include "../../obs-internal.h" #include "wasapi-output.h" @@ -78,8 +79,8 @@ static bool process_audio_delay(struct audio_monitor *monitor, float **data, monitor->prev_video_ts = last_frame_ts; } else if (monitor->prev_video_ts == last_frame_ts) { - monitor->time_since_prev += (uint64_t)*frames * 1000000000ULL / - (uint64_t)monitor->sample_rate; + monitor->time_since_prev += util_mul_div64( + *frames, 1000000000ULL, monitor->sample_rate); } else { monitor->time_since_prev = 0; } @@ -90,8 +91,8 @@ static bool process_audio_delay(struct audio_monitor *monitor, float **data, circlebuf_peek_front(&monitor->delay_buffer, &cur_ts, sizeof(ts)); - front_ts = cur_ts - ((uint64_t)pad * 1000000000ULL / - (uint64_t)monitor->sample_rate); + front_ts = cur_ts - util_mul_div64(pad, 1000000000ULL, + monitor->sample_rate); diff = (int64_t)front_ts - (int64_t)last_frame_ts; bad_diff = !last_frame_ts || llabs(diff) > 5000000000 || monitor->time_since_prev > 100000000ULL; diff --git a/libobs/media-io/audio-io.c b/libobs/media-io/audio-io.c index 83225e90b..1dfc5faa0 100644 --- a/libobs/media-io/audio-io.c +++ b/libobs/media-io/audio-io.c @@ -23,6 +23,7 @@ #include "../util/circlebuf.h" #include "../util/platform.h" #include "../util/profiler.h" +#include "../util/util_uint64.h" #include "audio-io.h" #include "audio-resampler.h" @@ -78,11 +79,7 @@ struct audio_output { static inline double ts_to_frames(const audio_t *audio, uint64_t ts) { - double audio_offset_d = (double)ts; - audio_offset_d /= 1000000000.0; - audio_offset_d *= (double)audio->info.samples_per_sec; - - return audio_offset_d; + return util_mul_div64(ts, audio->info.samples_per_sec, 1000000000ULL); } static inline double positive_round(double val) diff --git a/libobs/media-io/audio-io.h b/libobs/media-io/audio-io.h index f5dec6d80..3712f3a22 100644 --- a/libobs/media-io/audio-io.h +++ b/libobs/media-io/audio-io.h @@ -19,7 +19,7 @@ #include "media-io-defs.h" #include "../util/c99defs.h" -#include "../util/util_uint128.h" +#include "../util/util_uint64.h" #ifdef __cplusplus extern "C" { @@ -195,18 +195,12 @@ static inline size_t get_audio_size(enum audio_format format, static inline uint64_t audio_frames_to_ns(size_t sample_rate, uint64_t frames) { - util_uint128_t val; - val = util_mul64_64(frames, 1000000000ULL); - val = util_div128_32(val, (uint32_t)sample_rate); - return val.low; + return util_mul_div64(frames, 1000000000ULL, sample_rate); } static inline uint64_t ns_to_audio_frames(size_t sample_rate, uint64_t frames) { - util_uint128_t val; - val = util_mul64_64(frames, sample_rate); - val = util_div128_32(val, 1000000000); - return val.low; + return util_mul_div64(frames, sample_rate, 1000000000ULL); } #define AUDIO_OUTPUT_SUCCESS 0 diff --git a/libobs/media-io/video-io.c b/libobs/media-io/video-io.c index 08d7be27f..eee61e3ed 100644 --- a/libobs/media-io/video-io.c +++ b/libobs/media-io/video-io.c @@ -22,6 +22,7 @@ #include "../util/profiler.h" #include "../util/threading.h" #include "../util/darray.h" +#include "../util/util_uint64.h" #include "format-conversion.h" #include "video-io.h" @@ -234,8 +235,8 @@ int video_output_open(video_t **video, struct video_output_info *info) goto fail; memcpy(&out->info, info, sizeof(struct video_output_info)); - out->frame_time = (uint64_t)(1000000000.0 * (double)info->fps_den / - (double)info->fps_num); + out->frame_time = + util_mul_div64(1000000000ULL, info->fps_den, info->fps_num); out->initialized = false; if (pthread_mutexattr_init(&attr) != 0) diff --git a/libobs/obs-audio.c b/libobs/obs-audio.c index c27237ede..7303df9f8 100644 --- a/libobs/obs-audio.c +++ b/libobs/obs-audio.c @@ -17,6 +17,7 @@ #include #include "obs-internal.h" +#include "util/util_uint64.h" struct ts_info { uint64_t start; @@ -41,7 +42,7 @@ static void push_audio_tree(obs_source_t *parent, obs_source_t *source, void *p) static inline size_t convert_time_to_frames(size_t sample_rate, uint64_t t) { - return (size_t)(t * (uint64_t)sample_rate / 1000000000ULL); + return util_mul_div64(t, sample_rate, 1000000000ULL); } static inline void mix_audio(struct audio_output_data *mixes, @@ -90,8 +91,8 @@ static void ignore_audio(obs_source_t *source, size_t channels, source->audio_input_buf[ch].size); source->last_audio_input_buf_size = 0; - source->audio_ts += (uint64_t)num_floats * 1000000000ULL / - (uint64_t)sample_rate; + source->audio_ts += + util_mul_div64(num_floats, 1000000000ULL, sample_rate); } } diff --git a/libobs/obs-encoder.c b/libobs/obs-encoder.c index 6ba2fafc6..079dc16f5 100644 --- a/libobs/obs-encoder.c +++ b/libobs/obs-encoder.c @@ -17,6 +17,7 @@ #include "obs.h" #include "obs-internal.h" +#include "util/util_uint64.h" #define encoder_active(encoder) os_atomic_load_bool(&encoder->active) #define set_encoder_active(encoder, val) \ @@ -1073,8 +1074,7 @@ static inline size_t calc_offset_size(struct obs_encoder *encoder, uint64_t v_start_ts, uint64_t a_start_ts) { uint64_t offset = v_start_ts - a_start_ts; - offset = (uint64_t)offset * (uint64_t)encoder->samplerate / - 1000000000ULL; + offset = util_mul_div64(offset, encoder->samplerate, 1000000000ULL); return (size_t)offset * encoder->blocksize; } @@ -1121,8 +1121,8 @@ static bool buffer_audio(struct obs_encoder *encoder, struct audio_data *data) /* audio starting point still not synced with video starting * point, so don't start audio */ - end_ts += (uint64_t)data->frames * 1000000000ULL / - (uint64_t)encoder->samplerate; + end_ts += util_mul_div64(data->frames, 1000000000ULL, + encoder->samplerate); if (end_ts <= v_start_ts) { success = false; goto fail; diff --git a/libobs/obs-output.c b/libobs/obs-output.c index a68097051..f465e60af 100644 --- a/libobs/obs-output.c +++ b/libobs/obs-output.c @@ -17,6 +17,7 @@ #include #include "util/platform.h" +#include "util/util_uint64.h" #include "obs.h" #include "obs-internal.h" @@ -1731,8 +1732,8 @@ static bool prepare_audio(struct obs_output *output, *new = *old; if (old->timestamp < output->video_start_ts) { - uint64_t duration = (uint64_t)old->frames * 1000000000 / - (uint64_t)output->sample_rate; + uint64_t duration = util_mul_div64(old->frames, 1000000000ULL, + output->sample_rate); uint64_t end_ts = (old->timestamp + duration); uint64_t cutoff; @@ -1742,7 +1743,8 @@ static bool prepare_audio(struct obs_output *output, cutoff = output->video_start_ts - old->timestamp; new->timestamp += cutoff; - cutoff = cutoff * (uint64_t)output->sample_rate / 1000000000; + cutoff = util_mul_div64(cutoff, output->sample_rate, + 1000000000ULL); for (size_t i = 0; i < output->planes; i++) new->data[i] += output->audio_size *(uint32_t)cutoff; diff --git a/libobs/obs-scene.c b/libobs/obs-scene.c index e0dc9b371..f2cce7b0f 100644 --- a/libobs/obs-scene.c +++ b/libobs/obs-scene.c @@ -17,6 +17,7 @@ ******************************************************************************/ #include "util/threading.h" +#include "util/util_uint64.h" #include "graphics/math-defs.h" #include "obs-scene.h" @@ -974,8 +975,8 @@ static void apply_scene_item_audio_actions(struct obs_scene_item *item, if (timestamp < ts) timestamp = ts; - new_frame_num = (timestamp - ts) * (uint64_t)sample_rate / - 1000000000ULL; + new_frame_num = util_mul_div64(timestamp - ts, sample_rate, + 1000000000ULL); if (ts && new_frame_num >= AUDIO_OUTPUT_FRAMES) break; @@ -1024,8 +1025,8 @@ static bool apply_scene_item_volume(struct obs_scene_item *item, float **buf, pthread_mutex_unlock(&item->actions_mutex); if (actions_pending) { - uint64_t duration = (uint64_t)AUDIO_OUTPUT_FRAMES * - 1000000000ULL / (uint64_t)sample_rate; + uint64_t duration = util_mul_div64(AUDIO_OUTPUT_FRAMES, + 1000000000ULL, sample_rate); if (!ts || action.timestamp < (ts + duration)) { apply_scene_item_audio_actions(item, buf, ts, diff --git a/libobs/obs-source-transition.c b/libobs/obs-source-transition.c index a62b186d3..614cbf8af 100644 --- a/libobs/obs-source-transition.c +++ b/libobs/obs-source-transition.c @@ -16,6 +16,7 @@ ******************************************************************************/ #include "obs-internal.h" +#include "util/util_uint64.h" #include "graphics/math-extra.h" #define lock_transition(transition) \ @@ -866,7 +867,7 @@ static inline float get_sample_time(obs_source_t *transition, uint64_t ts) { uint64_t sample_ts_offset = - (uint64_t)sample * 1000000000ULL / (uint64_t)sample_rate; + util_mul_div64(sample, 1000000000ULL, sample_rate); uint64_t i_ts = ts + sample_ts_offset; return calc_time(transition, i_ts); } diff --git a/libobs/obs-source.c b/libobs/obs-source.c index 90b6a35f3..5f85da072 100644 --- a/libobs/obs-source.c +++ b/libobs/obs-source.c @@ -23,6 +23,7 @@ #include "media-io/audio-io.h" #include "util/threading.h" #include "util/platform.h" +#include "util/util_uint64.h" #include "callback/calldata.h" #include "graphics/matrix3.h" #include "graphics/vec3.h" @@ -1165,13 +1166,13 @@ static inline uint64_t conv_frames_to_time(const size_t sample_rate, if (!sample_rate) return 0; - return (uint64_t)frames * 1000000000ULL / (uint64_t)sample_rate; + return util_mul_div64(frames, 1000000000ULL, sample_rate); } static inline size_t conv_time_to_frames(const size_t sample_rate, const uint64_t duration) { - return (size_t)(duration * (uint64_t)sample_rate / 1000000000ULL); + return (size_t)util_mul_div64(duration, sample_rate, 1000000000ULL); } /* maximum buffer size */ @@ -1235,7 +1236,7 @@ static inline uint64_t uint64_diff(uint64_t ts1, uint64_t ts2) static inline size_t get_buf_placement(audio_t *audio, uint64_t offset) { uint32_t sample_rate = audio_output_get_sample_rate(audio); - return (size_t)(offset * (uint64_t)sample_rate / 1000000000ULL); + return (size_t)util_mul_div64(offset, sample_rate, 1000000000ULL); } static void source_output_audio_place(obs_source_t *source, diff --git a/libobs/util/util_uint64.h b/libobs/util/util_uint64.h new file mode 100644 index 000000000..f3c2be18c --- /dev/null +++ b/libobs/util/util_uint64.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2020 Hans Petter Selasky + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#pragma once + +static inline uint64_t util_mul_div64(uint64_t num, uint64_t mul, uint64_t div) +{ + const uint64_t rem = num % div; + + return (num / div) * mul + (rem * mul) / div; +} diff --git a/plugins/decklink/decklink-device-instance.cpp b/plugins/decklink/decklink-device-instance.cpp index 0fd9f3502..6c8b3d3e0 100644 --- a/plugins/decklink/decklink-device-instance.cpp +++ b/plugins/decklink/decklink-device-instance.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include @@ -90,8 +91,8 @@ void DeckLinkDeviceInstance::HandleAudioPacket( if (decklink && !static_cast(decklink)->buffering) { currentPacket.timestamp = os_gettime_ns(); currentPacket.timestamp -= - (uint64_t)frameCount * 1000000000ULL / - (uint64_t)currentPacket.samples_per_sec; + util_mul_div64(frameCount, 1000000000ULL, + currentPacket.samples_per_sec); } int maxdevicechannel = device->GetMaxChannel(); @@ -113,7 +114,7 @@ void DeckLinkDeviceInstance::HandleAudioPacket( } nextAudioTS = timestamp + - ((uint64_t)frameCount * 1000000000ULL / 48000ULL) + 1; + util_mul_div64(frameCount, 1000000000ULL, 48000ULL) + 1; obs_source_output_audio( static_cast(decklink)->GetSource(), diff --git a/plugins/decklink/decklink-output.cpp b/plugins/decklink/decklink-output.cpp index 5a97dbaea..61ae3d890 100644 --- a/plugins/decklink/decklink-output.cpp +++ b/plugins/decklink/decklink-output.cpp @@ -9,6 +9,7 @@ #include "decklink-devices.hpp" #include "../../libobs/media-io/video-scaler.h" +#include "../../libobs/util/util_uint64.h" static void decklink_output_destroy(void *data) { @@ -127,8 +128,8 @@ static bool prepare_audio(DeckLinkOutput *decklink, *output = *frame; if (frame->timestamp < decklink->start_timestamp) { - uint64_t duration = (uint64_t)frame->frames * 1000000000 / - (uint64_t)decklink->audio_samplerate; + uint64_t duration = util_mul_div64(frame->frames, 1000000000ULL, + decklink->audio_samplerate); uint64_t end_ts = frame->timestamp + duration; uint64_t cutoff; @@ -138,7 +139,8 @@ static bool prepare_audio(DeckLinkOutput *decklink, cutoff = decklink->start_timestamp - frame->timestamp; output->timestamp += cutoff; - cutoff *= (uint64_t)decklink->audio_samplerate / 1000000000; + cutoff = util_mul_div64(cutoff, decklink->audio_samplerate, + 1000000000ULL); for (size_t i = 0; i < decklink->audio_planes; i++) output->data[i] += diff --git a/plugins/linux-alsa/alsa-input.c b/plugins/linux-alsa/alsa-input.c index 64e7382db..568a74614 100644 --- a/plugins/linux-alsa/alsa-input.c +++ b/plugins/linux-alsa/alsa-input.c @@ -20,6 +20,7 @@ along with this program. If not, see . #include #include #include +#include #include #include @@ -562,8 +563,9 @@ void *_alsa_listen(void *attr) } out.frames = frames; - out.timestamp = os_gettime_ns() - - ((frames * NSEC_PER_SEC) / data->rate); + out.timestamp = + os_gettime_ns() - + util_mul_div64(frames, NSEC_PER_SEC, data->rate); if (!data->first_ts) data->first_ts = out.timestamp + STARTUP_TIMEOUT_NS; diff --git a/plugins/linux-pulseaudio/pulse-input.c b/plugins/linux-pulseaudio/pulse-input.c index 15cb7f130..8b4af7904 100644 --- a/plugins/linux-pulseaudio/pulse-input.c +++ b/plugins/linux-pulseaudio/pulse-input.c @@ -17,6 +17,7 @@ along with this program. If not, see . #include #include +#include #include #include "pulse-wrapper.h" @@ -161,7 +162,7 @@ static pa_channel_map pulse_channel_map(enum speaker_layout layout) static inline uint64_t samples_to_ns(size_t frames, uint_fast32_t rate) { - return frames * NSEC_PER_SEC / rate; + return util_mul_div64(frames, NSEC_PER_SEC, rate); } static inline uint64_t get_sample_time(size_t frames, uint_fast32_t rate) diff --git a/plugins/obs-filters/async-delay-filter.c b/plugins/obs-filters/async-delay-filter.c index 862c78a46..8ccecb148 100644 --- a/plugins/obs-filters/async-delay-filter.c +++ b/plugins/obs-filters/async-delay-filter.c @@ -1,5 +1,6 @@ #include #include +#include #ifndef SEC_TO_NSEC #define SEC_TO_NSEC 1000000000ULL @@ -199,7 +200,8 @@ async_delay_filter_audio(void *data, struct obs_audio_data *audio) filter->last_audio_ts = audio->timestamp; - duration = (uint64_t)audio->frames * SEC_TO_NSEC / filter->samplerate; + duration = + util_mul_div64(audio->frames, SEC_TO_NSEC, filter->samplerate); end_ts = audio->timestamp + duration; for (size_t i = 0; i < MAX_AV_PLANES; i++) { diff --git a/plugins/obs-filters/gpu-delay.c b/plugins/obs-filters/gpu-delay.c index 4eb98e03f..e8f14e28d 100644 --- a/plugins/obs-filters/gpu-delay.c +++ b/plugins/obs-filters/gpu-delay.c @@ -1,5 +1,6 @@ #include #include +#include #define S_DELAY_MS "delay_ms" #define T_DELAY_MS obs_module_text("DelayMs") @@ -90,8 +91,7 @@ static inline void check_interval(struct gpu_delay_filter_data *f) obs_get_video_info(&ovi); - interval_ns = - (uint64_t)ovi.fps_den * 1000000000ULL / (uint64_t)ovi.fps_num; + interval_ns = util_mul_div64(ovi.fps_den, 1000000000ULL, ovi.fps_num); if (interval_ns != f->interval_ns) update_interval(f, interval_ns); diff --git a/plugins/win-capture/game-capture.c b/plugins/win-capture/game-capture.c index 9d0b0f2c2..fce3ce607 100644 --- a/plugins/win-capture/game-capture.c +++ b/plugins/win-capture/game-capture.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "obfuscate.h" #include "inject-library.h" @@ -729,7 +730,8 @@ static inline void reset_frame_interval(struct game_capture *gc) uint64_t interval = 0; if (obs_get_video_info(&ovi)) { - interval = ovi.fps_den * 1000000000ULL / ovi.fps_num; + interval = + util_mul_div64(ovi.fps_den, 1000000000ULL, ovi.fps_num); /* Always limit capture framerate to some extent. If a game * running at 900 FPS is being captured without some sort of diff --git a/plugins/win-wasapi/win-wasapi.cpp b/plugins/win-wasapi/win-wasapi.cpp index c60d3a9dd..9d2e384ff 100644 --- a/plugins/win-wasapi/win-wasapi.cpp +++ b/plugins/win-wasapi/win-wasapi.cpp @@ -8,6 +8,7 @@ #include #include #include +#include using namespace std; @@ -464,8 +465,8 @@ bool WASAPISource::ProcessCaptureData() data.timestamp = useDeviceTiming ? ts * 100 : os_gettime_ns(); if (!useDeviceTiming) - data.timestamp -= (uint64_t)frames * 1000000000ULL / - (uint64_t)sampleRate; + data.timestamp -= util_mul_div64(frames, 1000000000ULL, + sampleRate); obs_source_output_audio(source, &data); diff --git a/test/test-input/sync-pair-aud.c b/test/test-input/sync-pair-aud.c index 6ee57dcbc..1ff0b7c81 100644 --- a/test/test-input/sync-pair-aud.c +++ b/test/test-input/sync-pair-aud.c @@ -2,6 +2,7 @@ #include #include #include +#include #include struct sync_pair_aud { @@ -54,7 +55,8 @@ static void *sync_pair_aud_thread(void *pdata) for (uint64_t i = 0; i < frames; i++) { uint64_t ts = - last_time + i * 1000000000ULL / sample_rate; + last_time + + util_mul_div64(i, 1000000000ULL, sample_rate); if (whitelist_time(ts, interval, fps_num, fps_den)) { cos_val += rate * M_PI_X2;